@geml/geml 1.1.1 → 1.3.2

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/dist/history.js CHANGED
@@ -205,16 +205,46 @@ function applyReverse(textLf, ops, blobs) {
205
205
  throw new Error(`history: unresolved blob:${id}`);
206
206
  return p.split("\n");
207
207
  };
208
+ // `delete`/`replace` name units in the INPUT document (v_new) — their `~n`
209
+ // occurrence suffixes are numbered over the whole of v_new. So resolve them
210
+ // against ONE snapshot of the unmutated input, not the live array: re-keying
211
+ // per op lets an earlier delete renumber a later op's occurrence (delete @h
212
+ // then delete @h~1 — after the first splice the survivor renumbers @h~1→@h
213
+ // and the second op can no longer find it). Keys are unique within a single
214
+ // keyedUnits() call, so the snapshot map is a bijection.
215
+ const snap = keyedUnits(lines);
216
+ const byKey = new Map(snap.map((k) => [k.key, k.u]));
217
+ const resolveSnap = (key) => {
218
+ const u = byKey.get(key);
219
+ if (!u)
220
+ throw new Error(`history: unit ${key} not found while applying reverse patch`);
221
+ return u;
222
+ };
223
+ const rangeEdits = [];
224
+ const anchored = []; // insert / move — resolved against the LIVE array below
208
225
  for (const op of ops) {
209
226
  if (op.kind === "delete") {
210
- const u = locateUnit(lines, op.key);
211
- lines.splice(u.start, u.endExcl - u.start);
227
+ const u = resolveSnap(op.key);
228
+ rangeEdits.push({ start: u.start, endExcl: u.endExcl, repl: [] });
212
229
  }
213
230
  else if (op.kind === "replace") {
214
- const u = locateUnit(lines, op.key);
215
- lines.splice(u.start, u.endExcl - u.start, ...blob(op.blob));
231
+ const u = resolveSnap(op.key);
232
+ rangeEdits.push({ start: u.start, endExcl: u.endExcl, repl: blob(op.blob) });
216
233
  }
217
- else if (op.kind === "insert") {
234
+ else {
235
+ anchored.push(op);
236
+ }
237
+ }
238
+ rangeEdits.sort((a, b) => b.start - a.start);
239
+ for (const e of rangeEdits)
240
+ lines.splice(e.start, e.endExcl - e.start, ...e.repl);
241
+ // Inserts (and moves) run only after every delete/replace, so the array is
242
+ // already in its reconstructed (v_parent) shape: an insert's anchor names a
243
+ // v_parent unit, and chained inserts build on units added just before them —
244
+ // both need LIVE re-keying, which is correct here precisely because the
245
+ // occurrence keying is now stable (no more same-keyspace ops pending).
246
+ for (const op of anchored) {
247
+ if (op.kind === "insert") {
218
248
  insertAt(lines, op.anchor, blob(op.blob));
219
249
  }
220
250
  else { // move: cut the unit (with its owned blanks) and re-insert at anchor
@@ -228,68 +258,49 @@ function applyReverse(textLf, ops, blobs) {
228
258
  }
229
259
  /** LCS alignment of unit-key sequences; aMatch[i] = matched index in b, or -1.
230
260
  *
231
- * Fast path: content-keyed units (`@hash~n`) are unique by construction, and in
232
- * a well-formed document `#id` keys are unique too and the LCS of two
233
- * all-unique sequences is exactly the longest increasing subsequence of a's
234
- * keys mapped to b's positions: O(n log n) instead of the O(n·m) DP table.
235
- * That difference is GEP-0002's measured bottleneck (seconds at 10⁴ units,
236
- * minutes at 10 code-graph documents live there).
261
+ * keyedUnits() assigns keys that are unique WITHIN each sequence (the `~n`
262
+ * occurrence suffix disambiguates equal content / repeated ids), so both `a`
263
+ * and `b` are all-unique. The LCS of two all-unique sequences is exactly the
264
+ * longest increasing subsequence of a's keys mapped to b's positions:
265
+ * O(n log n) instead of an O(n·m) DP table — GEP-0002's measured bottleneck
266
+ * (seconds at 10⁴ units, minutes at 10⁵; code-graph documents live there).
237
267
  *
238
- * A document with DUPLICATE `#id`s (a GEML error, but history never parses) can
239
- * break uniqueness, so the DP remains as the fallback for that case. Either
240
- * path yields *a* maximal alignment; commit()'s byte-exact round-trip gate
241
- * rejects any diff that fails to reproduce the parent, whichever path ran. */
268
+ * If keys were ever non-unique (no public entry point produces that the only
269
+ * caller, diffReverse, feeds keyedUnits output), posInB keeps b's LAST index
270
+ * per key and the LIS still yields *a* valid monotonic matching; commit()'s
271
+ * byte-exact round-trip gate rejects any diff that fails to reproduce the
272
+ * parent regardless. */
242
273
  function lcsMatch(a, b) {
243
274
  const n = a.length, m = b.length;
244
275
  const aMatch = new Array(n).fill(-1);
245
- if (new Set(a).size === n && new Set(b).size === m) {
246
- const posInB = new Map();
247
- for (let j = 0; j < m; j++)
248
- posInB.set(b[j], j);
249
- const ai = [], bj = []; // a-index / b-position of common keys, in a-order
250
- for (let i = 0; i < n; i++) {
251
- const j = posInB.get(a[i]);
252
- if (j !== undefined) {
253
- ai.push(i);
254
- bj.push(j);
255
- }
256
- }
257
- // Patience LIS over bj (strictly increasing) with predecessor links.
258
- const tails = []; // index into bj of the smallest tail per LIS length
259
- const prev = new Array(bj.length).fill(-1);
260
- for (let x = 0; x < bj.length; x++) {
261
- let lo = 0, hi = tails.length;
262
- while (lo < hi) {
263
- const mid = (lo + hi) >> 1;
264
- if (bj[tails[mid]] < bj[x])
265
- lo = mid + 1;
266
- else
267
- hi = mid;
268
- }
269
- prev[x] = lo > 0 ? tails[lo - 1] : -1;
270
- tails[lo] = x;
271
- }
272
- for (let cur = tails.length ? tails[tails.length - 1] : -1; cur >= 0; cur = prev[cur]) {
273
- aMatch[ai[cur]] = bj[cur];
276
+ const posInB = new Map();
277
+ for (let j = 0; j < m; j++)
278
+ posInB.set(b[j], j);
279
+ const ai = [], bj = []; // a-index / b-position of common keys, in a-order
280
+ for (let i = 0; i < n; i++) {
281
+ const j = posInB.get(a[i]);
282
+ if (j !== undefined) {
283
+ ai.push(i);
284
+ bj.push(j);
274
285
  }
275
- return aMatch;
276
286
  }
277
- // Fallback (duplicate keys): classic DP.
278
- const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
279
- for (let i = n - 1; i >= 0; i--)
280
- for (let j = m - 1; j >= 0; j--)
281
- dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
282
- let i = 0, j = 0;
283
- while (i < n && j < m) {
284
- if (a[i] === b[j]) {
285
- aMatch[i] = j;
286
- i++;
287
- j++;
288
- }
289
- else if (dp[i + 1][j] >= dp[i][j + 1])
290
- i++;
291
- else
292
- j++;
287
+ // Patience LIS over bj (strictly increasing) with predecessor links.
288
+ const tails = []; // index into bj of the smallest tail per LIS length
289
+ const prev = new Array(bj.length).fill(-1);
290
+ for (let x = 0; x < bj.length; x++) {
291
+ let lo = 0, hi = tails.length;
292
+ while (lo < hi) {
293
+ const mid = (lo + hi) >> 1;
294
+ if (bj[tails[mid]] < bj[x])
295
+ lo = mid + 1;
296
+ else
297
+ hi = mid;
298
+ }
299
+ prev[x] = lo > 0 ? tails[lo - 1] : -1;
300
+ tails[lo] = x;
301
+ }
302
+ for (let cur = tails.length ? tails[tails.length - 1] : -1; cur >= 0; cur = prev[cur]) {
303
+ aMatch[ai[cur]] = bj[cur];
293
304
  }
294
305
  return aMatch;
295
306
  }
@@ -529,17 +540,46 @@ export function verify(historyPath, gemlPath) {
529
540
  catch (e) {
530
541
  errors.push(String(e.message));
531
542
  }
543
+ // Reconstruct every revision INCREMENTALLY. `reconstruct(h, id)` on its own
544
+ // rebuilds each revision from the nearest keyframe, replaying up to O(N) ops
545
+ // each time; calling it once per revision is therefore O(N²·K) and lets a
546
+ // ~91 KB sidecar take ~a minute. Because the chain runs newest->oldest and
547
+ // (for revisions without their own keyframe) nearest-keyframe(i) is always
548
+ // nearest-keyframe(i-1), reconstruct(chain[i]) == applyReverse(reconstruct(
549
+ // chain[i-1]), chain[i-1].ops). So we carry the previous revision's content
550
+ // forward and apply ONE reverse patch per step — O(N·K) overall — while
551
+ // validating the exact same reconstructed bytes for every revision. Whenever
552
+ // that carried base is not trustworthy (a keyframe-less head, or right after
553
+ // a step threw) we fall back to the full `reconstruct`, which reproduces the
554
+ // original's behaviour and error messages verbatim.
555
+ let prevContent = null;
556
+ let prevOps = null;
557
+ let baseValid = false;
532
558
  for (const r of chain) {
533
559
  try {
534
- const content = reconstruct(h, r.id);
560
+ let content;
561
+ if (h.keyframes.has(r.id)) {
562
+ content = h.keyframes.get(r.id);
563
+ }
564
+ else if (baseValid && prevContent !== null && prevOps !== null) {
565
+ content = applyReverse(prevContent, prevOps, h.blobs);
566
+ }
567
+ else {
568
+ content = reconstruct(h, r.id);
569
+ }
535
570
  if (!hashMatchesRecorded(content, r, h.nl)) {
536
571
  errors.push(`revision ${r.id}: reconstructed hash ${fullHash(content, nlOf(r.newline) ?? h.nl)} != recorded ${r.hash}`);
537
572
  }
573
+ prevContent = content;
574
+ baseValid = true;
538
575
  checked++;
539
576
  }
540
577
  catch (e) {
541
578
  errors.push(`revision ${r.id}: ${e.message}`);
579
+ prevContent = null;
580
+ baseValid = false; // a failed step is not a valid base for the next
542
581
  }
582
+ prevOps = r.ops;
543
583
  }
544
584
  if (gemlPath && existsSync(gemlPath)) {
545
585
  const { lf, nl } = loadBytes(gemlPath);
package/dist/inline.d.ts CHANGED
@@ -49,4 +49,5 @@ export interface Ref {
49
49
  export interface RefSink {
50
50
  refs: Ref[];
51
51
  }
52
- export declare function parseInline(s: string, line: number, sink: RefSink): Inline[];
52
+ export declare const META_REF_SRC = "\\{\\{\\s*([A-Za-z_][A-Za-z0-9_-]*)\\s*\\}\\}";
53
+ export declare function parseInline(s: string, line: number, sink: RefSink, depth?: number): Inline[];
package/dist/inline.js CHANGED
@@ -6,7 +6,41 @@
6
6
  // internal/cross-document reference is reported to a `RefSink` so the document
7
7
  // layer can resolve and validate it at build time (§8).
8
8
  import { parseAttrs } from "./attrs.js";
9
- const SCHEME = /^[a-z][a-z0-9+.-]*:/i; // http:, https:, mailto:,
9
+ const MAX_INLINE_NESTING = 100; // cap parseInline<->scanAtoms recursion (R2-7 DoS)
10
+ // §4: the source pattern of a `{{key}}` metadata reference. Owned here as the
11
+ // single definition of what a reference looks like — the parser substitutes it
12
+ // (geml.ts), the serializer escapes it on emit (serialize.ts), and the md
13
+ // converter escapes it on conversion (from-md.ts). Build flagged variants with
14
+ // `new RegExp(META_REF_SRC, flags)`.
15
+ export const META_REF_SRC = "\\{\\{\\s*([A-Za-z_][A-Za-z0-9_-]*)\\s*\\}\\}";
16
+ // §5: URL schemes that may be emitted as an href/src. A destination that names
17
+ // any other scheme (javascript:, vbscript:, data:text/html, file:, …) is a
18
+ // script-injection / local-read vector at the HTML sink, so it is neutralized
19
+ // here at the parse layer — every consumer of the model inherits the guard.
20
+ const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
21
+ // The leading `scheme:` (RFC-3986 grammar), lowercased — or null when the
22
+ // destination has none (a relative path, `#anchor`, or cross-document ref).
23
+ function schemeOf(url) {
24
+ // Browsers strip leading/embedded C0 controls and spaces before acting on a
25
+ // URL, so `java\tscript:` and `\x01javascript:` execute as javascript:. Strip
26
+ // every [\x00-\x20] before detecting the scheme so the allowlist can't be
27
+ // evaded that way (R2-2).
28
+ const m = /^([a-z][a-z0-9+.-]*):/i.exec(url.replace(/[\x00-\x20]/g, ""));
29
+ return m ? m[1].toLowerCase() : null;
30
+ }
31
+ // A destination is safe to emit when it has no scheme (relative / anchor /
32
+ // cross-doc), or names an allowlisted scheme. `data:` is permitted only for
33
+ // media and only for `image/*` payloads (never `data:text/html`, which scripts).
34
+ function isSafeUrl(url, allowDataImage = false) {
35
+ const scheme = schemeOf(url);
36
+ if (scheme === null)
37
+ return true;
38
+ if (SAFE_SCHEMES.has(scheme))
39
+ return true;
40
+ if (allowDataImage && scheme === "data")
41
+ return /^\s*data:image\//i.test(url);
42
+ return false;
43
+ }
10
44
  // §5.1: when `as` is omitted, infer the media kind from the source extension.
11
45
  const VIDEO_EXT = /\.(mp4|webm|mov|m4v|ogv|mkv)(?:[?#].*)?$/i;
12
46
  const AUDIO_EXT = /\.(mp3|wav|ogg|oga|m4a|flac|aac|opus)(?:[?#].*)?$/i;
@@ -23,8 +57,12 @@ function inferAs(src) {
23
57
  // Classify a link/image destination into {href|doc, anchor}.
24
58
  function classifyDest(dest) {
25
59
  const d = dest.trim();
26
- if (SCHEME.test(d))
27
- return { href: d };
60
+ if (schemeOf(d) !== null) {
61
+ // Scheme-bearing destination: emit as an href only if the scheme is
62
+ // allowlisted; otherwise drop it entirely so the link renders inert
63
+ // (render() defaults a hrefless link to `#`, keeping the visible text).
64
+ return isSafeUrl(d) ? { href: d } : {};
65
+ }
28
66
  const hash = d.indexOf("#");
29
67
  if (hash === 0)
30
68
  return { anchor: d.slice(1) };
@@ -82,7 +120,7 @@ function readAttrs(s, i) {
82
120
  // Phase A: pull out high-priority atoms (escapes, code, math, media, links,
83
121
  // auto-refs, footnotes, hard breaks). Everything else is left as text runs for
84
122
  // phase B (emphasis). Children of links are fully re-parsed.
85
- function scanAtoms(s, line, sink) {
123
+ function scanAtoms(s, line, sink, depth = 0) {
86
124
  const out = [];
87
125
  let buf = "";
88
126
  const flush = () => { if (buf) {
@@ -150,8 +188,14 @@ function scanAtoms(s, line, sink) {
150
188
  if (label && paren) {
151
189
  const a = readAttrs(s, paren.end);
152
190
  const attrObj = a ? a.attrs : { classes: [], attrs: {} };
191
+ // Media src bypasses classifyDest, so guard the scheme here: a disallowed
192
+ // scheme (javascript:, data:text/html, …) is neutralized to an empty src
193
+ // so the HTML sink cannot load/execute it. Relative paths, http(s), and
194
+ // image/* data URIs pass through.
195
+ const rawSrc = paren.content.trim();
196
+ const src = isSafeUrl(rawSrc, true) ? rawSrc : "";
153
197
  const node = {
154
- type: "image", alt: label.content, src: paren.content.trim(), attrs: attrObj.attrs,
198
+ type: "image", alt: label.content, src, attrs: attrObj.attrs,
155
199
  };
156
200
  const as = attrObj.attrs["as"];
157
201
  if (typeof as === "string")
@@ -207,7 +251,7 @@ function scanAtoms(s, line, sink) {
207
251
  const dest = classifyDest(paren.content);
208
252
  const node = {
209
253
  type: "link",
210
- children: parseInline(label.content, line, sink),
254
+ children: parseInline(label.content, line, sink, depth + 1),
211
255
  attrs: attrObj.attrs,
212
256
  };
213
257
  if (dest.href)
@@ -405,8 +449,17 @@ function mergeText(ns) {
405
449
  }
406
450
  return out;
407
451
  }
408
- export function parseInline(s, line, sink) {
409
- const atoms = scanAtoms(s, line, sink);
452
+ export function parseInline(s, line, sink, depth = 0) {
453
+ if (depth > MAX_INLINE_NESTING) {
454
+ // Pathological nesting (thousands of nested link labels) would overflow the
455
+ // call stack (R2-7). Degrade the over-deep content to text — emphasis only,
456
+ // no further link recursion — and flag it; never throw RangeError.
457
+ const diags = sink.diags;
458
+ if (Array.isArray(diags) && !diags.some((d) => d.message.startsWith("inline nesting too deep")))
459
+ diags.push({ severity: "error", message: `inline nesting too deep (max ${MAX_INLINE_NESTING})`, line });
460
+ return mergeText(emphasize(s));
461
+ }
462
+ const atoms = scanAtoms(s, line, sink, depth);
410
463
  const out = [];
411
464
  for (const a of atoms) {
412
465
  if (typeof a === "string")
@@ -0,0 +1,3 @@
1
+ import { type Document } from "./geml.js";
2
+ import { type RenderOptions } from "./render.js";
3
+ export declare function renderHtml(doc: Document, opts?: RenderOptions): string;
@@ -0,0 +1,95 @@
1
+ // GEML CLI HTML export — the standalone, self-contained page.
2
+ //
3
+ // This is the CLI-only entry point that wraps a rendered document body in a
4
+ // full HTML page shell. Math (KaTeX) and Mermaid load from a CDN, and only when
5
+ // the document actually uses them, so a document of prose, tables and charts is
6
+ // fully self-contained with zero network.
7
+ //
8
+ // It lives in its OWN module (separate from ./render) so that consumers who only
9
+ // need the in-browser graph runtime (buildCodeGraph/codeGraphRuntime/
10
+ // codeGraphWaves) — notably the browser-extension viewer bundle — never pull in
11
+ // these CDN/remote-script string literals. The Chrome Web Store scanner rejects
12
+ // bundles that contain remotely-hosted-code references.
13
+ import { CSS, JS, CODE_GRAPH_JS, RenderCtx, esc, escAttr, } from "./render.js";
14
+ function page(title, body, ctx, source) {
15
+ const mathHead = ctx.usedMath
16
+ ? `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">\n` +
17
+ `<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>\n` +
18
+ `<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body,{delimiters:[{left:'\\\\[',right:'\\\\]',display:true},{left:'\\\\(',right:'\\\\)',display:false}]})"></script>\n`
19
+ : "";
20
+ const mermaidHead = ctx.usedMermaid
21
+ ? `<script type="module">import m from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";m.initialize({startOnLoad:true});</script>\n`
22
+ : "";
23
+ const footer = source
24
+ ? `<footer class="geml-footer">Rendered from <code>${esc(source)}</code> by the GEML runtime. Tables are sortable and filterable; the chart is inline SVG drawn from its bound table.</footer>`
25
+ : "";
26
+ // Live enhancement for served pages: attach _cgView loaders after the
27
+ // static bootstrap has drawn. The runtime reads the hook lazily, so late
28
+ // binding works with no redraw; if this module never loads (offline copy,
29
+ // old browser), the page simply stays static. The parser dist imports
30
+ // node:* for its CLI paths — an import map points those at the served stub
31
+ // (same trick as the viewer's esbuild alias), and the process shim must be
32
+ // in place BEFORE the modules evaluate, hence the dynamic import().
33
+ const wantLive = ctx.usedCodeGraph && !!ctx.opts.liveGraph;
34
+ const lg = wantLive ? escAttr(ctx.opts.liveGraph) : "";
35
+ const importMap = wantLive
36
+ ? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js"}}</script>\n`
37
+ : "";
38
+ const liveJs = wantLive
39
+ ? `<script type="module">
40
+ globalThis.process ??= { argv: [], env: {} };
41
+ const { parse } = await import("${lg}geml.js");
42
+ const { codeGraphWaves } = await import("${lg}render.js");
43
+ const w = codeGraphWaves(async (rel) => {
44
+ try { const r = await fetch(rel, { cache: "no-cache" }); return r.ok ? await r.text() : null; } catch { return null; }
45
+ }, parse);
46
+ for (const m of document.querySelectorAll(".cg-mount[data-start]")) {
47
+ const start = m.getAttribute("data-start");
48
+ m._cgView = async (view) => {
49
+ // A directed view builds from the node's OWN document (its meta names the
50
+ // module and graph-depth); {doc} opens that document; else the mount's.
51
+ const src = view && view.doc ? view.doc
52
+ : view && view.node ? view.node.slice(0, view.node.lastIndexOf("#"))
53
+ : start;
54
+ const r = await w.build(src, view && view.doc ? undefined : view);
55
+ return r.error !== undefined ? null : r.data;
56
+ };
57
+ }
58
+ </script>\n`
59
+ : "";
60
+ return `<!doctype html>
61
+ <html lang="en">
62
+ <head>
63
+ <meta charset="utf-8">
64
+ <meta name="viewport" content="width=device-width, initial-scale=1">
65
+ <title>${esc(title)}</title>
66
+ <style>${CSS}</style>
67
+ ${importMap}${mathHead}${mermaidHead}</head>
68
+ <body>
69
+ <main>
70
+ ${body}
71
+ </main>
72
+ ${footer}
73
+ <script>${JS}</script>
74
+ ${ctx.usedCodeGraph ? `<script>${CODE_GRAPH_JS}</script>\n` : ""}${liveJs}</body>
75
+ </html>
76
+ `;
77
+ }
78
+ export function renderHtml(doc, opts = {}) {
79
+ const ctx = new RenderCtx(doc, opts);
80
+ let body = doc.children.map((b) => ctx.block(b)).filter((s) => s !== "").join("\n");
81
+ // Codemap scenario ① (GEP-0003): a codemap document (meta declares module=
82
+ // or container=, plus an entry surface) IS the graph data — offer the layered
83
+ // method-flow view at the top, an implicit self-embed.
84
+ const meta = doc.children.find((b) => b.kind === "block" && b.type === "meta" && b.data);
85
+ const md = meta?.data ?? {};
86
+ if ((md["module"] !== undefined || md["container"] !== undefined)
87
+ && opts.loadDoc && opts.parseDoc && opts.source) {
88
+ const cap = md["entry"] !== undefined || md["container"] !== undefined
89
+ ? `layered method flow — roots from this document's <code>entry</code>`
90
+ : `layered method flow — roots: in-degree-zero methods (no <code>entry</code> declared)`;
91
+ body = ctx.codeGraphFigure(opts.source, "", `<figcaption>${cap}</figcaption>`) + "\n" + body;
92
+ }
93
+ const title = opts.title ?? ctx.docTitle() ?? "GEML document";
94
+ return page(title, body, ctx, opts.source);
95
+ }
package/dist/render.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type Document } from "./geml.js";
1
+ import { type Block, type Document } from "./geml.js";
2
+ import { type Inline } from "./inline.js";
2
3
  export interface RenderOptions {
3
4
  title?: string;
4
5
  source?: string;
@@ -8,6 +9,32 @@ export interface RenderOptions {
8
9
  liveGraph?: string;
9
10
  graphSidecar?: string;
10
11
  }
12
+ export declare function esc(s: string): string;
13
+ export declare function escAttr(s: string): string;
14
+ export declare class RenderCtx {
15
+ private doc;
16
+ readonly opts: RenderOptions;
17
+ usedMath: boolean;
18
+ usedMermaid: boolean;
19
+ usedCodeGraph: boolean;
20
+ private renderDepth;
21
+ labels: Map<string, string>;
22
+ constructor(doc: Document, opts?: RenderOptions);
23
+ get isCodemapDoc(): boolean;
24
+ private indexLabels;
25
+ docTitle(): string | undefined;
26
+ inlines(ns: Inline[]): string;
27
+ private inline;
28
+ private media;
29
+ private link;
30
+ block(b: Block): string;
31
+ private blockInner;
32
+ private list;
33
+ private typed;
34
+ private diagram;
35
+ codeGraphFigure(src: string, idAttr: string, cap: string): string;
36
+ private table;
37
+ }
11
38
  interface CGNode {
12
39
  n: string;
13
40
  doc?: string;
@@ -16,6 +43,7 @@ interface CGNode {
16
43
  test?: boolean;
17
44
  acc?: boolean;
18
45
  more?: boolean;
46
+ entry?: boolean;
19
47
  grp?: string[];
20
48
  ext?: number;
21
49
  }
@@ -47,9 +75,12 @@ declare function buildCodeGraph(startRel: string, opts: RenderOptions, view?: {
47
75
  error?: string;
48
76
  truncated?: boolean;
49
77
  };
78
+ export declare const CSS = "\n:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }\n* { box-sizing: border-box; }\nbody { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,\"PingFang SC\",\"Microsoft Yahei\",sans-serif; }\nmain { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }\nh1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }\nh1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }\nh3 { font-size:1.25em; } h4 { font-size:1em; }\np { margin:.7em 0; }\na { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }\ncode { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }\npre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }\npre code { background:none; padding:0; font-size:.85em; }\npre.output { background:#0d1117; color:#e6edf3; }\npre.output code { color:inherit; }\nul,ol { padding-left:1.6em; } li { margin:.2em 0; }\nul.task-list { list-style:none; padding-left:.2em; }\nli.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; }\nli.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }\nli.task input[type=checkbox]:checked::after { content:\"\u2713\"; 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; }\naside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }\naside.aside { border-left-color:#8b949e; background:#f6f8fa; }\naside.warning { border-left-color:#d97706; background:#fff8f0; }\naside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }\nfigure { margin:1.2em 0; }\nfigcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }\ntable.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }\ntable.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }\ntable.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }\ntable.geml-table thead th::after { content:\" \\2195\"; color:var(--muted); font-size:.8em; }\ntable.geml-table thead th.asc::after { content:\" \\2191\"; color:var(--accent); }\ntable.geml-table thead th.desc::after { content:\" \\2193\"; color:var(--accent); }\ntable.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }\ntable.geml-table td.computed { color:#0a7c52; }\ntable.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }\n.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; }\n.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }\n.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }\n.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }\n.c-title { font-size:15px; font-weight:600; fill:var(--fg); }\n.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }\n.media { max-width:100%; border-radius:8px; }\n.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }\n.math-block { overflow-x:auto; padding:.4em 0; }\nsup.fn a { font-size:.75em; }\n.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }\n.geml-footer code { font-size:.95em; }\n.code-graph { margin:1.4em 0; }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }\n.cg-svg { display:block; }\n.cg-search-wrap { position:relative; display:inline-block; }\n.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; }\n.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); }\n.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; }\n.cg-search-row:hover { background:var(--bd); }\n.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }\n.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }\n.cg-search-grp:first-of-type { border-top:0; }\n.cg-stage { display:flex; gap:10px; align-items:flex-start; }\n.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }\n.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); }\n.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; }\n.cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }\n.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; }\n.cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }\n.cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }\n.cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }\n.cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }\n.cg-crumb .cg-seg:hover { text-decoration:underline; }\n.cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }\n.cg-flash { color:#b42318; }\n.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; }\n.cg-upbtn { cursor:pointer; }\n.cg-upbtn circle { fill:#fff; stroke:#94a3b8; }\n.cg-upbtn text { font-size:11px; fill:#57606a; }\n.cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }\n.cg-upbtn:hover text { fill:var(--accent); }\n.cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }\n.cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }\n.cg-chip { display:inline-flex; align-items:center; gap:4px; }\n.cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }\n.cg-note { font-size:.8em; color:#9a6700; }\n.cg-n rect { fill:#eef2f7; stroke:#94a3b8; }\n.cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }\n.cg-n { cursor:pointer; }\n.cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }\n.cg-n.leaf { opacity:.45; }\n.cg-n.test rect { stroke-dasharray:3 2; }\n.cg-n.grp rect { stroke-width:1.8; }\n.cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }\n.cg-e.cand { stroke-dasharray:2 3; }\n.cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }\n.cg-e.soft { opacity:.55; }\n.cg-svg.hl .cg-n { opacity:.22; }\n.cg-svg.hl .cg-e { opacity:.1; }\n.cg-svg.hl .cg-n.hl { opacity:1; }\n.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }\n";
79
+ export declare const JS = "\n(function () {\n function cmp(a, b) {\n var na = a.dataset.sort, nb = b.dataset.sort;\n if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);\n return (a.textContent || \"\").localeCompare(b.textContent || \"\");\n }\n document.querySelectorAll(\"table.geml-table\").forEach(function (table) {\n var tbody = table.tBodies[0];\n if (!tbody) return;\n // Sort on header click.\n var ths = table.tHead ? table.tHead.rows[0].cells : [];\n Array.prototype.forEach.call(ths, function (th, col) {\n th.addEventListener(\"click\", function () {\n var dir = th.classList.contains(\"asc\") ? \"desc\" : \"asc\";\n Array.prototype.forEach.call(ths, function (h) { h.classList.remove(\"asc\", \"desc\"); });\n th.classList.add(dir);\n var rows = Array.prototype.slice.call(tbody.rows);\n rows.sort(function (r1, r2) {\n var c = cmp(r1.cells[col], r2.cells[col]);\n return dir === \"asc\" ? c : -c;\n });\n rows.forEach(function (r) { tbody.appendChild(r); });\n });\n });\n // Filter rows.\n var fig = table.closest(\".table-figure\");\n var input = fig ? fig.querySelector(\".table-filter\") : null;\n if (input) input.addEventListener(\"input\", function () {\n var q = input.value.toLowerCase();\n Array.prototype.forEach.call(tbody.rows, function (r) {\n r.style.display = (r.textContent || \"\").toLowerCase().indexOf(q) >= 0 ? \"\" : \"none\";\n });\n });\n });\n})();\n";
50
80
  export declare function codeGraphRuntime(root: {
51
81
  querySelectorAll(sel: string): ArrayLike<Element>;
52
82
  }): void;
83
+ export declare const CODE_GRAPH_JS: string;
53
84
  export declare function codeGraphWaves(fetchDoc: (rel: string) => Promise<string | null>, parseFn: (s: string) => Document): {
54
85
  build: (src: string, view?: {
55
86
  dir?: "up" | "down";
@@ -62,4 +93,3 @@ export declare function codeGraphWaves(fetchDoc: (rel: string) => Promise<string
62
93
  seed: (name: string, text: string | null) => void;
63
94
  };
64
95
  export { buildCodeGraph };
65
- export declare function renderHtml(doc: Document, opts?: RenderOptions): string;