@aruzone/aze-forge 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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +408 -0
  3. package/dist/acceptance.d.ts +48 -0
  4. package/dist/acceptance.js +180 -0
  5. package/dist/acceptance.js.map +1 -0
  6. package/dist/assets.d.ts +19 -0
  7. package/dist/assets.js +509 -0
  8. package/dist/assets.js.map +1 -0
  9. package/dist/atomic-write.d.ts +1 -0
  10. package/dist/atomic-write.js +23 -0
  11. package/dist/atomic-write.js.map +1 -0
  12. package/dist/callout.d.ts +27 -0
  13. package/dist/callout.js +78 -0
  14. package/dist/callout.js.map +1 -0
  15. package/dist/capabilities-json.d.ts +4 -0
  16. package/dist/capabilities-json.js +225 -0
  17. package/dist/capabilities-json.js.map +1 -0
  18. package/dist/capabilities.d.ts +110 -0
  19. package/dist/capabilities.js +270 -0
  20. package/dist/capabilities.js.map +1 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +1096 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/compiler.d.ts +3 -0
  25. package/dist/compiler.js +1034 -0
  26. package/dist/compiler.js.map +1 -0
  27. package/dist/configuration-error.d.ts +4 -0
  28. package/dist/configuration-error.js +9 -0
  29. package/dist/configuration-error.js.map +1 -0
  30. package/dist/diagnostics-json.d.ts +23 -0
  31. package/dist/diagnostics-json.js +143 -0
  32. package/dist/diagnostics-json.js.map +1 -0
  33. package/dist/diagnostics.d.ts +16 -0
  34. package/dist/diagnostics.js +154 -0
  35. package/dist/diagnostics.js.map +1 -0
  36. package/dist/equation.d.ts +74 -0
  37. package/dist/equation.js +485 -0
  38. package/dist/equation.js.map +1 -0
  39. package/dist/font.d.ts +19 -0
  40. package/dist/font.js +75 -0
  41. package/dist/font.js.map +1 -0
  42. package/dist/format.d.ts +2 -0
  43. package/dist/format.js +308 -0
  44. package/dist/format.js.map +1 -0
  45. package/dist/hash.d.ts +5 -0
  46. package/dist/hash.js +189 -0
  47. package/dist/hash.js.map +1 -0
  48. package/dist/help.d.ts +9 -0
  49. package/dist/help.js +102 -0
  50. package/dist/help.js.map +1 -0
  51. package/dist/html-fragment.d.ts +7 -0
  52. package/dist/html-fragment.js +70 -0
  53. package/dist/html-fragment.js.map +1 -0
  54. package/dist/index.d.ts +39 -0
  55. package/dist/index.js +32 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/markdown.d.ts +14 -0
  58. package/dist/markdown.js +425 -0
  59. package/dist/markdown.js.map +1 -0
  60. package/dist/mermaid-browser.d.ts +43 -0
  61. package/dist/mermaid-browser.js +358 -0
  62. package/dist/mermaid-browser.js.map +1 -0
  63. package/dist/mermaid.d.ts +84 -0
  64. package/dist/mermaid.js +740 -0
  65. package/dist/mermaid.js.map +1 -0
  66. package/dist/model.d.ts +406 -0
  67. package/dist/model.js +2 -0
  68. package/dist/model.js.map +1 -0
  69. package/dist/parse.d.ts +3 -0
  70. package/dist/parse.js +1374 -0
  71. package/dist/parse.js.map +1 -0
  72. package/dist/preview-server.d.ts +39 -0
  73. package/dist/preview-server.js +156 -0
  74. package/dist/preview-server.js.map +1 -0
  75. package/dist/preview-shell.d.ts +29 -0
  76. package/dist/preview-shell.js +102 -0
  77. package/dist/preview-shell.js.map +1 -0
  78. package/dist/reference-validation.d.ts +7 -0
  79. package/dist/reference-validation.js +42 -0
  80. package/dist/reference-validation.js.map +1 -0
  81. package/dist/registry.d.ts +29 -0
  82. package/dist/registry.js +384 -0
  83. package/dist/registry.js.map +1 -0
  84. package/dist/render-html.d.ts +24 -0
  85. package/dist/render-html.js +149 -0
  86. package/dist/render-html.js.map +1 -0
  87. package/dist/render-pdf.d.ts +64 -0
  88. package/dist/render-pdf.js +503 -0
  89. package/dist/render-pdf.js.map +1 -0
  90. package/dist/render-png.d.ts +32 -0
  91. package/dist/render-png.js +427 -0
  92. package/dist/render-png.js.map +1 -0
  93. package/dist/render-svg.d.ts +23 -0
  94. package/dist/render-svg.js +243 -0
  95. package/dist/render-svg.js.map +1 -0
  96. package/dist/runtime-support.d.ts +26 -0
  97. package/dist/runtime-support.js +62 -0
  98. package/dist/runtime-support.js.map +1 -0
  99. package/dist/source-map.d.ts +12 -0
  100. package/dist/source-map.js +75 -0
  101. package/dist/source-map.js.map +1 -0
  102. package/dist/table.d.ts +22 -0
  103. package/dist/table.js +90 -0
  104. package/dist/table.js.map +1 -0
  105. package/dist/theme.d.ts +6 -0
  106. package/dist/theme.js +89 -0
  107. package/dist/theme.js.map +1 -0
  108. package/dist/tool-version.d.ts +3 -0
  109. package/dist/tool-version.js +3 -0
  110. package/dist/tool-version.js.map +1 -0
  111. package/dist/type-guards.d.ts +1 -0
  112. package/dist/type-guards.js +4 -0
  113. package/dist/type-guards.js.map +1 -0
  114. package/dist/validate-document.d.ts +2 -0
  115. package/dist/validate-document.js +248 -0
  116. package/dist/validate-document.js.map +1 -0
  117. package/dist/version.d.ts +37 -0
  118. package/dist/version.js +111 -0
  119. package/dist/version.js.map +1 -0
  120. package/dist/watch-dependencies.d.ts +15 -0
  121. package/dist/watch-dependencies.js +93 -0
  122. package/dist/watch-dependencies.js.map +1 -0
  123. package/dist/watch-events.d.ts +51 -0
  124. package/dist/watch-events.js +27 -0
  125. package/dist/watch-events.js.map +1 -0
  126. package/dist/watch-loop.d.ts +39 -0
  127. package/dist/watch-loop.js +117 -0
  128. package/dist/watch-loop.js.map +1 -0
  129. package/package.json +70 -0
  130. package/schemas/acceptance.json +104 -0
  131. package/schemas/capabilities.json +491 -0
  132. package/schemas/diagnostics.json +219 -0
  133. package/schemas/version.json +167 -0
