@geml/geml 1.7.8 → 1.8.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/LICENSE +21 -21
- package/README.md +288 -285
- 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 +34 -34
- package/codemap/build.mjs +629 -629
- package/codemap/cross-stack.mjs +303 -303
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +510 -510
- package/codemap/entries.mjs +129 -129
- package/codemap/exclude.mjs +56 -56
- package/codemap/find.mjs +49 -49
- package/codemap/foldings.mjs +110 -110
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +434 -431
- package/codemap/normalize.mjs +275 -275
- package/codemap/recipe-trust.mjs +103 -103
- package/codemap/refresh.mjs +313 -313
- package/codemap/render-all.mjs +90 -90
- package/codemap/serve.mjs +585 -585
- package/codemap/sfc-virtualize.mjs +367 -367
- package/codemap/verify.mjs +158 -158
- package/dist/attrs.d.ts +8 -0
- package/dist/attrs.js +24 -0
- package/dist/cli.js +190 -139
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +5 -1
- package/dist/geml.js +63 -25
- package/dist/inline.d.ts +7 -1
- package/dist/inline.js +203 -137
- package/dist/mcp.js +68 -26
- package/dist/render-html.js +35 -35
- package/dist/render.js +157 -157
- package/package.json +1 -1
- package/skill/SKILL.md +167 -167
- package/skill/references/authoring.geml +369 -365
package/codemap/verify.mjs
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
-
//
|
|
4
|
-
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
-
// native references).
|
|
6
|
-
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
-
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
-
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
-
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
-
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
-
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
-
//
|
|
13
|
-
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
-
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
-
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
-
import { posix } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { spawnSync } from "node:child_process";
|
|
19
|
-
|
|
20
|
-
const args = process.argv.slice(2);
|
|
21
|
-
const flagI = args.indexOf("--geml");
|
|
22
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
-
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
-
process.exit(2);
|
|
25
|
-
}
|
|
26
|
-
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
-
const rootDir = resolve(dir);
|
|
28
|
-
|
|
29
|
-
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
-
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
-
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
-
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
-
// win32 cmd.exe quoting (same rationale as build.mjs). q: space-aware quote for
|
|
34
|
-
// the PROGRAM token — a bare launcher name (geml resolved via PATH) whose
|
|
35
|
-
// .cmd/.bat shim uses %~dp0 breaks if the name is blanket-quoted. shq: ALWAYS
|
|
36
|
-
// double-quote ARGUMENTS — Node does not escape args under shell:true, so a
|
|
37
|
-
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
|
-
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
|
-
// CRT rules for embedded " / trailing \.
|
|
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));
|
|
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];
|
|
52
|
-
const runCheck = (file) => {
|
|
53
|
-
// The built-parser path: run node on geml.js directly (array args, no shell).
|
|
54
|
-
if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, ...checkArgs(file)], { encoding: "utf8" });
|
|
55
|
-
// A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
|
|
56
|
-
// Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
|
|
57
|
-
// string (never an args array — that is the unescaped, injection-prone path).
|
|
58
|
-
if (process.platform === "win32") {
|
|
59
|
-
return spawnSync([q(cli), ...checkArgs(file).map(shq)].join(" "), { encoding: "utf8", shell: true });
|
|
60
|
-
}
|
|
61
|
-
return spawnSync(cli, checkArgs(file), { encoding: "utf8" });
|
|
62
|
-
};
|
|
63
|
-
if (!existsSync(localParser)) {
|
|
64
|
-
console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
const { parse } = await import(`file://${localParser.replace(/\\/g, "/")}`);
|
|
68
|
-
|
|
69
|
-
const files = [];
|
|
70
|
-
const walk = (d) => {
|
|
71
|
-
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
72
|
-
const p = join(d, e.name);
|
|
73
|
-
if (e.isDirectory()) walk(p);
|
|
74
|
-
else if (e.name.endsWith(".geml")) files.push(p);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
walk(rootDir);
|
|
78
|
-
files.sort();
|
|
79
|
-
|
|
80
|
-
// ---- pass 1: geml check ----
|
|
81
|
-
let failed = 0;
|
|
82
|
-
for (const f of files) {
|
|
83
|
-
const r = runCheck(f);
|
|
84
|
-
if (r.status !== 0) {
|
|
85
|
-
failed++;
|
|
86
|
-
console.error(`FAIL ${f}`);
|
|
87
|
-
console.error((r.stderr || r.stdout || "").split("\n").slice(0, 4).map((l) => ` ${l}`).join("\n"));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---- pass 2: codemap profile references ----
|
|
92
|
-
const REF_TABLES = new Set(["calls", "called-by", "ref-by"]);
|
|
93
|
-
// Cross-stack link tables: `from`/`to` may be a #ref (resolved cross-tree
|
|
94
|
-
// link — checked) OR plain `file:line` text (a call/route outside any indexed
|
|
95
|
-
// function — tolerated, nothing to resolve).
|
|
96
|
-
const LINK_TABLES = new Set(["api-calls", "api-served-by"]);
|
|
97
|
-
const relDoc = (f) => relative(rootDir, f).replace(/\\/g, "/");
|
|
98
|
-
const docs = new Map(); // relPath -> { ids:Set, blocks }
|
|
99
|
-
const collectIds = (blocks, ids) => {
|
|
100
|
-
for (const b of blocks) {
|
|
101
|
-
if (b.id) ids.add(b.id);
|
|
102
|
-
if (b.children) collectIds(b.children, ids);
|
|
103
|
-
if (b.items) for (const it of b.items) if (it.children) collectIds(it.children, ids);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
for (const f of files) {
|
|
107
|
-
const doc = parse(readFileSync(f, "utf8"));
|
|
108
|
-
const ids = new Set();
|
|
109
|
-
collectIds(doc.children, ids);
|
|
110
|
-
docs.set(relDoc(f), { ids, blocks: doc.children });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let refErrors = 0;
|
|
114
|
-
const err = (doc, where, msg) => {
|
|
115
|
-
refErrors++;
|
|
116
|
-
console.error(`REF ${doc} ${where}: ${msg}`);
|
|
117
|
-
};
|
|
118
|
-
const checkRef = (fromDoc, where, ref, lenient = false) => {
|
|
119
|
-
ref = String(ref).trim();
|
|
120
|
-
if (!ref) return lenient ? undefined : err(fromDoc, where, "empty reference cell");
|
|
121
|
-
const h = ref.indexOf("#");
|
|
122
|
-
if (h < 0) return lenient ? undefined : err(fromDoc, where, `not a reference: \`${ref}\``);
|
|
123
|
-
let targetDoc = fromDoc;
|
|
124
|
-
if (h > 0) targetDoc = posix.normalize(posix.join(posix.dirname(fromDoc), ref.slice(0, h)));
|
|
125
|
-
const id = ref.slice(h + 1);
|
|
126
|
-
const target = docs.get(targetDoc);
|
|
127
|
-
if (!target) return err(fromDoc, where, `cannot resolve document \`${ref.slice(0, h)}\``);
|
|
128
|
-
// reads/writes values may carry a plain-text `.member` suffix (ids never contain '.')
|
|
129
|
-
const bare = id.split(".")[0];
|
|
130
|
-
if (!target.ids.has(bare)) return err(fromDoc, where, `unresolved reference \`${ref}\``);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
for (const [docPath, { blocks }] of docs) {
|
|
134
|
-
for (const b of blocks) {
|
|
135
|
-
if (b.kind !== "block") continue;
|
|
136
|
-
if (b.type === "table" && (REF_TABLES.has(b.id) || LINK_TABLES.has(b.id)) && b.table) {
|
|
137
|
-
const lenient = LINK_TABLES.has(b.id);
|
|
138
|
-
const fromCol = b.table.columns.indexOf("from");
|
|
139
|
-
const toCol = b.table.columns.indexOf("to");
|
|
140
|
-
if (fromCol < 0 || toCol < 0) { err(docPath, `#${b.id}`, "missing from/to columns"); continue; }
|
|
141
|
-
b.table.rows.forEach((row, i) => {
|
|
142
|
-
checkRef(docPath, `#${b.id} row ${i + 1} from`, row[fromCol]?.text ?? "", lenient);
|
|
143
|
-
checkRef(docPath, `#${b.id} row ${i + 1} to`, row[toCol]?.text ?? "", lenient);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (b.type === "meta" && b.data?.entry) {
|
|
147
|
-
for (const ref of String(b.data.entry).split(/\s+/).filter(Boolean)) {
|
|
148
|
-
checkRef(docPath, "meta entry", ref);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
console.error(
|
|
155
|
-
`verify: ${files.length - failed}/${files.length} documents pass geml check; `
|
|
156
|
-
+ `profile references: ${refErrors === 0 ? "all resolve" : `${refErrors} dangling`}`,
|
|
157
|
-
);
|
|
158
|
-
process.exit(failed || refErrors ? 1 : 0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
+
//
|
|
4
|
+
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
+
// native references).
|
|
6
|
+
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
+
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
+
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
+
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
+
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
+
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
+
//
|
|
13
|
+
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
+
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
+
import { posix } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const flagI = args.indexOf("--geml");
|
|
22
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
+
const rootDir = resolve(dir);
|
|
28
|
+
|
|
29
|
+
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
+
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
+
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
+
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
+
// win32 cmd.exe quoting (same rationale as build.mjs). q: space-aware quote for
|
|
34
|
+
// the PROGRAM token — a bare launcher name (geml resolved via PATH) whose
|
|
35
|
+
// .cmd/.bat shim uses %~dp0 breaks if the name is blanket-quoted. shq: ALWAYS
|
|
36
|
+
// double-quote ARGUMENTS — Node does not escape args under shell:true, so a
|
|
37
|
+
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
|
+
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
|
+
// CRT rules for embedded " / trailing \.
|
|
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));
|
|
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];
|
|
52
|
+
const runCheck = (file) => {
|
|
53
|
+
// The built-parser path: run node on geml.js directly (array args, no shell).
|
|
54
|
+
if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, ...checkArgs(file)], { encoding: "utf8" });
|
|
55
|
+
// A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
|
|
56
|
+
// Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
|
|
57
|
+
// string (never an args array — that is the unescaped, injection-prone path).
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
return spawnSync([q(cli), ...checkArgs(file).map(shq)].join(" "), { encoding: "utf8", shell: true });
|
|
60
|
+
}
|
|
61
|
+
return spawnSync(cli, checkArgs(file), { encoding: "utf8" });
|
|
62
|
+
};
|
|
63
|
+
if (!existsSync(localParser)) {
|
|
64
|
+
console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const { parse } = await import(`file://${localParser.replace(/\\/g, "/")}`);
|
|
68
|
+
|
|
69
|
+
const files = [];
|
|
70
|
+
const walk = (d) => {
|
|
71
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
72
|
+
const p = join(d, e.name);
|
|
73
|
+
if (e.isDirectory()) walk(p);
|
|
74
|
+
else if (e.name.endsWith(".geml")) files.push(p);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(rootDir);
|
|
78
|
+
files.sort();
|
|
79
|
+
|
|
80
|
+
// ---- pass 1: geml check ----
|
|
81
|
+
let failed = 0;
|
|
82
|
+
for (const f of files) {
|
|
83
|
+
const r = runCheck(f);
|
|
84
|
+
if (r.status !== 0) {
|
|
85
|
+
failed++;
|
|
86
|
+
console.error(`FAIL ${f}`);
|
|
87
|
+
console.error((r.stderr || r.stdout || "").split("\n").slice(0, 4).map((l) => ` ${l}`).join("\n"));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- pass 2: codemap profile references ----
|
|
92
|
+
const REF_TABLES = new Set(["calls", "called-by", "ref-by"]);
|
|
93
|
+
// Cross-stack link tables: `from`/`to` may be a #ref (resolved cross-tree
|
|
94
|
+
// link — checked) OR plain `file:line` text (a call/route outside any indexed
|
|
95
|
+
// function — tolerated, nothing to resolve).
|
|
96
|
+
const LINK_TABLES = new Set(["api-calls", "api-served-by"]);
|
|
97
|
+
const relDoc = (f) => relative(rootDir, f).replace(/\\/g, "/");
|
|
98
|
+
const docs = new Map(); // relPath -> { ids:Set, blocks }
|
|
99
|
+
const collectIds = (blocks, ids) => {
|
|
100
|
+
for (const b of blocks) {
|
|
101
|
+
if (b.id) ids.add(b.id);
|
|
102
|
+
if (b.children) collectIds(b.children, ids);
|
|
103
|
+
if (b.items) for (const it of b.items) if (it.children) collectIds(it.children, ids);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
for (const f of files) {
|
|
107
|
+
const doc = parse(readFileSync(f, "utf8"));
|
|
108
|
+
const ids = new Set();
|
|
109
|
+
collectIds(doc.children, ids);
|
|
110
|
+
docs.set(relDoc(f), { ids, blocks: doc.children });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let refErrors = 0;
|
|
114
|
+
const err = (doc, where, msg) => {
|
|
115
|
+
refErrors++;
|
|
116
|
+
console.error(`REF ${doc} ${where}: ${msg}`);
|
|
117
|
+
};
|
|
118
|
+
const checkRef = (fromDoc, where, ref, lenient = false) => {
|
|
119
|
+
ref = String(ref).trim();
|
|
120
|
+
if (!ref) return lenient ? undefined : err(fromDoc, where, "empty reference cell");
|
|
121
|
+
const h = ref.indexOf("#");
|
|
122
|
+
if (h < 0) return lenient ? undefined : err(fromDoc, where, `not a reference: \`${ref}\``);
|
|
123
|
+
let targetDoc = fromDoc;
|
|
124
|
+
if (h > 0) targetDoc = posix.normalize(posix.join(posix.dirname(fromDoc), ref.slice(0, h)));
|
|
125
|
+
const id = ref.slice(h + 1);
|
|
126
|
+
const target = docs.get(targetDoc);
|
|
127
|
+
if (!target) return err(fromDoc, where, `cannot resolve document \`${ref.slice(0, h)}\``);
|
|
128
|
+
// reads/writes values may carry a plain-text `.member` suffix (ids never contain '.')
|
|
129
|
+
const bare = id.split(".")[0];
|
|
130
|
+
if (!target.ids.has(bare)) return err(fromDoc, where, `unresolved reference \`${ref}\``);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const [docPath, { blocks }] of docs) {
|
|
134
|
+
for (const b of blocks) {
|
|
135
|
+
if (b.kind !== "block") continue;
|
|
136
|
+
if (b.type === "table" && (REF_TABLES.has(b.id) || LINK_TABLES.has(b.id)) && b.table) {
|
|
137
|
+
const lenient = LINK_TABLES.has(b.id);
|
|
138
|
+
const fromCol = b.table.columns.indexOf("from");
|
|
139
|
+
const toCol = b.table.columns.indexOf("to");
|
|
140
|
+
if (fromCol < 0 || toCol < 0) { err(docPath, `#${b.id}`, "missing from/to columns"); continue; }
|
|
141
|
+
b.table.rows.forEach((row, i) => {
|
|
142
|
+
checkRef(docPath, `#${b.id} row ${i + 1} from`, row[fromCol]?.text ?? "", lenient);
|
|
143
|
+
checkRef(docPath, `#${b.id} row ${i + 1} to`, row[toCol]?.text ?? "", lenient);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (b.type === "meta" && b.data?.entry) {
|
|
147
|
+
for (const ref of String(b.data.entry).split(/\s+/).filter(Boolean)) {
|
|
148
|
+
checkRef(docPath, "meta entry", ref);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.error(
|
|
155
|
+
`verify: ${files.length - failed}/${files.length} documents pass geml check; `
|
|
156
|
+
+ `profile references: ${refErrors === 0 ? "all resolve" : `${refErrors} dangling`}`,
|
|
157
|
+
);
|
|
158
|
+
process.exit(failed || refErrors ? 1 : 0);
|
package/dist/attrs.d.ts
CHANGED
|
@@ -3,7 +3,15 @@ export interface Attrs {
|
|
|
3
3
|
id?: string;
|
|
4
4
|
classes: string[];
|
|
5
5
|
attrs: Record<string, Value>;
|
|
6
|
+
odd?: {
|
|
7
|
+
kind: "id" | "class" | "flag" | "key";
|
|
8
|
+
name: string;
|
|
9
|
+
}[];
|
|
6
10
|
}
|
|
11
|
+
export declare function oddNames(a: Attrs): {
|
|
12
|
+
kind: "id" | "class" | "flag" | "key";
|
|
13
|
+
name: string;
|
|
14
|
+
}[];
|
|
7
15
|
export declare function coerce(raw: string): Value;
|
|
8
16
|
export declare function tokenize(s: string): string[];
|
|
9
17
|
export declare function parseAttrs(src: string): Attrs;
|
package/dist/attrs.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
// Shared attribute-object and value typing (§4), used by the block scanner
|
|
2
2
|
// and the inline parser.
|
|
3
|
+
// §4: NAME = NAME-CHAR+, NAME-CHAR = LETTER | DIGIT | "-" | "_", LETTER being
|
|
4
|
+
// any Unicode letter. Ids, classes and attribute keys are all NAMEs; only
|
|
5
|
+
// VALUES may hold anything else.
|
|
6
|
+
const NAME = /^[\p{L}\p{N}_-]+$/u;
|
|
7
|
+
// Why this is worth a diagnostic: the attribute object is whitespace-separated,
|
|
8
|
+
// so `{#Trade-offs & Laws}` parses as the id `Trade-offs` plus two boolean flags
|
|
9
|
+
// named `&` and `Laws` — a legal parse of a document nobody meant to write, and
|
|
10
|
+
// silent, because a bare word IS how a flag is spelled. The id you addressed is
|
|
11
|
+
// then not the id you have. Naming what cannot be a NAME turns that into a
|
|
12
|
+
// question the author can answer.
|
|
13
|
+
export function oddNames(a) {
|
|
14
|
+
const odd = [];
|
|
15
|
+
if (a.id !== undefined && !NAME.test(a.id))
|
|
16
|
+
odd.push({ kind: "id", name: a.id });
|
|
17
|
+
for (const c of a.classes)
|
|
18
|
+
if (!NAME.test(c))
|
|
19
|
+
odd.push({ kind: "class", name: c });
|
|
20
|
+
for (const [k, v] of Object.entries(a.attrs)) {
|
|
21
|
+
if (NAME.test(k))
|
|
22
|
+
continue;
|
|
23
|
+
odd.push({ kind: v === true ? "flag" : "key", name: k });
|
|
24
|
+
}
|
|
25
|
+
return odd;
|
|
26
|
+
}
|
|
3
27
|
// §4 value typing: quoted -> string, true/false -> boolean, integer/float
|
|
4
28
|
// syntax -> number, any other bare word -> string. No arrays/dates/tables.
|
|
5
29
|
export function coerce(raw) {
|