@geml/geml 1.4.6 → 1.5.1
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 +217 -217
- package/codemap/adapters/crg.mjs +120 -120
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -658
- package/codemap/browser-stub.mjs +29 -29
- package/codemap/build.mjs +609 -609
- package/codemap/cross-stack.mjs +303 -303
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +480 -480
- package/codemap/entries.mjs +129 -129
- package/codemap/exclude.mjs +52 -52
- package/codemap/find.mjs +49 -49
- package/codemap/foldings.mjs +110 -110
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +431 -431
- package/codemap/normalize.mjs +275 -275
- package/codemap/recipe-trust.mjs +103 -103
- package/codemap/refresh.mjs +310 -310
- package/codemap/render-all.mjs +77 -64
- package/codemap/serve.mjs +585 -578
- package/codemap/sfc-virtualize.mjs +367 -367
- package/codemap/verify.mjs +148 -148
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +12 -2
- package/dist/geml.d.ts +4 -0
- package/dist/geml.js +653 -156
- package/dist/inline.d.ts +20 -0
- package/dist/inline.js +36 -4
- package/dist/mcp.js +32 -24
- package/dist/render-html.js +35 -35
- package/dist/render.d.ts +19 -0
- package/dist/render.js +486 -182
- package/dist/serialize.js +9 -1
- package/dist/table.d.ts +0 -4
- package/dist/table.js +62 -53
- package/dist/to-md.js +17 -3
- package/package.json +66 -66
package/dist/inline.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export type Inline = {
|
|
|
36
36
|
type: "autoref";
|
|
37
37
|
anchor: string;
|
|
38
38
|
doc?: string;
|
|
39
|
+
} | {
|
|
40
|
+
type: "project";
|
|
41
|
+
anchor: string;
|
|
42
|
+
doc?: string;
|
|
39
43
|
} | {
|
|
40
44
|
type: "footnote";
|
|
41
45
|
ref: string;
|
|
@@ -48,6 +52,22 @@ export interface Ref {
|
|
|
48
52
|
}
|
|
49
53
|
export interface RefSink {
|
|
50
54
|
refs: Ref[];
|
|
55
|
+
embeds?: {
|
|
56
|
+
doc: string;
|
|
57
|
+
anchor?: string;
|
|
58
|
+
line: number;
|
|
59
|
+
}[];
|
|
60
|
+
mediaDocTargets?: {
|
|
61
|
+
src: string;
|
|
62
|
+
line: number;
|
|
63
|
+
}[];
|
|
64
|
+
projections?: {
|
|
65
|
+
doc?: string;
|
|
66
|
+
anchor: string;
|
|
67
|
+
line: number;
|
|
68
|
+
}[];
|
|
51
69
|
}
|
|
52
70
|
export declare const META_REF_SRC = "\\{\\{\\s*([A-Za-z_][A-Za-z0-9_-]*)\\s*\\}\\}";
|
|
71
|
+
export declare function schemeOf(url: string): string | null;
|
|
72
|
+
export declare function isSafeUrl(url: string, allowDataImage?: boolean): boolean;
|
|
53
73
|
export declare function parseInline(s: string, line: number, sink: RefSink, depth?: number): Inline[];
|
package/dist/inline.js
CHANGED
|
@@ -20,7 +20,7 @@ export const META_REF_SRC = "\\{\\{\\s*([A-Za-z_][A-Za-z0-9_-]*)\\s*\\}\\}";
|
|
|
20
20
|
const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
21
21
|
// The leading `scheme:` (RFC-3986 grammar), lowercased — or null when the
|
|
22
22
|
// destination has none (a relative path, `#anchor`, or cross-document ref).
|
|
23
|
-
function schemeOf(url) {
|
|
23
|
+
export function schemeOf(url) {
|
|
24
24
|
// Browsers strip leading/embedded C0 controls and spaces before acting on a
|
|
25
25
|
// URL, so `java\tscript:` and `\x01javascript:` execute as javascript:. Strip
|
|
26
26
|
// every [\x00-\x20] before detecting the scheme so the allowlist can't be
|
|
@@ -31,7 +31,7 @@ function schemeOf(url) {
|
|
|
31
31
|
// A destination is safe to emit when it has no scheme (relative / anchor /
|
|
32
32
|
// cross-doc), or names an allowlisted scheme. `data:` is permitted only for
|
|
33
33
|
// media and only for `image/*` payloads (never `data:text/html`, which scripts).
|
|
34
|
-
function isSafeUrl(url, allowDataImage = false) {
|
|
34
|
+
export function isSafeUrl(url, allowDataImage = false) {
|
|
35
35
|
const scheme = schemeOf(url);
|
|
36
36
|
if (scheme === null)
|
|
37
37
|
return true;
|
|
@@ -182,6 +182,29 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
182
182
|
continue;
|
|
183
183
|
}
|
|
184
184
|
// §5.3(2): image {…}.
|
|
185
|
+
// §5.3 precedence: inline projection `![[…]]` is tried BEFORE the image atom.
|
|
186
|
+
// Otherwise `![[#x]]` reads as an image whose label happens to be `[#x]`, and
|
|
187
|
+
// `![[#x]](y)` would be claimed whole — the parenthesis run has to stay
|
|
188
|
+
// literal text, which is what this ordering pins.
|
|
189
|
+
if (c === "!" && s[i + 1] === "[" && s[i + 2] === "[") {
|
|
190
|
+
const inner = readBracket(s, i + 2); // the inner [...] after `![`
|
|
191
|
+
if (inner && s[inner.end] === "]") {
|
|
192
|
+
const { doc, anchor } = classifyDest(inner.content.trim());
|
|
193
|
+
if (anchor) {
|
|
194
|
+
flush();
|
|
195
|
+
const node = { type: "project", anchor };
|
|
196
|
+
if (doc)
|
|
197
|
+
node.doc = doc;
|
|
198
|
+
out.push(node);
|
|
199
|
+
// Validated by the same §8 resolver as any reference; the target's TYPE
|
|
200
|
+
// is checked separately, since only inline content can be projected.
|
|
201
|
+
sink.refs.push({ kind: doc ? "cross" : "autoref", doc, anchor, line });
|
|
202
|
+
(sink.projections ??= []).push(doc === undefined ? { anchor, line } : { doc, anchor, line });
|
|
203
|
+
i = inner.end + 1;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
185
208
|
if (c === "!" && s[i + 1] === "[") {
|
|
186
209
|
const label = readBracket(s, i + 1);
|
|
187
210
|
const paren = label ? readParen(s, label.end) : null;
|
|
@@ -194,6 +217,10 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
194
217
|
// image/* data URIs pass through.
|
|
195
218
|
const rawSrc = paren.content.trim();
|
|
196
219
|
const src = isSafeUrl(rawSrc, true) ? rawSrc : "";
|
|
220
|
+
// A GEML target here means the author wanted a transclusion, which is a
|
|
221
|
+
// block: `=== embed`. Recorded for the caller to report.
|
|
222
|
+
if (/\.geml(#|$)/i.test(src))
|
|
223
|
+
(sink.mediaDocTargets ??= []).push({ src, line });
|
|
197
224
|
const node = {
|
|
198
225
|
type: "image", alt: label.content, src, attrs: attrObj.attrs,
|
|
199
226
|
};
|
|
@@ -288,8 +315,13 @@ function scanAtoms(s, line, sink, depth = 0) {
|
|
|
288
315
|
// guesswork. Delimiters pair only *within* one text run: they never reach across
|
|
289
316
|
// a code span, inline math, a link or image (atoms from phase A), or a block
|
|
290
317
|
// boundary. Any delimiter left unpaired is literal text.
|
|
291
|
-
|
|
292
|
-
|
|
318
|
+
// Unicode punctuation, not just ASCII (§5.3). With an ASCII-only test, `“` and
|
|
319
|
+
// `,` count as ordinary letters, and a run hugged by CJK punctuation on the
|
|
320
|
+
// outside and ASCII punctuation on the inside stops flanking: `“*(foo)*”` loses
|
|
321
|
+
// its emphasis. CommonMark's rule is Unicode-wide, and the algorithm here is
|
|
322
|
+
// meant to be that rule restricted to `*` and `~~` — not a narrower one.
|
|
323
|
+
const PUNCT = /[\p{P}\p{S}]/u;
|
|
324
|
+
const isPunct = (c) => c !== undefined && PUNCT.test(c);
|
|
293
325
|
const isWS = (c) => c === undefined || /\s/.test(c);
|
|
294
326
|
// Left/right-flanking for a delimiter run, given the chars on either side.
|
|
295
327
|
function flank(before, after) {
|
package/dist/mcp.js
CHANGED
|
@@ -163,7 +163,7 @@ function applyWrite(spec) {
|
|
|
163
163
|
const before = readFileSync(real, "utf8");
|
|
164
164
|
const root = realpathSync(OPTS.root);
|
|
165
165
|
const errorKey = (d) => `${d.code}:${d.message}`;
|
|
166
|
-
const preexisting = new Set(parse(before, { resolveDoc: docResolver(root) }).diagnostics
|
|
166
|
+
const preexisting = new Set(parse(before, { resolveDoc: docResolver(root, real) }).diagnostics
|
|
167
167
|
.filter((d) => d.severity === "error")
|
|
168
168
|
.map(errorKey));
|
|
169
169
|
// 1. Produce the mutated document WITHOUT touching the file. `--json` makes
|
|
@@ -190,7 +190,7 @@ function applyWrite(spec) {
|
|
|
190
190
|
}
|
|
191
191
|
// 2. Validate the RESULT independently of the CLI. This is what catches the
|
|
192
192
|
// tools the CLI lets through — deleting a referenced block, above all.
|
|
193
|
-
const diags = parse(after, { resolveDoc: docResolver(root) }).diagnostics;
|
|
193
|
+
const diags = parse(after, { resolveDoc: docResolver(root, real) }).diagnostics;
|
|
194
194
|
let blocking = diags.filter((d) => d.severity === "error" && !preexisting.has(errorKey(d)));
|
|
195
195
|
if (spec.danglingIsWarning) {
|
|
196
196
|
blocking = blocking.filter((d) => d.code !== "unresolved-reference" && d.code !== "unresolved-footnote");
|
|
@@ -221,10 +221,18 @@ function snapshot(realPath, summary) {
|
|
|
221
221
|
return undefined;
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
-
|
|
224
|
+
// A cross-document reference resolves FROM THE DOCUMENT'S OWN DIRECTORY, which is
|
|
225
|
+
// what the CLI resolver and the renderer both do. Resolving from the server root
|
|
226
|
+
// instead made the validator inspect a different file than the renderer expands:
|
|
227
|
+
// for `sub/a.geml` naming `b.geml`, it validated `<root>/b.geml` while the render
|
|
228
|
+
// pulled in `<root>/sub/b.geml` — phantom errors in one direction, and in the other
|
|
229
|
+
// a write signed off against a file that was never the target. The root stays the
|
|
230
|
+
// confinement boundary.
|
|
231
|
+
function docResolver(root, fromFile) {
|
|
232
|
+
const base = dirname(fromFile);
|
|
225
233
|
return (doc) => {
|
|
226
234
|
try {
|
|
227
|
-
const target = realpathSync(resolve(
|
|
235
|
+
const target = realpathSync(resolve(base, doc));
|
|
228
236
|
if (target !== root && !target.startsWith(root + sep))
|
|
229
237
|
return null;
|
|
230
238
|
return readFileSync(target, "utf8");
|
|
@@ -283,7 +291,7 @@ export const TOOLS = [
|
|
|
283
291
|
run: (args) => {
|
|
284
292
|
const real = resolveInRoot(args.file);
|
|
285
293
|
const root = resolveRoot(args.root);
|
|
286
|
-
const doc = parse(readFileSync(real, "utf8"), { resolveDoc: docResolver(root) });
|
|
294
|
+
const doc = parse(readFileSync(real, "utf8"), { resolveDoc: docResolver(root, real) });
|
|
287
295
|
const errors = doc.diagnostics.filter((d) => d.severity === "error").length;
|
|
288
296
|
return {
|
|
289
297
|
ok: errors === 0,
|
|
@@ -611,25 +619,25 @@ export function handleLine(line, write = (s) => process.stdout.write(s)) {
|
|
|
611
619
|
// ---------------------------------------------------------------------------
|
|
612
620
|
// Entry
|
|
613
621
|
// ---------------------------------------------------------------------------
|
|
614
|
-
export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
615
|
-
|
|
616
|
-
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
|
|
617
|
-
read-only code-graph tools when the root holds a code graph.
|
|
618
|
-
|
|
619
|
-
--root <dir> REQUIRED. Root directory holding the .geml documents.
|
|
620
|
-
Relative paths resolve against the server process's CWD,
|
|
621
|
-
which the CLIENT chooses — pass an absolute path.
|
|
622
|
-
Every path a client names is confined to this directory;
|
|
623
|
-
a client cannot widen or override it.
|
|
624
|
-
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
625
|
-
<root>/.geml-code-graph when that holds an index.geml.
|
|
626
|
-
With no graph, the code-graph tools are not served
|
|
627
|
-
at all (a client sees only the document tools).
|
|
628
|
-
--no-history Do not auto-commit a .gemlhistory revision before each
|
|
629
|
-
write. Default is to commit, so geml_revert always
|
|
630
|
-
has a revision to undo to.
|
|
631
|
-
|
|
632
|
-
Register with a client:
|
|
622
|
+
export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
623
|
+
|
|
624
|
+
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
|
|
625
|
+
read-only code-graph tools when the root holds a code graph.
|
|
626
|
+
|
|
627
|
+
--root <dir> REQUIRED. Root directory holding the .geml documents.
|
|
628
|
+
Relative paths resolve against the server process's CWD,
|
|
629
|
+
which the CLIENT chooses — pass an absolute path.
|
|
630
|
+
Every path a client names is confined to this directory;
|
|
631
|
+
a client cannot widen or override it.
|
|
632
|
+
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
633
|
+
<root>/.geml-code-graph when that holds an index.geml.
|
|
634
|
+
With no graph, the code-graph tools are not served
|
|
635
|
+
at all (a client sees only the document tools).
|
|
636
|
+
--no-history Do not auto-commit a .gemlhistory revision before each
|
|
637
|
+
write. Default is to commit, so geml_revert always
|
|
638
|
+
has a revision to undo to.
|
|
639
|
+
|
|
640
|
+
Register with a client:
|
|
633
641
|
claude mcp add geml -- geml mcp --root /abs/path/to/repo`;
|
|
634
642
|
export function parseArgs(args) {
|
|
635
643
|
let root;
|
package/dist/render-html.js
CHANGED
|
@@ -36,43 +36,43 @@ function page(title, body, ctx, source) {
|
|
|
36
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
37
|
: "";
|
|
38
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
|
-
}
|
|
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
58
|
</script>\n`
|
|
59
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>
|
|
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
76
|
`;
|
|
77
77
|
}
|
|
78
78
|
export function renderHtml(doc, opts = {}) {
|
package/dist/render.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ export declare class RenderCtx {
|
|
|
18
18
|
usedMermaid: boolean;
|
|
19
19
|
usedCodeGraph: boolean;
|
|
20
20
|
private renderDepth;
|
|
21
|
+
private embedStack;
|
|
22
|
+
private embedDocs;
|
|
23
|
+
private embedCount;
|
|
24
|
+
private embedBytes;
|
|
25
|
+
private embedCache;
|
|
26
|
+
private budgetExhausted;
|
|
27
|
+
private loadChildren;
|
|
21
28
|
labels: Map<string, string>;
|
|
22
29
|
constructor(doc: Document, opts?: RenderOptions);
|
|
23
30
|
get isCodemapDoc(): boolean;
|
|
@@ -25,6 +32,18 @@ export declare class RenderCtx {
|
|
|
25
32
|
docTitle(): string | undefined;
|
|
26
33
|
inlines(ns: Inline[]): string;
|
|
27
34
|
private inline;
|
|
35
|
+
private transclude;
|
|
36
|
+
private projectInline;
|
|
37
|
+
private projectFallback;
|
|
38
|
+
private get currentDocRel();
|
|
39
|
+
private get currentDocChildren();
|
|
40
|
+
private currentLabels;
|
|
41
|
+
private idAttr;
|
|
42
|
+
private remoteLabels;
|
|
43
|
+
private remoteLabel;
|
|
44
|
+
private fragmentHref;
|
|
45
|
+
private transclusionWrap;
|
|
46
|
+
private transclusionFallback;
|
|
28
47
|
private media;
|
|
29
48
|
private link;
|
|
30
49
|
block(b: Block): string;
|