// lib/canvas/export.js — assemble a Canvas QTI .zip from an assignment and download it. // Client-side only (like lib/exporter.js): no data leaves the browser. import { mapAssignmentToCanvas } from "./map"; import { renderAssessmentXml } from "./qti"; import { renderManifest, renderAssessmentMeta } from "./manifest"; import { storeZip } from "./zip"; // A QTI identifier must be a valid NCName-ish token (starts with a letter, no spaces). function slugIdent(seed) { const clean = String(seed || "").replace(/[^a-zA-Z0-9]/g, ""); return "g" + (clean || "quiz"); } function safeName(title) { const base = String(title || "quiz") .replace(/[^\w\- ]+/g, "").trim().replace(/\s+/g, "-").slice(0, 60) || "quiz"; return `${base}-canvas.zip`; } // Build the package in memory. Returns { bytes: Uint8Array, ir, files } — pure, testable. export function buildCanvasPackage(assignment, opts = {}) { const ir = mapAssignmentToCanvas(assignment, opts); const assessmentIdent = slugIdent(assignment?.id || assignment?.title); const manifestIdent = "m" + assessmentIdent; const files = [ { name: "imsmanifest.xml", data: renderManifest(assessmentIdent, manifestIdent) }, { name: `${assessmentIdent}/${assessmentIdent}.xml`, data: renderAssessmentXml(ir, assessmentIdent) }, { name: `${assessmentIdent}/assessment_meta.xml`, data: renderAssessmentMeta(ir, assessmentIdent) }, ]; return { bytes: storeZip(files), ir, files }; } function download(filename, blob) { const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 800); } // Build and trigger the browser download. Returns the IR (so callers can show a summary). export function exportCanvasZip(assignment, opts = {}) { const { bytes, ir } = buildCanvasPackage(assignment, opts); // Copy into a fresh ArrayBuffer so Blob gets a clean, correctly-sized buffer. const blob = new Blob([bytes.slice()], { type: "application/zip" }); download(safeName(assignment?.title), blob); return ir; }