@wxxb789/anc 0.0.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/astro.config.mjs +255 -0
  4. package/bin/anc.mjs +798 -0
  5. package/package.json +93 -0
  6. package/public/_headers +45 -0
  7. package/public/favicon.ico +0 -0
  8. package/public/favicon.svg +9 -0
  9. package/public/fonts/Temml.woff2 +0 -0
  10. package/scripts/build-snapshot.js +69 -0
  11. package/scripts/build-wasm.js +67 -0
  12. package/scripts/build-workspace.js +50 -0
  13. package/scripts/copy-snapshot.js +48 -0
  14. package/scripts/copy-wasm.js +57 -0
  15. package/scripts/emit-redirects.js +64 -0
  16. package/scripts/generate-corpus.js +421 -0
  17. package/scripts/init-repository.js +249 -0
  18. package/scripts/load-config.js +975 -0
  19. package/scripts/markdown-to-artifact.js +1286 -0
  20. package/scripts/preview-site.js +364 -0
  21. package/scripts/publish-set-review.js +157 -0
  22. package/scripts/resolve-links.js +640 -0
  23. package/scripts/run-pagefind.js +125 -0
  24. package/scripts/scan-residue.js +1016 -0
  25. package/scripts/scan-secrets.js +268 -0
  26. package/scripts/seed-gitignore.js +410 -0
  27. package/scripts/snapshot-rows.js +205 -0
  28. package/scripts/validate-content.js +95 -0
  29. package/scripts/vendor-provenance.js +307 -0
  30. package/scripts/verify-output-inventory.js +307 -0
  31. package/scripts/write-report.js +351 -0
  32. package/scripts/write-snapshot.js +136 -0
  33. package/src/components/CollectionExplorer.astro +149 -0
  34. package/src/components/FacetIndex.astro +45 -0
  35. package/src/components/LinkedNotes.astro +94 -0
  36. package/src/components/NoteGraph.astro +434 -0
  37. package/src/components/NoteList.astro +54 -0
  38. package/src/components/SiteMetadata.astro +143 -0
  39. package/src/components/TableOfContents.astro +69 -0
  40. package/src/components/TagBrowser.astro +70 -0
  41. package/src/layouts/Layout.astro +305 -0
  42. package/src/lib/artifact-source.js +72 -0
  43. package/src/lib/build-bindings.js +11 -0
  44. package/src/lib/collection-navigation.js +145 -0
  45. package/src/lib/content.js +92 -0
  46. package/src/lib/diagram-mode.js +18 -0
  47. package/src/lib/graph-layout.js +219 -0
  48. package/src/lib/graph-selection.js +84 -0
  49. package/src/lib/graph.js +42 -0
  50. package/src/lib/link-resolution.js +389 -0
  51. package/src/lib/markdown.js +1469 -0
  52. package/src/lib/math-mode.js +31 -0
  53. package/src/lib/math-output.js +131 -0
  54. package/src/lib/math.js +165 -0
  55. package/src/lib/mermaid-environment.js +475 -0
  56. package/src/lib/mermaid-render.js +769 -0
  57. package/src/lib/preview-model.js +96 -0
  58. package/src/lib/relations.js +242 -0
  59. package/src/lib/rendered-marker.js +55 -0
  60. package/src/lib/route-path.js +192 -0
  61. package/src/lib/routes.js +342 -0
  62. package/src/lib/schema.js +578 -0
  63. package/src/lib/site.js +578 -0
  64. package/src/lib/snapshot-contract.js +216 -0
  65. package/src/lib/snapshot-operations.js +147 -0
  66. package/src/lib/snapshot-queries.js +216 -0
  67. package/src/lib/snapshot-reader.js +210 -0
  68. package/src/lib/snapshot.js +233 -0
  69. package/src/lib/sqlite-warning.js +22 -0
  70. package/src/lib/sqlite.js +14 -0
  71. package/src/lib/tag-browser-model.js +18 -0
  72. package/src/lib/translations.js +533 -0
  73. package/src/lib/wasm-asset.js +47 -0
  74. package/src/lib/worker-protocol.js +93 -0
  75. package/src/pages/404.astro +35 -0
  76. package/src/pages/about.astro +100 -0
  77. package/src/pages/collections/[slug].astro +36 -0
  78. package/src/pages/collections/index.astro +33 -0
  79. package/src/pages/graph.astro +64 -0
  80. package/src/pages/index.astro +33 -0
  81. package/src/pages/notes/[slug].astro +509 -0
  82. package/src/pages/privacy.astro +121 -0
  83. package/src/pages/private.astro +59 -0
  84. package/src/pages/recent.astro +33 -0
  85. package/src/pages/robots.txt.js +15 -0
  86. package/src/pages/rss.xml.js +21 -0
  87. package/src/pages/sitemap.xml.js +20 -0
  88. package/src/pages/tags/[tag].astro +56 -0
  89. package/src/pages/tags/index.astro +37 -0
  90. package/src/scripts/code-copy.js +45 -0
  91. package/src/scripts/diagram-disabled.js +16 -0
  92. package/src/scripts/diagram.js +322 -0
  93. package/src/scripts/graph-client.js +222 -0
  94. package/src/scripts/graph-draw.js +166 -0
  95. package/src/scripts/link-preview.js +334 -0
  96. package/src/scripts/math-disabled.js +18 -0
  97. package/src/scripts/math.js +133 -0
  98. package/src/scripts/preferences.js +123 -0
  99. package/src/scripts/search-dialog.js +651 -0
  100. package/src/scripts/snapshot-client.js +157 -0
  101. package/src/scripts/snapshot-worker.js +341 -0
  102. package/src/scripts/tag-browser.js +153 -0
  103. package/src/scripts/theme-init.js +35 -0
  104. package/src/scripts/toc-active.js +38 -0
  105. package/src/styles/code.css +152 -0
  106. package/src/styles/diagram.css +149 -0
  107. package/src/styles/global.css +1739 -0
  108. package/src/styles/graph.css +317 -0
  109. package/src/styles/math.css +376 -0
  110. package/src/styles/tokens.css +171 -0
  111. package/tsconfig.json +25 -0
