@geml/geml 1.3.2 → 1.4.3

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/render.d.ts CHANGED
@@ -52,7 +52,7 @@ interface CGData {
52
52
  depth: number;
53
53
  roots: string[];
54
54
  nodes: Record<string, CGNode>;
55
- edges: [string, string, string, string][];
55
+ edges: [string, string, string, string, string?][];
56
56
  mode?: "modules";
57
57
  module?: string;
58
58
  dir?: "up";
@@ -75,7 +75,7 @@ declare function buildCodeGraph(startRel: string, opts: RenderOptions, view?: {
75
75
  error?: string;
76
76
  truncated?: boolean;
77
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";
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.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";
79
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";
80
80
  export declare function codeGraphRuntime(root: {
81
81
  querySelectorAll(sel: string): ArrayLike<Element>;
package/dist/render.js CHANGED
@@ -561,26 +561,42 @@ function buildCodeGraph(startRel, opts, view) {
561
561
  if (idx)
562
562
  return idx;
563
563
  idx = new Map();
564
+ const add = (from, rec) => {
565
+ if (!from.startsWith("#"))
566
+ return;
567
+ let list = idx.get(from.slice(1));
568
+ if (!list) {
569
+ list = [];
570
+ idx.set(from.slice(1), list);
571
+ }
572
+ list.push(rec);
573
+ };
574
+ // Honor only the FIRST table of each id — a crafted second #calls/#api-calls
575
+ // must not inject edges (pinned by render-html tests).
576
+ let sawCalls = false, sawApi = false;
564
577
  const d = loadParsed(docRel);
565
578
  if (d)
566
579
  for (const b of d.children) {
567
- if (b.kind === "block" && b.type === "table" && b.id === "calls" && b.table) {
568
- const cols = b.table.columns;
569
- const fi = cols.indexOf("from"), ti = cols.indexOf("to"), ki = cols.indexOf("kind"), ci = cols.indexOf("confidence");
570
- if (fi < 0 || ti < 0)
571
- break;
572
- for (const r of b.table.rows) {
573
- const from = r[fi]?.text ?? "";
574
- if (!from.startsWith("#"))
575
- continue;
576
- let list = idx.get(from.slice(1));
577
- if (!list) {
578
- list = [];
579
- idx.set(from.slice(1), list);
580
- }
581
- list.push({ to: r[ti]?.text ?? "", kind: r[ki]?.text || "call", conf: ci >= 0 ? (r[ci]?.text ?? "") : "" });
582
- }
583
- break;
580
+ if (b.kind !== "block" || b.type !== "table" || !b.table)
581
+ continue;
582
+ const cols = b.table.columns;
583
+ const fi = cols.indexOf("from"), ti = cols.indexOf("to");
584
+ if (fi < 0 || ti < 0)
585
+ continue;
586
+ if (b.id === "calls" && !sawCalls) {
587
+ sawCalls = true;
588
+ const ki = cols.indexOf("kind"), ci = cols.indexOf("confidence");
589
+ for (const r of b.table.rows)
590
+ add(r[fi]?.text ?? "", { to: r[ti]?.text ?? "", kind: r[ki]?.text || "call", conf: ci >= 0 ? (r[ci]?.text ?? "") : "" });
591
+ }
592
+ else if (b.id === "api-calls" && !sawApi) {
593
+ sawApi = true;
594
+ // cross-stack link: a frontend function its backend handler,
595
+ // labelled with the endpoint (METHOD path). Rendered as a distinct
596
+ // `http` edge; the handler is a boundary node (not auto-expanded).
597
+ const ei = cols.indexOf("endpoint");
598
+ for (const r of b.table.rows)
599
+ add(r[fi]?.text ?? "", { to: r[ti]?.text ?? "", kind: "http", conf: "", endpoint: ei >= 0 ? (r[ei]?.text ?? "") : "" });
584
600
  }
585
601
  }
586
602
  cache.set(docRel, idx);
@@ -596,8 +612,6 @@ function buildCodeGraph(startRel, opts, view) {
596
612
  const hi = view.node.lastIndexOf("#");
597
613
  if (hi <= 0)
598
614
  return { error: `bad view node \`${view.node}\`` };
599
- // Same once-per-document indexing as callRows — the upward BFS crosses
600
- // documents through their #called-by tables just as hot.
601
615
  const calledByIdxOf = (() => {
602
616
  const cache = new Map();
603
617
  return (docRel) => {
@@ -605,26 +619,38 @@ function buildCodeGraph(startRel, opts, view) {
605
619
  if (idx)
606
620
  return idx;
607
621
  idx = new Map();
622
+ const add = (to, rec) => {
623
+ if (!to.startsWith("#"))
624
+ return;
625
+ let list = idx.get(to.slice(1));
626
+ if (!list) {
627
+ list = [];
628
+ idx.set(to.slice(1), list);
629
+ }
630
+ list.push(rec);
631
+ };
632
+ let sawCb = false, sawApi = false; // first-of-each-id only (see callIdxOf)
608
633
  const d = loadParsed(docRel);
609
634
  if (d)
610
635
  for (const b of d.children) {
611
- if (b.kind === "block" && b.type === "table" && b.id === "called-by" && b.table) {
612
- const cols = b.table.columns;
613
- const fi = cols.indexOf("from"), ti = cols.indexOf("to"), ki = cols.indexOf("kind");
614
- if (fi < 0 || ti < 0)
615
- break;
616
- for (const r of b.table.rows) {
617
- const to = r[ti]?.text ?? "";
618
- if (!to.startsWith("#"))
619
- continue;
620
- let list = idx.get(to.slice(1));
621
- if (!list) {
622
- list = [];
623
- idx.set(to.slice(1), list);
624
- }
625
- list.push({ from: r[fi]?.text ?? "", kind: r[ki]?.text || "call" });
626
- }
627
- break;
636
+ if (b.kind !== "block" || b.type !== "table" || !b.table)
637
+ continue;
638
+ const cols = b.table.columns;
639
+ const fi = cols.indexOf("from"), ti = cols.indexOf("to");
640
+ if (fi < 0 || ti < 0)
641
+ continue;
642
+ if (b.id === "called-by" && !sawCb) {
643
+ sawCb = true;
644
+ const ki = cols.indexOf("kind");
645
+ for (const r of b.table.rows)
646
+ add(r[ti]?.text ?? "", { from: r[fi]?.text ?? "", kind: r[ki]?.text || "call" });
647
+ }
648
+ else if (b.id === "api-served-by" && !sawApi) {
649
+ sawApi = true;
650
+ // cross-stack: a backend handler its frontend caller.
651
+ const ei = cols.indexOf("endpoint");
652
+ for (const r of b.table.rows)
653
+ add(r[ti]?.text ?? "", { from: r[fi]?.text ?? "", kind: "http", endpoint: ei >= 0 ? (r[ei]?.text ?? "") : "" });
628
654
  }
629
655
  }
630
656
  cache.set(docRel, idx);
@@ -656,8 +682,13 @@ function buildCodeGraph(startRel, opts, view) {
656
682
  }
657
683
  nodes[callerKey] = blockInfo(c.doc, c.id);
658
684
  }
659
- edges.push([toKey, callerKey, row.kind, ""]);
660
- if (!seenUp.has(callerKey)) {
685
+ if (row.endpoint)
686
+ edges.push([toKey, callerKey, row.kind, "", row.endpoint]);
687
+ else
688
+ edges.push([toKey, callerKey, row.kind, ""]);
689
+ // Don't expand the frontend caller's subtree into a backend-rooted
690
+ // callers view across the http boundary; it's a boundary node.
691
+ if (row.kind !== "http" && !seenUp.has(callerKey)) {
661
692
  seenUp.add(callerKey);
662
693
  next.push(c);
663
694
  }
@@ -712,8 +743,14 @@ function buildCodeGraph(startRel, opts, view) {
712
743
  }
713
744
  nodes[toKey] = blockInfo(t.doc, t.id);
714
745
  }
715
- edges.push([fromKey, toKey, row.kind, row.conf]);
716
- if (!seen.has(toKey)) {
746
+ if (row.endpoint)
747
+ edges.push([fromKey, toKey, row.kind, row.conf, row.endpoint]);
748
+ else
749
+ edges.push([fromKey, toKey, row.kind, row.conf]);
750
+ // Cross-stack `http` links reach into the OTHER tree — pull the handler
751
+ // in as a boundary node but don't expand its (backend) subtree into a
752
+ // frontend view; the user clicks through to open it in its own flow.
753
+ if (row.kind !== "http" && !seen.has(toKey)) {
717
754
  seen.add(toKey);
718
755
  next.push(t);
719
756
  }
@@ -982,6 +1019,7 @@ sup.fn a { font-size:.75em; }
982
1019
  .cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
983
1020
  .cg-e.cand { stroke-dasharray:2 3; }
984
1021
  .cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
1022
+ .cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
985
1023
  .cg-e.soft { opacity:.55; }
986
1024
  .cg-svg.hl .cg-n { opacity:.22; }
987
1025
  .cg-svg.hl .cg-e { opacity:.1; }
@@ -1042,6 +1080,30 @@ export function codeGraphRuntime(root) {
1042
1080
  el.setAttribute(k, String(attrs[k]));
1043
1081
  return el;
1044
1082
  }
1083
+ // Same-origin confinement for every URL the runtime fetches or loads.
1084
+ // navBase is derived from the mount's document-controlled `data-src`, so a
1085
+ // crafted codemap doc could otherwise aim a fetch / HEAD probe / <script src>
1086
+ // at a third-party host (silent beacon, SSRF, or remote-code load) or read an
1087
+ // out-of-directory local file. Resolve the candidate against the page and
1088
+ // require the SAME origin (and, on file://, the same directory). With no
1089
+ // location context (unit tests / non-browser) there is nothing to confine to,
1090
+ // so allow — the browser/CLI callers always have one.
1091
+ function cgSameOrigin(u) {
1092
+ try {
1093
+ var here = (typeof location !== "undefined" && location.href) ? location.href : "";
1094
+ if (!here)
1095
+ return true;
1096
+ var abs = new URL(String(u), here), cur = new URL(here);
1097
+ if (abs.protocol !== cur.protocol)
1098
+ return false;
1099
+ if (cur.protocol === "file:")
1100
+ return abs.pathname.indexOf(cur.pathname.replace(/[^\/]*$/, "")) === 0;
1101
+ return abs.origin === cur.origin;
1102
+ }
1103
+ catch (e) {
1104
+ return false;
1105
+ }
1106
+ }
1045
1107
  // Arrow-marker ids must be unique per drawn svg — several mounts share one
1046
1108
  // document, and duplicate ids would make every graph point at the first.
1047
1109
  var arrowSeq = 0;
@@ -1446,7 +1508,7 @@ export function codeGraphRuntime(root) {
1446
1508
  if (!a || !b)
1447
1509
  return;
1448
1510
  var isBack = s.back[e[0] + ">" + e[1]] || (e[0] === e[1]);
1449
- var cls = "cg-e" + (e[2] === "candidate" ? " cand" : "") + (isBack ? " back" : "") + (e[3] === "medium" || e[3] === "low" ? " soft" : "");
1511
+ var cls = "cg-e" + (e[2] === "candidate" ? " cand" : "") + (e[2] === "http" ? " http" : "") + (isBack ? " back" : "") + (e[3] === "medium" || e[3] === "low" ? " soft" : "");
1450
1512
  var p;
1451
1513
  if (e[0] === e[1]) {
1452
1514
  p = LR
@@ -1472,6 +1534,11 @@ export function codeGraphRuntime(root) {
1472
1534
  p = "M" + x1 + " " + y1 + " C " + x1 + " " + (y1 + GY / 2) + " " + x2 + " " + (y2 - GY / 2) + " " + x2 + " " + y2;
1473
1535
  }
1474
1536
  var pathEl = h("path", { d: p, class: cls, "marker-end": "url(#" + arrId + (isBack ? "-b" : "") + ")" });
1537
+ if (e[2] === "http" && e[4]) {
1538
+ var tt = h("title", {});
1539
+ tt.textContent = e[4];
1540
+ pathEl.appendChild(tt);
1541
+ } // endpoint on hover
1475
1542
  var ek = e[0] + ">" + e[1];
1476
1543
  edgeEls[ek] = pathEl;
1477
1544
  edgeBase[ek] = cls;
@@ -1658,7 +1725,11 @@ export function codeGraphRuntime(root) {
1658
1725
  // blocked) — embed directly; the frame contains any error itself.
1659
1726
  try {
1660
1727
  if (/^https?:$/.test(window.location.protocol)) {
1661
- fetch(html, { method: "HEAD" }).then(function (r) {
1728
+ if (!cgSameOrigin(html)) {
1729
+ flash("cannot reach " + html + " (cross-origin blocked)");
1730
+ return;
1731
+ }
1732
+ fetch(html, { method: "HEAD", credentials: "omit" }).then(function (r) {
1662
1733
  if (r.ok)
1663
1734
  embed();
1664
1735
  else
@@ -1872,6 +1943,10 @@ export function codeGraphRuntime(root) {
1872
1943
  function withIndex(cb) {
1873
1944
  if (window.__gemlSearch)
1874
1945
  return cb(window.__gemlSearch);
1946
+ if (!cgSameOrigin(navBase + "_index/search-index.js")) {
1947
+ cb([]);
1948
+ return;
1949
+ }
1875
1950
  var s = document.createElement("script");
1876
1951
  s.src = navBase + "_index/search-index.js";
1877
1952
  s.onload = function () { cb(window.__gemlSearch || []); };
@@ -2191,8 +2266,16 @@ export function codeGraphRuntime(root) {
2191
2266
  degrade();
2192
2267
  return;
2193
2268
  }
2269
+ // `path` is the node's document-controlled `src=` route and `base` may
2270
+ // be empty (@self / bare-filename mounts), so an absolute or //-relative
2271
+ // src would fetch a third-party host (beacon / SSRF). Confine to the
2272
+ // page's origin and never send credentials.
2273
+ if (!cgSameOrigin(base + path)) {
2274
+ degrade();
2275
+ return;
2276
+ }
2194
2277
  try {
2195
- Promise.resolve(fetchFn(base + path)).then(function (r) {
2278
+ Promise.resolve(fetchFn(base + path, { credentials: "omit" })).then(function (r) {
2196
2279
  if (!r || r.ok === false) {
2197
2280
  degrade();
2198
2281
  return null;
package/dist/table.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type DiagnosticCode } from "./diagnostics.js";
1
2
  import { type Value } from "./attrs.js";
2
3
  import { type Inline, type RefSink } from "./inline.js";
3
4
  export type Align = "left" | "right" | "center";
@@ -23,6 +24,7 @@ export interface TableModel {
23
24
  }
24
25
  export interface TableDiag {
25
26
  severity: "error" | "warning";
27
+ code: DiagnosticCode;
26
28
  message: string;
27
29
  }
28
30
  export interface TableResult {
package/dist/table.js CHANGED
@@ -253,7 +253,7 @@ export function parseTable(body, attrs, line, sink) {
253
253
  const src = typeof attrs["src"] === "string" ? attrs["src"] : undefined;
254
254
  if (src !== undefined) {
255
255
  if (body.some((l) => l.trim() !== "")) {
256
- diagnostics.push({ severity: "error", message: "table has both `src` and an inline body; provide one, not both" });
256
+ diagnostics.push({ severity: "error", code: "table-src-and-body", message: "table has both `src` and an inline body; provide one, not both" });
257
257
  }
258
258
  const headerAttr = attrs["header"];
259
259
  const header = headerAttr === undefined ? true : headerAttr === true || headerAttr === 1 || headerAttr === "1";
@@ -271,7 +271,7 @@ export function parseTable(body, attrs, line, sink) {
271
271
  }
272
272
  else {
273
273
  if (fmt !== undefined)
274
- diagnostics.push({ severity: "warning", message: `unknown table format \`${fmt}\`; parsed as visual grid` });
274
+ diagnostics.push({ severity: "warning", code: "unknown-table-format", message: `unknown table format \`${fmt}\`; parsed as visual grid` });
275
275
  raw = parseVisual(body);
276
276
  }
277
277
  const columns = [...raw.columns];
@@ -369,7 +369,7 @@ export function parseTable(body, attrs, line, sink) {
369
369
  for (const f of formulas) {
370
370
  const eq = f.indexOf("=");
371
371
  if (eq <= 0) {
372
- diagnostics.push({ severity: "error", message: `bad compute formula \`${f}\` (want \`Name = expr\`)` });
372
+ diagnostics.push({ severity: "error", code: "bad-compute-formula", message: `bad compute formula \`${f}\` (want \`Name = expr\`)` });
373
373
  continue;
374
374
  }
375
375
  const { name, fmt } = splitName(f.slice(0, eq));
@@ -379,7 +379,7 @@ export function parseTable(body, attrs, line, sink) {
379
379
  toks = lexExpr(expr);
380
380
  }
381
381
  catch {
382
- diagnostics.push({ severity: "error", message: `cannot lex formula \`${f}\`` });
382
+ diagnostics.push({ severity: "error", code: "unlexable-compute-formula", message: `cannot lex formula \`${f}\`` });
383
383
  continue;
384
384
  }
385
385
  // Target is a header name (never a letter reference): match by name only.
@@ -405,7 +405,7 @@ export function parseTable(body, attrs, line, sink) {
405
405
  }
406
406
  }
407
407
  catch (e) {
408
- diagnostics.push({ severity: "error", message: `compute \`${name}\`: ${e.message}` });
408
+ diagnostics.push({ severity: "error", code: "compute-error", message: `compute \`${name}\`: ${e.message}` });
409
409
  failed = true;
410
410
  }
411
411
  }
@@ -429,14 +429,14 @@ export function parseTable(body, attrs, line, sink) {
429
429
  for (const s of summaryDecls) {
430
430
  const eq = s.indexOf("=");
431
431
  if (eq <= 0) {
432
- diagnostics.push({ severity: "error", message: `bad summary \`${s}\` (want \`Cell = value\`)` });
432
+ diagnostics.push({ severity: "error", code: "bad-summary-entry", message: `bad summary \`${s}\` (want \`Cell = value\`)` });
433
433
  continue;
434
434
  }
435
435
  const { name, fmt } = splitName(s.slice(0, eq));
436
436
  const rhs = s.slice(eq + 1).trim();
437
437
  const ci = colIndex(name);
438
438
  if (ci < 0) {
439
- diagnostics.push({ severity: "error", message: `summary targets unknown column \`${name}\`` });
439
+ diagnostics.push({ severity: "error", code: "summary-unknown-column", message: `summary targets unknown column \`${name}\`` });
440
440
  continue;
441
441
  }
442
442
  // String label: `Cell = 'Total'`.
@@ -451,7 +451,7 @@ export function parseTable(body, attrs, line, sink) {
451
451
  toks = lexExpr(rhs);
452
452
  }
453
453
  catch {
454
- diagnostics.push({ severity: "error", message: `cannot lex summary \`${s}\`` });
454
+ diagnostics.push({ severity: "error", code: "unlexable-summary-expression", message: `cannot lex summary \`${s}\`` });
455
455
  continue;
456
456
  }
457
457
  try {
@@ -464,7 +464,7 @@ export function parseTable(body, attrs, line, sink) {
464
464
  catch (e) {
465
465
  const msg = /unknown column `(.+)`/.exec(e.message);
466
466
  const hint = msg ? `summary \`${name}\`: column \`${msg[1]}\` must be reduced by an aggregate (e.g. sum(${msg[1]}))` : `summary \`${name}\`: ${e.message}`;
467
- diagnostics.push({ severity: "error", message: hint });
467
+ diagnostics.push({ severity: "error", code: "summary-error", message: hint });
468
468
  }
469
469
  }
470
470
  model.summary = summary;
@@ -477,12 +477,12 @@ export function parseTable(body, attrs, line, sink) {
477
477
  for (const sd of spanDecls) {
478
478
  const sp = parseSpan(sd);
479
479
  if (!sp) {
480
- diagnostics.push({ severity: "error", message: `bad span \`${sd}\` (want \`rNcM:RxC\`)` });
480
+ diagnostics.push({ severity: "error", code: "bad-span", message: `bad span \`${sd}\` (want \`rNcM:RxC\`)` });
481
481
  continue;
482
482
  }
483
483
  const cell = model.rows[sp.row - 1]?.[sp.col - 1];
484
484
  if (!cell) {
485
- diagnostics.push({ severity: "warning", message: `span \`${sd}\` targets a cell outside the table` });
485
+ diagnostics.push({ severity: "warning", code: "span-outside-table", message: `span \`${sd}\` targets a cell outside the table` });
486
486
  continue;
487
487
  }
488
488
  // A span can never extend past the grid: clamp its extent to the rows/cols
package/package.json CHANGED
@@ -1,63 +1,62 @@
1
- {
2
- "name": "@geml/geml",
3
- "version": "1.3.2",
4
- "publishConfig": {
5
- "access": "public"
6
- },
7
- "description": "GEML(General Expressive Markup Language) — one format, two readers. People and AI agents co-write the same document: plain text that stays legible for people, and addressable, verifiable, and versioned for machines. Reference parser, validator, renderer & CLI for GEML.",
8
- "type": "module",
9
- "bin": {
10
- "geml": "dist/geml.js"
11
- },
12
- "main": "dist/geml.js",
13
- "types": "dist/geml.d.ts",
14
- "files": [
15
- "dist",
16
- "codemap",
17
- "README.md",
18
- "LICENSE"
19
- ],
20
- "engines": {
21
- "node": ">=22"
22
- },
23
- "keywords": [
24
- "geml",
25
- "markup",
26
- "markdown",
27
- "parser",
28
- "cli",
29
- "document",
30
- "typed-block",
31
- "ai",
32
- "agent",
33
- "llm",
34
- "addressable",
35
- "versioning",
36
- "docs",
37
- "code-graph"
38
- ],
39
- "repository": {
40
- "type": "git",
41
- "url": "git+https://github.com/geml-spec/geml.git",
42
- "directory": "geml-parser"
43
- },
44
- "homepage": "https://github.com/geml-spec/geml#readme",
45
- "bugs": {
46
- "url": "https://github.com/geml-spec/geml/issues"
47
- },
48
- "scripts": {
49
- "build": "tsc",
50
- "test": "tsc && node test/all.mjs",
51
- "convert": "node dist/geml.js convert",
52
- "parse": "node dist/geml.js",
53
- "coverage": "tsc && c8 --all --include=dist/**/*.js --include=codemap/**/*.mjs --reporter=text --reporter=text-summary node test/all.mjs",
54
- "coverage:check": "tsc && c8 --all --include=dist/**/*.js --include=codemap/**/*.mjs --check-coverage --lines 95 --statements 95 --functions 95 --branches 95 node test/all.mjs",
55
- "prepublishOnly": "npm run build"
56
- },
57
- "license": "MIT",
58
- "devDependencies": {
59
- "@types/node": "^22.19.21",
60
- "c8": "^10.1.3",
61
- "typescript": "^5.9.3"
62
- }
63
- }
1
+ {
2
+ "name": "@geml/geml",
3
+ "version": "1.4.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "GEML(General Expressive Markup Language) — one format, two readers. People and AI agents co-write the same document: plain text that stays legible for people, and addressable, verifiable, and versioned for machines. Reference parser, validator, renderer & CLI for GEML.",
8
+ "type": "module",
9
+ "bin": {
10
+ "geml": "dist/geml.js"
11
+ },
12
+ "main": "dist/geml.js",
13
+ "types": "dist/geml.d.ts",
14
+ "files": [
15
+ "dist",
16
+ "codemap",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "keywords": [
24
+ "geml",
25
+ "markup",
26
+ "markdown",
27
+ "parser",
28
+ "cli",
29
+ "document",
30
+ "typed-block",
31
+ "ai",
32
+ "agent",
33
+ "llm",
34
+ "addressable",
35
+ "versioning",
36
+ "docs",
37
+ "code-graph"
38
+ ],
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/geml-spec/geml.git",
42
+ "directory": "geml-parser"
43
+ },
44
+ "homepage": "https://github.com/geml-spec/geml#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/geml-spec/geml/issues"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc",
50
+ "test": "tsc && node test/all.mjs",
51
+ "parse": "node dist/geml.js",
52
+ "coverage": "tsc && c8 --all --include=dist/**/*.js --include=codemap/**/*.mjs --reporter=text --reporter=text-summary node test/all.mjs",
53
+ "coverage:check": "tsc && c8 --all --include=dist/**/*.js --include=codemap/**/*.mjs --check-coverage --lines 95 --statements 95 --functions 95 --branches 95 node test/all.mjs",
54
+ "prepublishOnly": "npm run build"
55
+ },
56
+ "license": "MIT",
57
+ "devDependencies": {
58
+ "@types/node": "^22.19.21",
59
+ "c8": "^10.1.3",
60
+ "typescript": "^5.9.3"
61
+ }
62
+ }