@@ -0,0 +1,740 @@
1
+ import { arch, platform } from "node:os";
2
+ import { createDiagnostic } from "./diagnostics.js";
3
+ import { sha256 } from "./hash.js";
4
+ import { CHROME_HEADLESS_SHELL_VERSION, MermaidBrowserParseError, MermaidBrowserUnavailableError, renderMermaidInBrowser, } from "./mermaid-browser.js";
5
+ import { SaxesParser } from "saxes";
6
+ import { defaultTheme } from "./theme.js";
7
+ export const MERMAID_PLUGIN_TYPE = "mermaid";
8
+ export const MERMAID_PLUGIN_VERSION = "1.0.0";
9
+ export const MERMAID_BODY_SYNTAX_ID = "azeforge.mermaid/v1";
10
+ export const MERMAID_BODY_SYNTAX_VERSION = "1.0.0";
11
+ /** Pinned Mermaid version per the HTML-first output decision (#10). */
12
+ export const MERMAID_VERSION = "11.17.2";
13
+ export const MERMAID_HTML_BLOCK_RENDERER_ID = "azeforge.mermaid.html/v1";
14
+ export const MERMAID_HTML_BLOCK_RENDERER_VERSION = "1.0.0";
15
+ export const MAX_MERMAID_SOURCE_LENGTH = 8000;
16
+ export const MAX_MERMAID_TEXT_LENGTH = 8000;
17
+ export { CHROME_HEADLESS_SHELL_VERSION, MermaidBrowserParseError, MermaidBrowserUnavailableError, };
18
+ export const mermaidSourceSchema = Object.freeze({
19
+ $schema: "https://json-schema.org/draft/2020-12/schema",
20
+ $id: "azeforge.mermaid/source/v1",
21
+ type: "object",
22
+ additionalProperties: false,
23
+ properties: {
24
+ id: {
25
+ type: "string",
26
+ pattern: "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
27
+ },
28
+ title: { type: "string", minLength: 1, maxLength: 200 },
29
+ description: { type: "string", minLength: 1, maxLength: 1000 },
30
+ },
31
+ });
32
+ export const mermaidDataSchema = Object.freeze({
33
+ $schema: "https://json-schema.org/draft/2020-12/schema",
34
+ $id: "azeforge.mermaid/data/v1",
35
+ type: "object",
36
+ additionalProperties: false,
37
+ required: ["diagramType", "source", "pluginVersion"],
38
+ properties: {
39
+ kind: { const: "mermaid" },
40
+ diagramType: { type: "string", minLength: 1 },
41
+ source: { type: "string", minLength: 1, maxLength: 8000 },
42
+ pluginVersion: { const: "1.0.0" },
43
+ title: { type: "string", minLength: 1, maxLength: 200 },
44
+ description: { type: "string", minLength: 1, maxLength: 1000 },
45
+ },
46
+ });
47
+ export class MermaidSanitizerError extends Error {
48
+ reason;
49
+ constructor(reason) {
50
+ super(reason);
51
+ this.reason = reason;
52
+ }
53
+ }
54
+ function headerDiagnostic(code, message, range, sourceName, extra = {}) {
55
+ return createDiagnostic(code, "error", message, {
56
+ location: sourceName === undefined ? { range } : { source: sourceName, range },
57
+ ...(extra.suggestion === undefined
58
+ ? {}
59
+ : { suggestion: extra.suggestion }),
60
+ ...(extra.data === undefined ? {} : { data: extra.data }),
61
+ });
62
+ }
63
+ export function parseMermaidHeader(entries, _blockRange, sourceName) {
64
+ const diagnostics = [];
65
+ let id;
66
+ let title;
67
+ let description;
68
+ for (const entry of entries) {
69
+ if (entry.key === "id") {
70
+ if (entry.value.length === 0) {
71
+ diagnostics.push(headerDiagnostic("azeforge.mermaid#invalid-attribute", "Mermaid attribute id must not be empty.", entry.range, sourceName, { data: { attribute: "id", value: entry.value } }));
72
+ }
73
+ else {
74
+ id = entry.value;
75
+ }
76
+ continue;
77
+ }
78
+ if (entry.key === "title") {
79
+ if (entry.value.length === 0 || entry.value.length > 200) {
80
+ diagnostics.push(headerDiagnostic("azeforge.mermaid#invalid-attribute", "Mermaid attribute title must be 1..200 characters.", entry.range, sourceName, { data: { attribute: "title" } }));
81
+ }
82
+ else {
83
+ title = entry.value;
84
+ }
85
+ continue;
86
+ }
87
+ if (entry.key === "description") {
88
+ if (entry.value.length === 0 || entry.value.length > 1000) {
89
+ diagnostics.push(headerDiagnostic("azeforge.mermaid#invalid-attribute", "Mermaid attribute description must be 1..1000 characters.", entry.range, sourceName, { data: { attribute: "description" } }));
90
+ }
91
+ else {
92
+ description = entry.value;
93
+ }
94
+ continue;
95
+ }
96
+ diagnostics.push(headerDiagnostic("azeforge.mermaid#invalid-attribute", `Mermaid attribute "${entry.key}" is not a valid mermaid attribute.`, entry.range, sourceName, {
97
+ suggestion: "Valid mermaid attributes are id, title, and description.",
98
+ data: { attribute: entry.key },
99
+ }));
100
+ }
101
+ return {
102
+ ...(id === undefined ? {} : { id }),
103
+ ...(title === undefined ? {} : { title }),
104
+ ...(description === undefined ? {} : { description }),
105
+ diagnostics,
106
+ };
107
+ }
108
+ const SUPPORTED_DIAGRAMS = {
109
+ flowchart: true,
110
+ graph: true,
111
+ sequencediagram: true,
112
+ classdiagram: true,
113
+ statediagram: true,
114
+ "statediagram-v2": true,
115
+ erdiagram: true,
116
+ pie: true,
117
+ gantt: true,
118
+ journey: true,
119
+ mindmap: true,
120
+ };
121
+ function diagramKey(firstLine) {
122
+ const head = firstLine.trim().split(/[\s(:]+/, 2)[0]?.toLowerCase() ?? "";
123
+ if (head === "")
124
+ return undefined;
125
+ // "flowchart TD" -> flowchart; "stateDiagram-v2" normalizes below.
126
+ const normalized = head.replace(/_/g, "");
127
+ if (SUPPORTED_DIAGRAMS[normalized] === true)
128
+ return normalized;
129
+ if (normalized === "statediagram-v2" || normalized === "statediagramv2") {
130
+ return "statediagram-v2";
131
+ }
132
+ return undefined;
133
+ }
134
+ function canonicalDiagramType(key, firstLine) {
135
+ if (key === "flowchart" || key === "graph") {
136
+ const direction = firstLine.trim().split(/\s+/)[1]?.toUpperCase();
137
+ if (direction === "TD" ||
138
+ direction === "TB" ||
139
+ direction === "BT" ||
140
+ direction === "LR" ||
141
+ direction === "RL") {
142
+ return key === "graph" ? `graph ${direction}` : `flowchart ${direction}`;
143
+ }
144
+ return key;
145
+ }
146
+ if (key === "sequencediagram")
147
+ return "sequenceDiagram";
148
+ if (key === "classdiagram")
149
+ return "classDiagram";
150
+ if (key === "statediagram" || key === "statediagram-v2") {
151
+ return firstLine.trim().startsWith("stateDiagram-v2")
152
+ ? "stateDiagram-v2"
153
+ : "stateDiagram";
154
+ }
155
+ if (key === "erdiagram")
156
+ return "erDiagram";
157
+ return key;
158
+ }
159
+ const ACTIVE_CONTENT_PATTERNS = [
160
+ { pattern: /<\s*script[\s>]/i, kind: "script element" },
161
+ { pattern: /<\s*(iframe|object|embed|foreignobject|math)[\s>]/i, kind: "embedded element" },
162
+ { pattern: /\bon\w+\s*=/i, kind: "event handler" },
163
+ { pattern: /javascript\s*:/i, kind: "javascript URL" },
164
+ { pattern: /data\s*:\s*text\/html/i, kind: "data URL" },
165
+ { pattern: /<\s*style[\s>]/i, kind: "style element" },
166
+ { pattern: /url\s*\(\s*["']?(https?:|data:)/i, kind: "external style URL" },
167
+ ];
168
+ /**
169
+ * Synchronous Source preflight. Deep syntax is owned by pinned Mermaid in
170
+ * the isolated browser at compile time; this gate rejects empty,
171
+ * over-long, active-content, external-resource, and unknown-diagram input
172
+ * so obvious failures surface at parse with a stable diagnostic.
173
+ */
174
+ export function validateMermaidBody(options) {
175
+ const { header, body, bodyRanges, blockRange, sourceName } = options;
176
+ const location = (range) => sourceName === undefined ? { range } : { source: sourceName, range };
177
+ const trimmed = body.trim();
178
+ if (trimmed.length === 0) {
179
+ return {
180
+ diagnostics: [
181
+ createDiagnostic("azeforge.mermaid#empty", "error", "The mermaid Block must contain diagram Source.", {
182
+ location: location(blockRange),
183
+ suggestion: "Add a flowchart such as flowchart TD.",
184
+ }),
185
+ ],
186
+ };
187
+ }
188
+ if (trimmed.length > MAX_MERMAID_SOURCE_LENGTH) {
189
+ return {
190
+ diagnostics: [
191
+ createDiagnostic("azeforge.mermaid#invalid-syntax", "error", "The mermaid Block exceeds the maximum supported length.", {
192
+ location: location(bodyRanges[0] ?? blockRange),
193
+ data: { length: trimmed.length },
194
+ }),
195
+ ],
196
+ };
197
+ }
198
+ for (const { pattern, kind } of ACTIVE_CONTENT_PATTERNS) {
199
+ if (pattern.test(trimmed)) {
200
+ return {
201
+ diagnostics: [
202
+ createDiagnostic("azeforge.mermaid#active-content", "error", `The mermaid Block contains forbidden active content (${kind}).`, {
203
+ location: location(bodyRanges[0] ?? blockRange),
204
+ suggestion: "Remove executable markup; labels must be plain text.",
205
+ data: { kind },
206
+ }),
207
+ ],
208
+ };
209
+ }
210
+ }
211
+ if (/^\s*click\b/m.test(trimmed)) {
212
+ return {
213
+ diagnostics: [
214
+ createDiagnostic("azeforge.mermaid#external-resource", "error", "The mermaid Block links to an external resource.", {
215
+ location: location(bodyRanges[0] ?? blockRange),
216
+ suggestion: "Remove click directives; offline diagrams cannot dereference links.",
217
+ data: { construct: "click" },
218
+ }),
219
+ ],
220
+ };
221
+ }
222
+ const lines = trimmed.split("\n").filter((line) => line.trim().length > 0);
223
+ const firstContent = lines.find((line) => !line.trim().startsWith("%%")) ?? "";
224
+ const key = diagramKey(firstContent);
225
+ if (key === undefined) {
226
+ return {
227
+ diagnostics: [
228
+ createDiagnostic("azeforge.mermaid#unsupported-diagram", "error", "The mermaid Block names an unsupported diagram type.", {
229
+ location: location(bodyRanges[0] ?? blockRange),
230
+ suggestion: "Use flowchart, sequenceDiagram, classDiagram, stateDiagram, erDiagram, pie, gantt, journey, or mindmap.",
231
+ }),
232
+ ],
233
+ };
234
+ }
235
+ const diagramType = canonicalDiagramType(key, firstContent);
236
+ const block = {
237
+ kind: "mermaid",
238
+ range: blockRange,
239
+ ...(header.id === undefined ? {} : { id: header.id }),
240
+ pluginVersion: MERMAID_PLUGIN_VERSION,
241
+ diagramType,
242
+ source: trimmed,
243
+ ...(header.title === undefined ? {} : { title: header.title }),
244
+ ...(header.description === undefined
245
+ ? {}
246
+ : { description: header.description }),
247
+ };
248
+ return { block: Object.freeze(block), diagnostics: [] };
249
+ }
250
+ export function deriveMermaidSeed(options) {
251
+ return sha256(JSON.stringify({
252
+ source: options.source,
253
+ ordinal: options.ordinal,
254
+ pluginVersion: options.pluginVersion,
255
+ theme: `${options.theme.id}@${options.theme.version}`,
256
+ mermaid: options.mermaidVersion ?? MERMAID_VERSION,
257
+ })).slice("sha256:".length, "sha256:".length + 16);
258
+ }
259
+ function escapeXml(value) {
260
+ return value
261
+ .replace(/&/g, "&amp;")
262
+ .replace(/</g, "&lt;")
263
+ .replace(/>/g, "&gt;")
264
+ .replace(/"/g, "&quot;")
265
+ .replace(/'/g, "&#39;");
266
+ }
267
+ function defaultAccessibleName(block) {
268
+ const firstNode = [...block.source.split("\n")]
269
+ .map((line) => line.trim())
270
+ .find((line) => line !== "" &&
271
+ !line.startsWith("%%") &&
272
+ !/^(flowchart|graph|sequencediagram|classdiagram|statediagram|erdiagram|pie|gantt|journey|mindmap)\b/i.test(line));
273
+ if (firstNode !== undefined)
274
+ return firstNode.slice(0, 120);
275
+ return `${block.diagramType} diagram`;
276
+ }
277
+ /**
278
+ * Render pinned Mermaid in the isolated browser and return sanitized SVG.
279
+ * Every advertised diagram family executes through real Mermaid 11.17.2;
280
+ * deep syntax failures surface as MermaidBrowserParseError so the Compiler
281
+ * can report one scoped invalid-syntax diagnostic with no Artifact.
282
+ */
283
+ export async function renderMermaidSvg(block, options) {
284
+ const seed = deriveMermaidSeed({
285
+ source: block.source,
286
+ ordinal: options.ordinal,
287
+ pluginVersion: block.pluginVersion,
288
+ theme: options.theme,
289
+ });
290
+ const rendered = await renderMermaidInBrowser({
291
+ source: block.source,
292
+ elementId: `aze-m-${options.ordinal}-render`,
293
+ seed,
294
+ theme: options.theme,
295
+ });
296
+ const namespaced = namespaceMermaidIds(rendered.svg, options.ordinal);
297
+ const accessible = injectAccessibleName(namespaced, {
298
+ ordinal: options.ordinal,
299
+ title: block.title ?? defaultAccessibleName(block),
300
+ ...(block.description === undefined
301
+ ? {}
302
+ : { description: block.description }),
303
+ seed,
304
+ diagramType: block.diagramType,
305
+ });
306
+ return sanitizeMermaidSvg(accessible, { ordinal: options.ordinal });
307
+ }
308
+ /**
309
+ * Deterministically rewrite every surviving id into the
310
+ * aze-m-<ordinal> namespace and update local references. Mermaid can emit
311
+ * the same id more than once; each occurrence receives a unique name
312
+ * (`-r<k>` suffix from the second occurrence on) while references resolve
313
+ * to the first occurrence, matching document lookup behavior. Ordering is
314
+ * stable because Mermaid output is deterministic under a fixed seed.
315
+ */
316
+ function namespaceMermaidIds(svg, ordinal) {
317
+ const prefix = `aze-m-${ordinal}`;
318
+ const renderId = `${prefix}-render`;
319
+ let counter = 0;
320
+ const firstNew = new Map();
321
+ const occurrences = new Map();
322
+ // Render-root id, when referenced, resolves to the namespaced root.
323
+ firstNew.set(renderId, prefix);
324
+ const out = svg.replace(/ id="([^"]+)"/g, (whole, old) => {
325
+ if (old === "" || old === renderId) {
326
+ return old === "" ? whole : ` id="${prefix}"`;
327
+ }
328
+ const seen = (occurrences.get(old) ?? 0) + 1;
329
+ occurrences.set(old, seen);
330
+ if (seen === 1) {
331
+ counter += 1;
332
+ const next = `${prefix}-n-${counter}`;
333
+ firstNew.set(old, next);
334
+ return ` id="${next}"`;
335
+ }
336
+ return ` id="${firstNew.get(old) ?? `${prefix}-n-0`}-r${seen}"`;
337
+ });
338
+ const resolveReference = (old) => firstNew.get(old) ?? old;
339
+ // url() and aria references are live (marker and accessibility hooks);
340
+ // href/xlink:href are rejected unconditionally by the sanitizer, so no
341
+ // rewriting for them exists here by design.
342
+ const withReferences = out
343
+ .replaceAll(/url\(#([^)]+)\)/g, (_whole, old) => `url(#${resolveReference(old)})`)
344
+ .replaceAll(/aria-(labelledby|describedby)="([^"]*)"/g, (_whole, kind, refs) => {
345
+ const rewritten = refs
346
+ .split(/[\s,]+/)
347
+ .filter((ref) => ref !== "")
348
+ .map((ref) => resolveReference(ref))
349
+ .join(" ");
350
+ return `aria-${kind}="${rewritten}"`;
351
+ });
352
+ // Defense in depth: the browser normalizer already strips these, but
353
+ // raw Mermaid output must never contribute executable styling or
354
+ // unnamespaced hooks to the Artifact.
355
+ return withReferences
356
+ .replaceAll(/ class="[^"]*"/g, "")
357
+ .replaceAll(/ data-[a-zA-Z0-9_-]+="[^"]*"/g, "")
358
+ .replaceAll(/ name="[^"]*"/g, "");
359
+ }
360
+ function injectAccessibleName(svg, options) {
361
+ const prefix = `aze-m-${options.ordinal}`;
362
+ const openMatch = /^<svg([^>]*)>/.exec(svg);
363
+ if (openMatch === null)
364
+ throw new MermaidSanitizerError("SVG misses its root element.");
365
+ const rawAttrs = openMatch[1] ?? "";
366
+ const kept = rawAttrs
367
+ .replace(/ id="[^"]*"/g, "")
368
+ .replace(/ class="[^"]*"/g, "")
369
+ .replace(/ style="[^"]*"/g, "")
370
+ .replace(/ width="[^"]*"/g, "")
371
+ .replace(/ height="[^"]*"/g, "")
372
+ .replace(/ role="[^"]*"/g, "")
373
+ .replace(/ aria-[a-z-]*="[^"]*"/g, "");
374
+ const describedby = options.description === undefined
375
+ ? ""
376
+ : ` aria-describedby="${prefix}-desc"`;
377
+ const opening = `<svg${kept} id="${prefix}" role="img" aria-labelledby="${prefix}-title"${describedby}` +
378
+ ` data-seed="${options.seed}" data-diagram="${escapeXml(options.diagramType)}">`;
379
+ const title = `<title id="${prefix}-title">${escapeXml(options.title)}</title>`;
380
+ const desc = options.description === undefined
381
+ ? ""
382
+ : `<desc id="${prefix}-desc">${escapeXml(options.description)}</desc>`;
383
+ const rest = svg.slice(openMatch[0].length);
384
+ return `${opening}${title}${desc}${rest}`;
385
+ }
386
+ const ALLOWED_ELEMENTS = {
387
+ svg: true,
388
+ g: true,
389
+ defs: true,
390
+ marker: true,
391
+ filter: true,
392
+ fedropshadow: true,
393
+ fegaussianblur: true,
394
+ lineargradient: true,
395
+ radialgradient: true,
396
+ stop: true,
397
+ clippath: true,
398
+ mask: true,
399
+ symbol: true,
400
+ switch: true,
401
+ rect: true,
402
+ circle: true,
403
+ ellipse: true,
404
+ polygon: true,
405
+ polyline: true,
406
+ line: true,
407
+ path: true,
408
+ text: true,
409
+ tspan: true,
410
+ textpath: true,
411
+ title: true,
412
+ desc: true,
413
+ };
414
+ const ROOT_ONLY_ATTRIBUTES = {
415
+ xmlns: true,
416
+ role: true,
417
+ "aria-labelledby": true,
418
+ "aria-describedby": true,
419
+ "data-seed": true,
420
+ "data-diagram": true,
421
+ };
422
+ const GLOBAL_ATTRIBUTES = {
423
+ id: true,
424
+ x: true,
425
+ y: true,
426
+ x1: true,
427
+ y1: true,
428
+ x2: true,
429
+ y2: true,
430
+ cx: true,
431
+ cy: true,
432
+ r: true,
433
+ rx: true,
434
+ ry: true,
435
+ width: true,
436
+ height: true,
437
+ d: true,
438
+ points: true,
439
+ transform: true,
440
+ "transform-origin": true,
441
+ fill: true,
442
+ "fill-opacity": true,
443
+ viewbox: true,
444
+ preserveaspectratio: true,
445
+ "fill-rule": true,
446
+ stroke: true,
447
+ "stroke-width": true,
448
+ "stroke-opacity": true,
449
+ "stroke-dasharray": true,
450
+ "stroke-dashoffset": true,
451
+ "stroke-linecap": true,
452
+ "stroke-linejoin": true,
453
+ "stroke-miterlimit": true,
454
+ opacity: true,
455
+ color: true,
456
+ "flood-color": true,
457
+ "flood-opacity": true,
458
+ "font-family": true,
459
+ "font-size": true,
460
+ "font-style": true,
461
+ "font-weight": true,
462
+ "text-anchor": true,
463
+ "dominant-baseline": true,
464
+ "alignment-baseline": true,
465
+ "paint-order": true,
466
+ "shape-rendering": true,
467
+ "pointer-events": true,
468
+ visibility: true,
469
+ overflow: true,
470
+ "clip-path": true,
471
+ "clip-rule": true,
472
+ "marker-end": true,
473
+ "marker-start": true,
474
+ orient: true,
475
+ refx: true,
476
+ refy: true,
477
+ markerwidth: true,
478
+ markerheight: true,
479
+ markerunits: true,
480
+ dx: true,
481
+ dy: true,
482
+ stddeviation: true,
483
+ offset: true,
484
+ "stop-color": true,
485
+ "stop-opacity": true,
486
+ gradientunits: true,
487
+ gradienttransform: true,
488
+ "mask-type": true,
489
+ };
490
+ const ID_PATTERN = /^aze-m-\d+(?:-title|-desc|-n-\d+(?:-r\d+)?)?$/;
491
+ const ROOT_ID_PATTERN = /^aze-m-\d+$/;
492
+ /**
493
+ * Structural SVG sanitizer backed by a real XML parser. Strips only XML
494
+ * comments (inert); any other visible or security-relevant finding throws
495
+ * instead of silently rewriting meaning.
496
+ */
497
+ export function sanitizeMermaidSvg(svg, options) {
498
+ if (/<!DOCTYPE/i.test(svg))
499
+ throw new MermaidSanitizerError("SVG must not declare a document type.");
500
+ if (/<\?/.test(svg))
501
+ throw new MermaidSanitizerError("SVG must not contain processing instructions.");
502
+ if (/<!\[CDATA\[/.test(svg))
503
+ throw new MermaidSanitizerError("SVG must not contain CDATA sections.");
504
+ const withoutComments = svg.replace(/<!--[\s\S]*?-->/g, "");
505
+ const text = withoutComments.trim();
506
+ if (!text.startsWith("<svg") || !text.endsWith("</svg>")) {
507
+ throw new MermaidSanitizerError("SVG must be one root element.");
508
+ }
509
+ const parser = new SaxesParser({ xmlns: false, position: false });
510
+ const ids = new Set();
511
+ const references = [];
512
+ const stack = [];
513
+ let rootSeen = false;
514
+ let rootViewBox;
515
+ let rootRole;
516
+ let rootLabelledBy;
517
+ let titleText = "";
518
+ let titleId;
519
+ let inTitle = false;
520
+ let failures = [];
521
+ let topLevelElements = 0;
522
+ let topLevelText = "";
523
+ parser.on("error", (error) => {
524
+ failures.push(error.message);
525
+ });
526
+ parser.on("opentag", (tag) => {
527
+ const name = tag.name.toLowerCase();
528
+ if (ALLOWED_ELEMENTS[name] !== true) {
529
+ failures.push(`Forbidden SVG element: ${name}.`);
530
+ return;
531
+ }
532
+ const depth = stack.length;
533
+ if (depth === 0) {
534
+ topLevelElements += 1;
535
+ if (name !== "svg")
536
+ failures.push("SVG must be one root element.");
537
+ if (rootSeen)
538
+ failures.push("SVG must contain exactly one root element.");
539
+ rootSeen = true;
540
+ }
541
+ stack.push(name);
542
+ if (name === "title") {
543
+ inTitle = titleId === undefined;
544
+ }
545
+ for (const [rawName, rawValue] of Object.entries(tag.attributes)) {
546
+ const attrName = rawName.toLowerCase();
547
+ const value = String(rawValue);
548
+ if (attrName === "xmlns:xlink") {
549
+ failures.push("Unused XML namespace declaration.");
550
+ continue;
551
+ }
552
+ const rootOnly = ROOT_ONLY_ATTRIBUTES[attrName] === true;
553
+ if (rootOnly && depth !== 0) {
554
+ failures.push(`Root-only attribute ${attrName} on ${name}.`);
555
+ continue;
556
+ }
557
+ if (GLOBAL_ATTRIBUTES[attrName] !== true && !rootOnly) {
558
+ failures.push(`Forbidden attribute ${attrName} on ${name}.`);
559
+ continue;
560
+ }
561
+ if (/^on/i.test(attrName) || attrName === "href" || attrName === "xlink:href") {
562
+ failures.push(`Executable attribute: ${attrName}.`);
563
+ continue;
564
+ }
565
+ if (attrName === "id") {
566
+ const pattern = depth === 0 ? ROOT_ID_PATTERN : ID_PATTERN;
567
+ if (!pattern.test(value) || !value.startsWith(`aze-m-${options.ordinal}`)) {
568
+ failures.push(`Non-deterministic id: ${value}.`);
569
+ continue;
570
+ }
571
+ if (ids.has(value)) {
572
+ failures.push(`Duplicate id: ${value}.`);
573
+ continue;
574
+ }
575
+ ids.add(value);
576
+ if (depth === 0 && name === "svg" && value !== `aze-m-${options.ordinal}`) {
577
+ failures.push(`Unexpected SVG root id: ${value}.`);
578
+ }
579
+ if (name === "title" && titleId === undefined)
580
+ titleId = value;
581
+ continue;
582
+ }
583
+ if (attrName === "xmlns") {
584
+ if (value !== "http://www.w3.org/2000/svg") {
585
+ failures.push(`Unexpected XML namespace: ${value}.`);
586
+ }
587
+ continue;
588
+ }
589
+ for (const ref of value.matchAll(/url\(#([^)]+)\)/g)) {
590
+ references.push(ref[1] ?? "");
591
+ }
592
+ if (/https?:|data:|javascript:|file:|ftp:|vbscript:/i.test(value)) {
593
+ failures.push(`External resource URL: ${value}.`);
594
+ continue;
595
+ }
596
+ if (depth === 0 && name === "svg") {
597
+ if (attrName === "viewbox")
598
+ rootViewBox = value;
599
+ if (attrName === "role")
600
+ rootRole = value;
601
+ if (attrName === "aria-labelledby")
602
+ rootLabelledBy = value;
603
+ }
604
+ }
605
+ });
606
+ parser.on("closetag", () => {
607
+ const closed = stack.pop();
608
+ if (closed === "title")
609
+ inTitle = false;
610
+ });
611
+ parser.on("text", (value) => {
612
+ if (stack.length === 0) {
613
+ topLevelText += value;
614
+ }
615
+ else if (inTitle) {
616
+ titleText += value;
617
+ }
618
+ });
619
+ parser.on("cdata", () => {
620
+ failures.push("SVG must not contain CDATA sections.");
621
+ });
622
+ parser.on("comment", () => { });
623
+ parser.write(withoutComments).close();
624
+ if (failures.length > 0) {
625
+ throw new MermaidSanitizerError(failures[0] ?? "Invalid SVG.");
626
+ }
627
+ if (topLevelElements !== 1) {
628
+ throw new MermaidSanitizerError("SVG must contain exactly one root element.");
629
+ }
630
+ if (topLevelText.trim() !== "") {
631
+ throw new MermaidSanitizerError("SVG must not contain top-level text.");
632
+ }
633
+ const viewBox = /^([+-]?(?:\d+(?:\.\d*)?|\.\d+)) ([+-]?(?:\d+(?:\.\d*)?|\.\d+)) ([+-]?(?:\d+(?:\.\d*)?|\.\d+)) ([+-]?(?:\d+(?:\.\d*)?|\.\d+))$/.exec(rootViewBox ?? "");
634
+ const width = Number(viewBox?.[3]);
635
+ const height = Number(viewBox?.[4]);
636
+ if (viewBox === null ||
637
+ !Number.isFinite(width) ||
638
+ !Number.isFinite(height) ||
639
+ width <= 0 ||
640
+ height <= 0 ||
641
+ width > 10000 ||
642
+ height > 10000) {
643
+ throw new MermaidSanitizerError("SVG viewBox must be finite and nonzero.");
644
+ }
645
+ if (rootRole !== "img") {
646
+ throw new MermaidSanitizerError("SVG root must carry role img.");
647
+ }
648
+ const expectedTitle = `aze-m-${options.ordinal}-title`;
649
+ if (rootLabelledBy !== expectedTitle || titleId !== expectedTitle) {
650
+ throw new MermaidSanitizerError("SVG misses an accessible name.");
651
+ }
652
+ if (titleText.trim() === "") {
653
+ throw new MermaidSanitizerError("SVG misses an accessible name.");
654
+ }
655
+ for (const target of references) {
656
+ if (!ids.has(target)) {
657
+ throw new MermaidSanitizerError(`Dangling reference: ${target}.`);
658
+ }
659
+ }
660
+ return withoutComments;
661
+ }
662
+ /**
663
+ * Fragment post-pass for the Compiler: the Block renderer returns a
664
+ * figure wrapping one sanitized SVG. The entire Fragment is validated —
665
+ * wrapper attributes and embedded SVG alike — and any failure throws
666
+ * instead of silently rewriting.
667
+ */
668
+ export function sanitizeMermaidFragment(fragment, options) {
669
+ const text = fragment.trim();
670
+ const openEnd = text.indexOf(">");
671
+ if (!text.startsWith("<figure") || openEnd < 0) {
672
+ throw new MermaidSanitizerError("Fragment must be a figure wrapping one SVG.");
673
+ }
674
+ const opening = text.slice(0, openEnd + 1);
675
+ const allowedFigure = new RegExp(`^<figure class="aze-mermaid" data-ordinal="${options.ordinal}" data-diagram="[^"]*"( data-mermaid-id="[a-z][a-z0-9]*(?:-[a-z0-9]+)*")?>$`);
676
+ if (!allowedFigure.test(opening)) {
677
+ throw new MermaidSanitizerError("Fragment figure carries unexpected attributes.");
678
+ }
679
+ if (!text.endsWith("</figure>")) {
680
+ throw new MermaidSanitizerError("Fragment must close its figure element.");
681
+ }
682
+ const inner = text.slice(openEnd + 1, -"</figure>".length);
683
+ const innerTrimmed = inner.trim();
684
+ if (!innerTrimmed.startsWith("<svg") || !innerTrimmed.endsWith("</svg>")) {
685
+ throw new MermaidSanitizerError("Fragment misses embedded SVG.");
686
+ }
687
+ sanitizeMermaidSvg(innerTrimmed, options);
688
+ if (/<figure[\s>]/i.test(innerTrimmed) || /<\/figure>/i.test(innerTrimmed)) {
689
+ throw new MermaidSanitizerError("Fragment must wrap exactly one SVG.");
690
+ }
691
+ return fragment;
692
+ }
693
+ const pluginDescriptor = Object.freeze({
694
+ type: MERMAID_PLUGIN_TYPE,
695
+ version: MERMAID_PLUGIN_VERSION,
696
+ title: "Mermaid",
697
+ summary: "Offline deterministic diagrams rendered as sanitized SVG.",
698
+ diagnosticNamespace: "azeforge.mermaid",
699
+ sourceSchema: mermaidSourceSchema,
700
+ bodySyntax: Object.freeze({
701
+ id: MERMAID_BODY_SYNTAX_ID,
702
+ version: MERMAID_BODY_SYNTAX_VERSION,
703
+ }),
704
+ dataSchema: mermaidDataSchema,
705
+ });
706
+ export const mermaidPlugin = Object.freeze({
707
+ descriptor: pluginDescriptor,
708
+ });
709
+ const blockRendererDescriptor = Object.freeze({
710
+ id: MERMAID_HTML_BLOCK_RENDERER_ID,
711
+ version: MERMAID_HTML_BLOCK_RENDERER_VERSION,
712
+ blockType: MERMAID_PLUGIN_TYPE,
713
+ pluginVersionRange: "1.0.0",
714
+ rendererId: "html",
715
+ rendererVersionRange: "1.0.0",
716
+ });
717
+ async function renderMermaidFragment(block, context) {
718
+ const ordinal = context.ordinal ?? 0;
719
+ const theme = context.theme ?? defaultTheme;
720
+ const svg = await renderMermaidSvg(block, { ordinal, theme });
721
+ const label = block.id === undefined ? "" : ` data-mermaid-id="${escapeXml(block.id)}"`;
722
+ return `<figure class="aze-mermaid" data-ordinal="${ordinal}" data-diagram="${escapeXml(block.diagramType)}"${label}>${svg}</figure>`;
723
+ }
724
+ export const mermaidHtmlBlockRenderer = Object.freeze({
725
+ descriptor: blockRendererDescriptor,
726
+ render: renderMermaidFragment,
727
+ });
728
+ export function mermaidDependencyClosure() {
729
+ return {
730
+ mermaid: MERMAID_VERSION,
731
+ browser: `HeadlessChrome/${CHROME_HEADLESS_SHELL_VERSION}`,
732
+ platform: { os: platform(), architecture: arch() },
733
+ securityLevel: "strict",
734
+ deterministicIds: true,
735
+ suppressErrorRendering: true,
736
+ fonts: ["Inter"],
737
+ sanitizer: "azeforge-mermaid-svg/v2",
738
+ };
739
+ }
740
+ //# sourceMappingURL=mermaid.js.map