@geml/geml 1.6.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.
@@ -12,6 +12,9 @@ export const writeFileSync = () => {};
12
12
  export const existsSync = () => false;
13
13
  export const realpathSync = (p) => p;
14
14
  export const statSync = () => ({ isDirectory: () => false });
15
+ export const mkdirSync = () => {};
16
+ export const readdirSync = () => [];
17
+ export const copyFileSync = () => {};
15
18
  export const basename = (p) => p;
16
19
  export const dirname = (p) => p;
17
20
  export const resolve = (...p) => p.join("/");
@@ -21,6 +24,8 @@ export const relative = (_from, to) => to;
21
24
  export const sep = "/";
22
25
  export const fileURLToPath = (u) => String(u);
23
26
  export const spawnSync = () => ({ status: 1 });
27
+ // node:os — `geml skill install` resolves the home directory; CLI-only.
28
+ export const homedir = () => "/";
24
29
  export const createHash = () => ({
25
30
  update() { return this; },
26
31
  digest() { return ""; },
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], exec: execFileSync,
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
- const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
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
- exec: execFileSync,
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], exec: execFileSync,
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
- readText: (rel) => { try { return readFileSync(join(rootAbs, ...rel.split("/")), "utf8"); } catch { return null; } },
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) {
@@ -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 < glob.length; i++) {
20
- const c = glob[i];
21
- if (c === "*") {
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
- export function gitIgnored(root, files, exec = _execFileSync) {
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 = exec("git", ["-C", root, "check-ignore", "--stdin"], { input: files.join("\n"), encoding: "utf8" });
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 = [], exec } = {}) {
52
+ export function makeExcluder({ root, globs = [], gitignore = true, files = [], run } = {}) {
49
53
  const res = globs.map(globToRegExp);
50
- const ignored = gitignore ? gitIgnored(root, files, exec) : new Set();
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
  }
@@ -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
- const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
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.
@@ -37,18 +37,28 @@ 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
- const q = (s) => (/[\s"]/.test(String(s)) ? `"${String(s).replace(/"/g, '\\"')}"` : String(s));
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')}"`;
45
+ // A codemap document's `src=` routes are written relative to the indexed
46
+ // SOURCE root (`geml-parser/src/attrs.ts` from a document two levels down), so
47
+ // checking one has to be told that root — the same value `serve` derives, from
48
+ // the recorded recipe, falling back to the parent of the graph directory.
49
+ let srcRoot = resolve(rootDir, "..");
50
+ try { srcRoot = resolve(rootDir, JSON.parse(readFileSync(join(rootDir, "_index", "refresh.json"), "utf8")).root ?? ".."); } catch { /* no recipe: parent */ }
51
+ const checkArgs = (file) => ["check", "--root", srcRoot, file];
42
52
  const runCheck = (file) => {
43
53
  // The built-parser path: run node on geml.js directly (array args, no shell).
44
- if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, "check", file], { encoding: "utf8" });
54
+ if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, ...checkArgs(file)], { encoding: "utf8" });
45
55
  // A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
46
56
  // Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
47
57
  // string (never an args array — that is the unescaped, injection-prone path).
48
58
  if (process.platform === "win32") {
49
- return spawnSync([q(cli), ...["check", file].map(shq)].join(" "), { encoding: "utf8", shell: true });
59
+ return spawnSync([q(cli), ...checkArgs(file).map(shq)].join(" "), { encoding: "utf8", shell: true });
50
60
  }
51
- return spawnSync(cli, ["check", file], { encoding: "utf8" });
61
+ return spawnSync(cli, checkArgs(file), { encoding: "utf8" });
52
62
  };
53
63
  if (!existsSync(localParser)) {
54
64
  console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
package/dist/chart.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type Value } from "./attrs.js";
3
3
  import { type TableModel } from "./table.js";
4
4
  export type ChartType = "bar" | "line" | "area" | "pie" | "scatter";
5
5
  export type RowScope = "data" | "all" | "summary";
6
+ export declare const USES: Record<ChartType, Set<string>>;
6
7
  export interface ChartDataset {
7
8
  categories: string[];
8
9
  numbers: Record<string, number[]>;
package/dist/chart.js CHANGED
@@ -8,7 +8,10 @@
8
8
  // doc and §7.
9
9
  const TYPES = new Set(["bar", "line", "area", "pie", "scatter"]);
10
10
  // Channels each type can use; supplying any other is a warning (ignored).
11
- const USES = {
11
+ // Exported for the record-array projection (GEP-0005): it must validate only
12
+ // the channels the chart TYPE actually reads, matching buildChart's leniency
13
+ // (an unused channel is a warning here, so it must not be an error there).
14
+ export const USES = {
12
15
  bar: new Set(["x", "y", "series"]),
13
16
  line: new Set(["x", "y", "series"]),
14
17
  area: new Set(["x", "y", "series"]),
@@ -1,4 +1,4 @@
1
- export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "unknown-attribute" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "embed-missing-src" | "ignored-embed-body" | "transclusion-cycle" | "embed-target-not-geml" | "media-target-is-document" | "inline-transclusion-not-inline" | "unsafe-embed-scheme" | "unresolvable-table-source" | "table-source-not-a-table" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "compute-non-numeric-cell" | "compute-not-a-number" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value";
1
+ export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "unknown-attribute" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "stray-labeled-fence" | "fence-like-line" | "unresolvable-code-source" | "bad-code-source" | "bad-source-range" | "stale-code-snapshot" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "embed-missing-src" | "ignored-embed-body" | "transclusion-cycle" | "embed-target-not-geml" | "media-target-is-document" | "inline-transclusion-not-inline" | "unsafe-embed-scheme" | "unresolvable-table-source" | "table-source-not-a-table" | "unknown-metadata-reference" | "table-src-and-body" | "unknown-table-format" | "bad-table-delimiter" | "ignored-table-delimiter" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "compute-non-numeric-cell" | "compute-not-a-number" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value" | "chart-data-not-records" | "data-parse" | "unknown-data-format" | "data-format-no-engine" | "bad-data-schema" | "data-src-and-body" | "bad-data-source" | "unresolvable-data-source";
2
2
  export interface Diagnostic {
3
3
  severity: "error" | "warning";
4
4
  code: DiagnosticCode;
@@ -19,6 +19,12 @@ export const SEVERITY = {
19
19
  "block-nesting-too-deep": "error",
20
20
  "list-nesting-too-deep": "error",
21
21
  "inline-nesting-too-deep": "error",
22
+ "stray-labeled-fence": "warning",
23
+ "fence-like-line": "warning",
24
+ "unresolvable-code-source": "warning",
25
+ "bad-code-source": "error",
26
+ "bad-source-range": "error",
27
+ "stale-code-snapshot": "warning",
22
28
  "duplicate-id": "error",
23
29
  "unresolved-reference": "error",
24
30
  "unresolved-footnote": "error",
@@ -37,6 +43,8 @@ export const SEVERITY = {
37
43
  "unknown-metadata-reference": "error",
38
44
  "table-src-and-body": "error",
39
45
  "unknown-table-format": "warning",
46
+ "bad-table-delimiter": "error",
47
+ "ignored-table-delimiter": "warning",
40
48
  "bad-compute-formula": "error",
41
49
  "unlexable-compute-formula": "error",
42
50
  "compute-error": "error",
@@ -62,6 +70,14 @@ export const SEVERITY = {
62
70
  "chart-missing-summary-row": "error",
63
71
  "chart-summary-row-unavailable": "warning",
64
72
  "chart-non-numeric-value": "error",
73
+ "chart-data-not-records": "error",
74
+ "data-parse": "error",
75
+ "unknown-data-format": "warning",
76
+ "data-format-no-engine": "warning",
77
+ "bad-data-schema": "error",
78
+ "data-src-and-body": "error",
79
+ "bad-data-source": "error",
80
+ "unresolvable-data-source": "error",
65
81
  };
66
82
  // ---------------------------------------------------------------------------
67
83
  // Source normalization (spec §0)
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
- return bareSafe ? `${m[1]}=${v}` : `${m[1]}="${v.replace(/"/g, '\\"')}"`;
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.d.ts CHANGED
@@ -8,11 +8,14 @@ export { type Value } from "./attrs.js";
8
8
  export { type Inline } from "./inline.js";
9
9
  export { type TableModel } from "./table.js";
10
10
  export { mdToGeml, type ConvertResult } from "./from-md.js";
11
- export { renderHtml } from "./render-html.js";
11
+ export { renderHtml, pageAssets } from "./render-html.js";
12
12
  export { type RenderOptions } from "./render.js";
13
13
  export { serialize } from "./serialize.js";
14
14
  export { gemlToMd } from "./to-md.js";
15
15
  export type BodyMode = "raw" | "flow" | "data";
16
+ export type DataValue = null | boolean | number | string | DataValue[] | {
17
+ [key: string]: DataValue;
18
+ };
16
19
  export interface ListItem {
17
20
  text: string;
18
21
  inlines: Inline[];
@@ -53,6 +56,7 @@ export type Block = {
53
56
  data?: Record<string, Value>;
54
57
  table?: TableModel;
55
58
  chart?: ChartModel;
59
+ value?: DataValue;
56
60
  hidden?: boolean;
57
61
  };
58
62
  export { type Diagnostic, type DiagnosticCode, SEVERITY } from "./diagnostics.js";