/* Build a self-contained Markdown report from one stored CMap and its links. */ ((root, factory) => { const api = factory(); if (typeof module === "object" && module.exports) module.exports = api; if (root) root.RacketWikiCmapExport = api; })(typeof window !== "undefined" ? window : globalThis, () => { "use strict"; const labels = { en: { exportTitle: "CMap export", generated: "Generated", root: "Root CMap", depth: "Linked CMap depth", pagesIncluded: "Linked wiki pages included", yes: "yes", no: "no", cmap: "CMap", level: "level", address: "Address", tags: "Tags", none: "none", summary: "Summary", noSummary: "No summary supplied.", explanation: "CMap explanation", concepts: "Concepts", relations: "Relations", linkedMaps: "Linked CMaps", noConcepts: "No concepts.", noRelations: "No relations.", synopsis: "Summary", aspects: "Aspects", wikiPage: "Wiki page", conceptExplanation: "Concept explanation", linkedCmap: "Linked CMap", webPage: "Web page", people: "People (responsibility/action)", placements: "Placements", sourceView: "Derived view of", sourceMissing: "Source CMap unavailable", missingPage: "Page unavailable", linkedPages: "Linked wiki pages", page: "Page" }, nl: { exportTitle: "CMap-export", generated: "Gegenereerd", root: "Start-CMap", depth: "Diepte gekoppelde CMaps", pagesIncluded: "Gekoppelde wikipagina's opgenomen", yes: "ja", no: "nee", cmap: "CMap", level: "niveau", address: "Adres", tags: "Tags", none: "geen", summary: "Samenvatting", noSummary: "Geen samenvatting opgegeven.", explanation: "CMap-uitleg", concepts: "Concepten", relations: "Relaties", linkedMaps: "Gekoppelde CMaps", noConcepts: "Geen concepten.", noRelations: "Geen relaties.", synopsis: "Samenvatting", aspects: "Aspecten", wikiPage: "Wikipagina", conceptExplanation: "Conceptuitleg", linkedCmap: "Gekoppelde CMap", webPage: "Webpagina", people: "Personen (verantwoordelijkheid/actie)", placements: "Plaatsingen", sourceView: "Afgeleide weergave van", sourceMissing: "Bron-CMap niet beschikbaar", missingPage: "Pagina niet beschikbaar", linkedPages: "Gekoppelde wikipagina's", page: "Pagina" } }; function decodedDocument(value) { let documentValue = value; for (let attempt = 0; attempt < 2 && typeof documentValue === "string"; attempt += 1) { documentValue = JSON.parse(documentValue); } return documentValue && typeof documentValue === "object" && !Array.isArray(documentValue) ? documentValue : {}; } function cleanMetadata(documentValue) { const metadata = documentValue.metadata && typeof documentValue.metadata === "object" ? documentValue.metadata : {}; return { tags: Array.isArray(metadata.tags) ? metadata.tags.map(String).map((tag) => tag.trim()).filter(Boolean) : [], summary: String(metadata.summary || "").trim(), explanationPageSlug: String(metadata.explanationPageSlug || "").trim() }; } function itemRecords(documentValue) { const concepts = new Map((Array.isArray(documentValue.concepts) ? documentValue.concepts : []) .filter((concept) => concept && concept.id) .map((concept) => [String(concept.id), concept])); return (Array.isArray(documentValue.items) ? documentValue.items : []) .filter((item) => item && item.id !== undefined) .map((item) => ({ ...item, ...(concepts.get(String(item.conceptId)) || {}), id: item.id, conceptId: item.conceptId })); } function derivedDocument(sourceDocument, derivedDocumentValue) { const pointer = derivedDocumentValue.derivedView || {}; const rootId = Number(pointer.rootItemId); const allItems = Array.isArray(sourceDocument.items) ? sourceDocument.items : []; const byId = new Map(allItems.map((item) => [Number(item.id), item])); const belongsToRoot = (item) => { if (Number(item.id) === rootId) return true; let parentId = Number(item.parentSubmapId); const seen = new Set(); while (Number.isInteger(parentId) && parentId > 0 && !seen.has(parentId)) { if (parentId === rootId) return true; seen.add(parentId); parentId = Number(byId.get(parentId)?.parentSubmapId); } return false; }; const items = allItems.filter(belongsToRoot); const itemIds = new Set(items.map((item) => Number(item.id))); const conceptIds = new Set(items.map((item) => item.conceptId).filter(Boolean).map(String)); const rootItem = itemRecords(sourceDocument).find((item) => Number(item.id) === rootId) || byId.get(rootId) || {}; const ownMetadata = cleanMetadata(derivedDocumentValue); const metadata = { tags: ownMetadata.tags.length ? ownMetadata.tags : (Array.isArray(rootItem.aspects) ? rootItem.aspects.map(String) : []), summary: ownMetadata.summary || String(rootItem.synopsis || "").trim(), explanationPageSlug: ownMetadata.explanationPageSlug || String(rootItem.descriptionPageSlug || "").trim() }; return { ...sourceDocument, metadata, items, concepts: (Array.isArray(sourceDocument.concepts) ? sourceDocument.concepts : []) .filter((concept) => conceptIds.has(String(concept.id))), connectors: (Array.isArray(sourceDocument.connectors) ? sourceDocument.connectors : []) .filter((connector) => itemIds.has(Number(connector.sourceId)) && itemIds.has(Number(connector.targetId))) }; } async function resolveMapDocument(map, loadConceptMap) { const ownDocument = decodedDocument(map.document); const pointer = ownDocument.derivedView; if (!pointer || !pointer.sourceCmapSlug || !Number.isInteger(Number(pointer.rootItemId))) { return { document: ownDocument, sourceSlug: "" }; } try { const sourceMap = await loadConceptMap(pointer.sourceCmapSlug); return { document: derivedDocument(decodedDocument(sourceMap.document), ownDocument), sourceSlug: pointer.sourceCmapSlug }; } catch (_error) { return { document: ownDocument, sourceSlug: pointer.sourceCmapSlug, sourceMissing: true }; } } function linkedMapSlugs(documentValue) { return [...new Set(itemRecords(documentValue) .map((item) => String(item.cmapSlug || "").trim()) .filter(Boolean))]; } function headingText(value) { return String(value || "").replace(/[\r\n]+/g, " ").replace(/#+/g, "").trim(); } function inlineText(value) { return String(value || "").replace(/[\r\n]+/g, " ").replace(/([\\`*_[\]])/g, "\\$1").trim(); } function shiftHeadings(markdown, amount) { let fenced = false; return String(markdown || "").split("\n").map((line) => { if (/^\s*(```|~~~)/.test(line)) { fenced = !fenced; return line; } if (fenced) return line; return line.replace(/^(#{1,6})\s+/, (match, hashes) => `${"#".repeat(Math.min(6, hashes.length + amount))} `); }).join("\n"); } function relationLines(documentValue) { const items = itemRecords(documentValue); const byId = new Map(items.map((item) => [Number(item.id), item])); const connectors = (Array.isArray(documentValue.connectors) ? documentValue.connectors : []) .filter((connector) => connector && connector.sourceId !== undefined && connector.targetId !== undefined); const used = new Set(); const result = []; const itemLabel = (id) => headingText(byId.get(Number(id))?.label || `[${id}]`); for (const phrase of items.filter((item) => item.kind === "phrase")) { const incoming = connectors.filter((connector) => Number(connector.targetId) === Number(phrase.id)); const outgoing = connectors.filter((connector) => Number(connector.sourceId) === Number(phrase.id)); for (const before of incoming) { for (const after of outgoing) { used.add(before); used.add(after); result.push(`${itemLabel(before.sourceId)} — **${inlineText(phrase.label || "")}** → ${itemLabel(after.targetId)}`); } } } for (const connector of connectors) { if (used.has(connector)) continue; result.push(`${itemLabel(connector.sourceId)} ${connector.hasArrow === false ? "—" : "→"} ${itemLabel(connector.targetId)}`); } return result; } function conceptEntries(documentValue) { const entries = new Map(); for (const item of itemRecords(documentValue)) { if (item.kind === "phrase") continue; const key = item.conceptId ? `concept:${item.conceptId}` : `item:${item.id}`; if (!entries.has(key)) entries.set(key, { ...item, placementCount: 0 }); entries.get(key).placementCount += 1; } return [...entries.values()]; } async function generateMarkdown(options) { const rootMap = options.rootMap; if (!rootMap || !rootMap.slug) throw new Error("A root CMap is required."); if (typeof options.loadConceptMap !== "function") throw new Error("loadConceptMap is required."); const maximumDepth = Math.max(0, Math.min(10, Number(options.maxDepth) || 0)); const includeWikiPages = Boolean(options.includeWikiPages); const locale = String(options.language || "nl").toLowerCase().startsWith("nl") ? "nl" : "en"; const t = labels[locale]; const maps = []; const visited = new Set(); async function collectMap(map, depth) { if (!map?.slug || visited.has(map.slug)) return; visited.add(map.slug); const resolved = await resolveMapDocument(map, options.loadConceptMap); maps.push({ map, depth, ...resolved }); if (depth >= maximumDepth) return; for (const slug of linkedMapSlugs(resolved.document)) { if (visited.has(slug)) continue; try { await collectMap(await options.loadConceptMap(slug), depth + 1); } catch (error) { maps.push({ map: { slug, title: slug }, depth: depth + 1, document: {}, loadError: error }); visited.add(slug); } } } await collectMap(rootMap, 0); const pageCache = new Map(); const explanationReferences = new Set(); async function loadPage(reference) { if (!reference || typeof options.loadWikiPage !== "function") return null; if (!pageCache.has(reference)) { pageCache.set(reference, Promise.resolve().then(() => options.loadWikiPage(reference)) .catch((error) => ({ slug: reference, title: reference, loadError: error }))); } return pageCache.get(reference); } for (const entry of maps) { const metadata = cleanMetadata(entry.document); if (metadata.explanationPageSlug) { explanationReferences.add(metadata.explanationPageSlug); await loadPage(metadata.explanationPageSlug); } if (includeWikiPages) { for (const concept of conceptEntries(entry.document)) { await loadPage(concept.pageSlug); await loadPage(concept.descriptionPageSlug); } } } const lines = [ `# ${t.exportTitle}: ${headingText(rootMap.title || rootMap.slug)}`, "", `- **${t.generated}:** ${new Date().toISOString()}`, `- **${t.root}:** \`cmap:${rootMap.slug}\``, `- **${t.depth}:** ${maximumDepth}`, `- **${t.pagesIncluded}:** ${includeWikiPages ? t.yes : t.no}`, "" ]; for (const entry of maps) { const metadata = cleanMetadata(entry.document); lines.push(`## ${t.cmap}: ${headingText(entry.map.title || entry.map.slug)} (${t.level} ${entry.depth})`, ""); lines.push(`- **${t.address}:** \`cmap:${entry.map.slug}\``); lines.push(`- **${t.tags}:** ${metadata.tags.length ? metadata.tags.map((tag) => `\`${inlineText(tag)}\``).join(", ") : t.none}`); if (entry.sourceSlug) lines.push(`- **${t.sourceView}:** \`cmap:${entry.sourceSlug}\``); if (entry.sourceMissing) lines.push(`- **${t.sourceMissing}:** \`cmap:${entry.sourceSlug}\``); if (entry.loadError) lines.push(`- **Fout:** ${inlineText(entry.loadError.message || entry.loadError)}`); lines.push("", `### ${t.summary}`, "", metadata.summary || t.noSummary, ""); if (metadata.explanationPageSlug) { const page = await loadPage(metadata.explanationPageSlug); lines.push(`### ${t.explanation}`, "", `**${t.page}:** \`${metadata.explanationPageSlug}\``, ""); if (page?.loadError) lines.push(`_${t.missingPage}: ${inlineText(page.loadError.message || page.loadError)}_`, ""); else if (page) { if (Array.isArray(page.tags) && page.tags.length) { lines.push(`**${t.tags}:** ${page.tags.map((tag) => `\`${inlineText(tag)}\``).join(", ")}`, ""); } lines.push(shiftHeadings(page.markdown || "", 3).trim() || t.noSummary, ""); } } lines.push(`### ${t.concepts}`, ""); const concepts = conceptEntries(entry.document); if (!concepts.length) lines.push(`_${t.noConcepts}_`, ""); for (const concept of concepts) { lines.push(`#### ${headingText(concept.label || concept.id || "Concept")}`, ""); if (concept.synopsis) lines.push(`- **${t.synopsis}:** ${inlineText(concept.synopsis)}`); if (Array.isArray(concept.aspects) && concept.aspects.length) { lines.push(`- **${t.aspects}:** ${concept.aspects.map(inlineText).join(", ")}`); } const people = (Array.isArray(concept.tags) ? concept.tags : []) .filter((tag) => tag && typeof tag === "object" && tag.type === "person" && tag.value) .map((tag) => tag.value); if (people.length) { lines.push(`- **${t.people}:** ${people.map(inlineText).join(", ")}`); } if (concept.pageSlug) lines.push(`- **${t.wikiPage}:** \`${inlineText(concept.pageSlug)}\``); if (concept.descriptionPageSlug) lines.push(`- **${t.conceptExplanation}:** \`${inlineText(concept.descriptionPageSlug)}\``); if (concept.cmapSlug) lines.push(`- **${t.linkedCmap}:** \`cmap:${inlineText(concept.cmapSlug)}\``); if (concept.externalUrl) lines.push(`- **${t.webPage}:** ${inlineText(concept.externalUrl)}`); if (concept.placementCount > 1) lines.push(`- **${t.placements}:** ${concept.placementCount}`); lines.push(""); } lines.push(`### ${t.relations}`, ""); const relations = relationLines(entry.document); if (!relations.length) lines.push(`_${t.noRelations}_`, ""); else lines.push(...relations.map((relation) => `- ${relation}`), ""); const linked = linkedMapSlugs(entry.document); if (linked.length) { lines.push(`### ${t.linkedMaps}`, "", ...linked.map((slug) => `- \`cmap:${inlineText(slug)}\``), ""); } } if (includeWikiPages) { const pages = []; for (const [reference, promise] of pageCache) { if (explanationReferences.has(reference)) continue; pages.push(await promise); } if (pages.length) lines.push(`## ${t.linkedPages}`, ""); for (const page of pages) { lines.push(`### ${headingText(page.title || page.slug)}`, "", `- **${t.address}:** \`${inlineText(page.slug)}\``); if (Array.isArray(page.tags) && page.tags.length) { lines.push(`- **${t.tags}:** ${page.tags.map((tag) => `\`${inlineText(tag)}\``).join(", ")}`); } lines.push(""); if (page.loadError) lines.push(`_${t.missingPage}: ${inlineText(page.loadError.message || page.loadError)}_`, ""); else lines.push(shiftHeadings(page.markdown || "", 2).trim() || t.noSummary, ""); } } return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trim()}\n`; } return { generateMarkdown, decodedDocument, derivedDocument, relationLines }; });