@@ -0,0 +1,475 @@
1
+ /**
2
+ * The headless DOM Mermaid needs in order to lay a diagram out in Node.
3
+ *
4
+ * Mermaid measures text by putting it in a document and asking the browser how
5
+ * big it is. `happy-dom` supplies the document; it does not supply the
6
+ * measurement — `getBBox()` and `getBoundingClientRect()` both return `0×0`, so
7
+ * without a metrics shim every label measures zero and every diagram collapses
8
+ * into overlapping boxes. That failure is silent: the SVG is well-formed and
9
+ * wrong.
10
+ *
11
+ * The shim measures text from a font advance table rather than a font file.
12
+ * That is an approximation, and its accuracy was measured against the same
13
+ * diagrams rendered in a real browser: median worst-axis deviation 3.8%, 23 of
14
+ * 27 within 15%, and every deviation in the *safe* direction — headless boxes
15
+ * come out slightly larger, so a label gets more room than it needs rather than
16
+ * overflowing. CJK is more accurate than Latin, not less, because a full-width
17
+ * glyph is exactly 1 em by definition. Full evidence in
18
+ * the Mermaid feasibility probe, §2 and §5.
19
+ *
20
+ * This is a build-time module. Nothing here reaches the client in either mode:
21
+ * in build-time mode this module runs under Node and the build emits SVG. Both
22
+ * `mermaid` and `happy-dom` are runtime `dependencies` of the package, because
23
+ * a stranger's build must be able to take this path.
24
+ */
25
+ import { Window } from 'happy-dom';
26
+ /**
27
+ * Globals Mermaid and d3 read off `globalThis` *at import time*, so they must
28
+ * exist before `import('mermaid')` is evaluated. A missing one is not a
29
+ * degraded render; it is a `ReferenceError` during layout.
30
+ */
31
+ const REQUIRED_GLOBALS = [
32
+ 'document', 'DOMParser', 'XMLSerializer', 'Node', 'Element', 'SVGElement',
33
+ 'SVGGraphicsElement', 'HTMLElement', 'HTMLCanvasElement', 'CSSStyleSheet',
34
+ 'CSSStyleDeclaration', 'MutationObserver', 'CustomEvent', 'Event',
35
+ 'DocumentFragment', 'Range', 'NodeFilter', 'Image', 'SVGSVGElement',
36
+ 'HTMLIFrameElement',
37
+ ];
38
+ /**
39
+ * Arial advance widths in 1/1000 em for ASCII 32–126.
40
+ *
41
+ * Arial rather than the site's own stack because the diagram is laid out for a
42
+ * font the build machine does not have and the reader might not either. Arial's
43
+ * metrics are the closest widely-shared approximation of the sans-serif faces
44
+ * `--font-sans` resolves to, and the measured deviation above is the evidence
45
+ * that the approximation holds.
46
+ */
47
+ const ARIAL_ADVANCE = [
48
+ 278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278,
49
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
50
+ 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
51
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
52
+ 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
53
+ 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
54
+ ];
55
+ /**
56
+ * Code point ranges that are one full em wide: CJK ideographs, kana, hangul,
57
+ * full-width forms, and CJK punctuation. Every CJK font makes these exactly
58
+ * 1000/1000 em, so this is exact rather than approximate.
59
+ */
60
+ function isFullWidth(codePoint) {
61
+ return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
62
+ (codePoint >= 0x2e80 && codePoint <= 0x303e) ||
63
+ (codePoint >= 0x3041 && codePoint <= 0x33ff) ||
64
+ (codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
65
+ (codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
66
+ (codePoint >= 0xa000 && codePoint <= 0xa4cf) ||
67
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
68
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
69
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
70
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
71
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
72
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd));
73
+ }
74
+ /** Width of one line of text at a font size, in CSS pixels. */
75
+ export function measureLine(text, fontSizePx, isBold) {
76
+ let units = 0;
77
+ for (const character of text) {
78
+ const codePoint = character.codePointAt(0);
79
+ if (isFullWidth(codePoint))
80
+ units += 1000;
81
+ else if (codePoint >= 32 && codePoint <= 126)
82
+ units += ARIAL_ADVANCE[codePoint - 32];
83
+ else if (codePoint === 9)
84
+ units += 2224;
85
+ else if (codePoint >= 32)
86
+ units += 556;
87
+ }
88
+ return (units / 1000) * fontSizePx * (isBold ? 1.06 : 1);
89
+ }
90
+ /**
91
+ * Font size in effect on an element, walking up until one is declared.
92
+ *
93
+ * Mermaid sets its font sizes inline or through the `<style>` block it injects
94
+ * into the SVG, and a headless CSSOM cascades neither onto `getComputedStyle`.
95
+ * Walking the ancestors for an explicit declaration is what a browser's
96
+ * inheritance would have done.
97
+ */
98
+ function fontSizeOf(element) {
99
+ let node = element;
100
+ while (node) {
101
+ const declared = node.style?.getPropertyValue?.('font-size');
102
+ if (declared) {
103
+ const value = Number.parseFloat(declared);
104
+ if (!Number.isNaN(value))
105
+ return declared.endsWith('em') ? value * 16 : value;
106
+ }
107
+ const attribute = node.getAttribute?.('font-size');
108
+ if (attribute) {
109
+ const value = Number.parseFloat(attribute);
110
+ if (!Number.isNaN(value))
111
+ return value;
112
+ }
113
+ node = node.parentNode;
114
+ }
115
+ return 16;
116
+ }
117
+ function isBoldAt(element) {
118
+ let node = element;
119
+ while (node) {
120
+ const weight = node.style?.getPropertyValue?.('font-weight') ?? node.getAttribute?.('font-weight');
121
+ if (weight)
122
+ return weight === 'bold' || Number.parseInt(weight, 10) >= 600;
123
+ node = node.parentNode;
124
+ }
125
+ return false;
126
+ }
127
+ /**
128
+ * An element's text as the visual lines it occupies.
129
+ *
130
+ * Line structure matters because a multi-line label's width is its *widest*
131
+ * line, not the width of its concatenated text. Treating `A<br>BBBB` as one
132
+ * seven-character run makes the node twice as wide as it needs to be.
133
+ */
134
+ function linesOf(element) {
135
+ const BLOCK = new Set(['P', 'DIV', 'LI', 'BR', 'TSPAN', 'TR']);
136
+ const lines = [];
137
+ let current = '';
138
+ const walk = (node) => {
139
+ if (node.nodeType === 3) {
140
+ current += node.nodeValue ?? '';
141
+ return;
142
+ }
143
+ if (node.nodeType !== 1)
144
+ return;
145
+ const tag = (node.tagName ?? '').toUpperCase();
146
+ if (tag === 'BR') {
147
+ lines.push(current);
148
+ current = '';
149
+ return;
150
+ }
151
+ const isBlock = BLOCK.has(tag);
152
+ if (isBlock && current) {
153
+ lines.push(current);
154
+ current = '';
155
+ }
156
+ for (const child of node.childNodes ?? [])
157
+ walk(child);
158
+ if (isBlock) {
159
+ lines.push(current);
160
+ current = '';
161
+ }
162
+ };
163
+ walk(element);
164
+ if (current)
165
+ lines.push(current);
166
+ const nonEmpty = lines.filter((line) => line.length > 0);
167
+ return nonEmpty.length > 0 ? nonEmpty : [''];
168
+ }
169
+ /** The box one text-bearing element occupies. */
170
+ function textBox(element) {
171
+ const fontSize = fontSizeOf(element);
172
+ const bold = isBoldAt(element);
173
+ const lines = linesOf(element);
174
+ const width = Math.max(0, ...lines.map((line) => measureLine(line, fontSize, bold)));
175
+ // An SVG `<text>` line box is about 1.15 em; Mermaid's HTML labels declare
176
+ // `line-height: 1.5` inline, so the two differ and using one for both makes
177
+ // every foreignObject label too short.
178
+ const lineHeight = element.namespaceURI === 'http://www.w3.org/2000/svg' ? fontSize * 1.15 : fontSize * 1.5;
179
+ return { width, height: Math.max(lines.length, 1) * lineHeight };
180
+ }
181
+ /**
182
+ * Install the measurement surface Mermaid reads.
183
+ *
184
+ * Every assignment below writes a *partial* implementation onto a fully typed
185
+ * DOM prototype: `getBBox` returns a plain box rather than a `DOMRect`,
186
+ * `getScreenCTM` an identity object rather than a `DOMMatrix`, and three of the
187
+ * methods are not on happy-dom's `SVGGraphicsElement` type at all. That is the
188
+ * point of a shim — the real types describe a browser, and this is not one.
189
+ *
190
+ * So the prototypes are widened once, here, to an index signature rather than
191
+ * each assignment being cast at its own site. One widening with a stated reason
192
+ * is honest; eight scattered `as unknown as` casts hide the same thing while
193
+ * looking like eight separate accidents. Only what Mermaid actually calls is
194
+ * implemented, and the feasibility probe §1.2 lists exactly that set.
195
+ *
196
+ * The union walk is the subtle half. `getBBox()` on a container must return the
197
+ * union of its descendants' boxes in the container's own coordinate space —
198
+ * which means honouring each descendant's `translate()` while *excluding* the
199
+ * element's own, exactly as a browser does. Getting that backwards shifts every
200
+ * nested subgraph by its own offset.
201
+ */
202
+ function installMetrics(window) {
203
+ const { SVGGraphicsElement, SVGElement, Element } = window;
204
+ const svgPrototype = (SVGGraphicsElement?.prototype ?? SVGElement.prototype);
205
+ const svgElementPrototype = SVGElement.prototype;
206
+ const elementPrototype = Element.prototype;
207
+ const numeric = (node, attribute, fallback = 0) => {
208
+ const value = Number.parseFloat(node.getAttribute?.(attribute) ?? '');
209
+ return Number.isNaN(value) ? fallback : value;
210
+ };
211
+ const unionOf = (element) => {
212
+ let minX = Infinity;
213
+ let minY = Infinity;
214
+ let maxX = -Infinity;
215
+ let maxY = -Infinity;
216
+ const add = (x, y, width, height) => {
217
+ if (!Number.isFinite(x) || !Number.isFinite(y))
218
+ return;
219
+ minX = Math.min(minX, x);
220
+ minY = Math.min(minY, y);
221
+ maxX = Math.max(maxX, x + (width || 0));
222
+ maxY = Math.max(maxY, y + (height || 0));
223
+ };
224
+ const visit = (node, offsetX, offsetY, isRoot) => {
225
+ if (node.nodeType !== 1)
226
+ return;
227
+ const tag = (node.tagName ?? '').toLowerCase();
228
+ const transform = isRoot ? '' : (node.getAttribute?.('transform') ?? '');
229
+ const translate = /translate\(\s*([-\d.eE]+)[ ,]+([-\d.eE]+)?/.exec(transform);
230
+ const x = offsetX + (translate ? Number.parseFloat(translate[1]) || 0 : 0);
231
+ const y = offsetY + (translate ? Number.parseFloat(translate[2] ?? '0') || 0 : 0);
232
+ if (tag === 'rect' || tag === 'foreignobject' || tag === 'image' || tag === 'use') {
233
+ add(x + numeric(node, 'x'), y + numeric(node, 'y'), numeric(node, 'width'), numeric(node, 'height'));
234
+ }
235
+ else if (tag === 'circle') {
236
+ const r = numeric(node, 'r');
237
+ add(x + numeric(node, 'cx') - r, y + numeric(node, 'cy') - r, 2 * r, 2 * r);
238
+ }
239
+ else if (tag === 'ellipse') {
240
+ const rx = numeric(node, 'rx');
241
+ const ry = numeric(node, 'ry');
242
+ add(x + numeric(node, 'cx') - rx, y + numeric(node, 'cy') - ry, 2 * rx, 2 * ry);
243
+ }
244
+ else if (tag === 'line') {
245
+ const [x1, x2, y1, y2] = [numeric(node, 'x1'), numeric(node, 'x2'), numeric(node, 'y1'), numeric(node, 'y2')];
246
+ add(x + Math.min(x1, x2), y + Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
247
+ }
248
+ else if (tag === 'polygon' || tag === 'polyline') {
249
+ for (const point of (node.getAttribute?.('points') ?? '').trim().split(/\s+/)) {
250
+ const [px, py] = point.split(',').map(Number);
251
+ if (Number.isFinite(px) && Number.isFinite(py))
252
+ add(x + px, y + py, 0, 0);
253
+ }
254
+ }
255
+ else if (tag === 'path') {
256
+ for (const [, px, py] of (node.getAttribute?.('d') ?? '').matchAll(/(-?\d+(?:\.\d+)?)[ ,]+(-?\d+(?:\.\d+)?)/g)) {
257
+ add(x + Number.parseFloat(px), y + Number.parseFloat(py), 0, 0);
258
+ }
259
+ }
260
+ else if (tag === 'text' || tag === 'tspan') {
261
+ const box = node.getBBox?.() ?? { x: 0, y: 0, width: 0, height: 0 };
262
+ add(x + box.x, y + box.y, box.width, box.height);
263
+ return;
264
+ }
265
+ for (const child of node.childNodes ?? [])
266
+ visit(child, x, y, false);
267
+ };
268
+ visit(element, 0, 0, true);
269
+ if (!Number.isFinite(minX))
270
+ return { x: 0, y: 0, width: 0, height: 0 };
271
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
272
+ };
273
+ svgPrototype['getBBox'] = function getBBox() {
274
+ const tag = (this.tagName ?? '').toLowerCase();
275
+ if (tag !== 'text' && tag !== 'tspan')
276
+ return unionOf(this);
277
+ const { width, height } = textBox(this);
278
+ const x = numeric(this, 'x');
279
+ const y = numeric(this, 'y');
280
+ const anchor = this.getAttribute?.('text-anchor') ?? this.style?.getPropertyValue?.('text-anchor');
281
+ const shift = anchor === 'middle' ? -width / 2 : anchor === 'end' ? -width : 0;
282
+ // The 0.8 factor puts the box's top at the cap height above the baseline,
283
+ // which is where a browser puts it for a text element.
284
+ return { x: x + shift, y: y - height * 0.8, width, height };
285
+ };
286
+ // Non-graphics SVG elements (a `marker`, a child of `defs`) are asked too.
287
+ if (svgElementPrototype !== svgPrototype)
288
+ svgElementPrototype['getBBox'] = svgPrototype['getBBox'];
289
+ const originalRect = Element.prototype.getBoundingClientRect;
290
+ elementPrototype['getBoundingClientRect'] = function getBoundingClientRect() {
291
+ if (this.namespaceURI === 'http://www.w3.org/1999/xhtml' || !this.namespaceURI) {
292
+ const { width, height } = textBox(this);
293
+ return { x: 0, y: 0, top: 0, left: 0, right: width, bottom: height, width, height, toJSON: () => ({}) };
294
+ }
295
+ const box = this.getBBox ? this.getBBox() : originalRect.call(this);
296
+ return {
297
+ x: box.x, y: box.y, top: box.y, left: box.x,
298
+ right: box.x + box.width, bottom: box.y + box.height,
299
+ width: box.width, height: box.height, toJSON: () => ({}),
300
+ };
301
+ };
302
+ svgPrototype['getComputedTextLength'] = function getComputedTextLength() {
303
+ return textBox(this).width;
304
+ };
305
+ svgPrototype['getSubStringLength'] = function getSubStringLength(start, length) {
306
+ return measureLine((this.textContent ?? '').slice(start, start + length), fontSizeOf(this), isBoldAt(this));
307
+ };
308
+ svgPrototype['getNumberOfChars'] = function getNumberOfChars() {
309
+ return (this.textContent ?? '').length;
310
+ };
311
+ const identityMatrix = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
312
+ svgPrototype['getScreenCTM'] = function getScreenCTM() {
313
+ return { ...identityMatrix, inverse: () => ({ ...identityMatrix }) };
314
+ };
315
+ svgPrototype['getCTM'] = svgPrototype['getScreenCTM'];
316
+ }
317
+ /**
318
+ * The layout width diagrams size themselves from.
319
+ *
320
+ * Not a constant with no meaning: gantt reads `parentElement.offsetWidth` as
321
+ * its total chart width and timeline does the same, so this *is* their rendered
322
+ * width. It matches `--measure` at the article's own text size — the column a
323
+ * diagram actually appears in — so a gantt chart comes out the width of the
324
+ * prose above it rather than of an arbitrary viewport.
325
+ */
326
+ export const DIAGRAM_VIEWPORT_WIDTH_PX = 720;
327
+ const DIAGRAM_VIEWPORT_HEIGHT_PX = 768;
328
+ /**
329
+ * Replace `Math.random` with a seeded generator, and hand back a reset.
330
+ *
331
+ * Mermaid's `deterministicIds` option is necessary and not sufficient. Gitgraph
332
+ * mints commit hashes with `makeRandomHex`, which calls `Math.random` directly,
333
+ * and those hashes are *rendered as text* — so their measured width feeds back
334
+ * into the layout and the diagram's own `viewBox` height changes between
335
+ * builds. Measured: five renders of one gitgraph produced five different
336
+ * heights. The hand-drawn renderers used by `class` and `er` jitter their path
337
+ * coordinates from the same source unless `handDrawnSeed` is set.
338
+ *
339
+ * The replacement is installed before `import('mermaid')` because the module
340
+ * graph captures the reference at import time, and reset before each render so
341
+ * that a diagram's output depends only on its own source and not on how many
342
+ * diagrams preceded it.
343
+ *
344
+ * A linear congruential generator is the right size here: this seeds label text
345
+ * in a static site generator, not anything that needs unpredictability.
346
+ *
347
+ * **The replacement is process-lifetime and deliberate.** It is installed once,
348
+ * when the environment is created, and never restored — so every consumer of
349
+ * `Math.random` in a build that rendered a diagram shares the sequence. That is
350
+ * acceptable because nothing else in this build reads it: the renderer is
351
+ * deterministic by design, and a grep over `src/`, `scripts/`, and `tests/`
352
+ * finds no other caller. Restoring it per render would defeat the purpose, and
353
+ * restoring it at the end would leave the ordering dependent on when that
354
+ * happened. An earlier comment here claimed the original was restored; it never
355
+ * was, and a guarantee the code does not provide is worse than none.
356
+ */
357
+ const RANDOM_SEED = 0x2f6e2b1;
358
+ let randomState = RANDOM_SEED;
359
+ function installSeededRandom() {
360
+ Math.random = () => {
361
+ randomState = (randomState * 1103515245 + 12345) % 2147483648;
362
+ return randomState / 2147483648;
363
+ };
364
+ }
365
+ /** Restart the sequence, so render N does not depend on renders 1..N-1. */
366
+ export function resetSeededRandom() {
367
+ randomState = RANDOM_SEED;
368
+ }
369
+ /**
370
+ * A Mermaid instance with the DOM and metrics it needs, plus the window so a
371
+ * caller can query the rendered tree.
372
+ *
373
+ * One per process. Mermaid keeps module-level counters that feed element ids,
374
+ * so a second instance in the same process would continue the first's numbering
375
+ * rather than restarting it — which is why ids are normalized after rendering
376
+ * instead of being relied upon to repeat. See `renderDiagram`.
377
+ */
378
+ export async function createMermaidEnvironment() {
379
+ installSeededRandom();
380
+ // `localhost` rather than the site's own origin. Nothing in a diagram
381
+ // resolves a URL against it — the post-processing pass rejects every external
382
+ // reference — so this is only the base a DOM must have, and naming the real
383
+ // origin here would put a second copy of it outside `astro.config.mjs`, which
384
+ // `tests/metadata.test.ts` forbids for exactly that reason.
385
+ const window = new Window({ url: 'http://localhost/' });
386
+ const globals = globalThis;
387
+ globals['window'] = window;
388
+ for (const name of REQUIRED_GLOBALS) {
389
+ const value = window[name];
390
+ if (value !== undefined)
391
+ globals[name] = value;
392
+ }
393
+ // cytoscape, which lays out mindmaps, reads `padding-*` off the computed
394
+ // style and does arithmetic with it. happy-dom returns `''` for an unset
395
+ // property, and `'' * 1` is `NaN`, which propagates into every coordinate.
396
+ //
397
+ // Typed loosely on purpose: happy-dom declares `getComputedStyle` with one
398
+ // parameter while the DOM's own signature takes a pseudo-element too, and the
399
+ // proxy must forward whatever it is called with rather than the narrower of
400
+ // the two.
401
+ const computeStyle = window.getComputedStyle.bind(window);
402
+ const patchedComputeStyle = ((...args) => {
403
+ const style = computeStyle(...args);
404
+ return new Proxy(style, {
405
+ get(target, key) {
406
+ if (key === 'getPropertyValue') {
407
+ return (name) => {
408
+ const value = target.getPropertyValue(name);
409
+ return (value === '' || value == null) && /^(?:padding|margin|border)/.test(name) ? '0px' : value;
410
+ };
411
+ }
412
+ const value = target[key];
413
+ return typeof value === 'function' ? value.bind(target) : value;
414
+ },
415
+ });
416
+ });
417
+ globals['getComputedStyle'] = patchedComputeStyle;
418
+ window.getComputedStyle = patchedComputeStyle;
419
+ globals['requestAnimationFrame'] = (callback) => setTimeout(() => callback(Date.now()), 0);
420
+ globals['cancelAnimationFrame'] = clearTimeout;
421
+ Object.defineProperty(globalThis, 'navigator', { value: window.navigator, configurable: true });
422
+ // C4 reads `screen` directly and throws a ReferenceError without it.
423
+ globals['screen'] = window.screen ?? {
424
+ width: 1920, height: 1080, availWidth: 1920, availHeight: 1080,
425
+ };
426
+ installMetrics(window);
427
+ // mindmap and architecture measure text through a 2d canvas context rather
428
+ // than through the DOM. happy-dom has no canvas at all, so this backs
429
+ // `measureText` with the same advance table the DOM path uses — the two must
430
+ // agree, or a mindmap's nodes and its labels are sized by different rulers.
431
+ const fontSizeFrom = (font) => {
432
+ const match = /(\d+(?:\.\d+)?)px/.exec(font);
433
+ return match ? Number.parseFloat(match[1]) : 16;
434
+ };
435
+ window.HTMLCanvasElement.prototype.getContext = function getContext(type) {
436
+ if (type !== '2d')
437
+ return null;
438
+ let font = '16px Arial';
439
+ const noop = () => { };
440
+ return {
441
+ get font() { return font; },
442
+ set font(value) { font = value; },
443
+ measureText(text) {
444
+ const size = fontSizeFrom(font);
445
+ const width = measureLine(String(text), size, /bold/.test(font));
446
+ return {
447
+ width,
448
+ actualBoundingBoxAscent: size * 0.8, actualBoundingBoxDescent: size * 0.2,
449
+ actualBoundingBoxLeft: 0, actualBoundingBoxRight: width,
450
+ fontBoundingBoxAscent: size * 0.9, fontBoundingBoxDescent: size * 0.25,
451
+ };
452
+ },
453
+ fillText: noop, strokeText: noop, save: noop, restore: noop, scale: noop,
454
+ translate: noop, rotate: noop, beginPath: noop, closePath: noop, moveTo: noop,
455
+ lineTo: noop, arc: noop, fill: noop, stroke: noop, clearRect: noop,
456
+ fillRect: noop, drawImage: noop, setTransform: noop,
457
+ getImageData: () => ({ data: new Uint8ClampedArray(4) }),
458
+ };
459
+ };
460
+ // The layout viewport. cytoscape divides by `clientWidth` and throws on a
461
+ // zero; gantt and timeline read `offsetWidth` as their chart width.
462
+ for (const [property, value] of [
463
+ ['clientWidth', DIAGRAM_VIEWPORT_WIDTH_PX],
464
+ ['clientHeight', DIAGRAM_VIEWPORT_HEIGHT_PX],
465
+ ['offsetWidth', DIAGRAM_VIEWPORT_WIDTH_PX],
466
+ ['offsetHeight', DIAGRAM_VIEWPORT_HEIGHT_PX],
467
+ ]) {
468
+ Object.defineProperty(window.HTMLElement.prototype, property, {
469
+ configurable: true,
470
+ get: () => value,
471
+ });
472
+ }
473
+ const mermaid = (await import('mermaid')).default;
474
+ return { mermaid, window };
475
+ }