@geml/geml 1.7.0 → 1.7.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/codemap/build.mjs +20 -6
- package/codemap/exclude.mjs +18 -14
- package/codemap/refresh.mjs +4 -1
- package/codemap/verify.mjs +4 -1
- package/dist/from-md.js +6 -1
- package/dist/geml.js +7 -1
- package/dist/render.js +36 -3
- package/dist/to-md.js +24 -2
- package/package.json +1 -1
package/codemap/build.mjs
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
//
|
|
37
37
|
// After building, run: geml codemap verify <out-dir>
|
|
38
38
|
import { writeFileSync, mkdirSync, existsSync, readFileSync, statSync } from "node:fs";
|
|
39
|
-
import { join, resolve, basename, dirname, relative } from "node:path";
|
|
39
|
+
import { join, resolve, basename, dirname, relative, sep } from "node:path";
|
|
40
40
|
import { fileURLToPath } from "node:url";
|
|
41
41
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
42
42
|
import { emit } from "./emit.mjs";
|
|
@@ -100,7 +100,7 @@ if (root && !inputs.length) {
|
|
|
100
100
|
const { files, manifests, pkgs } = collectSourceFiles(rootAbs);
|
|
101
101
|
const excluder = makeExcluder({
|
|
102
102
|
root: rootAbs, globs: excludeGlobs0, gitignore: !args.includes("--no-gitignore"),
|
|
103
|
-
files: [...files, ...manifests, ...pkgs],
|
|
103
|
+
files: [...files, ...manifests, ...pkgs], run: execFileSync,
|
|
104
104
|
});
|
|
105
105
|
const jobs = detectLanguages(rootAbs, { files, manifests, pkgs, excluder });
|
|
106
106
|
detectedLanguages = [...new Set(jobs.map((j) => j.language))];
|
|
@@ -136,7 +136,10 @@ if (root && !inputs.length) {
|
|
|
136
136
|
// PATH (npx / joern) whose .cmd/.bat shim uses %~dp0 breaks if the name is
|
|
137
137
|
// quoted — cmd then resolves %~dp0 against the cwd, not the shim's dir. A
|
|
138
138
|
// spaced launcher PATH is a full path, so quoting it keeps %~dp0 correct.
|
|
139
|
-
|
|
139
|
+
// WHEN it does quote, it defers to shq below: escaping only `"` left a
|
|
140
|
+
// trailing backslash (a path ending `...\`) escaping our own closing quote,
|
|
141
|
+
// which merges the next token into this one. Same CRT rules, one source.
|
|
142
|
+
const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s));
|
|
140
143
|
// Hardened quote for command ARGUMENTS on win32. Node does NOT escape args
|
|
141
144
|
// under shell:true — it only concatenates them (Node DEP0190) — so an
|
|
142
145
|
// unquoted argument such as a source directory named `a&calc` reaching the
|
|
@@ -378,7 +381,7 @@ const excluder = makeExcluder({
|
|
|
378
381
|
globs: excludeGlobs,
|
|
379
382
|
gitignore: !args.includes("--no-gitignore"),
|
|
380
383
|
files: [...new Set(symbols.map((s) => s.file))],
|
|
381
|
-
|
|
384
|
+
run: execFileSync,
|
|
382
385
|
});
|
|
383
386
|
const kept = symbols.filter((s) => !excluder(s.file));
|
|
384
387
|
const excludedCount = symbols.length - kept.length;
|
|
@@ -391,7 +394,7 @@ if (root && !recordRecipe && !entryHints.length) {
|
|
|
391
394
|
const c = collectSourceFiles(rootAbs);
|
|
392
395
|
const excl = makeExcluder({
|
|
393
396
|
root: rootAbs, globs: excludeGlobs, gitignore: !args.includes("--no-gitignore"),
|
|
394
|
-
files: [...c.files, ...c.manifests, ...c.pkgs],
|
|
397
|
+
files: [...c.files, ...c.manifests, ...c.pkgs], run: execFileSync,
|
|
395
398
|
});
|
|
396
399
|
entryHints = detectEntries(rootAbs, {
|
|
397
400
|
files: c.files.filter((f) => !excl(f)),
|
|
@@ -420,7 +423,18 @@ try {
|
|
|
420
423
|
const { edges: httpEdges, audit } = buildCrossStackOverlay({
|
|
421
424
|
symbols,
|
|
422
425
|
files: scanFiles,
|
|
423
|
-
|
|
426
|
+
// `rel` is indexer OUTPUT, not a path this build authored, so a `..`
|
|
427
|
+
// segment must not turn a source read into an escape from the scanned
|
|
428
|
+
// root. Same gate the document resolver uses: resolve first, then require
|
|
429
|
+
// the result to BE the root or sit under it — compared with the separator,
|
|
430
|
+
// so a sibling named `<root>-evil` is not mistaken for a child.
|
|
431
|
+
readText: (rel) => {
|
|
432
|
+
try {
|
|
433
|
+
const p = resolve(rootAbs, ...String(rel).split("/"));
|
|
434
|
+
if (p !== rootAbs && !p.startsWith(rootAbs + sep)) return null;
|
|
435
|
+
return readFileSync(p, "utf8");
|
|
436
|
+
} catch { return null; }
|
|
437
|
+
},
|
|
424
438
|
});
|
|
425
439
|
for (const e of httpEdges) edges.push(e);
|
|
426
440
|
if (httpEdges.length) {
|
package/codemap/exclude.mjs
CHANGED
|
@@ -15,17 +15,17 @@ import { execFileSync as _execFileSync } from "node:child_process";
|
|
|
15
15
|
// Minimal gitignore-flavoured glob: `**` spans path separators, `*` stays
|
|
16
16
|
// within a segment, everything else is literal. Anchored to the whole path.
|
|
17
17
|
export function globToRegExp(glob) {
|
|
18
|
+
// The glob comes from a `--exclude` argument, so nothing in it may reach the
|
|
19
|
+
// compiled pattern as *syntax*. Split on the wildcards, keeping them (the
|
|
20
|
+
// capture group), which leaves the array strictly alternating: even indices
|
|
21
|
+
// are literal text, odd indices are `*`, `**` or `**/`. Literals go through a
|
|
22
|
+
// total regex-metacharacter escape; wildcards map to fixed patterns. Neither
|
|
23
|
+
// path can carry an unescaped metacharacter through.
|
|
24
|
+
const parts = String(glob).split(/(\*\*\/?|\*)/);
|
|
18
25
|
let re = "";
|
|
19
|
-
for (let i = 0; i <
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++; }
|
|
23
|
-
else re += "[^/]*";
|
|
24
|
-
} else if ("\\^$+?.()|{}[]".includes(c)) {
|
|
25
|
-
re += "\\" + c;
|
|
26
|
-
} else {
|
|
27
|
-
re += c;
|
|
28
|
-
}
|
|
26
|
+
for (let i = 0; i < parts.length; i++) {
|
|
27
|
+
if (i % 2 === 0) re += parts[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
+
else re += parts[i] === "*" ? "[^/]*" : ".*"; // `**` and `**/` span separators
|
|
29
29
|
}
|
|
30
30
|
return new RegExp("^" + re + "$");
|
|
31
31
|
}
|
|
@@ -33,10 +33,14 @@ export function globToRegExp(glob) {
|
|
|
33
33
|
// Ask git which of `files` it ignores. Returns a Set of the ignored paths.
|
|
34
34
|
// check-ignore exits 1 when nothing matches and 128 when git is unavailable /
|
|
35
35
|
// the dir is not a repo — both mean "ignore nothing", not a build failure.
|
|
36
|
-
|
|
36
|
+
// The injected runner is named `run`, not `exec`: it is always an execFile-shaped
|
|
37
|
+
// (program, args[]) call that spawns NO shell, whereas a callback named `exec`
|
|
38
|
+
// reads — to a human skimming, and to a static analyser — as the shell-string
|
|
39
|
+
// child_process API. The name should not imply the dangerous one.
|
|
40
|
+
export function gitIgnored(root, files, run = _execFileSync) {
|
|
37
41
|
if (!files.length) return new Set();
|
|
38
42
|
try {
|
|
39
|
-
const out =
|
|
43
|
+
const out = run("git", ["-C", root, "check-ignore", "--stdin"], { input: files.join("\n"), encoding: "utf8" });
|
|
40
44
|
return new Set(out.split(/\r?\n/).filter(Boolean));
|
|
41
45
|
} catch (e) {
|
|
42
46
|
const out = e && e.stdout ? String(e.stdout) : "";
|
|
@@ -45,8 +49,8 @@ export function gitIgnored(root, files, exec = _execFileSync) {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
// Build a predicate (file) => shouldExclude.
|
|
48
|
-
export function makeExcluder({ root, globs = [], gitignore = true, files = [],
|
|
52
|
+
export function makeExcluder({ root, globs = [], gitignore = true, files = [], run } = {}) {
|
|
49
53
|
const res = globs.map(globToRegExp);
|
|
50
|
-
const ignored = gitignore ? gitIgnored(root, files,
|
|
54
|
+
const ignored = gitignore ? gitIgnored(root, files, run) : new Set();
|
|
51
55
|
return (file) => ignored.has(file) || res.some((r) => r.test(file));
|
|
52
56
|
}
|
package/codemap/refresh.mjs
CHANGED
|
@@ -95,7 +95,10 @@ let trusted = isRecipeTrusted(fingerprint);
|
|
|
95
95
|
// full path is quoted), every argument via shq (ALWAYS double-quoted, so
|
|
96
96
|
// cmd.exe treats & | < > ( ) ^ and whitespace as literal). An injected
|
|
97
97
|
// metachar inside a dir-name argument is therefore inert.
|
|
98
|
-
|
|
98
|
+
// q quotes only when it must; WHEN it does it defers to shq, so a program path
|
|
99
|
+
// ending in a backslash cannot escape our own closing quote and swallow the
|
|
100
|
+
// next token. One set of CRT rules, in one place.
|
|
101
|
+
const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s));
|
|
99
102
|
const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
100
103
|
// Human-readable render of a step for the log / refusal message — DISPLAY ONLY,
|
|
101
104
|
// never executed. Falls back to String() for a stale (non-structured) step.
|
package/codemap/verify.mjs
CHANGED
|
@@ -37,7 +37,10 @@ if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
|
37
37
|
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
38
|
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
39
|
// CRT rules for embedded " / trailing \.
|
|
40
|
-
|
|
40
|
+
// q quotes only when it must; WHEN it does it defers to shq, so a program path
|
|
41
|
+
// ending in a backslash cannot escape our own closing quote and swallow the
|
|
42
|
+
// next token. One set of CRT rules, in one place.
|
|
43
|
+
const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s));
|
|
41
44
|
const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
42
45
|
// A codemap document's `src=` routes are written relative to the indexed
|
|
43
46
|
// SOURCE root (`geml-parser/src/attrs.ts` from a document two levels down), so
|
package/dist/from-md.js
CHANGED
|
@@ -55,7 +55,12 @@ function metaLine(line) {
|
|
|
55
55
|
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))
|
|
56
56
|
v = v.slice(1, -1);
|
|
57
57
|
const bareSafe = /^[^\s"]+$/.test(v);
|
|
58
|
-
|
|
58
|
+
// No backslash escape here — a `data` block value is read by `coerce`, which
|
|
59
|
+
// simply strips the outer quotes (§4); GEML defines no `\"` escape at all.
|
|
60
|
+
// Emitting one wrote the backslash into the parsed value, so `a"b` came back
|
|
61
|
+
// as `a\"b`. Quoting the value verbatim round-trips instead: coerce keeps
|
|
62
|
+
// everything between the first and last quote, embedded quotes included.
|
|
63
|
+
return bareSafe ? `${m[1]}=${v}` : `${m[1]}="${v}"`;
|
|
59
64
|
}
|
|
60
65
|
// Rewrite Markdown autolinks `<https://…>` / `<mailto:…>` into GEML links
|
|
61
66
|
// `[url](url)` (GEML has no autolink syntax). Inline code spans are left intact.
|
package/dist/geml.js
CHANGED
|
@@ -909,6 +909,12 @@ function validateProjections(children, ctx, opts) {
|
|
|
909
909
|
function relJoinPath(base, target) {
|
|
910
910
|
if (base === "" || target === "" || target.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(target))
|
|
911
911
|
return target;
|
|
912
|
+
// A POSIX-absolute base must stay absolute. The segment loop below drops empty
|
|
913
|
+
// segments, and the leading "" of "/tmp/x" IS the root — dropping it silently
|
|
914
|
+
// turned `/tmp/x/part.geml` into the relative `tmp/x/part.geml`, which then
|
|
915
|
+
// resolved against the wrong directory. Windows never showed it: a `C:\…` base
|
|
916
|
+
// has no "/", so relDirPath returns "" and the early return above takes over.
|
|
917
|
+
const rooted = base.startsWith("/");
|
|
912
918
|
const out = [];
|
|
913
919
|
for (const s of (base + "/" + target).split("/")) {
|
|
914
920
|
if (s === "" || s === ".")
|
|
@@ -918,7 +924,7 @@ function relJoinPath(base, target) {
|
|
|
918
924
|
else
|
|
919
925
|
out.push(s);
|
|
920
926
|
}
|
|
921
|
-
return out.join("/");
|
|
927
|
+
return (rooted ? "/" : "") + out.join("/");
|
|
922
928
|
}
|
|
923
929
|
function relDirPath(p) {
|
|
924
930
|
const i = p.lastIndexOf("/");
|
package/dist/render.js
CHANGED
|
@@ -1990,7 +1990,24 @@ export function codeGraphRuntime(root) {
|
|
|
1990
1990
|
// Rendered pages sit next to their codemap documents: a live mount
|
|
1991
1991
|
// (viewer/playground) carries data-src, a CLI embed carries the src
|
|
1992
1992
|
// path in data.start — either directory anchors doc-relative links.
|
|
1993
|
-
|
|
1993
|
+
// navBase prefixes EVERY url this view reaches for — the breadcrumb's
|
|
1994
|
+
// `location.href`, the search-index `<script src>`, a hit's jump target.
|
|
1995
|
+
// Both of its inputs are page data (a DOM attribute, the embedded graph
|
|
1996
|
+
// JSON), so a document that carried `data-src="javascript:…"` would turn
|
|
1997
|
+
// a breadcrumb click into script execution, and a `//host/` or
|
|
1998
|
+
// `https://host/` value would pull the search index off another origin.
|
|
1999
|
+
// A base is a doc-relative DIRECTORY and nothing else: anything bearing a
|
|
2000
|
+
// scheme or a network-path prefix is refused outright and links resolve
|
|
2001
|
+
// against the current page instead.
|
|
2002
|
+
// Everything this view navigates to or loads is built from page data, so
|
|
2003
|
+
// every such string is filtered here first: keep it if it is a
|
|
2004
|
+
// document-RELATIVE path, drop it to "" if it carries a scheme
|
|
2005
|
+
// (`javascript:`, `data:`) or a `//host` network-path prefix.
|
|
2006
|
+
function relOnly(u) {
|
|
2007
|
+
var s = String(u == null ? "" : u);
|
|
2008
|
+
return /^[a-zA-Z][a-zA-Z0-9+.\-]*:/.test(s) || s.slice(0, 2) === "//" ? "" : s;
|
|
2009
|
+
}
|
|
2010
|
+
var navBase = relOnly(String(mount.getAttribute("data-src") || data.start || "").replace(/[^\/]*$/, ""));
|
|
1994
2011
|
// A live mount (viewer/playground/served page) navigates IN PLACE over
|
|
1995
2012
|
// the geml documents through this loader; only truly static pages fall
|
|
1996
2013
|
// back to their pre-rendered sibling .html pages. Read LAZILY on every
|
|
@@ -2028,7 +2045,16 @@ export function codeGraphRuntime(root) {
|
|
|
2028
2045
|
}
|
|
2029
2046
|
catch (e) { /* stub */ }
|
|
2030
2047
|
}
|
|
2031
|
-
function openDoc(
|
|
2048
|
+
function openDoc(rel0, gpath) {
|
|
2049
|
+
// The target can come from a node's own data (a `data-k` key), not just
|
|
2050
|
+
// from a breadcrumb we built — so it goes through the same relative-path
|
|
2051
|
+
// filter as navBase. An absolute or scheme-bearing target says why it
|
|
2052
|
+
// was refused rather than navigating.
|
|
2053
|
+
var rel = relOnly(rel0);
|
|
2054
|
+
if (!rel) {
|
|
2055
|
+
flash("refusing to open " + String(rel0) + " — not a document-relative path");
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2032
2058
|
var lv = live();
|
|
2033
2059
|
if (lv) {
|
|
2034
2060
|
Promise.resolve(lv({ doc: rel })).then(function (nd) {
|
|
@@ -2340,8 +2366,15 @@ export function codeGraphRuntime(root) {
|
|
|
2340
2366
|
});
|
|
2341
2367
|
}
|
|
2342
2368
|
}
|
|
2343
|
-
function gotoHit(
|
|
2369
|
+
function gotoHit(doc0, id, locate) {
|
|
2344
2370
|
searchMenu.hidden = true;
|
|
2371
|
+
// `doc` is a row from the loaded search index — page data, same as any
|
|
2372
|
+
// other target, so it gets the same relative-path filter.
|
|
2373
|
+
var doc = relOnly(doc0);
|
|
2374
|
+
if (!doc) {
|
|
2375
|
+
flash("refusing to open " + String(doc0) + " — not a document-relative path");
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2345
2378
|
if (live() && !locate) {
|
|
2346
2379
|
showCallees(doc + "#" + id);
|
|
2347
2380
|
return;
|
package/dist/to-md.js
CHANGED
|
@@ -48,10 +48,32 @@ function inline(n) {
|
|
|
48
48
|
function seq(ns) {
|
|
49
49
|
return ns.map(inline).join("");
|
|
50
50
|
}
|
|
51
|
+
// Escape a `|` so GFM keeps it inside the cell instead of splitting the row.
|
|
52
|
+
// GFM resolves backslash escapes in a row BEFORE it splits on `|`, so a
|
|
53
|
+
// backslash run sitting right in front of our escape would eat it: a code span
|
|
54
|
+
// holding `a\|b` became `a\\|b`, which reads as a literal backslash followed by
|
|
55
|
+
// an UNescaped pipe — a spurious cell break. Double any such run first, then
|
|
56
|
+
// escape the pipe. Runs already produced by escText (`\\` for a literal
|
|
57
|
+
// backslash) survive this unchanged, so pre-rendered Markdown stays intact.
|
|
58
|
+
//
|
|
59
|
+
// The backslash run is matched as `\\+\|?` — one ATOMIC token, run and pipe
|
|
60
|
+
// together — not as `(\\*)\|`. The latter is quadratic: on a cell holding a
|
|
61
|
+
// long run of backslashes and no pipe, the engine matches the run from every
|
|
62
|
+
// index in it and fails at the required `|` each time. Here the greedy `\\+`
|
|
63
|
+
// takes the whole run in one match and the trailing `\|?` is optional, so
|
|
64
|
+
// nothing backtracks and each character is visited once.
|
|
65
|
+
function escPipe(s) {
|
|
66
|
+
return s.replace(/\\+\|?|\|/g, (m) => {
|
|
67
|
+
if (m.charAt(m.length - 1) !== "|")
|
|
68
|
+
return m; // a run with no pipe after it
|
|
69
|
+
const bs = m.slice(0, -1); // the run that would otherwise eat our escape
|
|
70
|
+
return bs + bs + "\\|";
|
|
71
|
+
});
|
|
72
|
+
}
|
|
51
73
|
// Inline text for a table cell: render inlines, then neutralise the two bytes
|
|
52
74
|
// that would break a GFM cell.
|
|
53
75
|
function cellText(c) {
|
|
54
|
-
return seq(c.inlines)
|
|
76
|
+
return escPipe(seq(c.inlines)).replace(/\n/g, " ");
|
|
55
77
|
}
|
|
56
78
|
// ---------------------------------------------------------------------------
|
|
57
79
|
// Tables
|
|
@@ -72,7 +94,7 @@ function tableToMd(t, notes) {
|
|
|
72
94
|
const lines = [];
|
|
73
95
|
if (t.caption)
|
|
74
96
|
lines.push(`*${t.caption}*`, "");
|
|
75
|
-
lines.push(`| ${cols.map(
|
|
97
|
+
lines.push(`| ${cols.map(escPipe).join(" | ")} |`);
|
|
76
98
|
lines.push(`| ${cols.map((_, i) => sep(t.align[i])).join(" | ")} |`);
|
|
77
99
|
const pad = (cells) => {
|
|
78
100
|
while (cells.length < cols.length)
|