@geml/geml 1.1.1 → 1.4.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/LICENSE +21 -21
- package/README.md +155 -79
- package/codemap/adapters/crg.mjs +120 -109
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -229
- package/codemap/browser-stub.mjs +29 -24
- package/codemap/build.mjs +609 -354
- package/codemap/cross-stack.mjs +303 -0
- package/codemap/detect.mjs +399 -185
- package/codemap/emit.mjs +480 -361
- package/codemap/entries.mjs +129 -0
- package/codemap/exclude.mjs +52 -52
- package/codemap/find.mjs +63 -0
- package/codemap/foldings.mjs +110 -0
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +172 -143
- package/codemap/normalize.mjs +0 -0
- package/codemap/recipe-trust.mjs +103 -0
- package/codemap/refresh.mjs +310 -160
- package/codemap/render-all.mjs +64 -64
- package/codemap/serve.mjs +578 -378
- package/codemap/sfc-virtualize.mjs +367 -0
- package/codemap/verify.mjs +148 -126
- package/dist/block-edit.d.ts +1 -0
- package/dist/block-edit.js +112 -0
- package/dist/from-md.js +66 -7
- package/dist/geml.d.ts +2 -1
- package/dist/geml.js +1103 -233
- package/dist/history.js +102 -62
- package/dist/inline.d.ts +2 -1
- package/dist/inline.js +61 -8
- package/dist/render-html.d.ts +3 -0
- package/dist/render-html.js +95 -0
- package/dist/render.d.ts +33 -3
- package/dist/render.js +558 -283
- package/dist/serialize.js +22 -2
- package/dist/table.js +40 -5
- package/dist/to-md.js +4 -0
- package/package.json +62 -58
package/dist/render.js
CHANGED
|
@@ -15,21 +15,37 @@ const PALETTE = ["#2563eb", "#dc2626", "#059669", "#d97706", "#7c3aed", "#db2777
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Escaping
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
|
-
function esc(s) {
|
|
18
|
+
export function esc(s) {
|
|
19
19
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
20
20
|
}
|
|
21
|
-
function escAttr(s) {
|
|
21
|
+
export function escAttr(s) {
|
|
22
22
|
return esc(s).replace(/"/g, """);
|
|
23
23
|
}
|
|
24
|
+
// Build a `class="…"` value from document-author-controlled tokens. Class names
|
|
25
|
+
// are dropped to the HTML token charset ([A-Za-z0-9_-]) so a crafted `.class`
|
|
26
|
+
// (e.g. `.x" onmouseover="alert(1)`) cannot break out of the attribute, then the
|
|
27
|
+
// joined result is escAttr'd as well (defense-in-depth). §4.
|
|
28
|
+
function classAttr(tokens) {
|
|
29
|
+
const safe = tokens
|
|
30
|
+
.map((t) => t.replace(/[^A-Za-z0-9_-]/g, ""))
|
|
31
|
+
.filter((t) => t !== "");
|
|
32
|
+
return escAttr(safe.join(" "));
|
|
33
|
+
}
|
|
34
|
+
// Maximum block-nesting depth the renderer will descend before bailing out with
|
|
35
|
+
// a diagnostic instead of overflowing the call stack (block()↔list()↔typed() are
|
|
36
|
+
// mutually recursive). 256 is far past any legitimate document yet well under the
|
|
37
|
+
// few-thousand-frame native stack limit. Kept in step with the parser's cap.
|
|
38
|
+
const MAX_NESTING = 256;
|
|
24
39
|
// ---------------------------------------------------------------------------
|
|
25
40
|
// Render context
|
|
26
41
|
// ---------------------------------------------------------------------------
|
|
27
|
-
class RenderCtx {
|
|
42
|
+
export class RenderCtx {
|
|
28
43
|
doc;
|
|
29
44
|
opts;
|
|
30
45
|
usedMath = false;
|
|
31
46
|
usedMermaid = false;
|
|
32
47
|
usedCodeGraph = false;
|
|
48
|
+
renderDepth = 0;
|
|
33
49
|
labels = new Map(); // id -> link label for [[#id]] auto-refs
|
|
34
50
|
constructor(doc, opts = {}) {
|
|
35
51
|
this.doc = doc;
|
|
@@ -121,6 +137,20 @@ class RenderCtx {
|
|
|
121
137
|
}
|
|
122
138
|
// ----- blocks -----
|
|
123
139
|
block(b) {
|
|
140
|
+
// Guard the block()↔list()↔typed() mutual recursion so a pathologically
|
|
141
|
+
// nested document degrades to a diagnostic rather than a RangeError.
|
|
142
|
+
if (this.renderDepth >= MAX_NESTING) {
|
|
143
|
+
return `<div class="render-error">block nesting too deep (max ${MAX_NESTING})</div>`;
|
|
144
|
+
}
|
|
145
|
+
this.renderDepth++;
|
|
146
|
+
try {
|
|
147
|
+
return this.blockInner(b);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
this.renderDepth--;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
blockInner(b) {
|
|
124
154
|
switch (b.kind) {
|
|
125
155
|
case "hidden": return "";
|
|
126
156
|
case "heading": {
|
|
@@ -172,10 +202,17 @@ class RenderCtx {
|
|
|
172
202
|
this.usedMath = true;
|
|
173
203
|
return `<div class="math-block"${idAttr}>\\[${esc(raw)}\\]</div>`;
|
|
174
204
|
case "note": {
|
|
175
|
-
const classes = ["callout", b.type, ...b.classes]
|
|
205
|
+
const classes = classAttr(["callout", b.type, ...b.classes]);
|
|
176
206
|
const inner = (b.children ?? []).map((c) => this.block(c)).filter((s) => s).join("\n");
|
|
177
207
|
return `<aside class="${classes}"${idAttr}>\n${inner}\n</aside>`;
|
|
178
208
|
}
|
|
209
|
+
case "text": {
|
|
210
|
+
// Addressable prose (§3): flow children in a NEUTRAL container — the
|
|
211
|
+
// block exists for its id/attrs, not for callout chrome (that's note).
|
|
212
|
+
const inner = (b.children ?? []).map((c) => this.block(c)).filter((s) => s).join("\n");
|
|
213
|
+
const classes = classAttr(["text", ...b.classes]);
|
|
214
|
+
return `<div class="${classes}"${idAttr}>\n${inner}\n</div>`;
|
|
215
|
+
}
|
|
179
216
|
case "table":
|
|
180
217
|
return b.table ? this.table(b.table, b.id, caption) : `<p class="render-error">table failed to parse</p>`;
|
|
181
218
|
case "diagram":
|
|
@@ -243,7 +280,10 @@ class RenderCtx {
|
|
|
243
280
|
// model keeps every row: charts, computed summaries and the code-graph
|
|
244
281
|
// never read the HTML). Codemap edge tables additionally fold shut —
|
|
245
282
|
// they are machine data; elsewhere the table is content and stays open.
|
|
246
|
-
|
|
283
|
+
// The codemap index's #modules table IS the page's content — the module
|
|
284
|
+
// inventory people scan and filter — so it renders in full. Edge tables
|
|
285
|
+
// (#calls / #called-by) stay previewed+folded: machine data at scale.
|
|
286
|
+
const maxRows = this.isCodemapDoc && id === "modules" ? Infinity : (this.opts.tableRows ?? 500);
|
|
247
287
|
const allRows = t.rows;
|
|
248
288
|
const rows = allRows.length > maxRows ? allRows.slice(0, maxRows) : allRows;
|
|
249
289
|
// Coverage grid for declared spans, so cells a span covers are not emitted.
|
|
@@ -251,8 +291,12 @@ class RenderCtx {
|
|
|
251
291
|
rows.forEach((row, r) => row.forEach((cell, c) => {
|
|
252
292
|
if (!cell.span)
|
|
253
293
|
return;
|
|
254
|
-
|
|
255
|
-
|
|
294
|
+
// Bound the sweep to the rendered grid regardless of the declared span, so
|
|
295
|
+
// an oversized span can never drive an O(hugerows×hugecols) loop (DoS).
|
|
296
|
+
const spanRows = Math.min(cell.span.rows, rows.length - r);
|
|
297
|
+
const spanCols = Math.min(cell.span.cols, row.length - c);
|
|
298
|
+
for (let dr = 0; dr < spanRows; dr++)
|
|
299
|
+
for (let dc = 0; dc < spanCols; dc++) {
|
|
256
300
|
if (dr === 0 && dc === 0)
|
|
257
301
|
continue;
|
|
258
302
|
const rr = r + dr, cc = c + dc;
|
|
@@ -313,7 +357,7 @@ function niceMax(v) {
|
|
|
313
357
|
// depth-limited; the layered LAYOUT happens in the page runtime (draw time).
|
|
314
358
|
// ---------------------------------------------------------------------------
|
|
315
359
|
// Hard payload ceiling only — the VIEW paces itself: the runtime draws the
|
|
316
|
-
// first
|
|
360
|
+
// first 600 by BFS order and offers "+600"/"all" to walk deeper. The data in
|
|
317
361
|
// the codemap documents is always complete regardless.
|
|
318
362
|
const CG_MAX_NODES = 4000;
|
|
319
363
|
// Tiny posix-path helpers (no node:path dependency in the renderer).
|
|
@@ -399,6 +443,13 @@ function buildCodeGraph(startRel, opts, view) {
|
|
|
399
443
|
entryDocs.push(d);
|
|
400
444
|
}
|
|
401
445
|
}
|
|
446
|
+
// …plus documents whose app entry is FILE-level (app-entry-docs meta:
|
|
447
|
+
// top-level bootstrap code with no function symbol, e.g. a Nuxt app.vue).
|
|
448
|
+
for (const t of String(meta0["app-entry-docs"] ?? "").split(/\s+/).filter(Boolean)) {
|
|
449
|
+
const d = cgJoin(cgDir(start), t);
|
|
450
|
+
if (!entryDocs.includes(d))
|
|
451
|
+
entryDocs.push(d);
|
|
452
|
+
}
|
|
402
453
|
return { data: { start, depth: 99, roots: [], nodes: {}, edges: [], mode: "modules", mods: list, medges: em, entryDocs } };
|
|
403
454
|
}
|
|
404
455
|
}
|
|
@@ -494,6 +545,8 @@ function buildCodeGraph(startRel, opts, view) {
|
|
|
494
545
|
node.test = true;
|
|
495
546
|
if (b.classes.includes("accessor"))
|
|
496
547
|
node.acc = true;
|
|
548
|
+
if (b.classes.includes("app-entry"))
|
|
549
|
+
node.entry = true;
|
|
497
550
|
idx.set(b.id, node);
|
|
498
551
|
}
|
|
499
552
|
cache.set(docRel, idx);
|
|
@@ -508,26 +561,42 @@ function buildCodeGraph(startRel, opts, view) {
|
|
|
508
561
|
if (idx)
|
|
509
562
|
return idx;
|
|
510
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;
|
|
511
577
|
const d = loadParsed(docRel);
|
|
512
578
|
if (d)
|
|
513
579
|
for (const b of d.children) {
|
|
514
|
-
if (b.kind
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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 ?? "") : "" });
|
|
531
600
|
}
|
|
532
601
|
}
|
|
533
602
|
cache.set(docRel, idx);
|
|
@@ -543,8 +612,6 @@ function buildCodeGraph(startRel, opts, view) {
|
|
|
543
612
|
const hi = view.node.lastIndexOf("#");
|
|
544
613
|
if (hi <= 0)
|
|
545
614
|
return { error: `bad view node \`${view.node}\`` };
|
|
546
|
-
// Same once-per-document indexing as callRows — the upward BFS crosses
|
|
547
|
-
// documents through their #called-by tables just as hot.
|
|
548
615
|
const calledByIdxOf = (() => {
|
|
549
616
|
const cache = new Map();
|
|
550
617
|
return (docRel) => {
|
|
@@ -552,26 +619,38 @@ function buildCodeGraph(startRel, opts, view) {
|
|
|
552
619
|
if (idx)
|
|
553
620
|
return idx;
|
|
554
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)
|
|
555
633
|
const d = loadParsed(docRel);
|
|
556
634
|
if (d)
|
|
557
635
|
for (const b of d.children) {
|
|
558
|
-
if (b.kind
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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 ?? "") : "" });
|
|
575
654
|
}
|
|
576
655
|
}
|
|
577
656
|
cache.set(docRel, idx);
|
|
@@ -603,8 +682,13 @@ function buildCodeGraph(startRel, opts, view) {
|
|
|
603
682
|
}
|
|
604
683
|
nodes[callerKey] = blockInfo(c.doc, c.id);
|
|
605
684
|
}
|
|
606
|
-
|
|
607
|
-
|
|
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)) {
|
|
608
692
|
seenUp.add(callerKey);
|
|
609
693
|
next.push(c);
|
|
610
694
|
}
|
|
@@ -659,8 +743,14 @@ function buildCodeGraph(startRel, opts, view) {
|
|
|
659
743
|
}
|
|
660
744
|
nodes[toKey] = blockInfo(t.doc, t.id);
|
|
661
745
|
}
|
|
662
|
-
|
|
663
|
-
|
|
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)) {
|
|
664
754
|
seen.add(toKey);
|
|
665
755
|
next.push(t);
|
|
666
756
|
}
|
|
@@ -835,134 +925,143 @@ function trunc(s, n) {
|
|
|
835
925
|
// ---------------------------------------------------------------------------
|
|
836
926
|
// Page shell, inline CSS, inline interactivity JS
|
|
837
927
|
// ---------------------------------------------------------------------------
|
|
838
|
-
const CSS = `
|
|
839
|
-
:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
|
|
840
|
-
* { box-sizing: border-box; }
|
|
841
|
-
body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
|
|
842
|
-
main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
|
|
843
|
-
h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
|
|
844
|
-
h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
845
|
-
h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
846
|
-
h3 { font-size:1.25em; } h4 { font-size:1em; }
|
|
847
|
-
p { margin:.7em 0; }
|
|
848
|
-
a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
|
|
849
|
-
code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
850
|
-
pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
|
|
851
|
-
pre code { background:none; padding:0; font-size:.85em; }
|
|
852
|
-
pre.output { background:#0d1117; color:#e6edf3; }
|
|
853
|
-
pre.output code { color:inherit; }
|
|
854
|
-
ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
|
|
855
|
-
ul.task-list { list-style:none; padding-left:.2em; }
|
|
856
|
-
li.task input[type=checkbox] { appearance:none; -webkit-appearance:none; width:1.1em; height:1.1em; margin:0 .5em 0 0; vertical-align:-.2em; border:1.5px solid #c8ccd0; border-radius:4px; background:#fff; position:relative; opacity:1; cursor:default; box-sizing:border-box; }
|
|
857
|
-
li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
|
|
858
|
-
li.task input[type=checkbox]:checked::after { content:"✓"; position:absolute; top:0; right:0; bottom:0; left:0; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.8em; line-height:1; font-weight:700; }
|
|
859
|
-
aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
|
|
860
|
-
aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
|
|
861
|
-
aside.warning { border-left-color:#d97706; background:#fff8f0; }
|
|
862
|
-
aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
|
|
863
|
-
figure { margin:1.2em 0; }
|
|
864
|
-
figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
|
|
865
|
-
table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
|
|
866
|
-
table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
|
|
867
|
-
table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
|
|
868
|
-
table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
|
|
869
|
-
table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
|
|
870
|
-
table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
|
|
871
|
-
table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
|
|
872
|
-
table.geml-table td.computed { color:#0a7c52; }
|
|
873
|
-
table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
|
|
874
|
-
.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; }
|
|
875
|
-
.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
|
|
876
|
-
.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
|
|
877
|
-
.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
|
|
878
|
-
.c-title { font-size:15px; font-weight:600; fill:var(--fg); }
|
|
879
|
-
.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
|
|
880
|
-
.media { max-width:100%; border-radius:8px; }
|
|
881
|
-
.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
|
|
882
|
-
.math-block { overflow-x:auto; padding:.4em 0; }
|
|
883
|
-
sup.fn a { font-size:.75em; }
|
|
884
|
-
.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
|
|
885
|
-
.geml-footer code { font-size:.95em; }
|
|
886
|
-
.code-graph { margin:1.4em 0; }
|
|
887
|
-
.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
|
|
888
|
-
.cg-scroll { overflow:auto; max-height:72vh; }
|
|
889
|
-
.cg-svg { display:block; }
|
|
890
|
-
.cg-
|
|
891
|
-
.cg-
|
|
892
|
-
.cg-
|
|
893
|
-
.cg-
|
|
894
|
-
.cg-
|
|
895
|
-
.cg-
|
|
896
|
-
.cg-
|
|
897
|
-
.cg-
|
|
898
|
-
.cg-
|
|
899
|
-
.cg-
|
|
900
|
-
.cg-
|
|
901
|
-
.cg-
|
|
902
|
-
.cg-
|
|
903
|
-
.cg-
|
|
904
|
-
.cg-
|
|
905
|
-
.cg-
|
|
906
|
-
.cg-
|
|
907
|
-
.cg-
|
|
908
|
-
.cg-
|
|
909
|
-
.cg-
|
|
910
|
-
.cg-
|
|
911
|
-
.cg-
|
|
912
|
-
.cg-
|
|
913
|
-
.cg-
|
|
914
|
-
.cg-
|
|
915
|
-
.cg-
|
|
916
|
-
.cg-
|
|
917
|
-
.cg-
|
|
918
|
-
.cg-
|
|
919
|
-
.cg-
|
|
920
|
-
.cg-
|
|
921
|
-
.cg-
|
|
922
|
-
.cg-
|
|
923
|
-
.cg-
|
|
924
|
-
.cg-
|
|
925
|
-
.cg-
|
|
926
|
-
.cg-
|
|
927
|
-
.cg-
|
|
928
|
-
.cg-
|
|
928
|
+
export const CSS = `
|
|
929
|
+
:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
|
|
930
|
+
* { box-sizing: border-box; }
|
|
931
|
+
body { margin:0; color:var(--fg); background:#fafbfc; font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif; }
|
|
932
|
+
main { max-width: 860px; margin: 0 auto; padding: 48px 24px 96px; background:var(--bg); }
|
|
933
|
+
h1,h2,h3,h4,h5,h6 { line-height:1.25; margin:1.6em 0 .6em; scroll-margin-top:16px; }
|
|
934
|
+
h1 { font-size:2em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
935
|
+
h2 { font-size:1.5em; border-bottom:1px solid var(--bd); padding-bottom:.3em; }
|
|
936
|
+
h3 { font-size:1.25em; } h4 { font-size:1em; }
|
|
937
|
+
p { margin:.7em 0; }
|
|
938
|
+
a { color:var(--accent); text-decoration:none; } a:hover { text-decoration:underline; }
|
|
939
|
+
code { background:var(--code-bg); padding:.15em .35em; border-radius:6px; font:.88em ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }
|
|
940
|
+
pre { background:var(--code-bg); padding:14px 16px; border-radius:8px; overflow:auto; }
|
|
941
|
+
pre code { background:none; padding:0; font-size:.85em; }
|
|
942
|
+
pre.output { background:#0d1117; color:#e6edf3; }
|
|
943
|
+
pre.output code { color:inherit; }
|
|
944
|
+
ul,ol { padding-left:1.6em; } li { margin:.2em 0; }
|
|
945
|
+
ul.task-list { list-style:none; padding-left:.2em; }
|
|
946
|
+
li.task input[type=checkbox] { appearance:none; -webkit-appearance:none; width:1.1em; height:1.1em; margin:0 .5em 0 0; vertical-align:-.2em; border:1.5px solid #c8ccd0; border-radius:4px; background:#fff; position:relative; opacity:1; cursor:default; box-sizing:border-box; }
|
|
947
|
+
li.task input[type=checkbox]:checked { background-color:#1f883d; border-color:#1f883d; }
|
|
948
|
+
li.task input[type=checkbox]:checked::after { content:"✓"; position:absolute; top:0; right:0; bottom:0; left:0; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.8em; line-height:1; font-weight:700; }
|
|
949
|
+
aside.callout { border-left:4px solid var(--accent); background:#f0f6ff; padding:.4em 16px; border-radius:0 8px 8px 0; margin:1em 0; }
|
|
950
|
+
aside.aside { border-left-color:#8b949e; background:#f6f8fa; }
|
|
951
|
+
aside.warning { border-left-color:#d97706; background:#fff8f0; }
|
|
952
|
+
aside.callout > :first-child { margin-top:0; } aside.callout > :last-child { margin-bottom:0; }
|
|
953
|
+
figure { margin:1.2em 0; }
|
|
954
|
+
figcaption { color:var(--muted); font-size:.86em; text-align:center; margin-top:.5em; }
|
|
955
|
+
table.geml-table { border-collapse:collapse; width:100%; font-size:.92em; }
|
|
956
|
+
table.geml-table th, table.geml-table td { border:1px solid var(--bd); padding:6px 12px; }
|
|
957
|
+
table.geml-table thead th { background:var(--code-bg); cursor:pointer; user-select:none; white-space:nowrap; }
|
|
958
|
+
table.geml-table thead th::after { content:" \\2195"; color:var(--muted); font-size:.8em; }
|
|
959
|
+
table.geml-table thead th.asc::after { content:" \\2191"; color:var(--accent); }
|
|
960
|
+
table.geml-table thead th.desc::after { content:" \\2193"; color:var(--accent); }
|
|
961
|
+
table.geml-table tbody tr:nth-child(2n) { background:#fafbfc; }
|
|
962
|
+
table.geml-table td.computed { color:#0a7c52; }
|
|
963
|
+
table.geml-table tfoot td { background:var(--code-bg); font-weight:600; border-top:2px solid var(--bd); }
|
|
964
|
+
.table-tools { margin-bottom:6px; } .table-filter { width:240px; max-width:100%; padding:5px 9px; border:1px solid var(--bd); border-radius:7px; font-size:.85em; }
|
|
965
|
+
.table-figure details > summary { cursor:pointer; color:var(--muted); font-size:.86em; padding:4px 0; }
|
|
966
|
+
.table-note { color:var(--muted); font-size:.82em; margin:6px 0 0; }
|
|
967
|
+
.geml-chart { width:100%; height:auto; background:var(--bg); border:1px solid var(--bd); border-radius:8px; }
|
|
968
|
+
.c-title { font-size:15px; font-weight:600; fill:var(--fg); }
|
|
969
|
+
.c-grid { stroke:#eaecef; } .c-axis { stroke:#aab1b8; } .c-tick { font-size:11px; fill:var(--muted); } .c-legend { font-size:12px; fill:var(--fg); }
|
|
970
|
+
.media { max-width:100%; border-radius:8px; }
|
|
971
|
+
.diagram-src { color:var(--muted); } .render-error { color:#cf222e; }
|
|
972
|
+
.math-block { overflow-x:auto; padding:.4em 0; }
|
|
973
|
+
sup.fn a { font-size:.75em; }
|
|
974
|
+
.geml-footer { max-width:860px; margin:0 auto; padding:16px 24px 40px; color:var(--muted); font-size:.82em; }
|
|
975
|
+
.geml-footer code { font-size:.95em; }
|
|
976
|
+
.code-graph { margin:1.4em 0; }
|
|
977
|
+
.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
|
|
978
|
+
.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }
|
|
979
|
+
.cg-svg { display:block; }
|
|
980
|
+
.cg-search-wrap { position:relative; display:inline-block; }
|
|
981
|
+
.cg-search { font:12px/1.4 inherit; padding:2px 7px; border:1px solid var(--bd); border-radius:4px; background:var(--bg); color:var(--fg); min-width:13ch; }
|
|
982
|
+
.cg-search-menu { position:absolute; z-index:30; top:calc(100% + 2px); left:0; min-width:24ch; max-width:52ch; max-height:52vh; overflow:auto; background:var(--bg); border:1px solid var(--bd); border-radius:6px; box-shadow:0 6px 20px rgba(0,0,0,.18); }
|
|
983
|
+
.cg-search-row { display:block; width:100%; text-align:left; padding:4px 9px 4px 18px; border:0; background:none; color:var(--fg); cursor:pointer; font:12px/1.4 inherit; }
|
|
984
|
+
.cg-search-row:hover { background:var(--bd); }
|
|
985
|
+
.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
|
|
986
|
+
.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
|
|
987
|
+
.cg-search-grp:first-of-type { border-top:0; }
|
|
988
|
+
.cg-stage { display:flex; gap:10px; align-items:flex-start; }
|
|
989
|
+
.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
|
|
990
|
+
.cg-src { flex:0 0 42%; max-width:46%; display:flex; flex-direction:column; border:1px solid var(--bd); border-radius:6px; overflow:hidden; background:var(--bg); }
|
|
991
|
+
.cg-src-hd { display:flex; gap:8px; align-items:center; justify-content:space-between; padding:4px 8px; border-bottom:1px solid var(--bd); color:var(--muted); font:.76em ui-monospace,Consolas,monospace; word-break:break-all; }
|
|
992
|
+
.cg-src-hd button { font:inherit; border:1px solid var(--bd); border-radius:5px; background:transparent; color:var(--muted); cursor:pointer; padding:0 6px; }
|
|
993
|
+
.cg-src-body { margin:0; padding:8px 10px; overflow:auto; max-height:72vh; color:var(--fg); font:12px/1.5 ui-monospace,Consolas,monospace; white-space:pre; }
|
|
994
|
+
.cg-src-note { color:var(--muted); font-style:italic; white-space:pre-wrap; }
|
|
995
|
+
.cg-bar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; font-size:.82em; color:var(--muted); margin-bottom:6px; }
|
|
996
|
+
.cg-bar button { font:inherit; padding:1px 8px; border:1px solid var(--bd); border-radius:5px; background:transparent; cursor:pointer; }
|
|
997
|
+
.cg-crumb .cg-seg { border:0; border-radius:0; padding:0; background:none; color:var(--accent); cursor:pointer; font:inherit; }
|
|
998
|
+
.cg-crumb .cg-seg:hover { text-decoration:underline; }
|
|
999
|
+
.cg-frame { display:block; width:100%; height:72vh; border:0; background:var(--bg); }
|
|
1000
|
+
.cg-flash { color:#b42318; }
|
|
1001
|
+
.cg-legend { display:flex; gap:14px; align-items:center; justify-content:space-between; flex-wrap:wrap; font-size:.75em; color:var(--muted); margin-top:6px; }
|
|
1002
|
+
.cg-upbtn { cursor:pointer; }
|
|
1003
|
+
.cg-upbtn circle { fill:#fff; stroke:#94a3b8; }
|
|
1004
|
+
.cg-upbtn text { font-size:11px; fill:#57606a; }
|
|
1005
|
+
.cg-upbtn:hover circle { stroke:var(--accent); stroke-width:1.6; }
|
|
1006
|
+
.cg-upbtn:hover text { fill:var(--accent); }
|
|
1007
|
+
.cg-uplink { fill:none; stroke:#94a3b8; stroke-dasharray:3 2.5; pointer-events:none; }
|
|
1008
|
+
.cg-groups { display:flex; flex-wrap:wrap; gap:4px 12px; margin-top:6px; font-size:.75em; color:var(--muted); }
|
|
1009
|
+
.cg-chip { display:inline-flex; align-items:center; gap:4px; }
|
|
1010
|
+
.cg-chip i { width:10px; height:10px; border-radius:2px; border:1px solid #94a3b8; display:inline-block; }
|
|
1011
|
+
.cg-note { font-size:.8em; color:#9a6700; }
|
|
1012
|
+
.cg-n rect { fill:#eef2f7; stroke:#94a3b8; }
|
|
1013
|
+
.cg-n text { font-size:12px; fill:var(--fg); font-family:ui-monospace,Consolas,monospace; }
|
|
1014
|
+
.cg-n { cursor:pointer; }
|
|
1015
|
+
.cg-n.root rect { fill:#dbeafe; stroke:#2563eb; stroke-width:2; }
|
|
1016
|
+
.cg-n.leaf { opacity:.45; }
|
|
1017
|
+
.cg-n.test rect { stroke-dasharray:3 2; }
|
|
1018
|
+
.cg-n.grp rect { stroke-width:1.8; }
|
|
1019
|
+
.cg-e { fill:none; stroke:#94a3b8; stroke-width:.9; }
|
|
1020
|
+
.cg-e.cand { stroke-dasharray:2 3; }
|
|
1021
|
+
.cg-e.back { stroke:#dc2626; stroke-dasharray:5 3; }
|
|
1022
|
+
.cg-e.http { stroke:#0891b2; stroke-width:1.5; stroke-dasharray:5 2; } /* cross-stack API link */
|
|
1023
|
+
.cg-e.soft { opacity:.55; }
|
|
1024
|
+
.cg-svg.hl .cg-n { opacity:.22; }
|
|
1025
|
+
.cg-svg.hl .cg-e { opacity:.1; }
|
|
1026
|
+
.cg-svg.hl .cg-n.hl { opacity:1; }
|
|
1027
|
+
.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
|
|
929
1028
|
`;
|
|
930
|
-
const JS = `
|
|
931
|
-
(function () {
|
|
932
|
-
function cmp(a, b) {
|
|
933
|
-
var na = a.dataset.sort, nb = b.dataset.sort;
|
|
934
|
-
if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
|
|
935
|
-
return (a.textContent || "").localeCompare(b.textContent || "");
|
|
936
|
-
}
|
|
937
|
-
document.querySelectorAll("table.geml-table").forEach(function (table) {
|
|
938
|
-
var tbody = table.tBodies[0];
|
|
939
|
-
if (!tbody) return;
|
|
940
|
-
// Sort on header click.
|
|
941
|
-
var ths = table.tHead ? table.tHead.rows[0].cells : [];
|
|
942
|
-
Array.prototype.forEach.call(ths, function (th, col) {
|
|
943
|
-
th.addEventListener("click", function () {
|
|
944
|
-
var dir = th.classList.contains("asc") ? "desc" : "asc";
|
|
945
|
-
Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
|
|
946
|
-
th.classList.add(dir);
|
|
947
|
-
var rows = Array.prototype.slice.call(tbody.rows);
|
|
948
|
-
rows.sort(function (r1, r2) {
|
|
949
|
-
var c = cmp(r1.cells[col], r2.cells[col]);
|
|
950
|
-
return dir === "asc" ? c : -c;
|
|
951
|
-
});
|
|
952
|
-
rows.forEach(function (r) { tbody.appendChild(r); });
|
|
953
|
-
});
|
|
954
|
-
});
|
|
955
|
-
// Filter rows.
|
|
956
|
-
var fig = table.closest(".table-figure");
|
|
957
|
-
var input = fig ? fig.querySelector(".table-filter") : null;
|
|
958
|
-
if (input) input.addEventListener("input", function () {
|
|
959
|
-
var q = input.value.toLowerCase();
|
|
960
|
-
Array.prototype.forEach.call(tbody.rows, function (r) {
|
|
961
|
-
r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
|
|
962
|
-
});
|
|
963
|
-
});
|
|
964
|
-
});
|
|
965
|
-
})();
|
|
1029
|
+
export const JS = `
|
|
1030
|
+
(function () {
|
|
1031
|
+
function cmp(a, b) {
|
|
1032
|
+
var na = a.dataset.sort, nb = b.dataset.sort;
|
|
1033
|
+
if (na !== undefined && nb !== undefined) return parseFloat(na) - parseFloat(nb);
|
|
1034
|
+
return (a.textContent || "").localeCompare(b.textContent || "");
|
|
1035
|
+
}
|
|
1036
|
+
document.querySelectorAll("table.geml-table").forEach(function (table) {
|
|
1037
|
+
var tbody = table.tBodies[0];
|
|
1038
|
+
if (!tbody) return;
|
|
1039
|
+
// Sort on header click.
|
|
1040
|
+
var ths = table.tHead ? table.tHead.rows[0].cells : [];
|
|
1041
|
+
Array.prototype.forEach.call(ths, function (th, col) {
|
|
1042
|
+
th.addEventListener("click", function () {
|
|
1043
|
+
var dir = th.classList.contains("asc") ? "desc" : "asc";
|
|
1044
|
+
Array.prototype.forEach.call(ths, function (h) { h.classList.remove("asc", "desc"); });
|
|
1045
|
+
th.classList.add(dir);
|
|
1046
|
+
var rows = Array.prototype.slice.call(tbody.rows);
|
|
1047
|
+
rows.sort(function (r1, r2) {
|
|
1048
|
+
var c = cmp(r1.cells[col], r2.cells[col]);
|
|
1049
|
+
return dir === "asc" ? c : -c;
|
|
1050
|
+
});
|
|
1051
|
+
rows.forEach(function (r) { tbody.appendChild(r); });
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
// Filter rows.
|
|
1055
|
+
var fig = table.closest(".table-figure");
|
|
1056
|
+
var input = fig ? fig.querySelector(".table-filter") : null;
|
|
1057
|
+
if (input) input.addEventListener("input", function () {
|
|
1058
|
+
var q = input.value.toLowerCase();
|
|
1059
|
+
Array.prototype.forEach.call(tbody.rows, function (r) {
|
|
1060
|
+
r.style.display = (r.textContent || "").toLowerCase().indexOf(q) >= 0 ? "" : "none";
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
})();
|
|
966
1065
|
`;
|
|
967
1066
|
// geml-code-graph runtime: layered layout AT DRAW TIME (GEP-0003 / v2-D8) so
|
|
968
1067
|
// clicking a node re-roots the view inside the embedded slice. Algorithm as
|
|
@@ -981,10 +1080,34 @@ export function codeGraphRuntime(root) {
|
|
|
981
1080
|
el.setAttribute(k, String(attrs[k]));
|
|
982
1081
|
return el;
|
|
983
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
|
+
}
|
|
984
1107
|
// Arrow-marker ids must be unique per drawn svg — several mounts share one
|
|
985
1108
|
// document, and duplicate ids would make every graph point at the first.
|
|
986
1109
|
var arrowSeq = 0;
|
|
987
|
-
function boot(mount, data0) {
|
|
1110
|
+
function boot(mount, data0, gpath) {
|
|
988
1111
|
var data, out;
|
|
989
1112
|
function setData(d) {
|
|
990
1113
|
data = d;
|
|
@@ -1076,8 +1199,14 @@ export function codeGraphRuntime(root) {
|
|
|
1076
1199
|
if (!p)
|
|
1077
1200
|
return;
|
|
1078
1201
|
var kk = keyOf(p);
|
|
1079
|
-
if (kk && kk.indexOf("x:") !== 0
|
|
1080
|
-
roots.
|
|
1202
|
+
if (kk && kk.indexOf("x:") !== 0) {
|
|
1203
|
+
if (roots.indexOf(kk) < 0)
|
|
1204
|
+
roots.push(kk);
|
|
1205
|
+
// Badge the module (or the group holding it): this is where the
|
|
1206
|
+
// program starts — the ▶ the label renderer prepends.
|
|
1207
|
+
if (nodes[kk])
|
|
1208
|
+
nodes[kk].appEntry = 1;
|
|
1209
|
+
}
|
|
1081
1210
|
});
|
|
1082
1211
|
var hasIn = {};
|
|
1083
1212
|
edges.forEach(function (e) { hasIn[e[1]] = 1; });
|
|
@@ -1092,10 +1221,10 @@ export function codeGraphRuntime(root) {
|
|
|
1092
1221
|
function homeData() {
|
|
1093
1222
|
return data0.mode === "modules" && data0.mods ? deriveView([]) : data0;
|
|
1094
1223
|
}
|
|
1095
|
-
setData(homeData());
|
|
1224
|
+
setData(data0.mode === "modules" && data0.mods && gpath && gpath.length ? deriveView(gpath) : homeData());
|
|
1096
1225
|
// scale null = fit-to-width on first draw. Left-right is the default —
|
|
1097
1226
|
// call flow reads with the text; the toggle persists per reader.
|
|
1098
|
-
var state = { roots: data.roots.slice(), trail: [], scale: null, dir: "LR", frame: null, cap:
|
|
1227
|
+
var state = { roots: data.roots.slice(), trail: [], scale: null, dir: "LR", frame: null, cap: 600, showAcc: false };
|
|
1099
1228
|
// Direction survives module -> container navigation (each page is a fresh
|
|
1100
1229
|
// document); best-effort only — file:// or the DOM stub may lack storage.
|
|
1101
1230
|
try {
|
|
@@ -1276,7 +1405,9 @@ export function codeGraphRuntime(root) {
|
|
|
1276
1405
|
// width no longer reserves room for it.
|
|
1277
1406
|
function label(k) {
|
|
1278
1407
|
var n = data.nodes[k];
|
|
1279
|
-
|
|
1408
|
+
// ▶ = this module (or group) holds an app entry — where the program
|
|
1409
|
+
// starts, from index meta entry= / app-entry-docs.
|
|
1410
|
+
var full = (n.appEntry || n.entry ? "▶ " : "") + n.n + (n.more ? " ›" : "");
|
|
1280
1411
|
if (full.length <= 32)
|
|
1281
1412
|
return full;
|
|
1282
1413
|
return data.mode === "modules" ? "…" + full.slice(full.length - 31) : full.slice(0, 31) + "…";
|
|
@@ -1377,7 +1508,7 @@ export function codeGraphRuntime(root) {
|
|
|
1377
1508
|
if (!a || !b)
|
|
1378
1509
|
return;
|
|
1379
1510
|
var isBack = s.back[e[0] + ">" + e[1]] || (e[0] === e[1]);
|
|
1380
|
-
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" : "");
|
|
1381
1512
|
var p;
|
|
1382
1513
|
if (e[0] === e[1]) {
|
|
1383
1514
|
p = LR
|
|
@@ -1403,6 +1534,11 @@ export function codeGraphRuntime(root) {
|
|
|
1403
1534
|
p = "M" + x1 + " " + y1 + " C " + x1 + " " + (y1 + GY / 2) + " " + x2 + " " + (y2 - GY / 2) + " " + x2 + " " + y2;
|
|
1404
1535
|
}
|
|
1405
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
|
|
1406
1542
|
var ek = e[0] + ">" + e[1];
|
|
1407
1543
|
edgeEls[ek] = pathEl;
|
|
1408
1544
|
edgeBase[ek] = cls;
|
|
@@ -1552,7 +1688,7 @@ export function codeGraphRuntime(root) {
|
|
|
1552
1688
|
}
|
|
1553
1689
|
catch (e) { /* stub */ }
|
|
1554
1690
|
}
|
|
1555
|
-
function openDoc(rel) {
|
|
1691
|
+
function openDoc(rel, gpath) {
|
|
1556
1692
|
var lv = live();
|
|
1557
1693
|
if (lv) {
|
|
1558
1694
|
Promise.resolve(lv({ doc: rel })).then(function (nd) {
|
|
@@ -1565,7 +1701,7 @@ export function codeGraphRuntime(root) {
|
|
|
1565
1701
|
// payload so its grouping tree derives; pushView alone would draw
|
|
1566
1702
|
// the empty raw payload (nodes come out {}).
|
|
1567
1703
|
if (nd.mode === "modules" && nd.mods)
|
|
1568
|
-
boot(mount, nd);
|
|
1704
|
+
boot(mount, nd, gpath);
|
|
1569
1705
|
else
|
|
1570
1706
|
pushView(nd);
|
|
1571
1707
|
}, function () { flash("cannot load " + rel); });
|
|
@@ -1589,7 +1725,11 @@ export function codeGraphRuntime(root) {
|
|
|
1589
1725
|
// blocked) — embed directly; the frame contains any error itself.
|
|
1590
1726
|
try {
|
|
1591
1727
|
if (/^https?:$/.test(window.location.protocol)) {
|
|
1592
|
-
|
|
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) {
|
|
1593
1733
|
if (r.ok)
|
|
1594
1734
|
embed();
|
|
1595
1735
|
else
|
|
@@ -1646,9 +1786,12 @@ export function codeGraphRuntime(root) {
|
|
|
1646
1786
|
seg("modules", function () { openDoc(navBase + "index.geml"); });
|
|
1647
1787
|
sepEl();
|
|
1648
1788
|
var modName = String(data.module || String(data.start || "").replace(/^.*\//, "").replace(/\.geml$/, "") || "container");
|
|
1789
|
+
// The middle crumb reads as the OVERVIEW level ("modules / <module>"),
|
|
1790
|
+
// so clicking it goes THERE — the module tier listing this container's
|
|
1791
|
+
// siblings — not a reload of the page you are already on.
|
|
1649
1792
|
seg(modName, function () {
|
|
1650
1793
|
if (live())
|
|
1651
|
-
openDoc(
|
|
1794
|
+
openDoc(navBase + "index.geml", [modName.split("/")[0]]);
|
|
1652
1795
|
else {
|
|
1653
1796
|
state.trail = [];
|
|
1654
1797
|
setData(homeData());
|
|
@@ -1659,8 +1802,12 @@ export function codeGraphRuntime(root) {
|
|
|
1659
1802
|
sepEl();
|
|
1660
1803
|
seg(data.dir === "up"
|
|
1661
1804
|
? "callers of " + (data.nodes[data.focus] ? data.nodes[data.focus].n : "") + (data.partial ? " (in-slice)" : "") + (Object.keys(data.nodes).length <= 1 ? " — none recorded" : "")
|
|
1662
|
-
: state.trail.length ? "root: " +
|
|
1663
|
-
|
|
1805
|
+
: state.trail.length && state.roots.length === 1 ? "root: " + (data.nodes[state.roots[0]] || {}).n
|
|
1806
|
+
// Many roots = this IS the module's own view (its whole entry
|
|
1807
|
+
// list) — the methods are already on the graph; naming them all
|
|
1808
|
+
// here just makes a paragraph-long crumb.
|
|
1809
|
+
: state.trail.length ? "roots: entry"
|
|
1810
|
+
: "roots: entry", null);
|
|
1664
1811
|
}
|
|
1665
1812
|
bar.appendChild(crumb);
|
|
1666
1813
|
var scroller = document.createElement("div");
|
|
@@ -1753,8 +1900,8 @@ export function codeGraphRuntime(root) {
|
|
|
1753
1900
|
capInfo.textContent = "showing " + (s.total - s.capped) + " of " + s.total + " reachable";
|
|
1754
1901
|
bar.appendChild(capInfo);
|
|
1755
1902
|
var moreBtn = document.createElement("button");
|
|
1756
|
-
moreBtn.textContent = "+
|
|
1757
|
-
moreBtn.onclick = function () { state.cap +=
|
|
1903
|
+
moreBtn.textContent = "+600";
|
|
1904
|
+
moreBtn.onclick = function () { state.cap += 600; draw(); };
|
|
1758
1905
|
bar.appendChild(moreBtn);
|
|
1759
1906
|
var allBtn = document.createElement("button");
|
|
1760
1907
|
allBtn.textContent = "all";
|
|
@@ -1771,6 +1918,156 @@ export function codeGraphRuntime(root) {
|
|
|
1771
1918
|
resetBtn.onclick = function () { state.trail = []; setData(homeData()); state.roots = data.roots.slice(); draw(); };
|
|
1772
1919
|
bar.appendChild(resetBtn);
|
|
1773
1920
|
}
|
|
1921
|
+
// Find a method by name -> jump to its node. Data source: a served page
|
|
1922
|
+
// (http) queries the /_search endpoint (top matches only — a huge index
|
|
1923
|
+
// never ships); a static page (file://) lazy-loads the compact
|
|
1924
|
+
// _index/search-index.js via <script> (fetch is CORS-blocked on file://,
|
|
1925
|
+
// a script tag is not). Picking a hit opens its FOCUSED call graph (B)
|
|
1926
|
+
// in place on a served page; on a static page (no live loader) it
|
|
1927
|
+
// navigates to the node's document (A). Alt-click always just locates.
|
|
1928
|
+
if (typeof location !== "undefined") { // browser only — skipped in the fake-DOM runtime test
|
|
1929
|
+
var searchWrap = document.createElement("span");
|
|
1930
|
+
searchWrap.className = "cg-search-wrap";
|
|
1931
|
+
var searchBox = document.createElement("input");
|
|
1932
|
+
searchBox.type = "search";
|
|
1933
|
+
searchBox.className = "cg-search";
|
|
1934
|
+
searchBox.placeholder = "find a method…";
|
|
1935
|
+
searchBox.setAttribute("aria-label", "Find a method by name");
|
|
1936
|
+
var searchMenu = document.createElement("div");
|
|
1937
|
+
searchMenu.className = "cg-search-menu";
|
|
1938
|
+
searchMenu.hidden = true;
|
|
1939
|
+
searchWrap.appendChild(searchBox);
|
|
1940
|
+
searchWrap.appendChild(searchMenu);
|
|
1941
|
+
bar.appendChild(searchWrap);
|
|
1942
|
+
var srvSearch = /^https?:$/.test(location.protocol);
|
|
1943
|
+
function withIndex(cb) {
|
|
1944
|
+
if (window.__gemlSearch)
|
|
1945
|
+
return cb(window.__gemlSearch);
|
|
1946
|
+
if (!cgSameOrigin(navBase + "_index/search-index.js")) {
|
|
1947
|
+
cb([]);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
var s = document.createElement("script");
|
|
1951
|
+
s.src = navBase + "_index/search-index.js";
|
|
1952
|
+
s.onload = function () { cb(window.__gemlSearch || []); };
|
|
1953
|
+
s.onerror = function () { cb([]); };
|
|
1954
|
+
document.head.appendChild(s);
|
|
1955
|
+
}
|
|
1956
|
+
// Rank exactly like serve's /_search (exact > prefix > qualified-tail
|
|
1957
|
+
// prefix > substring), so both data paths order hits the same way.
|
|
1958
|
+
function hitScore(n, q) {
|
|
1959
|
+
if (n === q)
|
|
1960
|
+
return 0;
|
|
1961
|
+
if (n.indexOf(q) === 0)
|
|
1962
|
+
return 1;
|
|
1963
|
+
var c2 = n.lastIndexOf("::"), d = n.lastIndexOf(".");
|
|
1964
|
+
var cut = Math.max(c2 >= 0 ? c2 + 2 : 0, d >= 0 ? d + 1 : 0);
|
|
1965
|
+
if (cut > 0 && n.slice(cut).indexOf(q) === 0)
|
|
1966
|
+
return 2;
|
|
1967
|
+
return n.indexOf(q) >= 0 ? 3 : -1;
|
|
1968
|
+
}
|
|
1969
|
+
function candidates(q, cb) {
|
|
1970
|
+
q = q.trim().toLowerCase();
|
|
1971
|
+
if (q.length < 2)
|
|
1972
|
+
return cb({ total: 0, hits: [] });
|
|
1973
|
+
if (srvSearch) {
|
|
1974
|
+
fetch("/_search?q=" + encodeURIComponent(q))
|
|
1975
|
+
.then(function (r) { return r.ok ? r.json() : { total: 0, hits: [] }; })
|
|
1976
|
+
.then(function (a) { cb(a && a.hits ? a : { total: 0, hits: [] }); })
|
|
1977
|
+
.catch(function () { cb({ total: 0, hits: [] }); });
|
|
1978
|
+
}
|
|
1979
|
+
else {
|
|
1980
|
+
withIndex(function (rows) {
|
|
1981
|
+
var ranked = [];
|
|
1982
|
+
for (var i = 0; i < rows.length; i++) {
|
|
1983
|
+
var s = hitScore(String(rows[i][0]).toLowerCase(), q);
|
|
1984
|
+
if (s >= 0)
|
|
1985
|
+
ranked.push({ s: s, name: rows[i][0], doc: rows[i][1], id: rows[i][2] });
|
|
1986
|
+
}
|
|
1987
|
+
ranked.sort(function (a, b) { return a.s - b.s || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0); });
|
|
1988
|
+
// The lookup aliases bare member names to the same node — dedupe
|
|
1989
|
+
// on doc#id, keeping the best-ranked row.
|
|
1990
|
+
var seen = {}, hits = [];
|
|
1991
|
+
for (var j = 0; j < ranked.length; j++) {
|
|
1992
|
+
var rj = ranked[j];
|
|
1993
|
+
var k = rj.doc + "#" + rj.id;
|
|
1994
|
+
if (seen[k])
|
|
1995
|
+
continue;
|
|
1996
|
+
seen[k] = 1;
|
|
1997
|
+
hits.push(rj);
|
|
1998
|
+
}
|
|
1999
|
+
cb({ total: hits.length, hits: hits.slice(0, 100) });
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
function gotoHit(doc, id, locate) {
|
|
2004
|
+
searchMenu.hidden = true;
|
|
2005
|
+
if (live() && !locate) {
|
|
2006
|
+
showCallees(doc + "#" + id);
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
location.href = navBase + doc.replace(/\.geml$/, ".html") + "#" + encodeURIComponent(id);
|
|
2010
|
+
}
|
|
2011
|
+
var searchSeq = 0, searchTop = null; // best-ranked hit — Enter opens it
|
|
2012
|
+
searchBox.addEventListener("input", function () {
|
|
2013
|
+
var my = ++searchSeq, qv = searchBox.value;
|
|
2014
|
+
candidates(qv, function (res) {
|
|
2015
|
+
if (my !== searchSeq)
|
|
2016
|
+
return; // a newer keystroke already fired
|
|
2017
|
+
searchMenu.replaceChildren();
|
|
2018
|
+
var hits = res.hits || [];
|
|
2019
|
+
searchTop = hits.length ? hits[0] : null;
|
|
2020
|
+
if (!hits.length) {
|
|
2021
|
+
searchMenu.hidden = true;
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
// Honest count first — a capped list must say so.
|
|
2025
|
+
var count = document.createElement("div");
|
|
2026
|
+
count.className = "cg-search-count";
|
|
2027
|
+
count.textContent = (res.total > hits.length ? "showing " + hits.length + " of " + res.total + " matches" : res.total + (res.total === 1 ? " match" : " matches")) + " · Enter opens the first";
|
|
2028
|
+
searchMenu.appendChild(count);
|
|
2029
|
+
// Group by module (document), groups in best-hit order — hits arrive
|
|
2030
|
+
// globally ranked, so first appearance = the group's best rank.
|
|
2031
|
+
var order = [], byDoc = {};
|
|
2032
|
+
hits.forEach(function (c) {
|
|
2033
|
+
if (!byDoc[c.doc]) {
|
|
2034
|
+
byDoc[c.doc] = [];
|
|
2035
|
+
order.push(c.doc);
|
|
2036
|
+
}
|
|
2037
|
+
byDoc[c.doc].push(c);
|
|
2038
|
+
});
|
|
2039
|
+
order.forEach(function (doc) {
|
|
2040
|
+
var hd = document.createElement("div");
|
|
2041
|
+
hd.className = "cg-search-grp";
|
|
2042
|
+
hd.textContent = String(doc).replace(/\.geml$/, "").replace(/--/g, "/");
|
|
2043
|
+
searchMenu.appendChild(hd);
|
|
2044
|
+
byDoc[doc].forEach(function (c) {
|
|
2045
|
+
var row = document.createElement("button");
|
|
2046
|
+
row.className = "cg-search-row";
|
|
2047
|
+
row.type = "button";
|
|
2048
|
+
var nm = document.createElement("b");
|
|
2049
|
+
nm.textContent = c.name;
|
|
2050
|
+
row.appendChild(nm);
|
|
2051
|
+
row.onclick = function (ev) { gotoHit(c.doc, c.id, !!ev.altKey); };
|
|
2052
|
+
searchMenu.appendChild(row);
|
|
2053
|
+
});
|
|
2054
|
+
});
|
|
2055
|
+
searchMenu.hidden = false;
|
|
2056
|
+
});
|
|
2057
|
+
});
|
|
2058
|
+
searchBox.addEventListener("keydown", function (ev) {
|
|
2059
|
+
if (ev.key === "Escape") {
|
|
2060
|
+
searchMenu.hidden = true;
|
|
2061
|
+
searchBox.blur();
|
|
2062
|
+
}
|
|
2063
|
+
else if (ev.key === "Enter" && searchTop) {
|
|
2064
|
+
ev.preventDefault();
|
|
2065
|
+
gotoHit(searchTop.doc, searchTop.id, !!ev.altKey);
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
document.addEventListener("click", function (ev) { if (!searchWrap.contains(ev.target))
|
|
2069
|
+
searchMenu.hidden = true; });
|
|
2070
|
+
} // end browser-only search box
|
|
1774
2071
|
mount.appendChild(bar);
|
|
1775
2072
|
mount.appendChild(stage);
|
|
1776
2073
|
if (state.scale === null)
|
|
@@ -1784,6 +2081,33 @@ export function codeGraphRuntime(root) {
|
|
|
1784
2081
|
else
|
|
1785
2082
|
scroller.scrollTop = 1e6;
|
|
1786
2083
|
}
|
|
2084
|
+
// Centre the CROSS axis (the fit-to-pane one): the tree fans out around
|
|
2085
|
+
// its midline, so a big graph clamped to the 2/3 scale floor would
|
|
2086
|
+
// otherwise open on an empty top/left corner with every node off-screen.
|
|
2087
|
+
// Reading the scroll extent forces the post-applyScale reflow; when the
|
|
2088
|
+
// cross axis already fits, the delta is ≤0 and this is a no-op. The
|
|
2089
|
+
// reading axis is untouched (root start, or far-end for callers above).
|
|
2090
|
+
// If the view holds an app entry (▶), aim the midline at the FIRST one
|
|
2091
|
+
// (roots first, then any node) instead of the geometric centre — the
|
|
2092
|
+
// reader lands where the program starts.
|
|
2093
|
+
var entryK = null;
|
|
2094
|
+
state.roots.concat(Object.keys(data.nodes)).some(function (ek) {
|
|
2095
|
+
var en = data.nodes[ek];
|
|
2096
|
+
if (en && (en.appEntry || en.entry) && pos[ek]) {
|
|
2097
|
+
entryK = ek;
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
return false;
|
|
2101
|
+
});
|
|
2102
|
+
var aim = function (full, pane, at) { return Math.max(0, Math.min(full - pane, at - pane / 2)); };
|
|
2103
|
+
if (LR)
|
|
2104
|
+
scroller.scrollTop = entryK
|
|
2105
|
+
? aim(scroller.scrollHeight, scroller.clientHeight, (pos[entryK].y + NH / 2) * state.scale)
|
|
2106
|
+
: Math.max(0, (scroller.scrollHeight - scroller.clientHeight) / 2);
|
|
2107
|
+
else
|
|
2108
|
+
scroller.scrollLeft = entryK
|
|
2109
|
+
? aim(scroller.scrollWidth, scroller.clientWidth, (pos[entryK].x + pos[entryK].w / 2) * state.scale)
|
|
2110
|
+
: Math.max(0, (scroller.scrollWidth - scroller.clientWidth) / 2);
|
|
1787
2111
|
// Footer: live facts, not a static cheat-sheet (navigation lives in
|
|
1788
2112
|
// the breadcrumb above).
|
|
1789
2113
|
var footer = document.createElement("div");
|
|
@@ -1823,16 +2147,35 @@ export function codeGraphRuntime(root) {
|
|
|
1823
2147
|
// document loader (mount._cgView, attached by the upgrade step); a
|
|
1824
2148
|
// static CLI page reverses its in-slice edges — partial but honest,
|
|
1825
2149
|
// and labelled as such in the crumb.
|
|
2150
|
+
// No recorded callers (an app/framework entry, or dead code): "up" from
|
|
2151
|
+
// a METHOD is its CONTAINER — one level, never the whole-repo overview
|
|
2152
|
+
// two levels up. From a focused/derived view that means the method's own
|
|
2153
|
+
// container page (the view its module node opens); already sitting on
|
|
2154
|
+
// that default view, say why and stay put — the "don't jump, say why"
|
|
2155
|
+
// contract.
|
|
2156
|
+
function noCallers(k) {
|
|
2157
|
+
var docRel = k.slice(0, k.lastIndexOf("#"));
|
|
2158
|
+
if (docRel !== data0.start) {
|
|
2159
|
+
openDoc(navBase + docRel);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
if (state.trail.length) {
|
|
2163
|
+
state.trail = [];
|
|
2164
|
+
setData(homeData());
|
|
2165
|
+
state.roots = data.roots.slice();
|
|
2166
|
+
draw();
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
flash("no recorded callers — an app/framework entry point");
|
|
2170
|
+
}
|
|
1826
2171
|
function showCallers(k) {
|
|
1827
2172
|
var lv = live();
|
|
1828
2173
|
if (lv) {
|
|
1829
2174
|
Promise.resolve(lv({ dir: "up", node: k })).then(function (nd) {
|
|
1830
|
-
// In-degree-zero entry (agent/AOP hook, app top): it has no callers,
|
|
1831
|
-
// so "up" means the module page — not an empty caller view.
|
|
1832
2175
|
if (nd && Object.keys(nd.nodes).length > 1)
|
|
1833
2176
|
pushView(nd);
|
|
1834
2177
|
else
|
|
1835
|
-
|
|
2178
|
+
noCallers(k);
|
|
1836
2179
|
});
|
|
1837
2180
|
return;
|
|
1838
2181
|
}
|
|
@@ -1849,9 +2192,9 @@ export function codeGraphRuntime(root) {
|
|
|
1849
2192
|
} });
|
|
1850
2193
|
}
|
|
1851
2194
|
if (Object.keys(keep).length <= 1) {
|
|
1852
|
-
|
|
2195
|
+
noCallers(k);
|
|
1853
2196
|
return;
|
|
1854
|
-
}
|
|
2197
|
+
}
|
|
1855
2198
|
var nodes = {}, edges = [];
|
|
1856
2199
|
for (var nk in keep)
|
|
1857
2200
|
nodes[nk] = data0.nodes[nk];
|
|
@@ -1923,8 +2266,16 @@ export function codeGraphRuntime(root) {
|
|
|
1923
2266
|
degrade();
|
|
1924
2267
|
return;
|
|
1925
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
|
+
}
|
|
1926
2277
|
try {
|
|
1927
|
-
Promise.resolve(fetchFn(base + path)).then(function (r) {
|
|
2278
|
+
Promise.resolve(fetchFn(base + path, { credentials: "omit" })).then(function (r) {
|
|
1928
2279
|
if (!r || r.ok === false) {
|
|
1929
2280
|
degrade();
|
|
1930
2281
|
return null;
|
|
@@ -1941,16 +2292,22 @@ export function codeGraphRuntime(root) {
|
|
|
1941
2292
|
var ub = tgt && tgt.closest ? tgt.closest(".cg-upbtn") : null;
|
|
1942
2293
|
if (ub) {
|
|
1943
2294
|
if (ub.getAttribute("data-act") === "down") {
|
|
1944
|
-
//
|
|
1945
|
-
//
|
|
1946
|
-
|
|
2295
|
+
// "Back to its callee chain" must LAND on the callee chain: pop
|
|
2296
|
+
// the trail only when the view underneath IS this method's own
|
|
2297
|
+
// focused view (the search/chain path that opened these callers).
|
|
2298
|
+
// Arriving from anywhere wider — the module page — popping would
|
|
2299
|
+
// land there instead, so build the method's chain fresh.
|
|
2300
|
+
var k0 = ub.getAttribute("data-k");
|
|
2301
|
+
var top0 = state.trail[state.trail.length - 1];
|
|
2302
|
+
var ownChain = top0 && top0.data && top0.data.dir !== "up" && top0.roots && top0.roots.length === 1 && top0.roots[0] === k0;
|
|
2303
|
+
if (ownChain) {
|
|
1947
2304
|
var tr0 = state.trail.pop();
|
|
1948
2305
|
setData(tr0.data);
|
|
1949
2306
|
state.roots = tr0.roots;
|
|
1950
2307
|
draw();
|
|
1951
2308
|
}
|
|
1952
2309
|
else
|
|
1953
|
-
showCallees(
|
|
2310
|
+
showCallees(k0);
|
|
1954
2311
|
}
|
|
1955
2312
|
else
|
|
1956
2313
|
showCallers(ub.getAttribute("data-k"));
|
|
@@ -2048,7 +2405,7 @@ export function codeGraphRuntime(root) {
|
|
|
2048
2405
|
});
|
|
2049
2406
|
}
|
|
2050
2407
|
// CLI inlining: the compiled runtime function, verbatim, run against document.
|
|
2051
|
-
const CODE_GRAPH_JS = `(${codeGraphRuntime.toString()})(document);`;
|
|
2408
|
+
export const CODE_GRAPH_JS = `(${codeGraphRuntime.toString()})(document);`;
|
|
2052
2409
|
// Browser-side wave builder: the slice builder is synchronous with a
|
|
2053
2410
|
// synchronous loader, but a browser fetches documents asynchronously — so
|
|
2054
2411
|
// run the build in WAVES: every pass records the documents it needed but did
|
|
@@ -2094,89 +2451,7 @@ export function codeGraphWaves(fetchDoc, parseFn) {
|
|
|
2094
2451
|
},
|
|
2095
2452
|
};
|
|
2096
2453
|
}
|
|
2097
|
-
function page(title, body, ctx, source) {
|
|
2098
|
-
const mathHead = ctx.usedMath
|
|
2099
|
-
? `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">\n` +
|
|
2100
|
-
`<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>\n` +
|
|
2101
|
-
`<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body,{delimiters:[{left:'\\\\[',right:'\\\\]',display:true},{left:'\\\\(',right:'\\\\)',display:false}]})"></script>\n`
|
|
2102
|
-
: "";
|
|
2103
|
-
const mermaidHead = ctx.usedMermaid
|
|
2104
|
-
? `<script type="module">import m from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";m.initialize({startOnLoad:true});</script>\n`
|
|
2105
|
-
: "";
|
|
2106
|
-
const footer = source
|
|
2107
|
-
? `<footer class="geml-footer">Rendered from <code>${esc(source)}</code> by the GEML runtime. Tables are sortable and filterable; the chart is inline SVG drawn from its bound table.</footer>`
|
|
2108
|
-
: "";
|
|
2109
|
-
// Live enhancement for served pages: attach _cgView loaders after the
|
|
2110
|
-
// static bootstrap has drawn. The runtime reads the hook lazily, so late
|
|
2111
|
-
// binding works with no redraw; if this module never loads (offline copy,
|
|
2112
|
-
// old browser), the page simply stays static. The parser dist imports
|
|
2113
|
-
// node:* for its CLI paths — an import map points those at the served stub
|
|
2114
|
-
// (same trick as the viewer's esbuild alias), and the process shim must be
|
|
2115
|
-
// in place BEFORE the modules evaluate, hence the dynamic import().
|
|
2116
|
-
const wantLive = ctx.usedCodeGraph && !!ctx.opts.liveGraph;
|
|
2117
|
-
const lg = wantLive ? escAttr(ctx.opts.liveGraph) : "";
|
|
2118
|
-
const importMap = wantLive
|
|
2119
|
-
? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js"}}</script>\n`
|
|
2120
|
-
: "";
|
|
2121
|
-
const liveJs = wantLive
|
|
2122
|
-
? `<script type="module">
|
|
2123
|
-
globalThis.process ??= { argv: [], env: {} };
|
|
2124
|
-
const { parse } = await import("${lg}geml.js");
|
|
2125
|
-
const { codeGraphWaves } = await import("${lg}render.js");
|
|
2126
|
-
const w = codeGraphWaves(async (rel) => {
|
|
2127
|
-
try { const r = await fetch(rel, { cache: "no-cache" }); return r.ok ? await r.text() : null; } catch { return null; }
|
|
2128
|
-
}, parse);
|
|
2129
|
-
for (const m of document.querySelectorAll(".cg-mount[data-start]")) {
|
|
2130
|
-
const start = m.getAttribute("data-start");
|
|
2131
|
-
m._cgView = async (view) => {
|
|
2132
|
-
// A directed view builds from the node's OWN document (its meta names the
|
|
2133
|
-
// module and graph-depth); {doc} opens that document; else the mount's.
|
|
2134
|
-
const src = view && view.doc ? view.doc
|
|
2135
|
-
: view && view.node ? view.node.slice(0, view.node.lastIndexOf("#"))
|
|
2136
|
-
: start;
|
|
2137
|
-
const r = await w.build(src, view && view.doc ? undefined : view);
|
|
2138
|
-
return r.error !== undefined ? null : r.data;
|
|
2139
|
-
};
|
|
2140
|
-
}
|
|
2141
|
-
</script>\n`
|
|
2142
|
-
: "";
|
|
2143
|
-
return `<!doctype html>
|
|
2144
|
-
<html lang="en">
|
|
2145
|
-
<head>
|
|
2146
|
-
<meta charset="utf-8">
|
|
2147
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2148
|
-
<title>${esc(title)}</title>
|
|
2149
|
-
<style>${CSS}</style>
|
|
2150
|
-
${importMap}${mathHead}${mermaidHead}</head>
|
|
2151
|
-
<body>
|
|
2152
|
-
<main>
|
|
2153
|
-
${body}
|
|
2154
|
-
</main>
|
|
2155
|
-
${footer}
|
|
2156
|
-
<script>${JS}</script>
|
|
2157
|
-
${ctx.usedCodeGraph ? `<script>${CODE_GRAPH_JS}</script>\n` : ""}${liveJs}</body>
|
|
2158
|
-
</html>
|
|
2159
|
-
`;
|
|
2160
|
-
}
|
|
2161
2454
|
// ---------------------------------------------------------------------------
|
|
2162
2455
|
// Public entry
|
|
2163
2456
|
// ---------------------------------------------------------------------------
|
|
2164
2457
|
export { buildCodeGraph };
|
|
2165
|
-
export function renderHtml(doc, opts = {}) {
|
|
2166
|
-
const ctx = new RenderCtx(doc, opts);
|
|
2167
|
-
let body = doc.children.map((b) => ctx.block(b)).filter((s) => s !== "").join("\n");
|
|
2168
|
-
// Codemap scenario ① (GEP-0003): a codemap document (meta declares module=
|
|
2169
|
-
// or container=, plus an entry surface) IS the graph data — offer the layered
|
|
2170
|
-
// method-flow view at the top, an implicit self-embed.
|
|
2171
|
-
const meta = doc.children.find((b) => b.kind === "block" && b.type === "meta" && b.data);
|
|
2172
|
-
const md = meta?.data ?? {};
|
|
2173
|
-
if ((md["module"] !== undefined || md["container"] !== undefined)
|
|
2174
|
-
&& opts.loadDoc && opts.parseDoc && opts.source) {
|
|
2175
|
-
const cap = md["entry"] !== undefined || md["container"] !== undefined
|
|
2176
|
-
? `layered method flow — roots from this document's <code>entry</code>`
|
|
2177
|
-
: `layered method flow — roots: in-degree-zero methods (no <code>entry</code> declared)`;
|
|
2178
|
-
body = ctx.codeGraphFigure(opts.source, "", `<figcaption>${cap}</figcaption>`) + "\n" + body;
|
|
2179
|
-
}
|
|
2180
|
-
const title = opts.title ?? ctx.docTitle() ?? "GEML document";
|
|
2181
|
-
return page(title, body, ctx, opts.source);
|
|
2182
|
-
}
|