import { buildBundle } from "./interchange.js"; /** Convert an ArrayBuffer to the base64 representation used in JSON bundles. */ function arrayBufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ""; const chunkSize = 0x8000; for (let offset = 0; offset < bytes.length; offset += chunkSize) { binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize)); } return btoa(binary); } /** * Export a stored CMap model and its linked resources as a complete JSON bundle. * CMaps are loaded through their repository. Page loading, bundle construction * and attachment encoding remain independent of workspace presentation. */ export class CmapJsonExporter { constructor(cmapRepository, loadWikiPage, fetchFile, generator = "Racket Wiki") { if (!cmapRepository || typeof cmapRepository.load !== "function" || typeof loadWikiPage !== "function" || typeof fetchFile !== "function") { throw new TypeError("A CMap repository, wiki-page loader and file loader are required"); } this.cmapRepository = cmapRepository; this.loadWikiPage = loadWikiPage; this.fetchFile = fetchFile; this.generator = generator; } /** * Build a validated JSON bundle without changing the source CMaps. * Linked CMaps are followed up to maxDepth; linked pages and attachments * are included by the interchange format. */ async export(rootMap, maxDepth = 0) { return buildBundle({ rootMap: this.bundleMap(rootMap), maxDepth, generator: this.generator, loadConceptMap: async (slug) => this.bundleMap(await this.cmapRepository.load(slug)), loadWikiPage: this.loadWikiPage, loadAttachment: (url) => this.loadAttachment(url) }); } /** Present one stored model through the public interchange record shape. */ bundleMap(storedMap) { if (!storedMap?.slug || typeof storedMap.toDocument !== "function") { throw new TypeError("JSON export requires a stored CMap"); } return { slug: storedMap.slug, title: storedMap.title, document: storedMap.toDocument() }; } /** Load and encode one attachment referenced by an exported wiki page. */ async loadAttachment(url) { const response = await this.fetchFile(url); if (!response.ok) throw new Error(`Attachment could not be exported: ${url}`); const content = await response.arrayBuffer(); return { mimeType: response.headers.get("content-type") || "application/octet-stream", contentBase64: arrayBufferToBase64(content) }; } }