@geml/geml 1.7.0 → 1.7.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
@@ -38,7 +38,11 @@ export function stampUTC(d) {
38
38
  // ---------------------------------------------------------------------------
39
39
  // Top-level fenced-block locator (for the history file's own structure)
40
40
  // ---------------------------------------------------------------------------
41
- const FENCE_OPEN = /^(={3,})[ \t]+([A-Za-z][A-Za-z0-9_-]*)[ \t]*(\{.*\})?[ \t]*$/;
41
+ // Trailing whitespace nested inside the optional attrs group — the flat form
42
+ // let two runs split the same tabs every possible way (quadratic; see the note
43
+ // on geml.ts's FENCE_OPEN, which this mirrors). Same language, verified over
44
+ // 150k random strings.
45
+ const FENCE_OPEN = /^(={3,})[ \t]+([A-Za-z][A-Za-z0-9_-]*)[ \t]*(?:(\{.*\})[ \t]*)?$/;
42
46
  function locate(lines) {
43
47
  const out = [];
44
48
  let i = 0;
@@ -50,7 +54,7 @@ function locate(lines) {
50
54
  const idm = /#([A-Za-z][A-Za-z0-9_-]*)/.exec(attrLine);
51
55
  let j = i + 1;
52
56
  while (j < lines.length) {
53
- const t = lines[j].replace(/\s+$/, "");
57
+ const t = lines[j].trimEnd();
54
58
  if (/^=+$/.test(t) && t.length === fenceLen)
55
59
  break;
56
60
  j++;
@@ -108,7 +112,7 @@ function tile(lines) {
108
112
  id = /#([A-Za-z][A-Za-z0-9_-]*)/.exec(fo[3] ?? "")?.[1];
109
113
  i++;
110
114
  while (i < n) {
111
- const t = lines[i].replace(/\s+$/, "");
115
+ const t = lines[i].trimEnd();
112
116
  const close = /^=+$/.test(t) && t.length === fenceLen;
113
117
  i++;
114
118
  if (close)
package/dist/mcp.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // `geml mcp` — MCP server for GEML documents and the code graph.
3
3
  //
4
- // Ten tools over a confined root directory of `.geml` documents: five read-only,
5
- // five that write, each named after the CLI verb it wraps (`geml set` ->
6
- // `geml_set`, the bare transform entry -> `geml_to`). When that root holds a code graph, the four read-only
4
+ // Eleven tools over a confined root directory of `.geml` documents: six
5
+ // read-only, five that write, each named after the CLI verb it wraps (`geml set`
6
+ // -> `geml_set`, the bare transform entry -> `geml_to`). When that root holds a code graph, the four read-only
7
7
  // code-graph tools from `codemap/mcp-server.mjs` are served from this SAME
8
8
  // process, so a client registers one server instead of two. That file stays a
9
9
  // standalone `geml codemap mcp` entry point; this one imports its tool table
@@ -273,6 +273,44 @@ export const TOOLS = [
273
273
  return run.stdout.trim();
274
274
  },
275
275
  },
276
+ {
277
+ name: "geml_find",
278
+ description: "Search block CONTENT across the served documents and get back ADDRESSES, one row of `<file>\\t<address>` per hit. This is the other half of geml_list: `list` says what a document contains, `find` says which block holds the words you are looking for — and it answers with an address that pastes straight into geml_get or geml_set, never a line number that the next edit invalidates. The address is the innermost block holding the match, and a block that matches on many lines is reported once. Substring, case-insensitive unless `case` is true. Omit `path` to search every `*.geml` under the server root, or give a file or directory to narrow it. No match is not an error: the result is empty.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ pattern: { type: "string", description: "Text to look for inside block bodies" },
283
+ path: { type: "string", description: "Optional file or directory under the server root; default: the whole root" },
284
+ case: { type: "boolean", description: "Match case exactly (default: case-insensitive)" },
285
+ head: { type: "boolean", description: "Add the matching line as a third column" },
286
+ },
287
+ required: ["pattern"],
288
+ },
289
+ run: (args) => {
290
+ if (typeof args.pattern !== "string" || args.pattern === "")
291
+ throw new Error("`pattern` is required");
292
+ // `path` goes through the same confinement gate as every other file
293
+ // argument; omitting it searches the root, which is confined by being it.
294
+ const where = args.path === undefined ? OPTS.root : resolveInRoot(args.path);
295
+ const flags = [...(args.case ? ["--case"] : []), ...(args.head ? ["--head"] : [])];
296
+ const run = runCli(["find", args.pattern, where, ...flags]);
297
+ // Exit 1 means "nothing matched" — a result, not a failure. Anything on
298
+ // stderr is the real error case.
299
+ if (!run.ok && run.stderr)
300
+ throw new Error(run.stderr);
301
+ // The CLI prints the path it was given, which here is absolute. Every
302
+ // other tool in this server speaks paths relative to the root, and a
303
+ // model is meant to paste a row's file straight into geml_get — so put
304
+ // the rows in those coordinates, and keep the server's own layout out of
305
+ // the client's view while we are at it.
306
+ const base = realpathSync(OPTS.root) + sep;
307
+ return run.stdout
308
+ .split("\n")
309
+ .map((line) => (line.startsWith(base) ? line.slice(base.length).replace(/\\/g, "/") : line))
310
+ .join("\n")
311
+ .trim();
312
+ },
313
+ },
276
314
  {
277
315
  name: "geml_get",
278
316
  description: "Read ONE block from a GEML document. Use this instead of reading the whole file: it returns only that block, typically a few percent of the document. Call `geml_list` first and pass back the `address` it gives — that also reaches blocks with no `#id`, which an id alone cannot.",
@@ -282,7 +320,7 @@ export const TOOLS = [
282
320
  file: FILE_ARG,
283
321
  id: {
284
322
  type: "string",
285
- description: "What to read: a block id (with or without `#`), a `## Heading` line (its whole section), `=== type` for every block of a type, or a `@<hex>` content address for a block with no id — the forms `geml_list` prints",
323
+ description: "What to read: a block id (with or without `#`), a `## Heading` line (its whole section), `=== type` for every block of a type, a `@<hex>` content address for a block with no id, or `L27`/`L27-58` for the smallest block holding those lines — the forms `geml_list` prints, plus the line numbers an editor or a diff hunk speaks",
286
324
  },
287
325
  view: {
288
326
  type: "boolean",
@@ -290,8 +328,8 @@ export const TOOLS = [
290
328
  },
291
329
  part: {
292
330
  type: "string",
293
- enum: ["whole", "head", "body"],
294
- description: "How much of the block to return (default: whole). `body` is usually what you want together with `view`.",
331
+ enum: ["whole", "head", "intro", "body"],
332
+ description: "How much of the block to return (default: whole). For a SECTION these cut it three ways: `head` is the heading line, `intro` everything under it up to its first subheading, `body` everything under it — so `body` always contains `intro`, and equals it when the section has no subheading. Reach for `intro` to read a section's opening without pulling its subsections into the conversation; a whole `#id` on a top-level heading is often the entire document. Only a heading has an intro. `body` is usually what you want together with `view`.",
295
333
  },
296
334
  },
297
335
  required: ["file", "id"],
@@ -429,7 +467,7 @@ export const TOOLS = [
429
467
  // ----- write -----
430
468
  {
431
469
  name: "geml_set",
432
- description: "Replace ONE block, leaving every other byte of the document untouched. Prefer this over rewriting a file. The replacement is VALIDATED BEFORE it is written: if it would break the document, nothing is written and you get the diagnostics back — re-read them and fix the body rather than retrying the same content. `part` selects whole block (default), just the head/fence line, or just the body. An address matching SEVERAL blocks is refused — this writes one block, so narrow it first.",
470
+ description: "Replace ONE block, leaving every other byte of the document untouched. Prefer this over rewriting a file. The replacement is VALIDATED BEFORE it is written: if it would break the document, nothing is written and you get the diagnostics back — re-read them and fix the body rather than retrying the same content. Breaking the document is what gets refused — removing content is not: if your replacement drops blocks, the write goes through and the result names every block that went, unnamed ones included, so check that line whenever you shortened a section. `geml_revert` puts one back. The way to keep a block is to keep it in the text you send; `geml_get` on the same address just handed it to you. `part` selects whole block (default), the head/fence line, a section's `intro`, or the body. An address matching SEVERAL blocks is refused — this writes one block, so narrow it first.",
433
471
  inputSchema: {
434
472
  type: "object",
435
473
  properties: {
@@ -439,7 +477,7 @@ export const TOOLS = [
439
477
  description: "Which block to replace: an id (with or without `#`), or a `@<hex>` content address from `geml_list` for a block with no id. Must match exactly one block",
440
478
  },
441
479
  body: { type: "string", description: "The replacement text" },
442
- part: { type: "string", enum: ["whole", "head", "body"], description: "What to replace (default: whole)" },
480
+ part: { type: "string", enum: ["whole", "head", "intro", "body"], description: "What to replace (default: whole). `intro` replaces a section's opening — everything under the heading up to its first subheading — and leaves every subsection byte-identical, which is what makes a read-edit-write cycle on a long section safe. An empty intro (a subheading follows the heading immediately) is written into, so this also adds an opening where there was none." },
443
481
  },
444
482
  required: ["file", "id", "body"],
445
483
  },
@@ -2,7 +2,7 @@ import { type Document } from "./geml.js";
2
2
  import { type RenderOptions } from "./render.js";
3
3
  export declare function renderHtml(doc: Document, opts?: RenderOptions): string;
4
4
  export declare const pageAssets: {
5
- readonly 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:\"✓\"; 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.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */\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";
5
+ readonly 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:\"✓\"; 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/* A table wider than the column scrolls INSIDE its own box — the page itself\n must never scroll sideways. Only the table is in here: the filter box above\n and the caption below stay put instead of sliding out of view with it. */\n.table-scroll { overflow-x:auto; max-width:100%; }\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/* The graph is the widest artifact on the page: let it break out of main's\n 860px reading column and take the viewport, centred, leaving the prose\n around it untouched. Negative inline margins, NOT transform — a transform\n would become the containing block for the fullscreen overlay below. */\n@media (min-width:900px) { .code-graph { margin-inline: calc((100% - min(96vw, 1600px)) / 2); } }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:60vh; max-height:84vh; }\n.cg-svg { display:block; }\n/* Fullscreen: one class drives the layout, whether the native Fullscreen API\n took (mount is in the top layer) or we fell back to a fixed overlay. The\n :fullscreen pseudo-class is deliberately NOT in these selectors — a browser\n that doesn't know it would drop the whole rule; the runtime keeps the class\n in sync with fullscreenchange instead. Toolbar/legend keep their height,\n the stage eats the rest. */\n.cg-mount.cg-full { position:fixed; inset:0; z-index:9999; margin:0; border:0; border-radius:0; padding:10px 14px; background:var(--bg); display:flex; flex-direction:column; }\n.cg-mount.cg-full > .cg-bar, .cg-mount.cg-full > .cg-legend, .cg-mount.cg-full > .cg-groups { flex:0 0 auto; }\n.cg-mount.cg-full .cg-stage { flex:1 1 auto; min-height:0; }\n.cg-mount.cg-full .cg-scroll { min-height:0; max-height:none; height:100%; }\n.cg-mount.cg-full .cg-src-body { max-height:none; }\n.cg-mount.cg-full .cg-frame { flex:1 1 auto; height:auto; }\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:84vh; 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:84vh; 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.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */\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";
6
6
  readonly 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";
7
7
  readonly codeGraphJs: string;
8
8
  };
package/dist/render.d.ts CHANGED
@@ -95,7 +95,7 @@ declare function buildCodeGraph(startRel: string, opts: RenderOptions, view?: {
95
95
  error?: string;
96
96
  truncated?: boolean;
97
97
  };
98
- 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.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */\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";
98
+ 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/* A table wider than the column scrolls INSIDE its own box \u2014 the page itself\n must never scroll sideways. Only the table is in here: the filter box above\n and the caption below stay put instead of sliding out of view with it. */\n.table-scroll { overflow-x:auto; max-width:100%; }\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/* The graph is the widest artifact on the page: let it break out of main's\n 860px reading column and take the viewport, centred, leaving the prose\n around it untouched. Negative inline margins, NOT transform \u2014 a transform\n would become the containing block for the fullscreen overlay below. */\n@media (min-width:900px) { .code-graph { margin-inline: calc((100% - min(96vw, 1600px)) / 2); } }\n.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }\n.cg-scroll { overflow:auto; min-height:60vh; max-height:84vh; }\n.cg-svg { display:block; }\n/* Fullscreen: one class drives the layout, whether the native Fullscreen API\n took (mount is in the top layer) or we fell back to a fixed overlay. The\n :fullscreen pseudo-class is deliberately NOT in these selectors \u2014 a browser\n that doesn't know it would drop the whole rule; the runtime keeps the class\n in sync with fullscreenchange instead. Toolbar/legend keep their height,\n the stage eats the rest. */\n.cg-mount.cg-full { position:fixed; inset:0; z-index:9999; margin:0; border:0; border-radius:0; padding:10px 14px; background:var(--bg); display:flex; flex-direction:column; }\n.cg-mount.cg-full > .cg-bar, .cg-mount.cg-full > .cg-legend, .cg-mount.cg-full > .cg-groups { flex:0 0 auto; }\n.cg-mount.cg-full .cg-stage { flex:1 1 auto; min-height:0; }\n.cg-mount.cg-full .cg-scroll { min-height:0; max-height:none; height:100%; }\n.cg-mount.cg-full .cg-src-body { max-height:none; }\n.cg-mount.cg-full .cg-frame { flex:1 1 auto; height:auto; }\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:84vh; 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:84vh; 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.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */\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";
99
99
  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";
100
100
  export declare function codeGraphRuntime(root: {
101
101
  querySelectorAll(sel: string): ArrayLike<Element>;
package/dist/render.js CHANGED
@@ -671,13 +671,13 @@ export class RenderCtx {
671
671
  if (this.isCodemapDoc) {
672
672
  const summary = `${esc(id ? "#" + id : "table")} · ${allRows.length} rows (preview: first ${maxRows})`;
673
673
  return `<figure class="table-figure"${idAttr}><details><summary>${summary}</summary>${tools}` +
674
- `<table class="geml-table">${thead}<tbody>\n${bodyRows}\n</tbody>${tfoot}</table>${note}</details>${cap}</figure>`;
674
+ `<div class="table-scroll"><table class="geml-table">${thead}<tbody>\n${bodyRows}\n</tbody>${tfoot}</table></div>${note}</details>${cap}</figure>`;
675
675
  }
676
676
  return `<figure class="table-figure"${idAttr}>${tools}` +
677
- `<table class="geml-table">${thead}<tbody>\n${bodyRows}\n</tbody>${tfoot}</table>${note}${cap}</figure>`;
677
+ `<div class="table-scroll"><table class="geml-table">${thead}<tbody>\n${bodyRows}\n</tbody>${tfoot}</table></div>${note}${cap}</figure>`;
678
678
  }
679
679
  return `<figure class="table-figure"${idAttr}>${tools}` +
680
- `<table class="geml-table">${thead}<tbody>\n${bodyRows}\n</tbody>${tfoot}</table>${cap}</figure>`;
680
+ `<div class="table-scroll"><table class="geml-table">${thead}<tbody>\n${bodyRows}\n</tbody>${tfoot}</table></div>${cap}</figure>`;
681
681
  }
682
682
  }
683
683
  // ---------------------------------------------------------------------------
@@ -1302,6 +1302,10 @@ table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
1302
1302
  table.geml-table td.computed { color:#0a7c52; }
1303
1303
  table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
1304
1304
  .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; }
1305
+ /* A table wider than the column scrolls INSIDE its own box — the page itself
1306
+ must never scroll sideways. Only the table is in here: the filter box above
1307
+ and the caption below stay put instead of sliding out of view with it. */
1308
+ .table-scroll { overflow-x:auto; max-width:100%; }
1305
1309
  .table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
1306
1310
  .table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
1307
1311
  .geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
@@ -1314,9 +1318,26 @@ sup.fn a { font-size:.75em; }
1314
1318
  .geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
1315
1319
  .geml-footer code { font-size:.95em; }
1316
1320
  .code-graph { margin:1.4em 0; }
1321
+ /* The graph is the widest artifact on the page: let it break out of main's
1322
+ 860px reading column and take the viewport, centred, leaving the prose
1323
+ around it untouched. Negative inline margins, NOT transform — a transform
1324
+ would become the containing block for the fullscreen overlay below. */
1325
+ @media (min-width:900px) { .code-graph { margin-inline: calc((100% - min(96vw, 1600px)) / 2); } }
1317
1326
  .cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
1318
- .cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }
1327
+ .cg-scroll { overflow:auto; min-height:60vh; max-height:84vh; }
1319
1328
  .cg-svg { display:block; }
1329
+ /* Fullscreen: one class drives the layout, whether the native Fullscreen API
1330
+ took (mount is in the top layer) or we fell back to a fixed overlay. The
1331
+ :fullscreen pseudo-class is deliberately NOT in these selectors — a browser
1332
+ that doesn't know it would drop the whole rule; the runtime keeps the class
1333
+ in sync with fullscreenchange instead. Toolbar/legend keep their height,
1334
+ the stage eats the rest. */
1335
+ .cg-mount.cg-full { position:fixed; inset:0; z-index:9999; margin:0; border:0; border-radius:0; padding:10px 14px; background:var(--bg); display:flex; flex-direction:column; }
1336
+ .cg-mount.cg-full > .cg-bar, .cg-mount.cg-full > .cg-legend, .cg-mount.cg-full > .cg-groups { flex:0 0 auto; }
1337
+ .cg-mount.cg-full .cg-stage { flex:1 1 auto; min-height:0; }
1338
+ .cg-mount.cg-full .cg-scroll { min-height:0; max-height:none; height:100%; }
1339
+ .cg-mount.cg-full .cg-src-body { max-height:none; }
1340
+ .cg-mount.cg-full .cg-frame { flex:1 1 auto; height:auto; }
1320
1341
  .cg-search-wrap { position:relative; display:inline-block; }
1321
1342
  .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; }
1322
1343
  .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); }
@@ -1330,13 +1351,13 @@ sup.fn a { font-size:.75em; }
1330
1351
  .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); }
1331
1352
  .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; }
