arcshape 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +91 -2
- package/bin/arcshape.mjs +93 -0
- package/bin/confluence-pages.mjs +569 -0
- package/bin/confluence.mjs +263 -0
- package/bin/init.mjs +160 -0
- package/bin/product-citations.mjs +104 -0
- package/bin/session.mjs +753 -0
- package/bin/site.mjs +59 -0
- package/bin/sync.mjs +352 -0
- package/bin/validate-links.mjs +98 -0
- package/bin/validate.mjs +126 -0
- package/package.json +62 -4
- package/templates/architecture/01-introduction-and-goals.md +13 -0
- package/templates/architecture/02-constraints.md +13 -0
- package/templates/architecture/03-context-and-scope.md +11 -0
- package/templates/architecture/04-solution-strategy.md +13 -0
- package/templates/architecture/05-building-block-view.md +11 -0
- package/templates/architecture/06-runtime-view.md +9 -0
- package/templates/architecture/07-deployment-view.md +11 -0
- package/templates/architecture/08-crosscutting-concepts.md +13 -0
- package/templates/architecture/09-architecture-decisions.md +9 -0
- package/templates/architecture/10-quality-requirements.md +11 -0
- package/templates/architecture/11-risks-and-technical-debt.md +11 -0
- package/templates/architecture/12-glossary.md +10 -0
- package/templates/architecture/README.md +20 -0
- package/templates/assistants/.agents/skills/arcshape-adr/SKILL.md +16 -0
- package/templates/assistants/.agents/skills/arcshape-architecture/SKILL.md +35 -0
- package/templates/assistants/.agents/skills/arcshape-c4/SKILL.md +13 -0
- package/templates/assistants/.agents/skills/arcshape-docs-sync/SKILL.md +12 -0
- package/templates/assistants/.claude/commands/arcshape/adr.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/c4.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/document.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/reverse-engineer.md +5 -0
- package/templates/assistants/.claude/commands/arcshape/sync.md +5 -0
- package/templates/assistants/.claude/skills/arcshape-adr/SKILL.md +16 -0
- package/templates/assistants/.claude/skills/arcshape-architecture/SKILL.md +35 -0
- package/templates/assistants/.claude/skills/arcshape-c4/SKILL.md +13 -0
- package/templates/assistants/.claude/skills/arcshape-docs-sync/SKILL.md +12 -0
- package/tools/arc42-site/LICENSE +21 -0
- package/tools/arc42-site/NOTICE.md +3 -0
- package/tools/arc42-site/README.md +3 -0
- package/tools/arc42-site/browser-options.mjs +14 -0
- package/tools/arc42-site/capture-evidence.mjs +84 -0
- package/tools/arc42-site/generate.mjs +227 -0
- package/tools/arc42-site/loadDocs.mjs +196 -0
- package/tools/arc42-site/render.mjs +411 -0
- package/tools/arc42-site/shell.mjs +761 -0
- package/tools/arc42-site/validate-diagrams.mjs +27 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { mkdtemp, readFile, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join, posix } from "node:path";
|
|
4
|
+
import { relative, resolve, sep } from "node:path";
|
|
5
|
+
import { existsSync, statSync } from "node:fs";
|
|
6
|
+
import { Marked } from "marked";
|
|
7
|
+
import GithubSlugger from "github-slugger";
|
|
8
|
+
import createDOMPurify from "dompurify";
|
|
9
|
+
import { JSDOM } from "jsdom";
|
|
10
|
+
import { getPuppeteerLaunchOptions } from "./browser-options.mjs";
|
|
11
|
+
|
|
12
|
+
const window = new JSDOM("").window;
|
|
13
|
+
const purifier = createDOMPurify(window);
|
|
14
|
+
|
|
15
|
+
export const MERMAID_CONFIG = Object.freeze({
|
|
16
|
+
securityLevel: "strict",
|
|
17
|
+
htmlLabels: false,
|
|
18
|
+
theme: "base",
|
|
19
|
+
themeVariables: {
|
|
20
|
+
fontFamily: "system-ui, sans-serif",
|
|
21
|
+
fontSize: "16px",
|
|
22
|
+
primaryColor: "#e6f4ea",
|
|
23
|
+
primaryTextColor: "#163b25",
|
|
24
|
+
primaryBorderColor: "#1b5e36",
|
|
25
|
+
lineColor: "#334e5c",
|
|
26
|
+
secondaryColor: "#eaf2f8",
|
|
27
|
+
secondaryTextColor: "#123045",
|
|
28
|
+
secondaryBorderColor: "#245679",
|
|
29
|
+
tertiaryColor: "#fff4d6",
|
|
30
|
+
tertiaryTextColor: "#3c2900",
|
|
31
|
+
tertiaryBorderColor: "#805500",
|
|
32
|
+
background: "#ffffff",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function getDiagramAccessibleTitle(source) {
|
|
37
|
+
return /^\s*accTitle:\s*(.+?)\s*$/im.exec(source)?.[1]?.trim() ?? "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function stripDiagramAccessibilityTitle(source) {
|
|
41
|
+
return String(source).replace(/^\s*accTitle:\s*.+?\s*\r?\n/im, "");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateDiagramTitles(sources) {
|
|
45
|
+
return sources.flatMap((source, index) =>
|
|
46
|
+
getDiagramAccessibleTitle(source)
|
|
47
|
+
? []
|
|
48
|
+
: [`Diagram ${index + 1} is missing an accTitle accessibility name`],
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Escape text for safe interpolation into HTML. */
|
|
53
|
+
export function escapeHtml(text) {
|
|
54
|
+
return String(text)
|
|
55
|
+
.replace(/&/g, "&")
|
|
56
|
+
.replace(/</g, "<")
|
|
57
|
+
.replace(/>/g, ">")
|
|
58
|
+
.replace(/"/g, """)
|
|
59
|
+
.replace(/'/g, "'");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeMermaidSvg(svg, index) {
|
|
63
|
+
const clean = purifier.sanitize(svg, {
|
|
64
|
+
USE_PROFILES: { html: true, svg: true, svgFilters: true },
|
|
65
|
+
ADD_TAGS: ["foreignObject"],
|
|
66
|
+
FORBID_TAGS: [
|
|
67
|
+
"script",
|
|
68
|
+
"iframe",
|
|
69
|
+
"object",
|
|
70
|
+
"embed",
|
|
71
|
+
"image",
|
|
72
|
+
"img",
|
|
73
|
+
"audio",
|
|
74
|
+
"video",
|
|
75
|
+
"link",
|
|
76
|
+
"use",
|
|
77
|
+
"animate",
|
|
78
|
+
"set",
|
|
79
|
+
"animateMotion",
|
|
80
|
+
"animateTransform",
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
const document = new window.DOMParser().parseFromString(
|
|
84
|
+
clean,
|
|
85
|
+
"image/svg+xml",
|
|
86
|
+
);
|
|
87
|
+
const root = document.documentElement;
|
|
88
|
+
if (root.localName !== "svg" || document.querySelector("parsererror")) {
|
|
89
|
+
throw new Error("Mermaid produced invalid SVG");
|
|
90
|
+
}
|
|
91
|
+
const rootId = `mmd-${index}`;
|
|
92
|
+
const idMap = new Map();
|
|
93
|
+
for (const element of [root, ...root.querySelectorAll("[id]")]) {
|
|
94
|
+
const oldId = element.getAttribute("id");
|
|
95
|
+
const newId = element === root ? rootId : `${rootId}-x${idMap.size}`;
|
|
96
|
+
if (oldId) idMap.set(oldId, newId);
|
|
97
|
+
element.setAttribute("id", newId);
|
|
98
|
+
}
|
|
99
|
+
const rewriteReferences = (value) =>
|
|
100
|
+
value.replace(/url\(\s*["']?#([^\s)"']+)["']?\s*\)/g, (match, id) =>
|
|
101
|
+
idMap.has(id) ? `url(#${idMap.get(id)})` : match,
|
|
102
|
+
);
|
|
103
|
+
const unsafeCss = (value) =>
|
|
104
|
+
/@import|(?:https?:|data:|file:|\/\/)|url\(\s*["']?(?!#)/i.test(value);
|
|
105
|
+
for (const element of [root, ...root.querySelectorAll("*")]) {
|
|
106
|
+
for (const attribute of [...element.attributes]) {
|
|
107
|
+
const { name, value } = attribute;
|
|
108
|
+
if (/^(?:href|xlink:href|src)$/i.test(name)) {
|
|
109
|
+
if (value.startsWith("#") && idMap.has(value.slice(1))) {
|
|
110
|
+
element.setAttribute(name, `#${idMap.get(value.slice(1))}`);
|
|
111
|
+
} else {
|
|
112
|
+
element.removeAttribute(name);
|
|
113
|
+
}
|
|
114
|
+
} else if (name === "aria-labelledby" || name === "aria-describedby") {
|
|
115
|
+
element.setAttribute(
|
|
116
|
+
name,
|
|
117
|
+
value
|
|
118
|
+
.split(/\s+/)
|
|
119
|
+
.map((id) => idMap.get(id) ?? id)
|
|
120
|
+
.join(" "),
|
|
121
|
+
);
|
|
122
|
+
} else if (unsafeCss(value)) {
|
|
123
|
+
element.removeAttribute(name);
|
|
124
|
+
} else {
|
|
125
|
+
element.setAttribute(name, rewriteReferences(value));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const style of root.querySelectorAll("style")) {
|
|
130
|
+
if (unsafeCss(style.textContent)) {
|
|
131
|
+
style.remove();
|
|
132
|
+
} else {
|
|
133
|
+
style.textContent = rewriteReferences(style.textContent).replace(
|
|
134
|
+
/#([a-zA-Z_][\w:.-]*)/g,
|
|
135
|
+
(match, id) => (idMap.has(id) ? `#${idMap.get(id)}` : match),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const width = /max-width:\s*([\d.]+)px/.exec(
|
|
140
|
+
root.getAttribute("style") ?? "",
|
|
141
|
+
);
|
|
142
|
+
root.removeAttribute("style");
|
|
143
|
+
if (width)
|
|
144
|
+
root.setAttribute("style", `max-width: ${Math.round(Number(width[1]))}px`);
|
|
145
|
+
return new window.XMLSerializer().serializeToString(root);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Pre-render mermaid diagram sources to normalized inline SVGs.
|
|
150
|
+
* @param {string[]} sources mermaid diagram sources, in document order
|
|
151
|
+
* @returns {Promise<string[]>} normalized SVG strings
|
|
152
|
+
*/
|
|
153
|
+
export async function renderMermaid(sources) {
|
|
154
|
+
if (sources.length === 0) return [];
|
|
155
|
+
const titleIssues = validateDiagramTitles(sources);
|
|
156
|
+
if (titleIssues.length > 0) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Mermaid accessibility contract violations:\n - ${titleIssues.join("\n - ")}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const { run } = await import("@mermaid-js/mermaid-cli");
|
|
162
|
+
const staging = await mkdtemp(join(tmpdir(), "arc42-mermaid-"));
|
|
163
|
+
try {
|
|
164
|
+
const svgFiles = [];
|
|
165
|
+
for (let i = 0; i < sources.length; i += 1) {
|
|
166
|
+
const inFile = join(staging, `d${i}.mmd`);
|
|
167
|
+
const outFile = join(staging, `d${i}.svg`);
|
|
168
|
+
await writeFile(
|
|
169
|
+
inFile,
|
|
170
|
+
stripDiagramAccessibilityTitle(sources[i]),
|
|
171
|
+
"utf8",
|
|
172
|
+
);
|
|
173
|
+
await run(inFile, outFile, {
|
|
174
|
+
quiet: true,
|
|
175
|
+
outputFormat: "svg",
|
|
176
|
+
puppeteerConfig: getPuppeteerLaunchOptions(),
|
|
177
|
+
parseMMDOptions: {
|
|
178
|
+
backgroundColor: "transparent",
|
|
179
|
+
mermaidConfig: MERMAID_CONFIG,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const raw = await readFile(outFile, "utf8");
|
|
183
|
+
svgFiles.push(normalizeMermaidSvg(raw, i));
|
|
184
|
+
}
|
|
185
|
+
return svgFiles;
|
|
186
|
+
} finally {
|
|
187
|
+
await rm(staging, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isMermaid(lang) {
|
|
192
|
+
return (
|
|
193
|
+
String(lang ?? "")
|
|
194
|
+
.trim()
|
|
195
|
+
.split(/\s+/)[0] === "mermaid"
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function normalizedHeading(text) {
|
|
200
|
+
return String(text ?? "")
|
|
201
|
+
.replace(/^\s*\d{1,2}[.)]\s*/, "")
|
|
202
|
+
.replace(/\s+/g, " ")
|
|
203
|
+
.trim()
|
|
204
|
+
.toLocaleLowerCase();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Remove a leading H1 that repeats the title already rendered from frontmatter. */
|
|
208
|
+
export function stripDuplicateTitle(body, title) {
|
|
209
|
+
if (!title) return body;
|
|
210
|
+
const marked = new Marked({ gfm: true });
|
|
211
|
+
const tokens = marked.lexer(body);
|
|
212
|
+
let offset = 0;
|
|
213
|
+
for (const token of tokens) {
|
|
214
|
+
if (
|
|
215
|
+
token.type === "space" ||
|
|
216
|
+
(token.type === "html" && /^\s*<!--[\s\S]*-->\s*$/.test(token.text))
|
|
217
|
+
) {
|
|
218
|
+
offset += token.raw.length;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (
|
|
222
|
+
token.type === "heading" &&
|
|
223
|
+
token.depth === 1 &&
|
|
224
|
+
normalizedHeading(token.text) === normalizedHeading(title)
|
|
225
|
+
) {
|
|
226
|
+
return body.slice(0, offset) + body.slice(offset + token.raw.length);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
return body;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function createMarked(
|
|
234
|
+
fileToRoute,
|
|
235
|
+
svgs,
|
|
236
|
+
diagramTitles,
|
|
237
|
+
currentRoute,
|
|
238
|
+
sourceContext,
|
|
239
|
+
sourceFile,
|
|
240
|
+
) {
|
|
241
|
+
const marked = new Marked({ gfm: true });
|
|
242
|
+
const slugger = new GithubSlugger();
|
|
243
|
+
let mermaidCount = 0;
|
|
244
|
+
|
|
245
|
+
marked.use({
|
|
246
|
+
renderer: {
|
|
247
|
+
html({ text }) {
|
|
248
|
+
// Markdownlint directives and other HTML comments are source metadata, not page content.
|
|
249
|
+
if (/^\s*<!--[\s\S]*-->\s*$/.test(text)) return "";
|
|
250
|
+
return escapeHtml(text);
|
|
251
|
+
},
|
|
252
|
+
heading({ tokens, depth }) {
|
|
253
|
+
const text = this.parser.parseInline(tokens);
|
|
254
|
+
const plain = window.document.createElement("div");
|
|
255
|
+
plain.innerHTML = text;
|
|
256
|
+
const id = slugger.slug(plain.textContent);
|
|
257
|
+
return `<h${depth} id="${escapeHtml(id)}">${text}</h${depth}>\n`;
|
|
258
|
+
},
|
|
259
|
+
code({ text, lang }) {
|
|
260
|
+
if (isMermaid(lang)) {
|
|
261
|
+
const index = mermaidCount++;
|
|
262
|
+
const svg = svgs[index];
|
|
263
|
+
if (!svg)
|
|
264
|
+
throw new Error(
|
|
265
|
+
"Mermaid diagram count does not match parsed Markdown",
|
|
266
|
+
);
|
|
267
|
+
const title = diagramTitles[index];
|
|
268
|
+
if (!title)
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Mermaid diagram ${index + 1} is missing an accTitle accessibility name`,
|
|
271
|
+
);
|
|
272
|
+
return `<figure class="diagram" role="img" aria-label="${escapeHtml(title)}">${svg}</figure>`;
|
|
273
|
+
}
|
|
274
|
+
const escaped = escapeHtml(text);
|
|
275
|
+
return `<pre><code${lang ? ` class="language-${escapeHtml(lang)}"` : ""}>${escaped}</code></pre>\n`;
|
|
276
|
+
},
|
|
277
|
+
link({ href, title, tokens }) {
|
|
278
|
+
const text = this.parser.parseInline(tokens);
|
|
279
|
+
const target = String(href ?? "");
|
|
280
|
+
const titleAttribute = title ? ` title="${escapeHtml(title)}"` : "";
|
|
281
|
+
if (/^(?:https?:|mailto:)/i.test(target)) {
|
|
282
|
+
return `<a href="${escapeHtml(target)}"${titleAttribute} rel="noopener noreferrer">${text}</a>`;
|
|
283
|
+
}
|
|
284
|
+
const hashIndex = target.indexOf("#");
|
|
285
|
+
let path = hashIndex < 0 ? target : target.slice(0, hashIndex);
|
|
286
|
+
let anchor = hashIndex < 0 ? "" : target.slice(hashIndex + 1);
|
|
287
|
+
try {
|
|
288
|
+
path = decodeURIComponent(path);
|
|
289
|
+
anchor = decodeURIComponent(anchor);
|
|
290
|
+
} catch {
|
|
291
|
+
return `<span class="xref-dead">${text} <code>${escapeHtml(target)}</code></span>`;
|
|
292
|
+
}
|
|
293
|
+
const route = path
|
|
294
|
+
? fileToRoute.get(posix.normalize(path))
|
|
295
|
+
: target.startsWith("#")
|
|
296
|
+
? currentRoute
|
|
297
|
+
: undefined;
|
|
298
|
+
if (route) {
|
|
299
|
+
const hash = anchor
|
|
300
|
+
? `${route.replace(/\/$/, "")}/${encodeURIComponent(anchor)}`
|
|
301
|
+
: route;
|
|
302
|
+
return `<a href="${escapeHtml(hash)}"${titleAttribute}>${text}</a>`;
|
|
303
|
+
}
|
|
304
|
+
const sourceLink = githubSourceLink(
|
|
305
|
+
sourceContext,
|
|
306
|
+
sourceFile,
|
|
307
|
+
path,
|
|
308
|
+
anchor,
|
|
309
|
+
);
|
|
310
|
+
if (sourceLink)
|
|
311
|
+
return `<a href="${escapeHtml(sourceLink)}"${titleAttribute} rel="noopener noreferrer">${text}</a>`;
|
|
312
|
+
return `<span class="xref-dead">${text} <code>${escapeHtml(target)}</code></span>`;
|
|
313
|
+
},
|
|
314
|
+
image({ href, title, text }) {
|
|
315
|
+
// No images are carried in the single file; name them honestly.
|
|
316
|
+
return `<span class="xref-dead">[image: ${escapeHtml(String(text ?? ""))}] <code>${escapeHtml(String(href ?? ""))}</code></span>`;
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return { marked, count: () => mermaidCount };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function githubSourceLink(sourceContext, sourceFile, path, anchor) {
|
|
325
|
+
if (!sourceContext || !sourceFile || !path) return undefined;
|
|
326
|
+
const sourcePath = posix.normalize(
|
|
327
|
+
posix.join(posix.dirname(sourceFile), path),
|
|
328
|
+
);
|
|
329
|
+
if (
|
|
330
|
+
sourcePath === ".." ||
|
|
331
|
+
sourcePath.startsWith("../") ||
|
|
332
|
+
posix.isAbsolute(sourcePath)
|
|
333
|
+
)
|
|
334
|
+
return undefined;
|
|
335
|
+
const absolutePath = resolve(sourceContext.root, ...sourcePath.split("/"));
|
|
336
|
+
const relativePath = relative(sourceContext.root, absolutePath);
|
|
337
|
+
if (
|
|
338
|
+
relativePath === ".." ||
|
|
339
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
340
|
+
!existsSync(absolutePath)
|
|
341
|
+
)
|
|
342
|
+
return undefined;
|
|
343
|
+
const kind = statSync(absolutePath).isDirectory() ? "tree" : "blob";
|
|
344
|
+
const encodedRef = encodeURIComponent(sourceContext.ref);
|
|
345
|
+
const encodedPath = sourcePath
|
|
346
|
+
.split("/")
|
|
347
|
+
.map((segment) => encodeURIComponent(segment))
|
|
348
|
+
.join("/");
|
|
349
|
+
const fragment = anchor ? `#${encodeURIComponent(anchor)}` : "";
|
|
350
|
+
return `${sourceContext.repositoryUrl}/${kind}/${encodedRef}/${encodedPath}${fragment}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Render one Markdown document to HTML.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} body markdown source (frontmatter already stripped)
|
|
357
|
+
* @param {object} opts
|
|
358
|
+
* @param {string} opts.title frontmatter title, used to suppress a repeated leading H1
|
|
359
|
+
* @param {Map<string,string>} opts.fileToRoute section file name -> hash route
|
|
360
|
+
* @param {string[]} opts.svgs pre-rendered mermaid SVGs for this document
|
|
361
|
+
* @param {string} opts.svgClass css class for inline svg wrappers
|
|
362
|
+
*/
|
|
363
|
+
export async function renderMarkdown(
|
|
364
|
+
body,
|
|
365
|
+
{
|
|
366
|
+
fileToRoute,
|
|
367
|
+
svgs = [],
|
|
368
|
+
currentRoute = "#/",
|
|
369
|
+
title = "",
|
|
370
|
+
sourceContext,
|
|
371
|
+
sourceFile,
|
|
372
|
+
},
|
|
373
|
+
) {
|
|
374
|
+
const diagramSources = extractMermaidSources(body);
|
|
375
|
+
const titleIssues = validateDiagramTitles(diagramSources);
|
|
376
|
+
if (titleIssues.length > 0) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Mermaid accessibility contract violations:\n - ${titleIssues.join("\n - ")}`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const diagramTitles = diagramSources.map(getDiagramAccessibleTitle);
|
|
382
|
+
const renderer = createMarked(
|
|
383
|
+
fileToRoute,
|
|
384
|
+
svgs,
|
|
385
|
+
diagramTitles,
|
|
386
|
+
currentRoute,
|
|
387
|
+
sourceContext,
|
|
388
|
+
sourceFile,
|
|
389
|
+
);
|
|
390
|
+
const html = await renderer.marked.parse(stripDuplicateTitle(body, title));
|
|
391
|
+
if (renderer.count() !== svgs.length)
|
|
392
|
+
throw new Error(
|
|
393
|
+
"Unused Mermaid SVGs: Markdown extraction and rendering disagree",
|
|
394
|
+
);
|
|
395
|
+
return { html, mermaidCount: renderer.count() };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Extract mermaid sources and a body with placeholders removed is not needed:
|
|
400
|
+
* instead we count diagrams by scanning fenced blocks before rendering, so the
|
|
401
|
+
* SVGs can be pre-rendered in one batch per document.
|
|
402
|
+
*/
|
|
403
|
+
export function extractMermaidSources(body) {
|
|
404
|
+
const marked = new Marked({ gfm: true });
|
|
405
|
+
const sources = [];
|
|
406
|
+
marked.walkTokens(marked.lexer(body), (token) => {
|
|
407
|
+
if (token.type === "code" && isMermaid(token.lang))
|
|
408
|
+
sources.push(token.text);
|
|
409
|
+
});
|
|
410
|
+
return sources;
|
|
411
|
+
}
|