added mermaid and a lot of cmap changes
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"use strict";
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {
|
||||
buildBundle, validateBundle, preparedMapDocument, attachmentUrls, replaceAttachmentUrls
|
||||
} = require("../static/js/cmap-interchange.js");
|
||||
|
||||
const conceptId = "24d27086-9f8b-4b57-a7fb-47b513277555";
|
||||
const childId = "53ef1cf3-c936-48f7-889a-ad644fab9321";
|
||||
const styledItem = {
|
||||
id: 10, conceptId, kind: "concept", x: 127.5, y: 88, width: 312,
|
||||
height: 97, backgroundColor: "#ffe5a8", borderColor: "#a97c00",
|
||||
textColor: "#172033", fontFamily: "Georgia", fontSize: 13.5,
|
||||
fontWeight: "700", synopsisFontSize: 9.5
|
||||
};
|
||||
const root = {
|
||||
slug: "architecture",
|
||||
title: "Architecture",
|
||||
document: {
|
||||
schemaVersion: 2,
|
||||
metadata: { explanationPageSlug: "cmap:architecture" },
|
||||
concepts: [{
|
||||
id: conceptId, label: "Process", synopsis: "Shared content",
|
||||
aspects: ["TODO"], pageSlug: "process:details", cmapSlug: "child",
|
||||
externalUrl: "https://example.com/process"
|
||||
}],
|
||||
items: [styledItem, { id: 11, kind: "phrase", label: "contains", x: 500, y: 90 }],
|
||||
connectors: [{ id: 1, sourceId: 10, targetId: 11, hasArrow: true }]
|
||||
}
|
||||
};
|
||||
const child = {
|
||||
slug: "child", title: "Child", document: {
|
||||
schemaVersion: 2,
|
||||
concepts: [{ id: childId, label: "Child concept" }],
|
||||
items: [{ id: 1, conceptId: childId, kind: "concept", x: 20, y: 30 }],
|
||||
connectors: []
|
||||
}
|
||||
};
|
||||
const pages = new Map([
|
||||
["cmap:architecture", { slug: "cmap:architecture", title: "Explanation", markdown: "# Explanation", tags: ["cmap"] }],
|
||||
["process:details", {
|
||||
slug: "process:details", title: "Details",
|
||||
markdown: "Full page\n\n", tags: []
|
||||
}]
|
||||
]);
|
||||
const exportedAttachment = {
|
||||
mimeType: "image/png",
|
||||
contentBase64: "aW1hZ2U="
|
||||
};
|
||||
const loadAttachment = async (url) => {
|
||||
assert.equal(url, "/uploads/process:details/1700000-diagram.png");
|
||||
return exportedAttachment;
|
||||
};
|
||||
|
||||
test("bundle separates shared content while preserving complete diagram layout", async () => {
|
||||
const bundle = await buildBundle({
|
||||
rootMap: root,
|
||||
maxDepth: 1,
|
||||
exportedAt: "2026-08-25T12:00:00.000Z",
|
||||
generator: "test",
|
||||
loadConceptMap: async (slug) => ({ child }[slug]),
|
||||
loadWikiPage: async (reference) => pages.get(reference),
|
||||
loadAttachment
|
||||
});
|
||||
|
||||
assert.equal(bundle.format, "racket-wiki-cmap-bundle");
|
||||
assert.deepEqual(bundle.cmaps.map((cmap) => cmap.slug), ["architecture", "child"]);
|
||||
assert.deepEqual(bundle.pages.map((page) => page.reference), ["cmap:architecture", "process:details"]);
|
||||
assert.equal(bundle.concepts.find((concept) => concept.id === conceptId).synopsis, "Shared content");
|
||||
assert.equal(bundle.concepts.find((concept) => concept.id === conceptId).externalUrl,
|
||||
"https://example.com/process");
|
||||
const exportedItem = bundle.cmaps[0].document.items[0];
|
||||
assert.equal(exportedItem.x, 127.5);
|
||||
assert.equal(exportedItem.fontSize, 13.5);
|
||||
assert.equal(exportedItem.backgroundColor, "#ffe5a8");
|
||||
assert.equal(exportedItem.synopsis, undefined, "shared content is not duplicated on a placement");
|
||||
assert.deepEqual(bundle.cmaps[0].document.connectors, root.document.connectors);
|
||||
assert.deepEqual(bundle.pages.find((page) => page.reference === "process:details").attachments, [{
|
||||
url: "/uploads/process:details/1700000-diagram.png",
|
||||
name: "1700000-diagram.png",
|
||||
mimeType: "image/png",
|
||||
contentBase64: "aW1hZ2U="
|
||||
}]);
|
||||
});
|
||||
|
||||
test("prepared import document rejoins concepts and placements without changing layout", async () => {
|
||||
const bundle = await buildBundle({
|
||||
rootMap: root, maxDepth: 0,
|
||||
loadConceptMap: async () => { throw new Error("not loaded"); },
|
||||
loadWikiPage: async (reference) => pages.get(reference),
|
||||
loadAttachment
|
||||
});
|
||||
const documentValue = preparedMapDocument(bundle, bundle.cmaps[0]);
|
||||
assert.equal(documentValue.concepts[0].synopsis, "Shared content");
|
||||
assert.equal(documentValue.concepts[0].externalUrl, "https://example.com/process");
|
||||
assert.equal(documentValue.items[0].width, 312);
|
||||
assert.equal(documentValue.items[0].fontFamily, "Georgia");
|
||||
assert.deepEqual(documentValue.connectors, root.document.connectors);
|
||||
});
|
||||
|
||||
test("a derived CMap always carries its source map as a layout dependency", async () => {
|
||||
const derived = {
|
||||
slug: "derived", title: "Derived", document: {
|
||||
schemaVersion: 2,
|
||||
derivedView: { sourceCmapSlug: "architecture", rootItemId: 10 },
|
||||
concepts: [], items: [], connectors: []
|
||||
}
|
||||
};
|
||||
const bundle = await buildBundle({
|
||||
rootMap: derived, maxDepth: 0,
|
||||
loadConceptMap: async (slug) => {
|
||||
assert.equal(slug, "architecture");
|
||||
return root;
|
||||
},
|
||||
loadWikiPage: async (reference) => pages.get(reference),
|
||||
loadAttachment
|
||||
});
|
||||
assert.deepEqual(bundle.cmaps.map((cmap) => cmap.slug), ["derived", "architecture"]);
|
||||
assert.equal(bundle.rootCmapSlug, "derived");
|
||||
assert.equal(bundle.cmaps[1].document.items[0].x, 127.5);
|
||||
});
|
||||
|
||||
test("temporary ids are accepted only when their concepts are placed", () => {
|
||||
const bundle = {
|
||||
format: "racket-wiki-cmap-bundle", formatVersion: 1,
|
||||
rootCmapSlug: "generated", pages: [], missing: { cmaps: [], pages: [] },
|
||||
concepts: [{ id: "new:generated-concept", label: "Generated concept" }],
|
||||
cmaps: [{ slug: "generated", title: "Generated", document: {
|
||||
concepts: [{ id: "new:generated-concept" }],
|
||||
items: [{ id: 1, conceptId: "new:generated-concept", kind: "concept", x: 10, y: 20 }],
|
||||
connectors: []
|
||||
}}]
|
||||
};
|
||||
assert.equal(validateBundle(bundle), bundle);
|
||||
const orphan = structuredClone(bundle);
|
||||
orphan.cmaps[0].document.items = [];
|
||||
assert.throws(() => validateBundle(orphan), /must occur as a diagram placement/);
|
||||
});
|
||||
|
||||
test("invalid connector endpoints and duplicate slugs are rejected", () => {
|
||||
const bundle = {
|
||||
format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map",
|
||||
concepts: [{ id: conceptId, label: "Concept" }], pages: [],
|
||||
cmaps: [
|
||||
{ slug: "map", title: "Map", document: { concepts: [{ id: conceptId }], items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: [{ sourceId: 1, targetId: 99 }] } },
|
||||
{ slug: "map", title: "Duplicate", document: { concepts: [], items: [], connectors: [] } }
|
||||
]
|
||||
};
|
||||
assert.throws(() => validateBundle(bundle), /unknown item[\s\S]*duplicated/);
|
||||
});
|
||||
|
||||
test("external concept links only accept complete http or https URLs", () => {
|
||||
const bundle = {
|
||||
format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map",
|
||||
concepts: [{ id: conceptId, label: "Concept", externalUrl: "javascript:alert(1)" }],
|
||||
pages: [],
|
||||
cmaps: [{ slug: "map", title: "Map", document: {
|
||||
concepts: [{ id: conceptId }],
|
||||
items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: []
|
||||
}}]
|
||||
};
|
||||
assert.throws(() => validateBundle(bundle), /externalUrl: must be a complete http or https URL/);
|
||||
bundle.concepts[0].externalUrl = "https://example.com/path";
|
||||
assert.equal(validateBundle(bundle), bundle);
|
||||
});
|
||||
|
||||
test("attachment URLs are detected once and can be rewritten after upload", () => {
|
||||
const oldUrl = "/uploads/process:details/1700000-diagram.png";
|
||||
const markdown = `\n<img src="${oldUrl}">`;
|
||||
assert.deepEqual(attachmentUrls(markdown), [oldUrl]);
|
||||
assert.equal(
|
||||
replaceAttachmentUrls(markdown, new Map([[oldUrl, "/uploads/process:details/new-diagram.png"]])),
|
||||
"\n" +
|
||||
"<img src=\"/uploads/process:details/new-diagram.png\">"
|
||||
);
|
||||
assert.deepEqual(
|
||||
attachmentUrls(""),
|
||||
["/uploads/page/1700000-system overview.png"]
|
||||
);
|
||||
});
|
||||
|
||||
test("invalid embedded attachment content is rejected", () => {
|
||||
const bundle = {
|
||||
format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map",
|
||||
concepts: [{ id: conceptId, label: "Concept" }],
|
||||
cmaps: [{ slug: "map", title: "Map", document: {
|
||||
concepts: [{ id: conceptId }],
|
||||
items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: []
|
||||
}}],
|
||||
pages: [{
|
||||
reference: "page", title: "Page", markdown: "", tags: [],
|
||||
attachments: [{
|
||||
url: "/uploads/page/file.png", name: "file.png",
|
||||
mimeType: "image/png", contentBase64: "not base64!"
|
||||
}]
|
||||
}]
|
||||
};
|
||||
assert.throws(() => validateBundle(bundle), /contentBase64: must be valid base64/);
|
||||
});
|
||||
Reference in New Issue
Block a user