@geml/geml 1.1.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -11
- package/codemap/adapters/crg.mjs +11 -0
- package/codemap/adapters/scip.mjs +481 -52
- package/codemap/browser-stub.mjs +5 -0
- package/codemap/build.mjs +270 -45
- package/codemap/detect.mjs +230 -16
- package/codemap/emit.mjs +78 -7
- package/codemap/entries.mjs +129 -0
- package/codemap/find.mjs +63 -0
- package/codemap/foldings.mjs +110 -0
- package/codemap/mcp-server.mjs +44 -15
- package/codemap/normalize.mjs +0 -0
- package/codemap/recipe-trust.mjs +103 -0
- package/codemap/refresh.mjs +158 -8
- package/codemap/serve.mjs +428 -228
- package/codemap/sfc-virtualize.mjs +367 -0
- package/codemap/verify.mjs +20 -3
- package/dist/from-md.js +66 -7
- package/dist/geml.d.ts +2 -1
- package/dist/geml.js +369 -97
- 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 +32 -2
- package/dist/render.js +308 -116
- package/dist/serialize.js +22 -2
- package/dist/table.js +40 -5
- package/dist/to-md.js +4 -0
- package/package.json +63 -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);
|
|
@@ -835,7 +888,7 @@ function trunc(s, n) {
|
|
|
835
888
|
// ---------------------------------------------------------------------------
|
|
836
889
|
// Page shell, inline CSS, inline interactivity JS
|
|
837
890
|
// ---------------------------------------------------------------------------
|
|
838
|
-
const CSS = `
|
|
891
|
+
export const CSS = `
|
|
839
892
|
:root { --fg:#1f2328; --muted:#656d76; --bd:#d0d7de; --bg:#fff; --accent:#2563eb; --code-bg:#f6f8fa; }
|
|
840
893
|
* { box-sizing: border-box; }
|
|
841
894
|
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; }
|
|
@@ -885,8 +938,16 @@ sup.fn a { font-size:.75em; }
|
|
|
885
938
|
.geml-footer code { font-size:.95em; }
|
|
886
939
|
.code-graph { margin:1.4em 0; }
|
|
887
940
|
.cg-mount { border:1px solid var(--bd); border-radius:8px; padding:10px 12px; background:var(--bg); }
|
|
888
|
-
.cg-scroll { overflow:auto; max-height:72vh; }
|
|
941
|
+
.cg-scroll { overflow:auto; min-height:52vh; max-height:72vh; }
|
|
889
942
|
.cg-svg { display:block; }
|
|
943
|
+
.cg-search-wrap { position:relative; display:inline-block; }
|
|
944
|
+
.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; }
|
|
945
|
+
.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); }
|
|
946
|
+
.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; }
|
|
947
|
+
.cg-search-row:hover { background:var(--bd); }
|
|
948
|
+
.cg-search-count { position:sticky; top:0; padding:4px 9px; font-size:11px; opacity:.65; background:var(--bg); border-bottom:1px solid var(--bd); }
|
|
949
|
+
.cg-search-grp { padding:6px 9px 2px; font-size:11px; font-weight:600; opacity:.7; border-top:1px solid var(--bd); }
|
|
950
|
+
.cg-search-grp:first-of-type { border-top:0; }
|
|
890
951
|
.cg-stage { display:flex; gap:10px; align-items:flex-start; }
|
|
891
952
|
.cg-stage .cg-scroll { flex:1 1 auto; min-width:0; }
|
|
892
953
|
.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); }
|
|
@@ -927,7 +988,7 @@ sup.fn a { font-size:.75em; }
|
|
|
927
988
|
.cg-svg.hl .cg-n.hl { opacity:1; }
|
|
928
989
|
.cg-svg.hl .cg-e.hl { opacity:1; stroke-width:1.6; }
|
|
929
990
|
`;
|
|
930
|
-
const JS = `
|
|
991
|
+
export const JS = `
|
|
931
992
|
(function () {
|
|
932
993
|
function cmp(a, b) {
|
|
933
994
|
var na = a.dataset.sort, nb = b.dataset.sort;
|
|
@@ -984,7 +1045,7 @@ export function codeGraphRuntime(root) {
|
|
|
984
1045
|
// Arrow-marker ids must be unique per drawn svg — several mounts share one
|
|
985
1046
|
// document, and duplicate ids would make every graph point at the first.
|
|
986
1047
|
var arrowSeq = 0;
|
|
987
|
-
function boot(mount, data0) {
|
|
1048
|
+
function boot(mount, data0, gpath) {
|
|
988
1049
|
var data, out;
|
|
989
1050
|
function setData(d) {
|
|
990
1051
|
data = d;
|
|
@@ -1076,8 +1137,14 @@ export function codeGraphRuntime(root) {
|
|
|
1076
1137
|
if (!p)
|
|
1077
1138
|
return;
|
|
1078
1139
|
var kk = keyOf(p);
|
|
1079
|
-
if (kk && kk.indexOf("x:") !== 0
|
|
1080
|
-
roots.
|
|
1140
|
+
if (kk && kk.indexOf("x:") !== 0) {
|
|
1141
|
+
if (roots.indexOf(kk) < 0)
|
|
1142
|
+
roots.push(kk);
|
|
1143
|
+
// Badge the module (or the group holding it): this is where the
|
|
1144
|
+
// program starts — the ▶ the label renderer prepends.
|
|
1145
|
+
if (nodes[kk])
|
|
1146
|
+
nodes[kk].appEntry = 1;
|
|
1147
|
+
}
|
|
1081
1148
|
});
|
|
1082
1149
|
var hasIn = {};
|
|
1083
1150
|
edges.forEach(function (e) { hasIn[e[1]] = 1; });
|
|
@@ -1092,10 +1159,10 @@ export function codeGraphRuntime(root) {
|
|
|
1092
1159
|
function homeData() {
|
|
1093
1160
|
return data0.mode === "modules" && data0.mods ? deriveView([]) : data0;
|
|
1094
1161
|
}
|
|
1095
|
-
setData(homeData());
|
|
1162
|
+
setData(data0.mode === "modules" && data0.mods && gpath && gpath.length ? deriveView(gpath) : homeData());
|
|
1096
1163
|
// scale null = fit-to-width on first draw. Left-right is the default —
|
|
1097
1164
|
// 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:
|
|
1165
|
+
var state = { roots: data.roots.slice(), trail: [], scale: null, dir: "LR", frame: null, cap: 600, showAcc: false };
|
|
1099
1166
|
// Direction survives module -> container navigation (each page is a fresh
|
|
1100
1167
|
// document); best-effort only — file:// or the DOM stub may lack storage.
|
|
1101
1168
|
try {
|
|
@@ -1276,7 +1343,9 @@ export function codeGraphRuntime(root) {
|
|
|
1276
1343
|
// width no longer reserves room for it.
|
|
1277
1344
|
function label(k) {
|
|
1278
1345
|
var n = data.nodes[k];
|
|
1279
|
-
|
|
1346
|
+
// ▶ = this module (or group) holds an app entry — where the program
|
|
1347
|
+
// starts, from index meta entry= / app-entry-docs.
|
|
1348
|
+
var full = (n.appEntry || n.entry ? "▶ " : "") + n.n + (n.more ? " ›" : "");
|
|
1280
1349
|
if (full.length <= 32)
|
|
1281
1350
|
return full;
|
|
1282
1351
|
return data.mode === "modules" ? "…" + full.slice(full.length - 31) : full.slice(0, 31) + "…";
|
|
@@ -1552,7 +1621,7 @@ export function codeGraphRuntime(root) {
|
|
|
1552
1621
|
}
|
|
1553
1622
|
catch (e) { /* stub */ }
|
|
1554
1623
|
}
|
|
1555
|
-
function openDoc(rel) {
|
|
1624
|
+
function openDoc(rel, gpath) {
|
|
1556
1625
|
var lv = live();
|
|
1557
1626
|
if (lv) {
|
|
1558
1627
|
Promise.resolve(lv({ doc: rel })).then(function (nd) {
|
|
@@ -1565,7 +1634,7 @@ export function codeGraphRuntime(root) {
|
|
|
1565
1634
|
// payload so its grouping tree derives; pushView alone would draw
|
|
1566
1635
|
// the empty raw payload (nodes come out {}).
|
|
1567
1636
|
if (nd.mode === "modules" && nd.mods)
|
|
1568
|
-
boot(mount, nd);
|
|
1637
|
+
boot(mount, nd, gpath);
|
|
1569
1638
|
else
|
|
1570
1639
|
pushView(nd);
|
|
1571
1640
|
}, function () { flash("cannot load " + rel); });
|
|
@@ -1646,9 +1715,12 @@ export function codeGraphRuntime(root) {
|
|
|
1646
1715
|
seg("modules", function () { openDoc(navBase + "index.geml"); });
|
|
1647
1716
|
sepEl();
|
|
1648
1717
|
var modName = String(data.module || String(data.start || "").replace(/^.*\//, "").replace(/\.geml$/, "") || "container");
|
|
1718
|
+
// The middle crumb reads as the OVERVIEW level ("modules / <module>"),
|
|
1719
|
+
// so clicking it goes THERE — the module tier listing this container's
|
|
1720
|
+
// siblings — not a reload of the page you are already on.
|
|
1649
1721
|
seg(modName, function () {
|
|
1650
1722
|
if (live())
|
|
1651
|
-
openDoc(
|
|
1723
|
+
openDoc(navBase + "index.geml", [modName.split("/")[0]]);
|
|
1652
1724
|
else {
|
|
1653
1725
|
state.trail = [];
|
|
1654
1726
|
setData(homeData());
|
|
@@ -1659,8 +1731,12 @@ export function codeGraphRuntime(root) {
|
|
|
1659
1731
|
sepEl();
|
|
1660
1732
|
seg(data.dir === "up"
|
|
1661
1733
|
? "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
|
-
|
|
1734
|
+
: state.trail.length && state.roots.length === 1 ? "root: " + (data.nodes[state.roots[0]] || {}).n
|
|
1735
|
+
// Many roots = this IS the module's own view (its whole entry
|
|
1736
|
+
// list) — the methods are already on the graph; naming them all
|
|
1737
|
+
// here just makes a paragraph-long crumb.
|
|
1738
|
+
: state.trail.length ? "roots: entry"
|
|
1739
|
+
: "roots: entry", null);
|
|
1664
1740
|
}
|
|
1665
1741
|
bar.appendChild(crumb);
|
|
1666
1742
|
var scroller = document.createElement("div");
|
|
@@ -1753,8 +1829,8 @@ export function codeGraphRuntime(root) {
|
|
|
1753
1829
|
capInfo.textContent = "showing " + (s.total - s.capped) + " of " + s.total + " reachable";
|
|
1754
1830
|
bar.appendChild(capInfo);
|
|
1755
1831
|
var moreBtn = document.createElement("button");
|
|
1756
|
-
moreBtn.textContent = "+
|
|
1757
|
-
moreBtn.onclick = function () { state.cap +=
|
|
1832
|
+
moreBtn.textContent = "+600";
|
|
1833
|
+
moreBtn.onclick = function () { state.cap += 600; draw(); };
|
|
1758
1834
|
bar.appendChild(moreBtn);
|
|
1759
1835
|
var allBtn = document.createElement("button");
|
|
1760
1836
|
allBtn.textContent = "all";
|
|
@@ -1771,6 +1847,152 @@ export function codeGraphRuntime(root) {
|
|
|
1771
1847
|
resetBtn.onclick = function () { state.trail = []; setData(homeData()); state.roots = data.roots.slice(); draw(); };
|
|
1772
1848
|
bar.appendChild(resetBtn);
|
|
1773
1849
|
}
|
|
1850
|
+
// Find a method by name -> jump to its node. Data source: a served page
|
|
1851
|
+
// (http) queries the /_search endpoint (top matches only — a huge index
|
|
1852
|
+
// never ships); a static page (file://) lazy-loads the compact
|
|
1853
|
+
// _index/search-index.js via <script> (fetch is CORS-blocked on file://,
|
|
1854
|
+
// a script tag is not). Picking a hit opens its FOCUSED call graph (B)
|
|
1855
|
+
// in place on a served page; on a static page (no live loader) it
|
|
1856
|
+
// navigates to the node's document (A). Alt-click always just locates.
|
|
1857
|
+
if (typeof location !== "undefined") { // browser only — skipped in the fake-DOM runtime test
|
|
1858
|
+
var searchWrap = document.createElement("span");
|
|
1859
|
+
searchWrap.className = "cg-search-wrap";
|
|
1860
|
+
var searchBox = document.createElement("input");
|
|
1861
|
+
searchBox.type = "search";
|
|
1862
|
+
searchBox.className = "cg-search";
|
|
1863
|
+
searchBox.placeholder = "find a method…";
|
|
1864
|
+
searchBox.setAttribute("aria-label", "Find a method by name");
|
|
1865
|
+
var searchMenu = document.createElement("div");
|
|
1866
|
+
searchMenu.className = "cg-search-menu";
|
|
1867
|
+
searchMenu.hidden = true;
|
|
1868
|
+
searchWrap.appendChild(searchBox);
|
|
1869
|
+
searchWrap.appendChild(searchMenu);
|
|
1870
|
+
bar.appendChild(searchWrap);
|
|
1871
|
+
var srvSearch = /^https?:$/.test(location.protocol);
|
|
1872
|
+
function withIndex(cb) {
|
|
1873
|
+
if (window.__gemlSearch)
|
|
1874
|
+
return cb(window.__gemlSearch);
|
|
1875
|
+
var s = document.createElement("script");
|
|
1876
|
+
s.src = navBase + "_index/search-index.js";
|
|
1877
|
+
s.onload = function () { cb(window.__gemlSearch || []); };
|
|
1878
|
+
s.onerror = function () { cb([]); };
|
|
1879
|
+
document.head.appendChild(s);
|
|
1880
|
+
}
|
|
1881
|
+
// Rank exactly like serve's /_search (exact > prefix > qualified-tail
|
|
1882
|
+
// prefix > substring), so both data paths order hits the same way.
|
|
1883
|
+
function hitScore(n, q) {
|
|
1884
|
+
if (n === q)
|
|
1885
|
+
return 0;
|
|
1886
|
+
if (n.indexOf(q) === 0)
|
|
1887
|
+
return 1;
|
|
1888
|
+
var c2 = n.lastIndexOf("::"), d = n.lastIndexOf(".");
|
|
1889
|
+
var cut = Math.max(c2 >= 0 ? c2 + 2 : 0, d >= 0 ? d + 1 : 0);
|
|
1890
|
+
if (cut > 0 && n.slice(cut).indexOf(q) === 0)
|
|
1891
|
+
return 2;
|
|
1892
|
+
return n.indexOf(q) >= 0 ? 3 : -1;
|
|
1893
|
+
}
|
|
1894
|
+
function candidates(q, cb) {
|
|
1895
|
+
q = q.trim().toLowerCase();
|
|
1896
|
+
if (q.length < 2)
|
|
1897
|
+
return cb({ total: 0, hits: [] });
|
|
1898
|
+
if (srvSearch) {
|
|
1899
|
+
fetch("/_search?q=" + encodeURIComponent(q))
|
|
1900
|
+
.then(function (r) { return r.ok ? r.json() : { total: 0, hits: [] }; })
|
|
1901
|
+
.then(function (a) { cb(a && a.hits ? a : { total: 0, hits: [] }); })
|
|
1902
|
+
.catch(function () { cb({ total: 0, hits: [] }); });
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
withIndex(function (rows) {
|
|
1906
|
+
var ranked = [];
|
|
1907
|
+
for (var i = 0; i < rows.length; i++) {
|
|
1908
|
+
var s = hitScore(String(rows[i][0]).toLowerCase(), q);
|
|
1909
|
+
if (s >= 0)
|
|
1910
|
+
ranked.push({ s: s, name: rows[i][0], doc: rows[i][1], id: rows[i][2] });
|
|
1911
|
+
}
|
|
1912
|
+
ranked.sort(function (a, b) { return a.s - b.s || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0); });
|
|
1913
|
+
// The lookup aliases bare member names to the same node — dedupe
|
|
1914
|
+
// on doc#id, keeping the best-ranked row.
|
|
1915
|
+
var seen = {}, hits = [];
|
|
1916
|
+
for (var j = 0; j < ranked.length; j++) {
|
|
1917
|
+
var rj = ranked[j];
|
|
1918
|
+
var k = rj.doc + "#" + rj.id;
|
|
1919
|
+
if (seen[k])
|
|
1920
|
+
continue;
|
|
1921
|
+
seen[k] = 1;
|
|
1922
|
+
hits.push(rj);
|
|
1923
|
+
}
|
|
1924
|
+
cb({ total: hits.length, hits: hits.slice(0, 100) });
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
function gotoHit(doc, id, locate) {
|
|
1929
|
+
searchMenu.hidden = true;
|
|
1930
|
+
if (live() && !locate) {
|
|
1931
|
+
showCallees(doc + "#" + id);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
location.href = navBase + doc.replace(/\.geml$/, ".html") + "#" + encodeURIComponent(id);
|
|
1935
|
+
}
|
|
1936
|
+
var searchSeq = 0, searchTop = null; // best-ranked hit — Enter opens it
|
|
1937
|
+
searchBox.addEventListener("input", function () {
|
|
1938
|
+
var my = ++searchSeq, qv = searchBox.value;
|
|
1939
|
+
candidates(qv, function (res) {
|
|
1940
|
+
if (my !== searchSeq)
|
|
1941
|
+
return; // a newer keystroke already fired
|
|
1942
|
+
searchMenu.replaceChildren();
|
|
1943
|
+
var hits = res.hits || [];
|
|
1944
|
+
searchTop = hits.length ? hits[0] : null;
|
|
1945
|
+
if (!hits.length) {
|
|
1946
|
+
searchMenu.hidden = true;
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
// Honest count first — a capped list must say so.
|
|
1950
|
+
var count = document.createElement("div");
|
|
1951
|
+
count.className = "cg-search-count";
|
|
1952
|
+
count.textContent = (res.total > hits.length ? "showing " + hits.length + " of " + res.total + " matches" : res.total + (res.total === 1 ? " match" : " matches")) + " · Enter opens the first";
|
|
1953
|
+
searchMenu.appendChild(count);
|
|
1954
|
+
// Group by module (document), groups in best-hit order — hits arrive
|
|
1955
|
+
// globally ranked, so first appearance = the group's best rank.
|
|
1956
|
+
var order = [], byDoc = {};
|
|
1957
|
+
hits.forEach(function (c) {
|
|
1958
|
+
if (!byDoc[c.doc]) {
|
|
1959
|
+
byDoc[c.doc] = [];
|
|
1960
|
+
order.push(c.doc);
|
|
1961
|
+
}
|
|
1962
|
+
byDoc[c.doc].push(c);
|
|
1963
|
+
});
|
|
1964
|
+
order.forEach(function (doc) {
|
|
1965
|
+
var hd = document.createElement("div");
|
|
1966
|
+
hd.className = "cg-search-grp";
|
|
1967
|
+
hd.textContent = String(doc).replace(/\.geml$/, "").replace(/--/g, "/");
|
|
1968
|
+
searchMenu.appendChild(hd);
|
|
1969
|
+
byDoc[doc].forEach(function (c) {
|
|
1970
|
+
var row = document.createElement("button");
|
|
1971
|
+
row.className = "cg-search-row";
|
|
1972
|
+
row.type = "button";
|
|
1973
|
+
var nm = document.createElement("b");
|
|
1974
|
+
nm.textContent = c.name;
|
|
1975
|
+
row.appendChild(nm);
|
|
1976
|
+
row.onclick = function (ev) { gotoHit(c.doc, c.id, !!ev.altKey); };
|
|
1977
|
+
searchMenu.appendChild(row);
|
|
1978
|
+
});
|
|
1979
|
+
});
|
|
1980
|
+
searchMenu.hidden = false;
|
|
1981
|
+
});
|
|
1982
|
+
});
|
|
1983
|
+
searchBox.addEventListener("keydown", function (ev) {
|
|
1984
|
+
if (ev.key === "Escape") {
|
|
1985
|
+
searchMenu.hidden = true;
|
|
1986
|
+
searchBox.blur();
|
|
1987
|
+
}
|
|
1988
|
+
else if (ev.key === "Enter" && searchTop) {
|
|
1989
|
+
ev.preventDefault();
|
|
1990
|
+
gotoHit(searchTop.doc, searchTop.id, !!ev.altKey);
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
document.addEventListener("click", function (ev) { if (!searchWrap.contains(ev.target))
|
|
1994
|
+
searchMenu.hidden = true; });
|
|
1995
|
+
} // end browser-only search box
|
|
1774
1996
|
mount.appendChild(bar);
|
|
1775
1997
|
mount.appendChild(stage);
|
|
1776
1998
|
if (state.scale === null)
|
|
@@ -1784,6 +2006,33 @@ export function codeGraphRuntime(root) {
|
|
|
1784
2006
|
else
|
|
1785
2007
|
scroller.scrollTop = 1e6;
|
|
1786
2008
|
}
|
|
2009
|
+
// Centre the CROSS axis (the fit-to-pane one): the tree fans out around
|
|
2010
|
+
// its midline, so a big graph clamped to the 2/3 scale floor would
|
|
2011
|
+
// otherwise open on an empty top/left corner with every node off-screen.
|
|
2012
|
+
// Reading the scroll extent forces the post-applyScale reflow; when the
|
|
2013
|
+
// cross axis already fits, the delta is ≤0 and this is a no-op. The
|
|
2014
|
+
// reading axis is untouched (root start, or far-end for callers above).
|
|
2015
|
+
// If the view holds an app entry (▶), aim the midline at the FIRST one
|
|
2016
|
+
// (roots first, then any node) instead of the geometric centre — the
|
|
2017
|
+
// reader lands where the program starts.
|
|
2018
|
+
var entryK = null;
|
|
2019
|
+
state.roots.concat(Object.keys(data.nodes)).some(function (ek) {
|
|
2020
|
+
var en = data.nodes[ek];
|
|
2021
|
+
if (en && (en.appEntry || en.entry) && pos[ek]) {
|
|
2022
|
+
entryK = ek;
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
return false;
|
|
2026
|
+
});
|
|
2027
|
+
var aim = function (full, pane, at) { return Math.max(0, Math.min(full - pane, at - pane / 2)); };
|
|
2028
|
+
if (LR)
|
|
2029
|
+
scroller.scrollTop = entryK
|
|
2030
|
+
? aim(scroller.scrollHeight, scroller.clientHeight, (pos[entryK].y + NH / 2) * state.scale)
|
|
2031
|
+
: Math.max(0, (scroller.scrollHeight - scroller.clientHeight) / 2);
|
|
2032
|
+
else
|
|
2033
|
+
scroller.scrollLeft = entryK
|
|
2034
|
+
? aim(scroller.scrollWidth, scroller.clientWidth, (pos[entryK].x + pos[entryK].w / 2) * state.scale)
|
|
2035
|
+
: Math.max(0, (scroller.scrollWidth - scroller.clientWidth) / 2);
|
|
1787
2036
|
// Footer: live facts, not a static cheat-sheet (navigation lives in
|
|
1788
2037
|
// the breadcrumb above).
|
|
1789
2038
|
var footer = document.createElement("div");
|
|
@@ -1823,16 +2072,35 @@ export function codeGraphRuntime(root) {
|
|
|
1823
2072
|
// document loader (mount._cgView, attached by the upgrade step); a
|
|
1824
2073
|
// static CLI page reverses its in-slice edges — partial but honest,
|
|
1825
2074
|
// and labelled as such in the crumb.
|
|
2075
|
+
// No recorded callers (an app/framework entry, or dead code): "up" from
|
|
2076
|
+
// a METHOD is its CONTAINER — one level, never the whole-repo overview
|
|
2077
|
+
// two levels up. From a focused/derived view that means the method's own
|
|
2078
|
+
// container page (the view its module node opens); already sitting on
|
|
2079
|
+
// that default view, say why and stay put — the "don't jump, say why"
|
|
2080
|
+
// contract.
|
|
2081
|
+
function noCallers(k) {
|
|
2082
|
+
var docRel = k.slice(0, k.lastIndexOf("#"));
|
|
2083
|
+
if (docRel !== data0.start) {
|
|
2084
|
+
openDoc(navBase + docRel);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
if (state.trail.length) {
|
|
2088
|
+
state.trail = [];
|
|
2089
|
+
setData(homeData());
|
|
2090
|
+
state.roots = data.roots.slice();
|
|
2091
|
+
draw();
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
flash("no recorded callers — an app/framework entry point");
|
|
2095
|
+
}
|
|
1826
2096
|
function showCallers(k) {
|
|
1827
2097
|
var lv = live();
|
|
1828
2098
|
if (lv) {
|
|
1829
2099
|
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
2100
|
if (nd && Object.keys(nd.nodes).length > 1)
|
|
1833
2101
|
pushView(nd);
|
|
1834
2102
|
else
|
|
1835
|
-
|
|
2103
|
+
noCallers(k);
|
|
1836
2104
|
});
|
|
1837
2105
|
return;
|
|
1838
2106
|
}
|
|
@@ -1849,9 +2117,9 @@ export function codeGraphRuntime(root) {
|
|
|
1849
2117
|
} });
|
|
1850
2118
|
}
|
|
1851
2119
|
if (Object.keys(keep).length <= 1) {
|
|
1852
|
-
|
|
2120
|
+
noCallers(k);
|
|
1853
2121
|
return;
|
|
1854
|
-
}
|
|
2122
|
+
}
|
|
1855
2123
|
var nodes = {}, edges = [];
|
|
1856
2124
|
for (var nk in keep)
|
|
1857
2125
|
nodes[nk] = data0.nodes[nk];
|
|
@@ -1941,16 +2209,22 @@ export function codeGraphRuntime(root) {
|
|
|
1941
2209
|
var ub = tgt && tgt.closest ? tgt.closest(".cg-upbtn") : null;
|
|
1942
2210
|
if (ub) {
|
|
1943
2211
|
if (ub.getAttribute("data-act") === "down") {
|
|
1944
|
-
//
|
|
1945
|
-
//
|
|
1946
|
-
|
|
2212
|
+
// "Back to its callee chain" must LAND on the callee chain: pop
|
|
2213
|
+
// the trail only when the view underneath IS this method's own
|
|
2214
|
+
// focused view (the search/chain path that opened these callers).
|
|
2215
|
+
// Arriving from anywhere wider — the module page — popping would
|
|
2216
|
+
// land there instead, so build the method's chain fresh.
|
|
2217
|
+
var k0 = ub.getAttribute("data-k");
|
|
2218
|
+
var top0 = state.trail[state.trail.length - 1];
|
|
2219
|
+
var ownChain = top0 && top0.data && top0.data.dir !== "up" && top0.roots && top0.roots.length === 1 && top0.roots[0] === k0;
|
|
2220
|
+
if (ownChain) {
|
|
1947
2221
|
var tr0 = state.trail.pop();
|
|
1948
2222
|
setData(tr0.data);
|
|
1949
2223
|
state.roots = tr0.roots;
|
|
1950
2224
|
draw();
|
|
1951
2225
|
}
|
|
1952
2226
|
else
|
|
1953
|
-
showCallees(
|
|
2227
|
+
showCallees(k0);
|
|
1954
2228
|
}
|
|
1955
2229
|
else
|
|
1956
2230
|
showCallers(ub.getAttribute("data-k"));
|
|
@@ -2048,7 +2322,7 @@ export function codeGraphRuntime(root) {
|
|
|
2048
2322
|
});
|
|
2049
2323
|
}
|
|
2050
2324
|
// CLI inlining: the compiled runtime function, verbatim, run against document.
|
|
2051
|
-
const CODE_GRAPH_JS = `(${codeGraphRuntime.toString()})(document);`;
|
|
2325
|
+
export const CODE_GRAPH_JS = `(${codeGraphRuntime.toString()})(document);`;
|
|
2052
2326
|
// Browser-side wave builder: the slice builder is synchronous with a
|
|
2053
2327
|
// synchronous loader, but a browser fetches documents asynchronously — so
|
|
2054
2328
|
// run the build in WAVES: every pass records the documents it needed but did
|
|
@@ -2094,89 +2368,7 @@ export function codeGraphWaves(fetchDoc, parseFn) {
|
|
|
2094
2368
|
},
|
|
2095
2369
|
};
|
|
2096
2370
|
}
|
|
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
2371
|
// ---------------------------------------------------------------------------
|
|
2162
2372
|
// Public entry
|
|
2163
2373
|
// ---------------------------------------------------------------------------
|
|
2164
2374
|
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
|
-
}
|