@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.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/astro.config.mjs +255 -0
- package/bin/anc.mjs +798 -0
- package/package.json +93 -0
- package/public/_headers +45 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +9 -0
- package/public/fonts/Temml.woff2 +0 -0
- package/scripts/build-snapshot.js +69 -0
- package/scripts/build-wasm.js +67 -0
- package/scripts/build-workspace.js +50 -0
- package/scripts/copy-snapshot.js +48 -0
- package/scripts/copy-wasm.js +57 -0
- package/scripts/emit-redirects.js +64 -0
- package/scripts/generate-corpus.js +421 -0
- package/scripts/init-repository.js +249 -0
- package/scripts/load-config.js +975 -0
- package/scripts/markdown-to-artifact.js +1286 -0
- package/scripts/preview-site.js +364 -0
- package/scripts/publish-set-review.js +157 -0
- package/scripts/resolve-links.js +640 -0
- package/scripts/run-pagefind.js +125 -0
- package/scripts/scan-residue.js +1016 -0
- package/scripts/scan-secrets.js +268 -0
- package/scripts/seed-gitignore.js +410 -0
- package/scripts/snapshot-rows.js +205 -0
- package/scripts/validate-content.js +95 -0
- package/scripts/vendor-provenance.js +307 -0
- package/scripts/verify-output-inventory.js +307 -0
- package/scripts/write-report.js +351 -0
- package/scripts/write-snapshot.js +136 -0
- package/src/components/CollectionExplorer.astro +149 -0
- package/src/components/FacetIndex.astro +45 -0
- package/src/components/LinkedNotes.astro +94 -0
- package/src/components/NoteGraph.astro +434 -0
- package/src/components/NoteList.astro +54 -0
- package/src/components/SiteMetadata.astro +143 -0
- package/src/components/TableOfContents.astro +69 -0
- package/src/components/TagBrowser.astro +70 -0
- package/src/layouts/Layout.astro +305 -0
- package/src/lib/artifact-source.js +72 -0
- package/src/lib/build-bindings.js +11 -0
- package/src/lib/collection-navigation.js +145 -0
- package/src/lib/content.js +92 -0
- package/src/lib/diagram-mode.js +18 -0
- package/src/lib/graph-layout.js +219 -0
- package/src/lib/graph-selection.js +84 -0
- package/src/lib/graph.js +42 -0
- package/src/lib/link-resolution.js +389 -0
- package/src/lib/markdown.js +1469 -0
- package/src/lib/math-mode.js +31 -0
- package/src/lib/math-output.js +131 -0
- package/src/lib/math.js +165 -0
- package/src/lib/mermaid-environment.js +475 -0
- package/src/lib/mermaid-render.js +769 -0
- package/src/lib/preview-model.js +96 -0
- package/src/lib/relations.js +242 -0
- package/src/lib/rendered-marker.js +55 -0
- package/src/lib/route-path.js +192 -0
- package/src/lib/routes.js +342 -0
- package/src/lib/schema.js +578 -0
- package/src/lib/site.js +578 -0
- package/src/lib/snapshot-contract.js +216 -0
- package/src/lib/snapshot-operations.js +147 -0
- package/src/lib/snapshot-queries.js +216 -0
- package/src/lib/snapshot-reader.js +210 -0
- package/src/lib/snapshot.js +233 -0
- package/src/lib/sqlite-warning.js +22 -0
- package/src/lib/sqlite.js +14 -0
- package/src/lib/tag-browser-model.js +18 -0
- package/src/lib/translations.js +533 -0
- package/src/lib/wasm-asset.js +47 -0
- package/src/lib/worker-protocol.js +93 -0
- package/src/pages/404.astro +35 -0
- package/src/pages/about.astro +100 -0
- package/src/pages/collections/[slug].astro +36 -0
- package/src/pages/collections/index.astro +33 -0
- package/src/pages/graph.astro +64 -0
- package/src/pages/index.astro +33 -0
- package/src/pages/notes/[slug].astro +509 -0
- package/src/pages/privacy.astro +121 -0
- package/src/pages/private.astro +59 -0
- package/src/pages/recent.astro +33 -0
- package/src/pages/robots.txt.js +15 -0
- package/src/pages/rss.xml.js +21 -0
- package/src/pages/sitemap.xml.js +20 -0
- package/src/pages/tags/[tag].astro +56 -0
- package/src/pages/tags/index.astro +37 -0
- package/src/scripts/code-copy.js +45 -0
- package/src/scripts/diagram-disabled.js +16 -0
- package/src/scripts/diagram.js +322 -0
- package/src/scripts/graph-client.js +222 -0
- package/src/scripts/graph-draw.js +166 -0
- package/src/scripts/link-preview.js +334 -0
- package/src/scripts/math-disabled.js +18 -0
- package/src/scripts/math.js +133 -0
- package/src/scripts/preferences.js +123 -0
- package/src/scripts/search-dialog.js +651 -0
- package/src/scripts/snapshot-client.js +157 -0
- package/src/scripts/snapshot-worker.js +341 -0
- package/src/scripts/tag-browser.js +153 -0
- package/src/scripts/theme-init.js +35 -0
- package/src/scripts/toc-active.js +38 -0
- package/src/styles/code.css +152 -0
- package/src/styles/diagram.css +149 -0
- package/src/styles/global.css +1739 -0
- package/src/styles/graph.css +317 -0
- package/src/styles/math.css +376 -0
- package/src/styles/tokens.css +171 -0
- 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
|
+
}
|