1332
1353
  .cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
1333
- .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; }
1354
+ .cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:84vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }
1334
1355
  .cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
1335
1356
  .cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
1336
1357
  .cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
1337
1358
  .cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
1338
1359
  .cg-crumb .cg-seg:hover { text-decoration:underline; }
1339
- .cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }
1360
+ .cg-frame { display:block; width:100%; height:84vh; border:0; background:var(--bg); }
1340
1361
  .cg-flash { color:#b42318; }
1341
1362
  .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; }
1342
1363
  .cg-upbtn { cursor:pointer; }
@@ -1564,7 +1585,11 @@ export function codeGraphRuntime(root) {
1564
1585
  setData(data0.mode === "modules" && data0.mods && gpath && gpath.length ? deriveView(gpath) : homeData());
1565
1586
  // scale null = fit-to-width on first draw. Left-right is the default —
1566
1587
  // call flow reads with the text; the toggle persists per reader.
1567
- var state = { roots: data.roots.slice(), trail: [], scale: null, dir: "LR", frame: null, cap: 600, showAcc: false };
1588
+ // autoScale mirrors the last scale the VIEW picked for itself. While
1589
+ // state.scale still equals it the zoom is ours to re-derive (entering
1590
+ // fullscreen re-fits); once the user works the zoom buttons the two
1591
+ // diverge and their choice is left alone.
1592
+ var state = { roots: data.roots.slice(), trail: [], scale: null, autoScale: null, dir: "LR", frame: null, cap: 600, showAcc: false };
1568
1593
  // Direction survives module -> container navigation (each page is a fresh
1569
1594
  // document); best-effort only — file:// or the DOM stub may lack storage.
1570
1595
  try {
@@ -1573,6 +1598,103 @@ export function codeGraphRuntime(root) {
1573
1598
  state.dir = sd;
1574
1599
  }
1575
1600
  catch (e) { /* no storage */ }
1601
+ // Fullscreen — the graph takes the viewport instead of its 84vh slot in the
1602
+ // reading column. Native Fullscreen API when the browser grants it (browser
1603
+ // chrome goes away, Esc is handled for us); a fixed overlay when it does not
1604
+ // — a sandboxed iframe or a denied permission still gets the space. Both
1605
+ // paths flip the SAME .cg-full class, so the stylesheet and the pane
1606
+ // measurement never have to ask which one is in force.
1607
+ var fullBtnEl = null;
1608
+ function isFull() { return !!(mount.classList && mount.classList.contains("cg-full")); }
1609
+ // The canvas was scaled for the old pane; after the class flips it must be
1610
+ // refitted to the new one. draw() publishes the hook (drawFrame's iframe
1611
+ // needs none and clears it), and layout has to settle before the pane can
1612
+ // be measured — hence the frame delay.
1613
+ function refit() { var f = mount._cgRefit; if (typeof f === "function")
1614
+ f(); }
1615
+ function scheduleRefit() { if (typeof requestAnimationFrame === "function")
1616
+ requestAnimationFrame(refit);
1617
+ else
1618
+ refit(); }
1619
+ function syncFullBtn() {
1620
+ if (!fullBtnEl)
1621
+ return;
1622
+ fullBtnEl.textContent = isFull() ? "⛶ exit fullscreen" : "⛶ fullscreen";
1623
+ fullBtnEl.title = isFull() ? "leave fullscreen (Esc)" : "fill the viewport with the graph";
1624
+ }
1625
+ function setFull(on) {
1626
+ if (mount.classList && mount.classList.toggle)
1627
+ mount.classList.toggle("cg-full", !!on);
1628
+ syncFullBtn();
1629
+ scheduleRefit();
1630
+ }
1631
+ function nativeFullEl() {
1632
+ try {
1633
+ return document.fullscreenElement || document.webkitFullscreenElement || null;
1634
+ }
1635
+ catch (e) {
1636
+ return null;
1637
+ }
1638
+ }
1639
+ function toggleFull() {
1640
+ if (isFull()) {
1641
+ if (nativeFullEl() === mount) {
1642
+ var exit = document.exitFullscreen || document.webkitExitFullscreen;
1643
+ // The class follows in the fullscreenchange handler.
1644
+ if (exit) {
1645
+ try {
1646
+ exit.call(document);
1647
+ return;
1648
+ }
1649
+ catch (e) { /* fall through to the overlay */ }
1650
+ }
1651
+ }
1652
+ setFull(false);
1653
+ return;
1654
+ }
1655
+ var req = mount.requestFullscreen || mount.webkitRequestFullscreen;
1656
+ if (req) {
1657
+ try {
1658
+ var p = req.call(mount);
1659
+ // A rejected promise means permission denied (sandboxed frame,
1660
+ // permissions policy): take the overlay rather than nothing.
1661
+ if (p && typeof p.catch === "function")
1662
+ p.catch(function () { setFull(true); });
1663
+ setFull(true);
1664
+ return;
1665
+ }
1666
+ catch (e) { /* no native fullscreen — overlay below */ }
1667
+ }
1668
+ setFull(true);
1669
+ }
1670
+ // Esc leaves the OVERLAY fallback; native fullscreen handles its own Esc and
1671
+ // reports through fullscreenchange. The listeners are document-wide because
1672
+ // focus may sit on a node, the search box, or nothing at all.
1673
+ try {
1674
+ document.addEventListener("keydown", function (ev) { if (ev.key === "Escape" && isFull() && !nativeFullEl())
1675
+ setFull(false); });
1676
+ var syncFull = function () {
1677
+ var el = nativeFullEl();
1678
+ if (!el && isFull())
1679
+ setFull(false); // left via Esc / F11 / browser UI
1680
+ else if (el === mount && !isFull())
1681
+ setFull(true);
1682
+ else if (el === mount)
1683
+ scheduleRefit(); // class truthful, geometry still changed
1684
+ };
1685
+ document.addEventListener("fullscreenchange", syncFull);
1686
+ document.addEventListener("webkitfullscreenchange", syncFull);
1687
+ }
1688
+ catch (e) { /* no document listeners (fake-DOM runtime test) */ }
1689
+ // Built into whichever toolbar is drawing — the graph view and the nested
1690
+ // iframe view both get one.
1691
+ function fullBtn(bar) {
1692
+ var b = document.createElement("button");
1693
+ b.onclick = toggleFull;
1694
+ fullBtnEl = b;
1695
+ syncFullBtn();
1696
+ bar.appendChild(b);
1697
+ }
1576
1698
  function slice(roots) {
1577
1699
  var keep = {}, layer = {}, q = [], qi = 0, order = [];
1578
1700
  // Accessor noise (bean get/set/is leaves, .accessor) is hidden unless
@@ -1678,6 +1800,10 @@ export function codeGraphRuntime(root) {
1678
1800
  open.href = state.frame.html;
1679
1801
  open.textContent = "open standalone ↗";
1680
1802
  bar.appendChild(open);
1803
+ // The iframe is height:auto/flex in fullscreen, so it needs no refit —
1804
+ // drop the previous view's hook rather than leave a stale canvas one.
1805
+ mount._cgRefit = null;
1806
+ fullBtn(bar);
1681
1807
  mount.appendChild(bar);
1682
1808
  var fr = document.createElement("iframe");
1683
1809
  fr.className = "cg-frame";
@@ -1990,7 +2116,45 @@ export function codeGraphRuntime(root) {
1990
2116
  // Rendered pages sit next to their codemap documents: a live mount
1991
2117
  // (viewer/playground) carries data-src, a CLI embed carries the src
1992
2118
  // path in data.start — either directory anchors doc-relative links.
1993
- var navBase = String(mount.getAttribute("data-src") || data.start || "").replace(/[^\/]*$/, "");
2119
+ // navBase prefixes EVERY url this view reaches for — the breadcrumb's
2120
+ // `location.href`, the search-index `<script src>`, a hit's jump target.
2121
+ // Both of its inputs are page data (a DOM attribute, the embedded graph
2122
+ // JSON), so a document that carried `data-src="javascript:…"` would turn
2123
+ // a breadcrumb click into script execution, and a `//host/` or
2124
+ // `https://host/` value would pull the search index off another origin.
2125
+ // A base is a doc-relative DIRECTORY and nothing else: anything bearing a
2126
+ // scheme or a network-path prefix is refused outright and links resolve
2127
+ // against the current page instead.
2128
+ // Everything this view navigates to or loads is built from page data, so
2129
+ // every such string is filtered here first: keep it if it is a
2130
+ // document-RELATIVE path, drop it to "" if it carries a scheme
2131
+ // (`javascript:`, `data:`) or a `//host` network-path prefix.
2132
+ //
2133
+ // DECIDE ON THE STRING THE BROWSER WILL SEE, NOT THE ONE WE WERE HANDED.
2134
+ // Before navigating, a browser strips leading and trailing C0 controls and
2135
+ // spaces and ignores TAB/CR/LF anywhere inside the scheme. Testing the raw
2136
+ // bytes let the whole classic bypass family through — ` javascript:…`,
2137
+ // `\tjavascript:…`, `java<TAB>script:…`, `\x01javascript:…`,
2138
+ // ` data:text/html,…`, ` //evil.host/` — each of which fails the scheme
2139
+ // test as written and then executes once the browser normalises it.
2140
+ // Normalise first, judge second, and return the NORMALISED value: handing
2141
+ // the sink the raw string would give it bytes that were never checked.
2142
+ function relOnly(u) {
2143
+ var raw = String(u == null ? "" : u).replace(/[\t\n\r]/g, "");
2144
+ // Trim C0-and-space by scan, not by /^[\x00-\x20]+|[\x00-\x20]+$/ — an
2145
+ // end-anchored run is the very quadratic shape this file's fence
2146
+ // scanners were just cured of.
2147
+ var a = 0, b = raw.length;
2148
+ while (a < b && raw.charCodeAt(a) <= 0x20)
2149
+ a++;
2150
+ while (b > a && raw.charCodeAt(b - 1) <= 0x20)
2151
+ b--;
2152
+ var s = raw.slice(a, b);
2153
+ if (!s)
2154
+ return "";
2155
+ return /^[a-zA-Z][a-zA-Z0-9+.\-]*:/.test(s) || s.slice(0, 2) === "//" ? "" : s;
2156
+ }
2157
+ var navBase = relOnly(String(mount.getAttribute("data-src") || data.start || "").replace(/[^\/]*$/, ""));
1994
2158
  // A live mount (viewer/playground/served page) navigates IN PLACE over
1995
2159
  // the geml documents through this loader; only truly static pages fall
1996
2160
  // back to their pre-rendered sibling .html pages. Read LAZILY on every
@@ -2028,7 +2192,16 @@ export function codeGraphRuntime(root) {
2028
2192
  }
2029
2193
  catch (e) { /* stub */ }
2030
2194
  }
2031
- function openDoc(rel, gpath) {
2195
+ function openDoc(rel0, gpath) {
2196
+ // The target can come from a node's own data (a `data-k` key), not just
2197
+ // from a breadcrumb we built — so it goes through the same relative-path
2198
+ // filter as navBase. An absolute or scheme-bearing target says why it
2199
+ // was refused rather than navigating.
2200
+ var rel = relOnly(rel0);
2201
+ if (!rel) {
2202
+ flash("refusing to open " + String(rel0) + " — not a document-relative path");
2203
+ return;
2204
+ }
2032
2205
  var lv = live();
2033
2206
  if (lv) {
2034
2207
  Promise.resolve(lv({ doc: rel })).then(function (nd) {
@@ -2163,16 +2336,20 @@ export function codeGraphRuntime(root) {
2163
2336
  stage.className = "cg-stage";
2164
2337
  stage.appendChild(scroller);
2165
2338
  stage.appendChild(srcPanel);
2166
- // The scroll pane is capped at 72vh by CSS; before first layout its
2167
- // clientHeight is the unconstrained content height, so derive the cap
2168
- // from the viewport. Guards keep a collapsed pane (mid-layout measure)
2169
- // from producing a negative or zero scale invalid CSS would silently
2170
- // keep the previous size.
2339
+ // The scroll pane is capped at 84vh by CSS (keep the 0.84 here in step with
2340
+ // it); before first layout its clientHeight is the unconstrained content
2341
+ // height, so derive the cap from the viewport. In fullscreen the pane is a
2342
+ // flex child with a real measured height, so trust clientHeight there and
2343
+ // fall back to nearly the whole viewport. Guards keep a collapsed pane
2344
+ // (mid-layout measure) from producing a negative or zero scale — invalid
2345
+ // CSS would silently keep the previous size.
2171
2346
  function paneSize() {
2172
2347
  var mw = scroller.clientWidth || mount.clientWidth || 0;
2173
2348
  var mh = 0;
2174
2349
  try {
2175
- mh = Math.floor(window.innerHeight * 0.72);
2350
+ mh = isFull()
2351
+ ? Math.max(120, scroller.clientHeight || Math.floor(window.innerHeight * 0.92))
2352
+ : Math.floor(window.innerHeight * 0.84);
2176
2353
  }
2177
2354
  catch (e) { /* no window (stub) */ }
2178
2355
  return { w: mw, h: mh };
@@ -2215,6 +2392,20 @@ export function codeGraphRuntime(root) {
2215
2392
  zoomBtn("+", function () { state.scale = Math.min(4, state.scale / 0.75); });
2216
2393
  zoomBtn("fit", function () { state.scale = fitScale(); });
2217
2394
  zoomBtn("1:1", function () { state.scale = 1; });
2395
+ // Entering or leaving fullscreen resizes the pane under a canvas that was
2396
+ // scaled for the old one. Re-derive the SAME kind of scale the view opens
2397
+ // with (cross-axis fit, floored at 2/3 so text stays readable) — not
2398
+ // "fit", which would shrink a tall graph to a stamp exactly when the user
2399
+ // asked for more room. A hand-picked zoom is never overridden.
2400
+ // applyScale/initialScale are per-draw closures over THIS canvas, so
2401
+ // republish the hook on every draw.
2402
+ mount._cgRefit = function () {
2403
+ if (state.scale !== state.autoScale)
2404
+ return;
2405
+ state.autoScale = state.scale = initialScale();
2406
+ applyScale();
2407
+ };
2408
+ fullBtn(bar);
2218
2409
  var dirBtn = document.createElement("button");
2219
2410
  dirBtn.textContent = LR ? "top-down" : "left-right";
2220
2411
  dirBtn.onclick = function () {
@@ -2340,8 +2531,15 @@ export function codeGraphRuntime(root) {
2340
2531
  });
2341
2532
  }
2342
2533
  }
2343
- function gotoHit(doc, id, locate) {
2534
+ function gotoHit(doc0, id, locate) {
2344
2535
  searchMenu.hidden = true;
2536
+ // `doc` is a row from the loaded search index — page data, same as any
2537
+ // other target, so it gets the same relative-path filter.
2538
+ var doc = relOnly(doc0);
2539
+ if (!doc) {
2540
+ flash("refusing to open " + String(doc0) + " — not a document-relative path");
2541
+ return;
2542
+ }
2345
2543
  if (live() && !locate) {
2346
2544
  showCallees(doc + "#" + id);
2347
2545
  return;
@@ -2411,7 +2609,7 @@ export function codeGraphRuntime(root) {
2411
2609
  mount.appendChild(bar);
2412
2610
  mount.appendChild(stage);
2413
2611
  if (state.scale === null)
2414
- state.scale = initialScale();
2612
+ state.autoScale = state.scale = initialScale();
2415
2613
  applyScale();
2416
2614
  if (isUp) {
2417
2615
  // The focused method sits at the FAR end of the callers chain —
@@ -23,6 +23,10 @@ export type Selector = {
23
23
  type?: string;
24
24
  hex: string;
25
25
  nth: number;
26
+ } | {
27
+ form: "line";
28
+ from: number;
29
+ to: number;
26
30
  } | {
27
31
  form: "attr";
28
32
  type: string;
@@ -52,4 +56,7 @@ export declare function matchContent(sel: Extract<Selector, {
52
56
  form: "content";
53
57
  }>, all: Addressed[]): ContentHit;
54
58
  export declare function discoveryHint(where: string): string;
59
+ export declare function matchLine(sel: Extract<Selector, {
60
+ form: "line";
61
+ }>, all: Addressed[]): Unit | undefined;
55
62
  export declare function matchType(type: string, all: Addressed[]): Unit[];