@geml/geml
Advanced tools
+20
-6
@@ -39,3 +39,3 @@ #!/usr/bin/env node | ||
| import { writeFileSync, mkdirSync, existsSync, readFileSync, statSync } from "node:fs"; | ||
| import { join, resolve, basename, dirname, relative } from "node:path"; | ||
| import { join, resolve, basename, dirname, relative, sep } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
@@ -104,3 +104,3 @@ import { execFileSync, spawnSync } from "node:child_process"; | ||
| root: rootAbs, globs: excludeGlobs0, gitignore: !args.includes("--no-gitignore"), | ||
| files: [...files, ...manifests, ...pkgs], exec: execFileSync, | ||
| files: [...files, ...manifests, ...pkgs], run: execFileSync, | ||
| }); | ||
@@ -141,3 +141,6 @@ const jobs = detectLanguages(rootAbs, { files, manifests, pkgs, excluder }); | ||
| // spaced launcher PATH is a full path, so quoting it keeps %~dp0 correct. | ||
| const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s)); | ||
| // WHEN it does quote, it defers to shq below: escaping only `"` left a | ||
| // trailing backslash (a path ending `...\`) escaping our own closing quote, | ||
| // which merges the next token into this one. Same CRT rules, one source. | ||
| const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s)); | ||
| // Hardened quote for command ARGUMENTS on win32. Node does NOT escape args | ||
@@ -384,3 +387,3 @@ // under shell:true — it only concatenates them (Node DEP0190) — so an | ||
| files: [...new Set(symbols.map((s) => s.file))], | ||
| exec: execFileSync, | ||
| run: execFileSync, | ||
| }); | ||
@@ -398,3 +401,3 @@ const kept = symbols.filter((s) => !excluder(s.file)); | ||
| root: rootAbs, globs: excludeGlobs, gitignore: !args.includes("--no-gitignore"), | ||
| files: [...c.files, ...c.manifests, ...c.pkgs], exec: execFileSync, | ||
| files: [...c.files, ...c.manifests, ...c.pkgs], run: execFileSync, | ||
| }); | ||
@@ -428,3 +431,14 @@ entryHints = detectEntries(rootAbs, { | ||
| files: scanFiles, | ||
| readText: (rel) => { try { return readFileSync(join(rootAbs, ...rel.split("/")), "utf8"); } catch { return null; } }, | ||
| // `rel` is indexer OUTPUT, not a path this build authored, so a `..` | ||
| // segment must not turn a source read into an escape from the scanned | ||
| // root. Same gate the document resolver uses: resolve first, then require | ||
| // the result to BE the root or sit under it — compared with the separator, | ||
| // so a sibling named `<root>-evil` is not mistaken for a child. | ||
| readText: (rel) => { | ||
| try { | ||
| const p = resolve(rootAbs, ...String(rel).split("/")); | ||
| if (p !== rootAbs && !p.startsWith(rootAbs + sep)) return null; | ||
| return readFileSync(p, "utf8"); | ||
| } catch { return null; } | ||
| }, | ||
| }); | ||
@@ -431,0 +445,0 @@ for (const e of httpEdges) edges.push(e); |
+18
-14
@@ -18,13 +18,13 @@ // Source exclusion for the codemap build. | ||
| export function globToRegExp(glob) { | ||
| // The glob comes from a `--exclude` argument, so nothing in it may reach the | ||
| // compiled pattern as *syntax*. Split on the wildcards, keeping them (the | ||
| // capture group), which leaves the array strictly alternating: even indices | ||
| // are literal text, odd indices are `*`, `**` or `**/`. Literals go through a | ||
| // total regex-metacharacter escape; wildcards map to fixed patterns. Neither | ||
| // path can carry an unescaped metacharacter through. | ||
| const parts = String(glob).split(/(\*\*\/?|\*)/); | ||
| let re = ""; | ||
| for (let i = 0; i < glob.length; i++) { | ||
| const c = glob[i]; | ||
| if (c === "*") { | ||
| if (glob[i + 1] === "*") { re += ".*"; i++; if (glob[i + 1] === "/") i++; } | ||
| else re += "[^/]*"; | ||
| } else if ("\\^$+?.()|{}[]".includes(c)) { | ||
| re += "\\" + c; | ||
| } else { | ||
| re += c; | ||
| } | ||
| for (let i = 0; i < parts.length; i++) { | ||
| if (i % 2 === 0) re += parts[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
| else re += parts[i] === "*" ? "[^/]*" : ".*"; // `**` and `**/` span separators | ||
| } | ||
@@ -37,6 +37,10 @@ return new RegExp("^" + re + "$"); | ||
| // the dir is not a repo — both mean "ignore nothing", not a build failure. | ||
| export function gitIgnored(root, files, exec = _execFileSync) { | ||
| // The injected runner is named `run`, not `exec`: it is always an execFile-shaped | ||
| // (program, args[]) call that spawns NO shell, whereas a callback named `exec` | ||
| // reads — to a human skimming, and to a static analyser — as the shell-string | ||
| // child_process API. The name should not imply the dangerous one. | ||
| export function gitIgnored(root, files, run = _execFileSync) { | ||
| if (!files.length) return new Set(); | ||
| try { | ||
| const out = exec("git", ["-C", root, "check-ignore", "--stdin"], { input: files.join("\n"), encoding: "utf8" }); | ||
| const out = run("git", ["-C", root, "check-ignore", "--stdin"], { input: files.join("\n"), encoding: "utf8" }); | ||
| return new Set(out.split(/\r?\n/).filter(Boolean)); | ||
@@ -50,6 +54,6 @@ } catch (e) { | ||
| // Build a predicate (file) => shouldExclude. | ||
| export function makeExcluder({ root, globs = [], gitignore = true, files = [], exec } = {}) { | ||
| export function makeExcluder({ root, globs = [], gitignore = true, files = [], run } = {}) { | ||
| const res = globs.map(globToRegExp); | ||
| const ignored = gitignore ? gitIgnored(root, files, exec) : new Set(); | ||
| const ignored = gitignore ? gitIgnored(root, files, run) : new Set(); | ||
| return (file) => ignored.has(file) || res.some((r) => r.test(file)); | ||
| } |
@@ -98,3 +98,6 @@ #!/usr/bin/env node | ||
| // metachar inside a dir-name argument is therefore inert. | ||
| const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s)); | ||
| // q quotes only when it must; WHEN it does it defers to shq, so a program path | ||
| // ending in a backslash cannot escape our own closing quote and swallow the | ||
| // next token. One set of CRT rules, in one place. | ||
| const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s)); | ||
| const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`; | ||
@@ -101,0 +104,0 @@ // Human-readable render of a step for the log / refusal message — DISPLAY ONLY, |
@@ -40,3 +40,6 @@ #!/usr/bin/env node | ||
| // CRT rules for embedded " / trailing \. | ||
| const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s)); | ||
| // q quotes only when it must; WHEN it does it defers to shq, so a program path | ||
| // ending in a backslash cannot escape our own closing quote and swallow the | ||
| // next token. One set of CRT rules, in one place. | ||
| const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s)); | ||
| const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`; | ||
@@ -43,0 +46,0 @@ // A codemap document's `src=` routes are written relative to the indexed |
+6
-1
@@ -58,3 +58,8 @@ // GEML reference parser — Markdown → GEML conversion. | ||
| const bareSafe = /^[^\s"]+$/.test(v); | ||
| return bareSafe ? `${m[1]}=${v}` : `${m[1]}="${v.replace(/"/g, '\\"')}"`; | ||
| // No backslash escape here — a `data` block value is read by `coerce`, which | ||
| // simply strips the outer quotes (§4); GEML defines no `\"` escape at all. | ||
| // Emitting one wrote the backslash into the parsed value, so `a"b` came back | ||
| // as `a\"b`. Quoting the value verbatim round-trips instead: coerce keeps | ||
| // everything between the first and last quote, embedded quotes included. | ||
| return bareSafe ? `${m[1]}=${v}` : `${m[1]}="${v}"`; | ||
| } | ||
@@ -61,0 +66,0 @@ // Rewrite Markdown autolinks `<https://…>` / `<mailto:…>` into GEML links |
+24
-2
@@ -51,6 +51,28 @@ // GEML -> Markdown projection (the inverse direction of from-md.ts). | ||
| } | ||
| // Escape a `|` so GFM keeps it inside the cell instead of splitting the row. | ||
| // GFM resolves backslash escapes in a row BEFORE it splits on `|`, so a | ||
| // backslash run sitting right in front of our escape would eat it: a code span | ||
| // holding `a\|b` became `a\\|b`, which reads as a literal backslash followed by | ||
| // an UNescaped pipe — a spurious cell break. Double any such run first, then | ||
| // escape the pipe. Runs already produced by escText (`\\` for a literal | ||
| // backslash) survive this unchanged, so pre-rendered Markdown stays intact. | ||
| // | ||
| // The backslash run is matched as `\\+\|?` — one ATOMIC token, run and pipe | ||
| // together — not as `(\\*)\|`. The latter is quadratic: on a cell holding a | ||
| // long run of backslashes and no pipe, the engine matches the run from every | ||
| // index in it and fails at the required `|` each time. Here the greedy `\\+` | ||
| // takes the whole run in one match and the trailing `\|?` is optional, so | ||
| // nothing backtracks and each character is visited once. | ||
| function escPipe(s) { | ||
| return s.replace(/\\+\|?|\|/g, (m) => { | ||
| if (m.charAt(m.length - 1) !== "|") | ||
| return m; // a run with no pipe after it | ||
| const bs = m.slice(0, -1); // the run that would otherwise eat our escape | ||
| return bs + bs + "\\|"; | ||
| }); | ||
| } | ||
| // Inline text for a table cell: render inlines, then neutralise the two bytes | ||
| // that would break a GFM cell. | ||
| function cellText(c) { | ||
| return seq(c.inlines).replace(/\|/g, "\\|").replace(/\n/g, " "); | ||
| return escPipe(seq(c.inlines)).replace(/\n/g, " "); | ||
| } | ||
@@ -76,3 +98,3 @@ // --------------------------------------------------------------------------- | ||
| lines.push(`*${t.caption}*`, ""); | ||
| lines.push(`| ${cols.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`); | ||
| lines.push(`| ${cols.map(escPipe).join(" | ")} |`); | ||
| lines.push(`| ${cols.map((_, i) => sep(t.align[i])).join(" | ")} |`); | ||
@@ -79,0 +101,0 @@ const pad = (cells) => { |
+1
-1
| { | ||
| "name": "@geml/geml", | ||
| "version": "1.7.0", | ||
| "version": "1.7.1", | ||
| "mcpName": "io.github.geml-spec/geml", | ||
@@ -5,0 +5,0 @@ "publishConfig": { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
882497
0.7%16317
0.55%