@geml/geml 1.4.6 → 1.5.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 -21
- package/README.md +217 -217
- package/codemap/adapters/crg.mjs +120 -120
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -658
- package/codemap/browser-stub.mjs +29 -29
- package/codemap/build.mjs +609 -609
- package/codemap/cross-stack.mjs +303 -303
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +480 -480
- package/codemap/entries.mjs +129 -129
- package/codemap/exclude.mjs +52 -52
- package/codemap/find.mjs +49 -49
- package/codemap/foldings.mjs +110 -110
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +431 -431
- package/codemap/normalize.mjs +275 -275
- package/codemap/recipe-trust.mjs +103 -103
- package/codemap/refresh.mjs +310 -310
- package/codemap/render-all.mjs +77 -64
- package/codemap/serve.mjs +585 -578
- package/codemap/sfc-virtualize.mjs +367 -367
- package/codemap/verify.mjs +148 -148
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +12 -2
- package/dist/geml.d.ts +4 -0
- package/dist/geml.js +653 -156
- package/dist/inline.d.ts +20 -0
- package/dist/inline.js +36 -4
- package/dist/mcp.js +32 -24
- package/dist/render-html.js +35 -35
- package/dist/render.d.ts +19 -0
- package/dist/render.js +486 -182
- package/dist/serialize.js +9 -1
- package/dist/table.d.ts +0 -4
- package/dist/table.js +62 -53
- package/dist/to-md.js +17 -3
- package/package.json +66 -66
package/dist/render.js
CHANGED
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
// exception. They load from a CDN, and only when the document actually uses them,
|
|
12
12
|
// so a document of prose, tables and charts is fully self-contained with zero
|
|
13
13
|
// network. Bundling those two engines offline is the next step (roadmap P0 #6).
|
|
14
|
+
import { projectableInlines } from "./geml.js";
|
|
15
|
+
import { isSafeUrl } from "./inline.js";
|
|
14
16
|
const PALETTE = ["#2563eb", "#dc2626", "#059669", "#d97706", "#7c3aed", "#db2777", "#0891b2", "#ea580c"];
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Escaping
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
18
20
|
export function esc(s) {
|
|
19
|
-
|
|
21
|
+
// C0 controls other than tab/LF/CR are not valid in HTML text and, passed
|
|
22
|
+
// through verbatim, can desynchronize a downstream sanitizer, proxy or log
|
|
23
|
+
// pipeline. §0.4 only normalizes NUL; a document can still carry the rest, and a
|
|
24
|
+
// transclusion of a `.geml`-named binary carries a lot of them.
|
|
25
|
+
return s.replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "�")
|
|
26
|
+
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
20
27
|
}
|
|
21
28
|
export function escAttr(s) {
|
|
22
29
|
return esc(s).replace(/"/g, """);
|
|
@@ -39,6 +46,97 @@ const MAX_NESTING = 256;
|
|
|
39
46
|
// ---------------------------------------------------------------------------
|
|
40
47
|
// Render context
|
|
41
48
|
// ---------------------------------------------------------------------------
|
|
49
|
+
// S5: how deep transclusions may nest before the renderer stops expanding and
|
|
50
|
+
// degrades to the reference link instead.
|
|
51
|
+
const EMBED_DEPTH_CAP = 8;
|
|
52
|
+
// Depth and cycle detection bound the SHAPE of a transclusion graph, never its
|
|
53
|
+
// total. A diamond is not a cycle, and the cycle key is `path#fragment`, so eight
|
|
54
|
+
// sections each embedding the next N times is eight distinct keys and N^8
|
|
55
|
+
// expansions: 1.5KB of input reached 402MB of output, and one step further died on
|
|
56
|
+
// an uncaught RangeError from string concatenation. These are the global budgets
|
|
57
|
+
// that actually bound it, checked before every expansion.
|
|
58
|
+
const EMBED_TOTAL_CAP = 1000; // expansions per render
|
|
59
|
+
const EMBED_BYTES_CAP = 8 * 1024 * 1024; // expanded bytes per render
|
|
60
|
+
const EMBED_DOC_BYTES_CAP = 4 * 1024 * 1024; // a single loaded document
|
|
61
|
+
// §9.5 requires a class token to be REDUCED to the identifier charset, not escaped
|
|
62
|
+
// — escaping keeps whatever was there. Only literals reach these call sites today,
|
|
63
|
+
// so this is about not letting that invariant rest on the caller.
|
|
64
|
+
function classAttrToken(s) {
|
|
65
|
+
return s.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
66
|
+
}
|
|
67
|
+
// Compose a target that is relative to `base` — itself relative to the rendered
|
|
68
|
+
// host file — into a path relative to that host. Pure string work on purpose:
|
|
69
|
+
// render.ts is bundled for the browser (the playground), so no node:path here.
|
|
70
|
+
// A scheme-bearing, protocol-relative or root-relative target is already
|
|
71
|
+
// absolute and passes through untouched.
|
|
72
|
+
function relJoin(base, target) {
|
|
73
|
+
if (base === "" || target === "" || target.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(target))
|
|
74
|
+
return target;
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const s of (base + "/" + target).split("/")) {
|
|
77
|
+
if (s === "" || s === ".")
|
|
78
|
+
continue;
|
|
79
|
+
if (s === ".." && out.length > 0 && out[out.length - 1] !== "..")
|
|
80
|
+
out.pop();
|
|
81
|
+
else
|
|
82
|
+
out.push(s);
|
|
83
|
+
}
|
|
84
|
+
return out.join("/");
|
|
85
|
+
}
|
|
86
|
+
function relDir(p) {
|
|
87
|
+
const i = p.lastIndexOf("/");
|
|
88
|
+
return i < 0 ? "" : p.slice(0, i);
|
|
89
|
+
}
|
|
90
|
+
// S2: which blocks a fragment selects. No fragment is the whole document body
|
|
91
|
+
// (meta is frontmatter, not content). A heading id selects its whole SECTION —
|
|
92
|
+
// the heading plus everything up to the next heading at the same or a higher
|
|
93
|
+
// level, the same boundary `geml get` uses — and any other id selects its own
|
|
94
|
+
// block. Nested children are searched too: an id can live inside a flow block.
|
|
95
|
+
function selectEmbed(children, anchor) {
|
|
96
|
+
if (anchor === undefined)
|
|
97
|
+
return children.filter((b) => !(b.kind === "block" && b.type === "meta"));
|
|
98
|
+
return findEmbedTarget(children, anchor);
|
|
99
|
+
}
|
|
100
|
+
function findEmbedTarget(blocks, id) {
|
|
101
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
102
|
+
const b = blocks[i];
|
|
103
|
+
if (b.kind === "heading" && b.id === id) {
|
|
104
|
+
const out = [b];
|
|
105
|
+
for (let j = i + 1; j < blocks.length; j++) {
|
|
106
|
+
const next = blocks[j];
|
|
107
|
+
if (next.kind === "heading" && next.level <= b.level)
|
|
108
|
+
break;
|
|
109
|
+
out.push(next);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
if (b.kind === "block" && b.id === id)
|
|
114
|
+
return [b];
|
|
115
|
+
if (b.kind === "block" && b.children) {
|
|
116
|
+
const inner = findEmbedTarget(b.children, id);
|
|
117
|
+
if (inner !== null)
|
|
118
|
+
return inner;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
// id -> label: a heading's text, a block's caption, else the id itself. Free of
|
|
124
|
+
// the render context so a TARGET document can be indexed the same way, which is
|
|
125
|
+
// what a cross-document auto-reference needs for its link text (§5.2).
|
|
126
|
+
function indexLabelsInto(blocks, into) {
|
|
127
|
+
for (const b of blocks) {
|
|
128
|
+
if (b.kind === "heading")
|
|
129
|
+
into.set(b.id ?? "", b.text);
|
|
130
|
+
else if (b.kind === "block") {
|
|
131
|
+
if (b.id) {
|
|
132
|
+
const cap = b.attrs["caption"];
|
|
133
|
+
into.set(b.id, typeof cap === "string" ? cap : (b.table?.caption ?? b.id));
|
|
134
|
+
}
|
|
135
|
+
if (b.children)
|
|
136
|
+
indexLabelsInto(b.children, into);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
42
140
|
export class RenderCtx {
|
|
43
141
|
doc;
|
|
44
142
|
opts;
|
|
@@ -46,6 +144,40 @@ export class RenderCtx {
|
|
|
46
144
|
usedMermaid = false;
|
|
47
145
|
usedCodeGraph = false;
|
|
48
146
|
renderDepth = 0;
|
|
147
|
+
// S5: the (path#fragment) chain currently being expanded, for cycle
|
|
148
|
+
// detection and the depth cap. `embedDocs` is the chain of documents being
|
|
149
|
+
// expanded — each with its path relative to the host, so relative targets inside
|
|
150
|
+
// borrowed content compose through it (S4) and a fragment-only reference
|
|
151
|
+
// resolves against the document it was written in.
|
|
152
|
+
embedStack = [];
|
|
153
|
+
embedDocs = [];
|
|
154
|
+
// The global budgets, and a memo so a document is read and parsed at most once
|
|
155
|
+
// per render — without it a 1.1KB corpus produced 21,845 filesystem reads and
|
|
156
|
+
// 21,845 full re-parses, because every expansion loaded its target again.
|
|
157
|
+
embedCount = 0;
|
|
158
|
+
embedBytes = 0;
|
|
159
|
+
embedCache = new Map();
|
|
160
|
+
budgetExhausted() {
|
|
161
|
+
if (this.embedCount >= EMBED_TOTAL_CAP)
|
|
162
|
+
return `transclusion budget spent (${EMBED_TOTAL_CAP} expansions)`;
|
|
163
|
+
if (this.embedBytes >= EMBED_BYTES_CAP)
|
|
164
|
+
return `transclusion budget spent (${EMBED_BYTES_CAP} bytes)`;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
// One read and one parse per document per render, and a size ceiling so a huge
|
|
168
|
+
// target cannot be expanded (or re-expanded) at all.
|
|
169
|
+
loadChildren(rel) {
|
|
170
|
+
const hit = this.embedCache.get(rel);
|
|
171
|
+
if (hit !== undefined)
|
|
172
|
+
return hit;
|
|
173
|
+
const { loadDoc, parseDoc } = this.opts;
|
|
174
|
+
let children = null;
|
|
175
|
+
const src = loadDoc && parseDoc ? loadDoc(rel) : null;
|
|
176
|
+
if (src !== null && src !== undefined && src.length <= EMBED_DOC_BYTES_CAP)
|
|
177
|
+
children = parseDoc(src).children;
|
|
178
|
+
this.embedCache.set(rel, children);
|
|
179
|
+
return children;
|
|
180
|
+
}
|
|
49
181
|
labels = new Map(); // id -> link label for [[#id]] auto-refs
|
|
50
182
|
constructor(doc, opts = {}) {
|
|
51
183
|
this.doc = doc;
|
|
@@ -66,18 +198,7 @@ export class RenderCtx {
|
|
|
66
198
|
}
|
|
67
199
|
// Build the id -> label map: a heading's text, or a block's caption, or its id.
|
|
68
200
|
indexLabels(blocks) {
|
|
69
|
-
|
|
70
|
-
if (b.kind === "heading")
|
|
71
|
-
this.labels.set(b.id ?? "", b.text);
|
|
72
|
-
else if (b.kind === "block") {
|
|
73
|
-
if (b.id) {
|
|
74
|
-
const cap = b.attrs["caption"];
|
|
75
|
-
this.labels.set(b.id, typeof cap === "string" ? cap : (b.table?.caption ?? b.id));
|
|
76
|
-
}
|
|
77
|
-
if (b.children)
|
|
78
|
-
this.indexLabels(b.children);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
201
|
+
indexLabelsInto(blocks, this.labels);
|
|
81
202
|
}
|
|
82
203
|
docTitle() {
|
|
83
204
|
for (const b of this.doc.children) {
|
|
@@ -108,15 +229,220 @@ export class RenderCtx {
|
|
|
108
229
|
case "image": return this.media(n);
|
|
109
230
|
case "link": return this.link(n);
|
|
110
231
|
case "autoref": {
|
|
111
|
-
const href = n.doc ? `${n.doc.replace(/\.geml$/, ".html")}#${n.anchor}` :
|
|
112
|
-
|
|
232
|
+
const href = n.doc ? `${relJoin(relDir(this.currentDocRel), n.doc).replace(/\.geml$/, ".html")}#${n.anchor}` : this.fragmentHref(n.anchor);
|
|
233
|
+
// §5.2: an auto-reference takes its text from the target's caption or
|
|
234
|
+
// heading. Across documents that means reading the target — which the
|
|
235
|
+
// build can do, since an embed pulls whole sections through the same hook.
|
|
236
|
+
// Inside borrowed content a fragment-only reference means an id of the
|
|
237
|
+
// BORROWED document, so its label has to come from there too. Taking it
|
|
238
|
+
// from `this.labels` showed the host's caption on a link whose destination
|
|
239
|
+
// is the source document's block — a text/target mismatch the host controls.
|
|
240
|
+
const label = n.doc
|
|
241
|
+
? (this.remoteLabel(n.doc, n.anchor) ?? n.anchor ?? n.doc)
|
|
242
|
+
: (this.currentLabels().get(n.anchor) ?? n.anchor);
|
|
113
243
|
return `<a href="${escAttr(href)}">${esc(label)}</a>`;
|
|
114
244
|
}
|
|
115
|
-
case "
|
|
245
|
+
case "project": return this.projectInline(n);
|
|
246
|
+
// Through fragmentHref like every other fragment-only reference: borrowed
|
|
247
|
+
// content owns no anchors, so a bare `#ref` here landed on a same-named
|
|
248
|
+
// footnote of the HOST — letting the host author choose what a borrowed
|
|
249
|
+
// sentence's citation says.
|
|
250
|
+
case "footnote": return `<sup class="fn"><a href="${escAttr(this.fragmentHref(n.ref))}">${esc(n.ref)}</a></sup>`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// S2/S3/S5: expand a transclusion in place, wrapped in a container carrying its
|
|
254
|
+
// provenance. Every path that cannot expand falls back to a link to the target
|
|
255
|
+
// with the reason visible — never silently blank, never a broken image.
|
|
256
|
+
transclude(b, idAttr) {
|
|
257
|
+
const written = typeof b.attrs["src"] === "string" ? b.attrs["src"].trim() : "";
|
|
258
|
+
if (written === "")
|
|
259
|
+
return this.transclusionFallback("", idAttr, "invalid", "embed: missing `src=`");
|
|
260
|
+
const hash = written.indexOf("#");
|
|
261
|
+
const docPath = hash < 0 ? written : written.slice(0, hash);
|
|
262
|
+
const anchor = hash < 0 ? undefined : written.slice(hash + 1);
|
|
263
|
+
const { loadDoc, parseDoc } = this.opts;
|
|
264
|
+
// A same-document target (`src=#id`) selects from the document CURRENTLY being
|
|
265
|
+
// expanded, which inside borrowed content is the borrowed document, not the
|
|
266
|
+
// host. And it takes a cycle key like any other: the slice a heading id
|
|
267
|
+
// selects contains the embed that selected it, which is the smallest cycle
|
|
268
|
+
// there is. Skipping the key here is what let a 7-line document expand into
|
|
269
|
+
// 256 copies of itself, stopped only by the generic block-nesting guard.
|
|
270
|
+
const rel = docPath === "" ? this.currentDocRel : relJoin(relDir(this.currentDocRel), docPath);
|
|
271
|
+
const key = anchor === undefined ? rel : `${rel}#${anchor}`;
|
|
272
|
+
if (this.embedStack.includes(key)) {
|
|
273
|
+
return `<div class="transclusion transclusion-error"${idAttr} data-src="${escAttr(written)}">transclusion cycle: ${esc([...this.embedStack, key].join(" → "))}</div>`;
|
|
274
|
+
}
|
|
275
|
+
if (this.embedStack.length >= EMBED_DEPTH_CAP) {
|
|
276
|
+
return this.transclusionFallback(written, idAttr, "too-deep", `transclusion depth cap (${EMBED_DEPTH_CAP}) reached`);
|
|
277
|
+
}
|
|
278
|
+
const spent = this.budgetExhausted();
|
|
279
|
+
if (spent !== null)
|
|
280
|
+
return this.transclusionFallback(written, idAttr, "too-large", spent);
|
|
281
|
+
let children;
|
|
282
|
+
if (docPath !== "" && !/\.geml$/i.test(docPath)) {
|
|
283
|
+
// Same constraint the parser reports: an embed stands for a GEML document.
|
|
284
|
+
// Parsing whatever else the target happens to contain injected its bytes
|
|
285
|
+
// into the page as prose.
|
|
286
|
+
return this.transclusionFallback(written, idAttr, "invalid", `\`${docPath}\` is not a GEML document`);
|
|
287
|
+
}
|
|
288
|
+
if (docPath === "") {
|
|
289
|
+
children = this.currentDocChildren;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
if (!loadDoc || !parseDoc)
|
|
293
|
+
return this.transclusionFallback(written, idAttr, "unexpanded", "no document resolver");
|
|
294
|
+
// Parsed on its own, so S4 holds for free: `{{key}}` inside borrowed content
|
|
295
|
+
// interpolates against the SOURCE document's meta, never the host's. Read
|
|
296
|
+
// through the cache: the same target is otherwise re-read and re-parsed once
|
|
297
|
+
// per expansion.
|
|
298
|
+
const loaded = this.loadChildren(rel);
|
|
299
|
+
if (loaded === null)
|
|
300
|
+
return this.transclusionFallback(written, idAttr, "unresolved", `cannot resolve document \`${docPath}\`, or it is too large`);
|
|
301
|
+
children = loaded;
|
|
302
|
+
}
|
|
303
|
+
const picked = selectEmbed(children, anchor);
|
|
304
|
+
if (picked === null) {
|
|
305
|
+
const what = docPath === "" ? `no \`${written}\` in this document` : `no \`#${anchor}\` in \`${docPath}\``;
|
|
306
|
+
return this.transclusionFallback(written, idAttr, "unresolved", what);
|
|
307
|
+
}
|
|
308
|
+
this.embedStack.push(key);
|
|
309
|
+
this.embedDocs.push({ rel, children });
|
|
310
|
+
try {
|
|
311
|
+
return this.transclusionWrap(written, idAttr, picked);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
this.embedDocs.pop();
|
|
315
|
+
this.embedStack.pop();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// An inline projection: the target block's body, rendered here. Deliberately the
|
|
319
|
+
// same machinery as the block form — the cycle stack, the depth cap, and the
|
|
320
|
+
// document chain that drives S4 rebasing and the fragment-only rewrite — rather
|
|
321
|
+
// than a second path that would have to be kept in step with it. A projected
|
|
322
|
+
// phrase carrying a link is the normal case, so that rewrite matters more here.
|
|
323
|
+
projectInline(n) {
|
|
324
|
+
const written = n.doc === undefined ? `#${n.anchor}` : `${n.doc}#${n.anchor}`;
|
|
325
|
+
const { loadDoc, parseDoc } = this.opts;
|
|
326
|
+
const rel = n.doc === undefined ? this.currentDocRel : relJoin(relDir(this.currentDocRel), n.doc);
|
|
327
|
+
const key = `${rel}#${n.anchor}`;
|
|
328
|
+
if (this.embedStack.includes(key))
|
|
329
|
+
return this.projectFallback(written, "error", "transclusion cycle");
|
|
330
|
+
if (this.embedStack.length >= EMBED_DEPTH_CAP)
|
|
331
|
+
return this.projectFallback(written, "too-deep", `depth cap (${EMBED_DEPTH_CAP})`);
|
|
332
|
+
const spentHere = this.budgetExhausted();
|
|
333
|
+
if (spentHere !== null)
|
|
334
|
+
return this.projectFallback(written, "too-large", spentHere);
|
|
335
|
+
let children;
|
|
336
|
+
if (n.doc === undefined)
|
|
337
|
+
children = this.currentDocChildren;
|
|
338
|
+
else {
|
|
339
|
+
if (!loadDoc || !parseDoc)
|
|
340
|
+
return this.projectFallback(written, "unexpanded", "no document resolver");
|
|
341
|
+
const loaded = this.loadChildren(rel);
|
|
342
|
+
if (loaded === null)
|
|
343
|
+
return this.projectFallback(written, "unresolved", "unresolvable document, or too large");
|
|
344
|
+
children = loaded;
|
|
345
|
+
}
|
|
346
|
+
const got = projectableInlines(children, n.anchor);
|
|
347
|
+
if (got === null || got === "not-inline")
|
|
348
|
+
return this.projectFallback(written, "unresolved", "not inline content");
|
|
349
|
+
this.embedStack.push(key);
|
|
350
|
+
this.embedDocs.push({ rel, children });
|
|
351
|
+
try {
|
|
352
|
+
this.embedCount++;
|
|
353
|
+
const inner = this.inlines(got.inlines);
|
|
354
|
+
this.embedBytes += inner.length;
|
|
355
|
+
return `<span class="transclusion-inline" data-src="${escAttr(written)}">${inner}</span>`;
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
this.embedDocs.pop();
|
|
359
|
+
this.embedStack.pop();
|
|
116
360
|
}
|
|
117
361
|
}
|
|
362
|
+
projectFallback(written, why, note) {
|
|
363
|
+
const hash = written.indexOf("#");
|
|
364
|
+
const docPath = written.slice(0, hash);
|
|
365
|
+
const href = docPath === "" ? written : relJoin(relDir(this.currentDocRel), docPath).replace(/\.geml$/, ".html") + written.slice(hash);
|
|
366
|
+
const safe = isSafeUrl(href) ? href : "#";
|
|
367
|
+
return `<span class="transclusion-inline transclusion-${classAttrToken(why)}" data-src="${escAttr(written)}" title="${escAttr(note)}">`
|
|
368
|
+
+ `<a href="${escAttr(safe)}">${esc(written)}</a></span>`;
|
|
369
|
+
}
|
|
370
|
+
// The document a transclusion is currently selecting from — the host until an
|
|
371
|
+
// expansion is in progress. `rel` is its path relative to the rendered host, so
|
|
372
|
+
// everything relative inside it composes through `relDir(rel)` (S4), and any
|
|
373
|
+
// fragment-only reference resolves against that document's own page.
|
|
374
|
+
get currentDocRel() {
|
|
375
|
+
return this.embedDocs.length === 0 ? "" : this.embedDocs[this.embedDocs.length - 1].rel;
|
|
376
|
+
}
|
|
377
|
+
get currentDocChildren() {
|
|
378
|
+
return this.embedDocs.length === 0 ? this.doc.children : this.embedDocs[this.embedDocs.length - 1].children;
|
|
379
|
+
}
|
|
380
|
+
// Labels of the document currently being expanded, built on first use per frame.
|
|
381
|
+
currentLabels() {
|
|
382
|
+
if (this.embedDocs.length === 0)
|
|
383
|
+
return this.labels;
|
|
384
|
+
const frame = this.embedDocs[this.embedDocs.length - 1];
|
|
385
|
+
if (frame.labels === undefined) {
|
|
386
|
+
frame.labels = new Map();
|
|
387
|
+
indexLabelsInto(frame.children, frame.labels);
|
|
388
|
+
}
|
|
389
|
+
return frame.labels;
|
|
390
|
+
}
|
|
391
|
+
// S9: borrowed content contributes no anchors to the host page. Two ids named
|
|
392
|
+
// the same is invalid HTML, and an in-page link to one of them would land on
|
|
393
|
+
// whichever the browser picked. The host keeps its own ids; a borrowed copy has
|
|
394
|
+
// none, and references into it resolve against its source document instead.
|
|
395
|
+
idAttr(id) {
|
|
396
|
+
return id === undefined || this.embedDocs.length > 0 ? "" : ` id="${escAttr(id)}"`;
|
|
397
|
+
}
|
|
398
|
+
// A fragment-only reference (`#id`, `[[#id]]`) inside borrowed content means an
|
|
399
|
+
// id of the BORROWED document. On the host page that anchor does not exist — or,
|
|
400
|
+
// worse, a same-named host block silently answers for it — so it points at the
|
|
401
|
+
// source document's page.
|
|
402
|
+
// The label a target document gives an id, for a cross-document auto-reference.
|
|
403
|
+
// Memoized per document: one page can reference the same document many times.
|
|
404
|
+
remoteLabels = new Map();
|
|
405
|
+
remoteLabel(doc, anchor) {
|
|
406
|
+
const { loadDoc, parseDoc } = this.opts;
|
|
407
|
+
if (!loadDoc || !parseDoc)
|
|
408
|
+
return undefined;
|
|
409
|
+
const rel = relJoin(relDir(this.currentDocRel), doc);
|
|
410
|
+
let labels = this.remoteLabels.get(rel);
|
|
411
|
+
if (labels === undefined) {
|
|
412
|
+
labels = new Map();
|
|
413
|
+
const src = loadDoc(rel);
|
|
414
|
+
if (src !== null)
|
|
415
|
+
indexLabelsInto(parseDoc(src).children, labels);
|
|
416
|
+
this.remoteLabels.set(rel, labels);
|
|
417
|
+
}
|
|
418
|
+
return labels.get(anchor);
|
|
419
|
+
}
|
|
420
|
+
fragmentHref(anchor) {
|
|
421
|
+
const rel = this.currentDocRel;
|
|
422
|
+
return rel === "" ? `#${anchor}` : `${rel.replace(/\.geml$/, ".html")}#${anchor}`;
|
|
423
|
+
}
|
|
424
|
+
transclusionWrap(written, idAttr, picked) {
|
|
425
|
+
this.embedCount++;
|
|
426
|
+
const inner = picked.map((x) => this.block(x)).filter((s) => s !== "").join("\n");
|
|
427
|
+
this.embedBytes += inner.length;
|
|
428
|
+
return `<section class="transclusion"${idAttr} data-src="${escAttr(written)}">${inner}</section>`;
|
|
429
|
+
}
|
|
430
|
+
transclusionFallback(written, idAttr, why, note) {
|
|
431
|
+
const hash = written.indexOf("#");
|
|
432
|
+
const docPath = hash < 0 ? written : written.slice(0, hash);
|
|
433
|
+
const frag = hash < 0 ? "" : written.slice(hash);
|
|
434
|
+
const href = docPath === "" ? frag : relJoin(relDir(this.currentDocRel), docPath).replace(/\.geml$/, ".html") + frag;
|
|
435
|
+
// Defence in depth: the parse layer already blanks an unsafe scheme (§9.5), so
|
|
436
|
+
// this should be unreachable. It is here because a fallback that composes an
|
|
437
|
+
// href from document text is exactly where a missed filter upstream becomes a
|
|
438
|
+
// live `javascript:` link — the shape of the one Critical finding in review.
|
|
439
|
+
const safe = isSafeUrl(href) ? href : "#";
|
|
440
|
+
const link = written === "" ? "" : `<a href="${escAttr(safe)}">${esc(written)}</a> `;
|
|
441
|
+
return `<div class="transclusion transclusion-${classAttrToken(why)}"${idAttr} data-src="${escAttr(written)}" title="${escAttr(note)}">`
|
|
442
|
+
+ `${link}<span class="transclusion-note">${esc(note)}</span></div>`;
|
|
443
|
+
}
|
|
118
444
|
media(n) {
|
|
119
|
-
const src = escAttr(n.src);
|
|
445
|
+
const src = escAttr(relJoin(relDir(this.currentDocRel), n.src));
|
|
120
446
|
if (n.as === "video")
|
|
121
447
|
return `<video class="media" src="${src}" controls></video>`;
|
|
122
448
|
if (n.as === "audio")
|
|
@@ -128,9 +454,9 @@ export class RenderCtx {
|
|
|
128
454
|
if (n.href)
|
|
129
455
|
href = n.href;
|
|
130
456
|
else if (n.doc)
|
|
131
|
-
href = `${n.doc.replace(/\.geml$/, ".html")}${n.anchor ? "#" + n.anchor : ""}`;
|
|
457
|
+
href = `${relJoin(relDir(this.currentDocRel), n.doc).replace(/\.geml$/, ".html")}${n.anchor ? "#" + n.anchor : ""}`;
|
|
132
458
|
else if (n.anchor)
|
|
133
|
-
href =
|
|
459
|
+
href = this.fragmentHref(n.anchor);
|
|
134
460
|
const rel = typeof n.attrs["rel"] === "string" ? ` rel="${escAttr(n.attrs["rel"])}"` : "";
|
|
135
461
|
const target = typeof n.attrs["target"] === "string" ? ` target="${escAttr(n.attrs["target"])}"` : "";
|
|
136
462
|
return `<a href="${escAttr(href)}"${rel}${target}>${this.inlines(n.children)}</a>`;
|
|
@@ -156,7 +482,7 @@ export class RenderCtx {
|
|
|
156
482
|
case "heading": {
|
|
157
483
|
if (b.hidden)
|
|
158
484
|
return "";
|
|
159
|
-
const id =
|
|
485
|
+
const id = this.idAttr(b.id);
|
|
160
486
|
const lvl = Math.min(6, Math.max(1, b.level));
|
|
161
487
|
return `<h${lvl}${id}>${this.inlines(b.inlines)}</h${lvl}>`;
|
|
162
488
|
}
|
|
@@ -188,7 +514,7 @@ export class RenderCtx {
|
|
|
188
514
|
return ""; // {hidden}: in the model, never rendered
|
|
189
515
|
const raw = (b.raw ?? []).join("\n");
|
|
190
516
|
const caption = typeof b.attrs["caption"] === "string" ? b.attrs["caption"] : undefined;
|
|
191
|
-
const idAttr =
|
|
517
|
+
const idAttr = this.idAttr(b.id);
|
|
192
518
|
switch (b.type) {
|
|
193
519
|
case "meta": return ""; // header metadata, not body content
|
|
194
520
|
case "code": {
|
|
@@ -196,8 +522,7 @@ export class RenderCtx {
|
|
|
196
522
|
const cls = lang ? ` class="language-${escAttr(lang)}"` : "";
|
|
197
523
|
return `<pre${idAttr}><code${cls}>${esc(raw)}</code></pre>`;
|
|
198
524
|
}
|
|
199
|
-
case "
|
|
200
|
-
return `<pre class="output"${idAttr}><code>${esc(raw)}</code></pre>`;
|
|
525
|
+
case "embed": return this.transclude(b, idAttr);
|
|
201
526
|
case "math":
|
|
202
527
|
this.usedMath = true;
|
|
203
528
|
return `<div class="math-block"${idAttr}>\\[${esc(raw)}\\]</div>`;
|
|
@@ -225,7 +550,7 @@ export class RenderCtx {
|
|
|
225
550
|
}
|
|
226
551
|
}
|
|
227
552
|
diagram(b, raw, caption) {
|
|
228
|
-
const idAttr =
|
|
553
|
+
const idAttr = this.idAttr(b.id);
|
|
229
554
|
const fmt = typeof b.attrs["format"] === "string" ? b.attrs["format"] : "";
|
|
230
555
|
const cap = caption ? `<figcaption>${esc(caption)}</figcaption>` : "";
|
|
231
556
|
if (fmt === "geml-chart") {
|
|
@@ -286,35 +611,14 @@ export class RenderCtx {
|
|
|
286
611
|
const maxRows = this.isCodemapDoc && id === "modules" ? Infinity : (this.opts.tableRows ?? 500);
|
|
287
612
|
const allRows = t.rows;
|
|
288
613
|
const rows = allRows.length > maxRows ? allRows.slice(0, maxRows) : allRows;
|
|
289
|
-
// Coverage grid for declared spans, so cells a span covers are not emitted.
|
|
290
|
-
const covered = rows.map((r) => r.map(() => false));
|
|
291
|
-
rows.forEach((row, r) => row.forEach((cell, c) => {
|
|
292
|
-
if (!cell.span)
|
|
293
|
-
return;
|
|
294
|
-
// Bound the sweep to the rendered grid regardless of the declared span, so
|
|
295
|
-
// an oversized span can never drive an O(hugerows×hugecols) loop (DoS).
|
|
296
|
-
const spanRows = Math.min(cell.span.rows, rows.length - r);
|
|
297
|
-
const spanCols = Math.min(cell.span.cols, row.length - c);
|
|
298
|
-
for (let dr = 0; dr < spanRows; dr++)
|
|
299
|
-
for (let dc = 0; dc < spanCols; dc++) {
|
|
300
|
-
if (dr === 0 && dc === 0)
|
|
301
|
-
continue;
|
|
302
|
-
const rr = r + dr, cc = c + dc;
|
|
303
|
-
if (covered[rr]?.[cc] !== undefined)
|
|
304
|
-
covered[rr][cc] = true;
|
|
305
|
-
}
|
|
306
|
-
}));
|
|
307
614
|
const thead = t.header
|
|
308
615
|
? `<thead><tr>${t.columns.map((col, c) => `<th${alignStyle(t.align[c])}>${esc(col)}</th>`).join("")}</tr></thead>`
|
|
309
616
|
: "";
|
|
310
617
|
const bodyRows = rows.map((row, r) => {
|
|
311
618
|
const cells = row.map((cell, c) => {
|
|
312
|
-
if (covered[r]?.[c])
|
|
313
|
-
return "";
|
|
314
|
-
const span = cell.span ? `${cell.span.rows > 1 ? ` rowspan="${cell.span.rows}"` : ""}${cell.span.cols > 1 ? ` colspan="${cell.span.cols}"` : ""}` : "";
|
|
315
619
|
const cls = cell.computed ? ' class="computed"' : "";
|
|
316
620
|
const sortVal = typeof cell.value === "number" ? ` data-sort="${cell.value}"` : "";
|
|
317
|
-
return `<td${alignStyle(cell.align ?? t.align[c])}${
|
|
621
|
+
return `<td${alignStyle(cell.align ?? t.align[c])}${cls}${sortVal}>${this.inlines(cell.inlines)}</td>`;
|
|
318
622
|
}).join("");
|
|
319
623
|
return `<tr>${cells}</tr>`;
|
|
320
624
|
}).join("\n");
|
|
@@ -925,143 +1229,143 @@ function trunc(s, n) {
|
|
|
925
1229
|
// ---------------------------------------------------------------------------
|
|
926
1230
|
// Page shell, inline CSS, inline interactivity JS
|
|
927
1231
|
// ---------------------------------------------------------------------------
|
|
928
|
-
export const CSS = `
|
|
929
|
-
:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
|
|
930
|
-
* { box-sizing: border-box; }
|
|
931
|
-
body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
|
|
932
|
-
main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
|
|
933
|
-
h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
|
|
934
|
-
h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
935
|
-
h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
936
|
-
h3 { font-size:1.25em; } h4 { font-size:1em; }
|
|
937
|
-
p { margin:.7em 0; }
|
|
938
|
-
a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
|
|
939
|
-
code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
940
|
-
pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
|
|
941
|
-
pre code { background:none; padding:0; font-size:.85em; }
|
|
942
|
-
pre.output { background:#0d1117; color:#e6edf3; }
|
|
943
|
-
pre.output code { color:inherit; }
|
|
944
|
-
ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
|
|
945
|
-
ul.task-list { list-style:none; padding-left:.2em; }
|
|
946
|
-
li.task input[type=checkbox] { appearance:none; -webkit-appearance:none; width:1.1em; height:1.1em; margin:0 .5em 0 0; vertical-align:-.2em; border:1.5px solid #c8ccd0; border-radius:4px; background:#fff; position:relative; opacity:1; cursor:default; box-sizing:border-box; }
|
|
947
|
-
li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
|
|
948
|
-
li.task input[type=checkbox]:checked::after { content:"✓"; position:absolute; top:0; right:0; bottom:0; left:0; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.8em; line-height:1; font-weight:700; }
|
|
949
|
-
aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
|
|
950
|
-
aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
|
|
951
|
-
aside.warning { border-left-color:#d97706; background:#fff8f0; }
|
|
952
|
-
aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
|
|
953
|
-
figure { margin:1.2em 0; }
|
|
954
|
-
figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
|
|
955
|
-
table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
|
|
956
|
-
table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
|
|
957
|
-
table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
|
|
958
|
-
table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
|
|
959
|
-
table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
|
|
960
|
-
table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
|
|
961
|
-
table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
|
|
962
|
-
table.geml-table td.computed { color:#0a7c52; }
|
|
963
|
-
table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
|
|
964
|
-
.table-tools { margin-bottom:6px; } .table-filter { width:240px; max-width:100%; padding:5px 9px; border:1px solid var(--bd); border-radius:7px; font-size:.85em; }
|
|
965
|
-
.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
|
|
966
|
-
.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
|
|
967
|
-
.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
|
|
968
|
-
.c-title { font-size:15px; font-weight:600; fill:var(--fg); }
|
|
969
|
-
.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
|
|
970
|
-
.media { max-width:100%; border-radius:8px; }
|
|
971
|
-
.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
|
|
972
|
-
.math-block { overflow-x:auto; padding:.4em 0; }
|
|
973
|
-
sup.fn a { font-size:.75em; }
|
|
974
|
-
.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
|
|
975
|
-
.geml-footer code { font-size:.95em; }
|
|
976
|
-
.code-graph { margin:1.4em 0; }
|
|
977
|
-
.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
|
|
978
|
-
.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }
|
|
979
|
-
.cg-svg { display:block; }
|
|
980
|
-
.cg-search-wrap { position:relative; display:inline-block; }
|
|
981
|
-
.cg-search { font:12px/1.4 inherit; padding:2px 7px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); min-width:13ch; }
|
|
982
|
-
.cg-search-menu { position:absolute; z-index:30; top:calc(100% + 2px); left:0; min-width:24ch; max-width:52ch; max-height:52vh; overflow:auto; background:var(--bg); border:1px solid var(--bd); border-radius:6px; box-shadow:0 6px 20px rgba(0,0,0,.18); }
|
|
983
|
-
.cg-search-row { display:block; width:100%; text-align:left; padding:4px 9px 4px 18px; border:0; background:none; color:var(--fg); cursor:pointer; font:12px/1.4 inherit; }
|
|
984
|
-
.cg-search-row:hover { background:var(--bd); }
|
|
985
|
-
.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
|
|
986
|
-
.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
|
|
987
|
-
.cg-search-grp:first-of-type { border-top:0; }
|
|
988
|
-
.cg-stage { display:flex; gap:10px; align-items:flex-start; }
|
|
989
|
-
.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
|
|
990
|
-
.cg-src { flex:0 0 42%; max-width:46%; display:flex; flex-direction:column; border:1px solid var(--bd); border-radius:6px; overflow:hidden; background:var(--bg); }
|
|
991
|
-
.cg-src-hd { display:flex; gap:8px; align-items:center; justify-content:space-between; padding:4px 8px; border-bottom:1px solid var(--bd); color:var(--muted); font:.76em ui-monospace,Consolas,monospace; word-break:break-all; }
|
|
992
|
-
.cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
|
|
993
|
-
.cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:72vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }
|
|
994
|
-
.cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
|
|
995
|
-
.cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
|
|
996
|
-
.cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
|
|
997
|
-
.cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
|
|
998
|
-
.cg-crumb .cg-seg:hover { text-decoration:underline; }
|
|
999
|
-
.cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }
|
|
1000
|
-
.cg-flash { color:#b42318; }
|
|
1001
|
-
.cg-legend { display:flex; gap:14px; align-items:center; justify-content:space-between; flex-wrap:wrap; font-size:.75em; color:var(--muted); margin-top:6px; }
|
|
1002
|
-
.cg-upbtn { cursor:pointer; }
|
|
1003
|
-
.cg-upbtn circle { fill:#fff; stroke:#94a3b8; }
|
|
1004
|
-
.cg-upbtn text { font-size:11px; fill:#57606a; }
|
|
1005
|
-
.cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }
|
|
1006
|
-
.cg-upbtn:hover text { fill:var(--accent); }
|
|
1007
|
-
.cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }
|
|
1008
|
-
.cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }
|
|
1009
|
-
.cg-chip { display:inline-flex; align-items:center; gap:4px; }
|
|
1010
|
-
.cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }
|
|
1011
|
-
.cg-note { font-size:.8em; color:#9a6700; }
|
|
1012
|
-
.cg-n rect { fill:#eef2f7; stroke:#94a3b8; }
|
|
1013
|
-
.cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }
|
|
1014
|
-
.cg-n { cursor:pointer; }
|
|
1015
|
-
.cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }
|
|
1016
|
-
.cg-n.leaf { opacity:.45; }
|
|
1017
|
-
.cg-n.test rect { stroke-dasharray:3 2; }
|
|
1018
|
-
.cg-n.grp rect { stroke-width:1.8; }
|
|
1019
|
-
.cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
|
|
1020
|
-
.cg-e.cand { stroke-dasharray:2 3; }
|
|
1021
|
-
.cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
|
|
1022
|
-
.cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
|
|
1023
|
-
.cg-e.soft { opacity:.55; }
|
|
1024
|
-
.cg-svg.hl .cg-n { opacity:.22; }
|
|
1025
|
-
.cg-svg.hl .cg-e { opacity:.1; }
|
|
1026
|
-
.cg-svg.hl .cg-n.hl { opacity:1; }
|
|
1027
|
-
.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
|
|
1232
|
+
export const CSS = `
|
|
1233
|
+
:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
|
|
1234
|
+
* { box-sizing: border-box; }
|
|
1235
|
+
body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
|
|
1236
|
+
main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
|
|
1237
|
+
h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
|
|
1238
|
+
h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
1239
|
+
h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
1240
|
+
h3 { font-size:1.25em; } h4 { font-size:1em; }
|
|
1241
|
+
p { margin:.7em 0; }
|
|
1242
|
+
a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
|
|
1243
|
+
code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
1244
|
+
pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
|
|
1245
|
+
pre code { background:none; padding:0; font-size:.85em; }
|
|
1246
|
+
pre.output { background:#0d1117; color:#e6edf3; }
|
|
1247
|
+
pre.output code { color:inherit; }
|
|
1248
|
+
ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
|
|
1249
|
+
ul.task-list { list-style:none; padding-left:.2em; }
|
|
1250
|
+
li.task input[type=checkbox] { appearance:none; -webkit-appearance:none; width:1.1em; height:1.1em; margin:0 .5em 0 0; vertical-align:-.2em; border:1.5px solid #c8ccd0; border-radius:4px; background:#fff; position:relative; opacity:1; cursor:default; box-sizing:border-box; }
|
|
1251
|
+
li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
|
|
1252
|
+
li.task input[type=checkbox]:checked::after { content:"✓"; position:absolute; top:0; right:0; bottom:0; left:0; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.8em; line-height:1; font-weight:700; }
|
|
1253
|
+
aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
|
|
1254
|
+
aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
|
|
1255
|
+
aside.warning { border-left-color:#d97706; background:#fff8f0; }
|
|
1256
|
+
aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
|
|
1257
|
+
figure { margin:1.2em 0; }
|
|
1258
|
+
figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
|
|
1259
|
+
table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
|
|
1260
|
+
table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
|
|
1261
|
+
table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
|
|
1262
|
+
table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
|
|
1263
|
+
table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
|
|
1264
|
+
table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
|
|
1265
|
+
table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
|
|
1266
|
+
table.geml-table td.computed { color:#0a7c52; }
|
|
1267
|
+
table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
|
|
1268
|
+
.table-tools { margin-bottom:6px; } .table-filter { width:240px; max-width:100%; padding:5px 9px; border:1px solid var(--bd); border-radius:7px; font-size:.85em; }
|
|
1269
|
+
.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
|
|
1270
|
+
.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
|
|
1271
|
+
.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
|
|
1272
|
+
.c-title { font-size:15px; font-weight:600; fill:var(--fg); }
|
|
1273
|
+
.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
|
|
1274
|
+
.media { max-width:100%; border-radius:8px; }
|
|
1275
|
+
.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
|
|
1276
|
+
.math-block { overflow-x:auto; padding:.4em 0; }
|
|
1277
|
+
sup.fn a { font-size:.75em; }
|
|
1278
|
+
.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
|
|
1279
|
+
.geml-footer code { font-size:.95em; }
|
|
1280
|
+
.code-graph { margin:1.4em 0; }
|
|
1281
|
+
.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
|
|
1282
|
+
.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }
|
|
1283
|
+
.cg-svg { display:block; }
|
|
1284
|
+
.cg-search-wrap { position:relative; display:inline-block; }
|
|
1285
|
+
.cg-search { font:12px/1.4 inherit; padding:2px 7px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); min-width:13ch; }
|
|
1286
|
+
.cg-search-menu { position:absolute; z-index:30; top:calc(100% + 2px); left:0; min-width:24ch; max-width:52ch; max-height:52vh; overflow:auto; background:var(--bg); border:1px solid var(--bd); border-radius:6px; box-shadow:0 6px 20px rgba(0,0,0,.18); }
|
|
1287
|
+
.cg-search-row { display:block; width:100%; text-align:left; padding:4px 9px 4px 18px; border:0; background:none; color:var(--fg); cursor:pointer; font:12px/1.4 inherit; }
|
|
1288
|
+
.cg-search-row:hover { background:var(--bd); }
|
|
1289
|
+
.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
|
|
1290
|
+
.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
|
|
1291
|
+
.cg-search-grp:first-of-type { border-top:0; }
|
|
1292
|
+
.cg-stage { display:flex; gap:10px; align-items:flex-start; }
|
|
1293
|
+
.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
|
|
1294
|
+
.cg-src { flex:0 0 42%; max-width:46%; display:flex; flex-direction:column; border:1px solid var(--bd); border-radius:6px; overflow:hidden; background:var(--bg); }
|
|
1295
|
+
.cg-src-hd { display:flex; gap:8px; align-items:center; justify-content:space-between; padding:4px 8px; border-bottom:1px solid var(--bd); color:var(--muted); font:.76em ui-monospace,Consolas,monospace; word-break:break-all; }
|
|
1296
|
+
.cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
|
|
1297
|
+
.cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:72vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }
|
|
1298
|
+
.cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
|
|
1299
|
+
.cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
|
|
1300
|
+
.cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
|
|
1301
|
+
.cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
|
|
1302
|
+
.cg-crumb .cg-seg:hover { text-decoration:underline; }
|
|
1303
|
+
.cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }
|
|
1304
|
+
.cg-flash { color:#b42318; }
|
|
1305
|
+
.cg-legend { display:flex; gap:14px; align-items:center; justify-content:space-between; flex-wrap:wrap; font-size:.75em; color:var(--muted); margin-top:6px; }
|
|
1306
|
+
.cg-upbtn { cursor:pointer; }
|
|
1307
|
+
.cg-upbtn circle { fill:#fff; stroke:#94a3b8; }
|
|
1308
|
+
.cg-upbtn text { font-size:11px; fill:#57606a; }
|
|
1309
|
+
.cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }
|
|
1310
|
+
.cg-upbtn:hover text { fill:var(--accent); }
|
|
1311
|
+
.cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }
|
|
1312
|
+
.cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }
|
|
1313
|
+
.cg-chip { display:inline-flex; align-items:center; gap:4px; }
|
|
1314
|
+
.cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }
|
|
1315
|
+
.cg-note { font-size:.8em; color:#9a6700; }
|
|
1316
|
+
.cg-n rect { fill:#eef2f7; stroke:#94a3b8; }
|
|
1317
|
+
.cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }
|
|
1318
|
+
.cg-n { cursor:pointer; }
|
|
1319
|
+
.cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }
|
|
1320
|
+
.cg-n.leaf { opacity:.45; }
|
|
1321
|
+
.cg-n.test rect { stroke-dasharray:3 2; }
|
|
1322
|
+
.cg-n.grp rect { stroke-width:1.8; }
|
|
1323
|
+
.cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
|
|
1324
|
+
.cg-e.cand { stroke-dasharray:2 3; }
|
|
1325
|
+
.cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
|
|
1326
|
+
.cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
|
|
1327
|
+
.cg-e.soft { opacity:.55; }
|
|
1328
|
+
.cg-svg.hl .cg-n { opacity:.22; }
|
|
1329
|
+
.cg-svg.hl .cg-e { opacity:.1; }
|
|
1330
|
+
.cg-svg.hl .cg-n.hl { opacity:1; }
|
|
1331
|
+
.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
|
|
1028
1332
|
`;
|
|
1029
|
-
export const JS = `
|
|
1030
|
-
(function () {
|
|
1031
|
-
function cmp(a, b) {
|
|
1032
|
-
var na = a.dataset.sort, nb = b.dataset.sort;
|
|
1033
|
-
if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
|
|
1034
|
-
return (a.textContent || "").localeCompare(b.textContent || "");
|
|
1035
|
-
}
|
|
1036
|
-
document.querySelectorAll("table.geml-table").forEach(function (table) {
|
|
1037
|
-
var tbody = table.tBodies[0];
|
|
1038
|
-
if (!tbody) return;
|
|
1039
|
-
// Sort on header click.
|
|
1040
|
-
var ths = table.tHead ? table.tHead.rows[0].cells : [];
|
|
1041
|
-
Array.prototype.forEach.call(ths, function (th, col) {
|
|
1042
|
-
th.addEventListener("click", function () {
|
|
1043
|
-
var dir = th.classList.contains("asc") ? "desc" : "asc";
|
|
1044
|
-
Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
|
|
1045
|
-
th.classList.add(dir);
|
|
1046
|
-
var rows = Array.prototype.slice.call(tbody.rows);
|
|
1047
|
-
rows.sort(function (r1, r2) {
|
|
1048
|
-
var c = cmp(r1.cells[col], r2.cells[col]);
|
|
1049
|
-
return dir === "asc" ? c : -c;
|
|
1050
|
-
});
|
|
1051
|
-
rows.forEach(function (r) { tbody.appendChild(r); });
|
|
1052
|
-
});
|
|
1053
|
-
});
|
|
1054
|
-
// Filter rows.
|
|
1055
|
-
var fig = table.closest(".table-figure");
|
|
1056
|
-
var input = fig ? fig.querySelector(".table-filter") : null;
|
|
1057
|
-
if (input) input.addEventListener("input", function () {
|
|
1058
|
-
var q = input.value.toLowerCase();
|
|
1059
|
-
Array.prototype.forEach.call(tbody.rows, function (r) {
|
|
1060
|
-
r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
|
|
1061
|
-
});
|
|
1062
|
-
});
|
|
1063
|
-
});
|
|
1064
|
-
})();
|
|
1333
|
+
export const JS = `
|
|
1334
|
+
(function () {
|
|
1335
|
+
function cmp(a, b) {
|
|
1336
|
+
var na = a.dataset.sort, nb = b.dataset.sort;
|
|
1337
|
+
if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
|
|
1338
|
+
return (a.textContent || "").localeCompare(b.textContent || "");
|
|
1339
|
+
}
|
|
1340
|
+
document.querySelectorAll("table.geml-table").forEach(function (table) {
|
|
1341
|
+
var tbody = table.tBodies[0];
|
|
1342
|
+
if (!tbody) return;
|
|
1343
|
+
// Sort on header click.
|
|
1344
|
+
var ths = table.tHead ? table.tHead.rows[0].cells : [];
|
|
1345
|
+
Array.prototype.forEach.call(ths, function (th, col) {
|
|
1346
|
+
th.addEventListener("click", function () {
|
|
1347
|
+
var dir = th.classList.contains("asc") ? "desc" : "asc";
|
|
1348
|
+
Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
|
|
1349
|
+
th.classList.add(dir);
|
|
1350
|
+
var rows = Array.prototype.slice.call(tbody.rows);
|
|
1351
|
+
rows.sort(function (r1, r2) {
|
|
1352
|
+
var c = cmp(r1.cells[col], r2.cells[col]);
|
|
1353
|
+
return dir === "asc" ? c : -c;
|
|
1354
|
+
});
|
|
1355
|
+
rows.forEach(function (r) { tbody.appendChild(r); });
|
|
1356
|
+
});
|
|
1357
|
+
});
|
|
1358
|
+
// Filter rows.
|
|
1359
|
+
var fig = table.closest(".table-figure");
|
|
1360
|
+
var input = fig ? fig.querySelector(".table-filter") : null;
|
|
1361
|
+
if (input) input.addEventListener("input", function () {
|
|
1362
|
+
var q = input.value.toLowerCase();
|
|
1363
|
+
Array.prototype.forEach.call(tbody.rows, function (r) {
|
|
1364
|
+
r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
});
|
|
1368
|
+
})();
|
|
1065
1369
|
`;
|
|
1066
1370
|
// geml-code-graph runtime: layered layout AT DRAW TIME (GEP-0003 / v2-D8) so
|
|
1067
1371
|
// clicking a node re-roots the view inside the embedded slice. Algorithm as
|