Sync Canvas LMS export feature + refreshed Settings screenshot
Brings the share build up to date with the main project: QTI .zip export and optional direct Canvas API push, plus the Canvas settings card. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// lib/canvas/zip.js — minimal, zero-dependency ZIP writer (STORE method, no compression).
|
||||
// Canvas accepts uncompressed QTI packages, so we skip DEFLATE entirely and avoid any
|
||||
// dependency. Output is a standard .zip as a Uint8Array. Deterministic (fixed timestamps).
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const t = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
t[n] = c >>> 0;
|
||||
}
|
||||
return t;
|
||||
})();
|
||||
|
||||
// CRC-32 (IEEE 802.3). crc32 of "123456789" === 0xCBF43926 (unit-test anchor).
|
||||
export function crc32(bytes) {
|
||||
let c = 0xffffffff;
|
||||
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
||||
return (c ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
const enc = new TextEncoder();
|
||||
const toBytes = (data) => (typeof data === "string" ? enc.encode(data) : data);
|
||||
|
||||
// files: [{ name: string, data: string|Uint8Array }] -> Uint8Array (.zip bytes)
|
||||
export function storeZip(files) {
|
||||
const DOS_TIME = 0;
|
||||
const DOS_DATE = 0x0021; // 1980-01-01, deterministic
|
||||
const chunks = [];
|
||||
const central = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const f of files) {
|
||||
const nameBytes = enc.encode(f.name);
|
||||
const data = toBytes(f.data);
|
||||
const crc = crc32(data);
|
||||
|
||||
const local = new Uint8Array(30 + nameBytes.length);
|
||||
const lv = new DataView(local.buffer);
|
||||
lv.setUint32(0, 0x04034b50, true); // local file header signature
|
||||
lv.setUint16(4, 20, true); // version needed to extract
|
||||
lv.setUint16(6, 0x0800, true); // flags: bit 11 = UTF-8 filenames
|
||||
lv.setUint16(8, 0, true); // compression method 0 = store
|
||||
lv.setUint16(10, DOS_TIME, true);
|
||||
lv.setUint16(12, DOS_DATE, true);
|
||||
lv.setUint32(14, crc, true);
|
||||
lv.setUint32(18, data.length, true); // compressed size (== uncompressed for store)
|
||||
lv.setUint32(22, data.length, true); // uncompressed size
|
||||
lv.setUint16(26, nameBytes.length, true);
|
||||
lv.setUint16(28, 0, true); // extra field length
|
||||
local.set(nameBytes, 30);
|
||||
chunks.push(local, data);
|
||||
|
||||
const cd = new Uint8Array(46 + nameBytes.length);
|
||||
const cv = new DataView(cd.buffer);
|
||||
cv.setUint32(0, 0x02014b50, true); // central directory header signature
|
||||
cv.setUint16(4, 20, true); // version made by
|
||||
cv.setUint16(6, 20, true); // version needed
|
||||
cv.setUint16(8, 0x0800, true);
|
||||
cv.setUint16(10, 0, true); // method
|
||||
cv.setUint16(12, DOS_TIME, true);
|
||||
cv.setUint16(14, DOS_DATE, true);
|
||||
cv.setUint32(16, crc, true);
|
||||
cv.setUint32(20, data.length, true);
|
||||
cv.setUint32(24, data.length, true);
|
||||
cv.setUint16(28, nameBytes.length, true);
|
||||
cv.setUint16(30, 0, true); // extra length
|
||||
cv.setUint16(32, 0, true); // comment length
|
||||
cv.setUint16(34, 0, true); // disk number start
|
||||
cv.setUint16(36, 0, true); // internal attributes
|
||||
cv.setUint32(38, 0, true); // external attributes
|
||||
cv.setUint32(42, offset, true); // relative offset of local header
|
||||
cd.set(nameBytes, 46);
|
||||
central.push(cd);
|
||||
|
||||
offset += local.length + data.length;
|
||||
}
|
||||
|
||||
const centralStart = offset;
|
||||
let centralSize = 0;
|
||||
for (const cd of central) { chunks.push(cd); centralSize += cd.length; }
|
||||
|
||||
const eocd = new Uint8Array(22);
|
||||
const ev = new DataView(eocd.buffer);
|
||||
ev.setUint32(0, 0x06054b50, true); // end of central directory signature
|
||||
ev.setUint16(8, central.length, true); // entries on this disk
|
||||
ev.setUint16(10, central.length, true); // total entries
|
||||
ev.setUint32(12, centralSize, true);
|
||||
ev.setUint32(16, centralStart, true);
|
||||
ev.setUint16(20, 0, true); // comment length
|
||||
chunks.push(eocd);
|
||||
|
||||
let total = 0;
|
||||
for (const c of chunks) total += c.length;
|
||||
const out = new Uint8Array(total);
|
||||
let p = 0;
|
||||
for (const c of chunks) { out.set(c, p); p += c.length; }
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user