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