@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/geml.js
CHANGED
|
@@ -18,7 +18,7 @@ import { renderHtml } from "./render-html.js";
|
|
|
18
18
|
import { normalizeBlockId } from "./block-edit.js";
|
|
19
19
|
import { normalizeSource } from "./diagnostics.js";
|
|
20
20
|
import { coerce, parseAttrs } from "./attrs.js";
|
|
21
|
-
import { META_REF_SRC, parseInline } from "./inline.js";
|
|
21
|
+
import { META_REF_SRC, parseInline, isSafeUrl, schemeOf } from "./inline.js";
|
|
22
22
|
import { parseTable } from "./table.js";
|
|
23
23
|
import { buildChart } from "./chart.js";
|
|
24
24
|
import { mdToGeml } from "./from-md.js";
|
|
@@ -46,7 +46,7 @@ const REGISTRY = {
|
|
|
46
46
|
diagram: "raw",
|
|
47
47
|
math: "raw",
|
|
48
48
|
table: "raw", // structured table parsing lands in M3
|
|
49
|
-
|
|
49
|
+
embed: "raw", // block transclusion: `src=` points at the content, body unused
|
|
50
50
|
note: "flow",
|
|
51
51
|
text: "flow", // addressable prose container: an id/attrs for a run of flow, no callout chrome
|
|
52
52
|
meta: "data",
|
|
@@ -154,7 +154,7 @@ function registerId(ctx, id, line) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
// §5: a list marker — `-`/`*` (unordered) or `N.` (ordered) — capturing the
|
|
157
|
-
// leading indent (in spaces; a tab counts as
|
|
157
|
+
// leading indent (in spaces; a tab counts as 4) and the item content. Nesting
|
|
158
158
|
// is decided by that indent.
|
|
159
159
|
const MARKER = /^([ \t]*)(?:[-*]|(\d+)\.)[ \t]+(.*)$/;
|
|
160
160
|
function matchMarker(line) {
|
|
@@ -162,7 +162,14 @@ function matchMarker(line) {
|
|
|
162
162
|
if (!m)
|
|
163
163
|
return null;
|
|
164
164
|
const ordered = m[2] !== undefined;
|
|
165
|
-
|
|
165
|
+
let indent = 0;
|
|
166
|
+
for (const ch of m[1]) {
|
|
167
|
+
if (ch === '\t')
|
|
168
|
+
indent += 4;
|
|
169
|
+
else
|
|
170
|
+
indent += 1;
|
|
171
|
+
}
|
|
172
|
+
const mk = { indent, ordered, rest: m[3] };
|
|
166
173
|
if (ordered)
|
|
167
174
|
mk.start = parseInt(m[2], 10);
|
|
168
175
|
return mk;
|
|
@@ -249,7 +256,26 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
249
256
|
const diags = ctx.diags;
|
|
250
257
|
let i = 0;
|
|
251
258
|
while (i < lines.length) {
|
|
252
|
-
|
|
259
|
+
let line = lines[i];
|
|
260
|
+
let consumed = 1;
|
|
261
|
+
// C-01: Attribute line continuation via `\`.
|
|
262
|
+
// If a line looks like a fence or heading and ends with `\`, fold subsequent lines.
|
|
263
|
+
if ((line.startsWith("===") || line.startsWith("#")) && line.endsWith("\\")) {
|
|
264
|
+
let folded = line.slice(0, -1).trimEnd();
|
|
265
|
+
while (i + consumed < lines.length) {
|
|
266
|
+
const next = lines[i + consumed].trim();
|
|
267
|
+
if (next.endsWith("\\")) {
|
|
268
|
+
folded += " " + next.slice(0, -1).trimEnd();
|
|
269
|
+
consumed++;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
folded += " " + next;
|
|
273
|
+
consumed++;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
line = folded;
|
|
278
|
+
}
|
|
253
279
|
if (line.trim() === "") {
|
|
254
280
|
i++;
|
|
255
281
|
continue;
|
|
@@ -259,24 +285,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
259
285
|
const hid = /^[ \t]*%%[ \t]?(.*)$/.exec(line);
|
|
260
286
|
if (hid) {
|
|
261
287
|
blocks.push({ kind: "hidden", text: hid[1] });
|
|
262
|
-
i
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
// §5.2: a Markdown-style footnote definition `[^id]: text` defines the
|
|
266
|
-
// target a `[^id]` reference points at — recorded as a note block with that
|
|
267
|
-
// id, so the reference resolves. (A model that reaches for Markdown
|
|
268
|
-
// footnotes by habit then "just works" instead of leaving a dangling ref.)
|
|
269
|
-
const fndef = /^\[\^([^\]]+)\]:[ \t]?(.*)$/.exec(line);
|
|
270
|
-
if (fndef) {
|
|
271
|
-
const id = fndef[1].trim();
|
|
272
|
-
const lineNo = base + i + 1;
|
|
273
|
-
registerId(ctx, id, lineNo);
|
|
274
|
-
const text = interpolate(fndef[2], lineNo, ctx);
|
|
275
|
-
blocks.push({
|
|
276
|
-
kind: "block", type: "note", mode: "flow", id, classes: ["footnote"], attrs: {},
|
|
277
|
-
children: [{ kind: "paragraph", text, inlines: parseInline(text, lineNo, ctx) }],
|
|
278
|
-
});
|
|
279
|
-
i++;
|
|
288
|
+
i += consumed;
|
|
280
289
|
continue;
|
|
281
290
|
}
|
|
282
291
|
const open = FENCE_OPEN.exec(line);
|
|
@@ -292,7 +301,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
292
301
|
// safe way to nest (§3).
|
|
293
302
|
const labeled = attrs.id !== undefined ? new RegExp(`^={3,}[ \\t]+#${reLit(attrs.id)}[ \\t]*$`) : null;
|
|
294
303
|
const body = [];
|
|
295
|
-
let j = i +
|
|
304
|
+
let j = i + consumed;
|
|
296
305
|
let closed = false;
|
|
297
306
|
for (; j < lines.length; j++) {
|
|
298
307
|
if (isCloseFence(lines[j], openLen) || (labeled && labeled.test(lines[j]))) {
|
|
@@ -310,6 +319,31 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
310
319
|
diags.push({ severity: "warning", code: "unknown-block-type", message: `unknown block type \`${type}\`; body kept as raw`, line: openLineNo });
|
|
311
320
|
mode = "raw";
|
|
312
321
|
}
|
|
322
|
+
else {
|
|
323
|
+
// `hidden` (§4) and `caption` (§4, and the label an auto-reference takes
|
|
324
|
+
// per §5.2) are not type-specific: every typed block may carry them. Only
|
|
325
|
+
// the extras below are per type.
|
|
326
|
+
let validRe;
|
|
327
|
+
if (type === "table")
|
|
328
|
+
validRe = /^(src|format|header|format-data|compute\d*|summary\d*|span\d*)$/;
|
|
329
|
+
else if (type === "embed")
|
|
330
|
+
validRe = /^(src)$/;
|
|
331
|
+
else if (type === "diagram")
|
|
332
|
+
validRe = /^(src|data|format|type|rows|x|y|size|series)$/;
|
|
333
|
+
// `src`/`anchor` on a `code` block are the code-graph profile's
|
|
334
|
+
// (docs/codemap-profile.md): every document `geml codemap build` writes
|
|
335
|
+
// carries them, so warning on them would warn on our own output.
|
|
336
|
+
else if (type === "code")
|
|
337
|
+
validRe = /^(lang|src|anchor|name|entry-via)$/;
|
|
338
|
+
else
|
|
339
|
+
validRe = /^$/;
|
|
340
|
+
const universal = /^(hidden|caption)$/;
|
|
341
|
+
for (const key of Object.keys(attrs.attrs)) {
|
|
342
|
+
if (!universal.test(key) && !validRe.test(key)) {
|
|
343
|
+
diags.push({ severity: "warning", code: "unknown-attribute", message: `unknown attribute \`${key}\` for block type \`${type}\``, line: openLineNo });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
313
347
|
const block = {
|
|
314
348
|
kind: "block", type, mode, classes: attrs.classes, attrs: attrs.attrs,
|
|
315
349
|
};
|
|
@@ -319,12 +353,50 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
319
353
|
}
|
|
320
354
|
if (attrs.attrs["hidden"] === true)
|
|
321
355
|
block.hidden = true; // §4: not rendered, still in model
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
356
|
+
// Block transclusion: `src=` names the content this block stands for, and
|
|
357
|
+
// is registered as an ordinary reference so the existing §8 resolver
|
|
358
|
+
// validates the document and the id. Without that, an embed would be the
|
|
359
|
+
// one reference shape whose rot is silent.
|
|
360
|
+
if (type === "embed") {
|
|
361
|
+
const src = typeof attrs.attrs["src"] === "string" ? attrs.attrs["src"].trim() : "";
|
|
362
|
+
if (src === "") {
|
|
363
|
+
diags.push({ severity: "error", code: "embed-missing-src", message: "embed: missing `src=`", line: openLineNo });
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const hash = src.indexOf("#");
|
|
367
|
+
const docPath = hash < 0 ? src : src.slice(0, hash);
|
|
368
|
+
const anchor = hash < 0 ? undefined : src.slice(hash + 1);
|
|
369
|
+
// §9.5: a destination naming a scheme outside the allowlist MUST NOT be
|
|
370
|
+
// emitted as a navigable or loadable target, and the check belongs HERE —
|
|
371
|
+
// when the model is built — so no consumer of the model can reintroduce
|
|
372
|
+
// it. The attribute is blanked as well as reported, the same treatment a
|
|
373
|
+
// media `src` already gets: a diagnostic alone would still leave the
|
|
374
|
+
// string in `attrs` for a renderer to put in an href.
|
|
375
|
+
if (!isSafeUrl(src)) {
|
|
376
|
+
diags.push({ severity: "error", code: "unsafe-embed-scheme", message: `embed: \`src=${src}\` names a disallowed URL scheme`, line: openLineNo });
|
|
377
|
+
block.attrs = { ...block.attrs, src: "" };
|
|
378
|
+
}
|
|
379
|
+
else if (docPath !== "" && !/\.geml$/i.test(docPath)) {
|
|
380
|
+
diags.push({ severity: "error", code: "embed-target-not-geml", message: `embed: \`${docPath}\` is not a GEML document; \`src=\` names a \`.geml\` file (optionally with a #fragment)`, line: openLineNo });
|
|
381
|
+
}
|
|
382
|
+
else if (docPath === "") {
|
|
383
|
+
// Recorded with an empty doc so the self-cycle pass can see it.
|
|
384
|
+
if (anchor !== undefined)
|
|
385
|
+
(ctx.embeds ??= []).push({ doc: "", anchor, line: openLineNo });
|
|
386
|
+
// `src=#id`: a block of THIS document. Validated against local ids.
|
|
387
|
+
if (anchor !== undefined)
|
|
388
|
+
ctx.refs.push({ kind: "internal", anchor, line: openLineNo });
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
ctx.refs.push({ kind: "cross", doc: docPath, anchor, line: openLineNo });
|
|
392
|
+
// Kept apart from refs: a transclusion can pull in a document that
|
|
393
|
+
// transcludes further, so cycle detection has to walk the graph.
|
|
394
|
+
(ctx.embeds ??= []).push(anchor === undefined ? { doc: docPath, line: openLineNo } : { doc: docPath, anchor, line: openLineNo });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (body.some((l) => l.trim() !== "")) {
|
|
398
|
+
diags.push({ severity: "warning", code: "ignored-embed-body", message: "embed body is ignored; the target lives in `src=`", line: openLineNo });
|
|
399
|
+
}
|
|
328
400
|
}
|
|
329
401
|
if (mode === "flow") {
|
|
330
402
|
if (depth >= MAX_NESTING) {
|
|
@@ -344,8 +416,11 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
344
416
|
else {
|
|
345
417
|
block.raw = body;
|
|
346
418
|
if (type === "table") {
|
|
419
|
+
const srcAttr = typeof attrs.attrs["src"] === "string" ? attrs.attrs["src"].trim() : undefined;
|
|
347
420
|
// §6: parse the raw body (visual or csv/tsv) into one table model.
|
|
348
421
|
const { model, diagnostics } = parseTable(body, attrs.attrs, openLineNo, ctx);
|
|
422
|
+
if (srcAttr !== undefined)
|
|
423
|
+
(ctx.tableSources ??= []).push({ block, line: openLineNo, target: srcAttr });
|
|
349
424
|
block.table = model;
|
|
350
425
|
for (const d of diagnostics)
|
|
351
426
|
diags.push({ ...d, line: openLineNo });
|
|
@@ -394,9 +469,10 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
394
469
|
if (h) {
|
|
395
470
|
const lineNo = base + i + 1;
|
|
396
471
|
const level = h[1].length;
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
const
|
|
472
|
+
const rawText = h[2];
|
|
473
|
+
const a = parseAttrs(h[3] ?? "");
|
|
474
|
+
const text = interpolate(rawText, lineNo, ctx);
|
|
475
|
+
const id = a.id ?? slug(rawText);
|
|
400
476
|
registerId(ctx, id, lineNo);
|
|
401
477
|
const block = {
|
|
402
478
|
kind: "heading", level, text, inlines: parseInline(text, lineNo, ctx), id, classes: a.classes, attrs: a.attrs,
|
|
@@ -404,7 +480,7 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
404
480
|
if (a.attrs["hidden"] === true)
|
|
405
481
|
block.hidden = true;
|
|
406
482
|
blocks.push(block);
|
|
407
|
-
i
|
|
483
|
+
i += consumed;
|
|
408
484
|
continue;
|
|
409
485
|
}
|
|
410
486
|
if (LIST_ITEM.test(line)) {
|
|
@@ -448,6 +524,342 @@ function parseData(lines) {
|
|
|
448
524
|
// ---------------------------------------------------------------------------
|
|
449
525
|
// Collect the block ids of a (cross-document) source, without validation, for
|
|
450
526
|
// resolving `other.geml#id` references.
|
|
527
|
+
// S5/S6: a transclusion may pull in a document that transcludes further, so a
|
|
528
|
+
// cycle is only visible by walking the graph. Reported at check time — before
|
|
529
|
+
// any rendering — so a build fails on the cycle rather than on a placeholder in
|
|
530
|
+
// the output. Paths compose the way the renderer composes them: a target inside
|
|
531
|
+
// a borrowed document is relative to THAT document.
|
|
532
|
+
// `data=rows.csv` on a chart, desugared: the anonymous table it stands for. Built
|
|
533
|
+
// by handing the loaded lines to the SAME body parser a `=== table {src=…}` uses,
|
|
534
|
+
// with the chart's own `format=`/`header=` carried over, so nothing about how the
|
|
535
|
+
// data is read is specific to charts. Returns null when the source could not be
|
|
536
|
+
// resolved — the diagnostic is already pushed, in the table rules' own words.
|
|
537
|
+
function chartSourceTable(ctx, opts, block, target, line) {
|
|
538
|
+
const scheme = schemeOf(target);
|
|
539
|
+
if (scheme === "http" || scheme === "https") {
|
|
540
|
+
// §9.4: fetched at render time, so there is nothing to chart at build time —
|
|
541
|
+
// the same state a remote-sourced table leaves behind.
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
if (!opts.resolveDoc) {
|
|
545
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `geml-chart: data source \`${target}\` not checked (no document resolver)`, line });
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
const text = opts.resolveDoc(target);
|
|
549
|
+
if (text === null) {
|
|
550
|
+
ctx.diags.push({ severity: "error", code: "unresolvable-table-source", message: `geml-chart: cannot resolve data source \`${target}\``, line });
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const attrs = {
|
|
554
|
+
format: typeof block.attrs["format-data"] === "string" ? block.attrs["format-data"] : inferDataFormat(target),
|
|
555
|
+
header: block.attrs["header"] === undefined ? true : block.attrs["header"],
|
|
556
|
+
};
|
|
557
|
+
const { model, diagnostics } = parseTable(normalizeSource(text).split("\n"), attrs, line, ctx);
|
|
558
|
+
for (const d of diagnostics)
|
|
559
|
+
ctx.diags.push({ ...d, line });
|
|
560
|
+
model.src = target;
|
|
561
|
+
return model;
|
|
562
|
+
}
|
|
563
|
+
const inferDataFormat = (target) => (/\.tsv$/i.test(target) ? "tsv" : "csv");
|
|
564
|
+
// The renderer's own cap (render.ts EMBED_DEPTH_CAP). Kept in step here so the
|
|
565
|
+
// check and the render agree on which documents are reachable at all.
|
|
566
|
+
const EMBED_DEPTH_LIMIT = 8;
|
|
567
|
+
function detectTransclusionCycles(ctx, opts) {
|
|
568
|
+
if (!opts.resolveDoc || ctx.embeds === undefined || ctx.embeds.length === 0)
|
|
569
|
+
return;
|
|
570
|
+
const resolve = opts.resolveDoc;
|
|
571
|
+
const embedsOf = new Map(); // memoized per path
|
|
572
|
+
const reported = new Set();
|
|
573
|
+
// A three-colour DFS over DOCUMENTS, not over paths. Enumerating every path
|
|
574
|
+
// through the graph is exponential in its fan-out: a chain of 21 tiny files,
|
|
575
|
+
// each embedding the next three times, took over two minutes — and `check` is
|
|
576
|
+
// the CI gate and the validator every MCP write runs twice. Grey means "on the
|
|
577
|
+
// current stack" and is the cycle; black means already fully explored, so each
|
|
578
|
+
// edge is walked once and the whole traversal is O(V+E).
|
|
579
|
+
const colour = new Map();
|
|
580
|
+
const walk = (path, base, stack, line) => {
|
|
581
|
+
const rel = relJoinPath(base, path);
|
|
582
|
+
if (colour.get(rel) === "grey") {
|
|
583
|
+
const chain = [...stack, rel].join(" → ");
|
|
584
|
+
if (reported.has(chain))
|
|
585
|
+
return;
|
|
586
|
+
reported.add(chain);
|
|
587
|
+
ctx.diags.push({ severity: "error", code: "transclusion-cycle", message: `transclusion cycle: ${chain}`, line });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (colour.get(rel) === "black")
|
|
591
|
+
return;
|
|
592
|
+
// Agree with the renderer about what is even reachable, instead of exploring
|
|
593
|
+
// eight times deeper than it will ever expand.
|
|
594
|
+
if (stack.length >= EMBED_DEPTH_LIMIT)
|
|
595
|
+
return;
|
|
596
|
+
colour.set(rel, "grey");
|
|
597
|
+
let inner = embedsOf.get(rel);
|
|
598
|
+
if (inner === undefined) {
|
|
599
|
+
const src = resolve(rel);
|
|
600
|
+
inner = src === null ? [] : gatherEmbeds(src); // an unresolvable doc is already an error
|
|
601
|
+
embedsOf.set(rel, inner);
|
|
602
|
+
}
|
|
603
|
+
for (const e of inner)
|
|
604
|
+
walk(e.doc, relDirPath(rel), [...stack, rel], line);
|
|
605
|
+
colour.set(rel, "black");
|
|
606
|
+
};
|
|
607
|
+
// The root is named so a chain can be seen returning to it. Falling back to ""
|
|
608
|
+
// only loses the A→…→A case, which is what happened before `self` existed.
|
|
609
|
+
const root = opts.self ?? "";
|
|
610
|
+
for (const e of ctx.embeds)
|
|
611
|
+
walk(e.doc, relDirPath(root), [root], e.line);
|
|
612
|
+
}
|
|
613
|
+
// The smallest cycle of all, and the one the cross-document walk above cannot
|
|
614
|
+
// see: `=== embed {src=#sec}` written INSIDE the section `#sec` selects the slice
|
|
615
|
+
// that contains it. Decided on spans, so the boundary is exactly the one `geml
|
|
616
|
+
// get` uses — a heading id spans its whole section, so an embed anywhere in that
|
|
617
|
+
// section is inside its own target.
|
|
618
|
+
function detectSelfEmbedCycles(source, ctx) {
|
|
619
|
+
const selfEmbeds = (ctx.embeds ?? []).filter((e) => e.doc === "" && e.anchor !== undefined);
|
|
620
|
+
if (selfEmbeds.length === 0)
|
|
621
|
+
return;
|
|
622
|
+
const spans = blockSpans(source);
|
|
623
|
+
for (const e of selfEmbeds) {
|
|
624
|
+
const span = spans.get(e.anchor);
|
|
625
|
+
if (span === undefined)
|
|
626
|
+
continue; // a missing id is already an unresolved reference
|
|
627
|
+
const line = e.line - 1; // spans are 0-based line indices
|
|
628
|
+
if (line >= span.start && line <= span.end) {
|
|
629
|
+
ctx.diags.push({
|
|
630
|
+
severity: "error",
|
|
631
|
+
code: "transclusion-cycle",
|
|
632
|
+
message: `transclusion cycle: \`#${e.anchor}\` selects the content this embed is part of`,
|
|
633
|
+
line: e.line,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
// A phrase that projects itself. The same shape as detectSelfEmbedCycles, and
|
|
639
|
+
// deliberately the same machinery rather than a second parallel one: decided on
|
|
640
|
+
// spans, so a projection written anywhere inside its own target is caught.
|
|
641
|
+
function detectSelfProjectionCycles(source, ctx) {
|
|
642
|
+
const local = (ctx.projections ?? []).filter((p) => p.doc === undefined);
|
|
643
|
+
if (local.length === 0)
|
|
644
|
+
return;
|
|
645
|
+
const spans = blockSpans(source);
|
|
646
|
+
for (const p of local) {
|
|
647
|
+
const span = spans.get(p.anchor);
|
|
648
|
+
if (span === undefined)
|
|
649
|
+
continue;
|
|
650
|
+
const line = p.line - 1;
|
|
651
|
+
if (line >= span.start && line <= span.end) {
|
|
652
|
+
ctx.diags.push({
|
|
653
|
+
severity: "error",
|
|
654
|
+
code: "transclusion-cycle",
|
|
655
|
+
message: `transclusion cycle: \`![[#${p.anchor}]]\` projects the content it is part of`,
|
|
656
|
+
line: p.line,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Inline content that a projection may stand for: a `text` block whose body is a
|
|
662
|
+
// single paragraph. Returned so the renderer and this validator agree on one
|
|
663
|
+
// definition. Anything else — a heading (and so a whole section), a table, a
|
|
664
|
+
// diagram, a multi-paragraph body — is block content, and no amount of syntax
|
|
665
|
+
// makes it fit inside a sentence.
|
|
666
|
+
export function projectableInlines(blocks, id) {
|
|
667
|
+
const found = (function find(bs) {
|
|
668
|
+
for (const b of bs) {
|
|
669
|
+
if ((b.kind === "block" || b.kind === "heading") && b.id === id)
|
|
670
|
+
return b;
|
|
671
|
+
if (b.kind === "block" && b.children) {
|
|
672
|
+
const inner = find(b.children);
|
|
673
|
+
if (inner)
|
|
674
|
+
return inner;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
})(blocks);
|
|
679
|
+
if (found === undefined)
|
|
680
|
+
return null;
|
|
681
|
+
if (found.kind !== "block" || found.type !== "text")
|
|
682
|
+
return "not-inline";
|
|
683
|
+
const kids = (found.children ?? []).filter((c) => !(c.kind === "paragraph" && c.text.trim() === ""));
|
|
684
|
+
if (kids.length !== 1 || kids[0].kind !== "paragraph")
|
|
685
|
+
return "not-inline";
|
|
686
|
+
return { inlines: kids[0].inlines };
|
|
687
|
+
}
|
|
688
|
+
// A projection may only stand for inline content, and the target decides — the
|
|
689
|
+
// same shape of rule as `table-source-not-a-table`, not a rule about where the
|
|
690
|
+
// reference was written.
|
|
691
|
+
function validateProjections(children, ctx, opts) {
|
|
692
|
+
for (const p of ctx.projections ?? []) {
|
|
693
|
+
let blocks = null;
|
|
694
|
+
if (p.doc === undefined)
|
|
695
|
+
blocks = children;
|
|
696
|
+
else if (opts.resolveDoc) {
|
|
697
|
+
const src = opts.resolveDoc(p.doc);
|
|
698
|
+
if (src === null)
|
|
699
|
+
continue; // already an unresolvable-document error
|
|
700
|
+
blocks = parse(src).children;
|
|
701
|
+
}
|
|
702
|
+
if (blocks === null)
|
|
703
|
+
continue; // unchecked without a resolver, like any cross-doc ref
|
|
704
|
+
const got = projectableInlines(blocks, p.anchor);
|
|
705
|
+
if (got === null)
|
|
706
|
+
continue; // already an unresolved-reference error
|
|
707
|
+
if (got === "not-inline") {
|
|
708
|
+
const target = p.doc === undefined ? `#${p.anchor}` : `${p.doc}#${p.anchor}`;
|
|
709
|
+
ctx.diags.push({
|
|
710
|
+
severity: "error",
|
|
711
|
+
code: "inline-transclusion-not-inline",
|
|
712
|
+
message: `\`![[${target}]]\` projects inline content, but the target is not a single-paragraph \`text\` block; for block content use \`=== embed {src=${target}}\``,
|
|
713
|
+
line: p.line,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Same pure-string path composition the renderer uses (relJoin/relDir there).
|
|
719
|
+
function relJoinPath(base, target) {
|
|
720
|
+
if (base === "" || target === "" || target.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(target))
|
|
721
|
+
return target;
|
|
722
|
+
const out = [];
|
|
723
|
+
for (const s of (base + "/" + target).split("/")) {
|
|
724
|
+
if (s === "" || s === ".")
|
|
725
|
+
continue;
|
|
726
|
+
if (s === ".." && out.length > 0 && out[out.length - 1] !== "..")
|
|
727
|
+
out.pop();
|
|
728
|
+
else
|
|
729
|
+
out.push(s);
|
|
730
|
+
}
|
|
731
|
+
return out.join("/");
|
|
732
|
+
}
|
|
733
|
+
function relDirPath(p) {
|
|
734
|
+
const i = p.lastIndexOf("/");
|
|
735
|
+
return i < 0 ? "" : p.slice(0, i);
|
|
736
|
+
}
|
|
737
|
+
function gatherEmbeds(source) {
|
|
738
|
+
const ctx = { diags: [], ids: new Map(), refs: [], meta: new Map(), embeds: [] };
|
|
739
|
+
scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
|
|
740
|
+
return (ctx.embeds ?? []).map((e) => (e.anchor === undefined ? { doc: e.doc } : { doc: e.doc, anchor: e.anchor }));
|
|
741
|
+
}
|
|
742
|
+
// One rule for "where this data comes from", shared by a table's `src=`
|
|
743
|
+
// and a chart's `data=`. Three target forms: a data file, `#id` naming a table
|
|
744
|
+
// block in this document, or `doc.geml#id` naming one in another document. An
|
|
745
|
+
// unresolvable target is an error — a table whose source silently produced no
|
|
746
|
+
// rows used to render as an empty table with no diagnostic at all.
|
|
747
|
+
function tableFromDocument(source, id) {
|
|
748
|
+
const ctx = { diags: [], ids: new Map(), refs: [], meta: new Map() };
|
|
749
|
+
const blocks = scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
|
|
750
|
+
const found = ctx.tables?.get(id);
|
|
751
|
+
if (found !== undefined)
|
|
752
|
+
return found;
|
|
753
|
+
const anyBlock = (function find(bs) {
|
|
754
|
+
for (const b of bs) {
|
|
755
|
+
if ((b.kind === "block" || b.kind === "heading") && b.id === id)
|
|
756
|
+
return b;
|
|
757
|
+
if (b.kind === "block" && b.children) {
|
|
758
|
+
const inner = find(b.children);
|
|
759
|
+
if (inner)
|
|
760
|
+
return inner;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return undefined;
|
|
764
|
+
})(blocks);
|
|
765
|
+
return anyBlock === undefined ? null : "not-a-table";
|
|
766
|
+
}
|
|
767
|
+
function resolveTableSources(ctx, opts) {
|
|
768
|
+
const pending = ctx.tableSources ?? [];
|
|
769
|
+
if (pending.length === 0)
|
|
770
|
+
return;
|
|
771
|
+
const err = (line, code, message) => void ctx.diags.push({ severity: "error", code, message, line });
|
|
772
|
+
// Data files first: a `#id` target may point at a table whose OWN rows come
|
|
773
|
+
// from a file, and this way that table is already populated when it is read.
|
|
774
|
+
for (const { block, line, target } of pending) {
|
|
775
|
+
if (target.includes("#"))
|
|
776
|
+
continue;
|
|
777
|
+
// §9.4: a remote source is fetched by the RENDERER, not the parser. Leaving
|
|
778
|
+
// `model.src` set with no columns is the state resolveCharts already handles,
|
|
779
|
+
// so a chart over it defers too. Passing it to resolveDoc treated a URL as a
|
|
780
|
+
// filesystem path and failed a spec-conformant document.
|
|
781
|
+
const scheme = schemeOf(target);
|
|
782
|
+
if (scheme === "http" || scheme === "https")
|
|
783
|
+
continue;
|
|
784
|
+
if (scheme !== null) {
|
|
785
|
+
err(line, "unresolvable-table-source", `table source \`${target}\` names a disallowed URL scheme`);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
// A data source is data. Without this the loader read any file under the base
|
|
789
|
+
// — a `.env`, a private key — split it into rows, and put it in the model and
|
|
790
|
+
// the page, with no diagnostic. `embed` already applies the same shape of rule.
|
|
791
|
+
if (!/\.(csv|tsv)$/i.test(target)) {
|
|
792
|
+
err(line, "unresolvable-table-source", `table source \`${target}\` is not a \`.csv\`/\`.tsv\` data file`);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
if (!opts.resolveDoc) {
|
|
796
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `table source \`${target}\` not checked (no document resolver)`, line });
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const text = opts.resolveDoc(target);
|
|
800
|
+
if (text === null) {
|
|
801
|
+
err(line, "unresolvable-table-source", `cannot resolve table source \`${target}\``);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
// Reuse the body parser: with `src` dropped, the file's lines are just
|
|
805
|
+
// this table's body, so format/header/compute/summary all behave identically.
|
|
806
|
+
const attrs = { ...block.attrs };
|
|
807
|
+
delete attrs["src"];
|
|
808
|
+
const { model, diagnostics } = parseTable(normalizeSource(text).split("\n"), attrs, line, ctx);
|
|
809
|
+
model.src = target;
|
|
810
|
+
block.table = model;
|
|
811
|
+
for (const d of diagnostics)
|
|
812
|
+
ctx.diags.push({ ...d, line });
|
|
813
|
+
if (block.id !== undefined)
|
|
814
|
+
(ctx.tables ??= new Map()).set(block.id, model);
|
|
815
|
+
}
|
|
816
|
+
for (const { block, line, target } of pending) {
|
|
817
|
+
const hash = target.indexOf("#");
|
|
818
|
+
if (hash < 0)
|
|
819
|
+
continue;
|
|
820
|
+
const docPath = target.slice(0, hash);
|
|
821
|
+
const id = target.slice(hash + 1);
|
|
822
|
+
let model;
|
|
823
|
+
if (docPath === "") {
|
|
824
|
+
const local = ctx.tables?.get(id);
|
|
825
|
+
if (local === undefined) {
|
|
826
|
+
if (ctx.ids.has(id))
|
|
827
|
+
err(line, "table-source-not-a-table", `table source \`#${id}\` is not a table`);
|
|
828
|
+
else
|
|
829
|
+
err(line, "unresolved-reference", `unresolved reference \`#${id}\``);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
model = local;
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
if (!opts.resolveDoc) {
|
|
836
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `table source \`${target}\` not checked (no document resolver)`, line });
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const text = opts.resolveDoc(docPath);
|
|
840
|
+
if (text === null) {
|
|
841
|
+
err(line, "unresolvable-document", `cannot resolve document \`${docPath}\``);
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const remote = tableFromDocument(text, id);
|
|
845
|
+
if (remote === null) {
|
|
846
|
+
err(line, "unresolved-cross-document-reference", `unresolved reference \`${target}\``);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (remote === "not-a-table") {
|
|
850
|
+
err(line, "table-source-not-a-table", `table source \`${target}\` is not a table`);
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
model = remote;
|
|
854
|
+
}
|
|
855
|
+
// Borrowed, not copied in the source: the model is shared, so the borrowing
|
|
856
|
+
// table means exactly what the original means. Its own caption still wins.
|
|
857
|
+
const caption = block.table?.caption;
|
|
858
|
+
block.table = caption === undefined ? model : { ...model, caption };
|
|
859
|
+
if (block.id !== undefined)
|
|
860
|
+
(ctx.tables ??= new Map()).set(block.id, block.table);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
451
863
|
function gatherIds(source) {
|
|
452
864
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: new Map() };
|
|
453
865
|
scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
|
|
@@ -512,26 +924,77 @@ function validateRefs(ctx, opts) {
|
|
|
512
924
|
}
|
|
513
925
|
// §7: resolve every geml-chart against its referenced table. Runs after the
|
|
514
926
|
// scan so that `data=#id` may point at a table defined anywhere in the doc.
|
|
515
|
-
function resolveCharts(ctx) {
|
|
927
|
+
function resolveCharts(ctx, opts) {
|
|
516
928
|
for (const { block, line } of ctx.charts ?? []) {
|
|
517
|
-
const ref = typeof block.attrs["data"] === "string" ? block.attrs["data"] : "";
|
|
518
|
-
|
|
519
|
-
if (id === "") {
|
|
929
|
+
const ref = typeof block.attrs["data"] === "string" ? block.attrs["data"].trim() : "";
|
|
930
|
+
if (ref === "" || ref === "#") {
|
|
520
931
|
ctx.diags.push({ severity: "error", code: "chart-missing-data", message: "geml-chart: missing `data=#id`", line });
|
|
521
932
|
continue;
|
|
522
933
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
934
|
+
// `data=` resolves by the same rule as a table's source: `#id` (or a bare id)
|
|
935
|
+
// names a table in THIS document, `doc.geml#id` one in another. Splitting on
|
|
936
|
+
// the LAST `#` is what the old code got wrong — it stripped the leading one
|
|
937
|
+
// and reported `#other.geml#fy25`, a target that never existed.
|
|
938
|
+
const hash = ref.indexOf("#");
|
|
939
|
+
const docPath = hash <= 0 ? "" : ref.slice(0, hash);
|
|
940
|
+
const id = hash < 0 ? ref : ref.slice(hash + 1);
|
|
941
|
+
let table;
|
|
942
|
+
if (docPath === "") {
|
|
943
|
+
table = ctx.tables?.get(id);
|
|
944
|
+
if (!table) {
|
|
945
|
+
// A chart is a view of a table, and a data file is one of the three ways
|
|
946
|
+
// §6 lets a table name its content. So `data=rows.csv` desugars: it is an
|
|
947
|
+
// anonymous table with that source, feeding this chart. Nothing new is
|
|
948
|
+
// invented — the resolution, the `.csv`/`.tsv` gate, the §9.4 remote rule
|
|
949
|
+
// and `format=` all come from the table rules, which is what makes the one
|
|
950
|
+
// source rule hold for charts too instead of charts being its exception.
|
|
951
|
+
if (hash < 0 && /\.(csv|tsv)$/i.test(id)) {
|
|
952
|
+
const sugar = chartSourceTable(ctx, opts, block, id, line);
|
|
953
|
+
if (sugar === null)
|
|
954
|
+
continue; // already reported by the table rules
|
|
955
|
+
table = sugar;
|
|
956
|
+
}
|
|
957
|
+
else if (hash < 0 && /\.[a-z0-9]+$/i.test(id)) {
|
|
958
|
+
ctx.diags.push({ severity: "error", code: "unresolvable-table-source", message: `geml-chart: \`data=${id}\` is not a \`.csv\`/\`.tsv\` data file, and not a \`#id\` naming a table`, line });
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
const known = ctx.ids.has(id);
|
|
963
|
+
const what = known ? `data target \`#${id}\` is not a table` : `unresolved reference \`#${id}\``;
|
|
964
|
+
const code = known ? "chart-data-not-a-table" : "unresolved-reference";
|
|
965
|
+
ctx.diags.push({ severity: "error", code, message: `geml-chart: ${what}`, line });
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
if (!opts.resolveDoc) {
|
|
972
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `geml-chart: data target \`${ref}\` not checked (no document resolver)`, line });
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const text = opts.resolveDoc(docPath);
|
|
976
|
+
if (text === null) {
|
|
977
|
+
ctx.diags.push({ severity: "error", code: "unresolvable-document", message: `geml-chart: cannot resolve document \`${docPath}\``, line });
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const remote = tableFromDocument(text, id);
|
|
981
|
+
if (remote === null) {
|
|
982
|
+
ctx.diags.push({ severity: "error", code: "unresolved-cross-document-reference", message: `geml-chart: unresolved reference \`${ref}\``, line });
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
if (remote === "not-a-table") {
|
|
986
|
+
ctx.diags.push({ severity: "error", code: "chart-data-not-a-table", message: `geml-chart: data target \`${ref}\` is not a table`, line });
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
table = remote;
|
|
530
990
|
}
|
|
531
|
-
if (table.src !== undefined) {
|
|
532
|
-
// §6: the table
|
|
533
|
-
//
|
|
534
|
-
//
|
|
991
|
+
if (table.src !== undefined && table.columns.length === 0) {
|
|
992
|
+
// §6: the table names a source whose data did not arrive at build time — a
|
|
993
|
+
// remote URL, or any source with no document resolver supplied. The chart is
|
|
994
|
+
// therefore resolved at render time, and its column names are checked there.
|
|
995
|
+
// The test is whether the data is actually here, not what the source looks
|
|
996
|
+
// like: skipping every `src` table unconditionally is what left a chart
|
|
997
|
+
// unbuilt with no diagnostic while the page said to go and read one.
|
|
535
998
|
continue;
|
|
536
999
|
}
|
|
537
1000
|
const { model, diagnostics } = buildChart(block.attrs, table);
|
|
@@ -545,19 +1008,34 @@ export function parse(source, opts = {}) {
|
|
|
545
1008
|
const lines = normalizeSource(source).split("\n");
|
|
546
1009
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: collectMeta(lines), resolveDoc: opts.resolveDoc };
|
|
547
1010
|
const children = scanBlocks(lines, 0, ctx);
|
|
548
|
-
|
|
1011
|
+
// Table sources first: a chart reads the build-time model of the table it
|
|
1012
|
+
// charts, so that model has to be filled before charts are resolved.
|
|
1013
|
+
resolveTableSources(ctx, opts);
|
|
1014
|
+
resolveCharts(ctx, opts);
|
|
549
1015
|
validateRefs(ctx, opts);
|
|
1016
|
+
detectTransclusionCycles(ctx, opts);
|
|
1017
|
+
detectSelfEmbedCycles(source, ctx);
|
|
1018
|
+
validateProjections(children, ctx, opts);
|
|
1019
|
+
detectSelfProjectionCycles(source, ctx);
|
|
1020
|
+
for (const m of ctx.mediaDocTargets ?? []) {
|
|
1021
|
+
ctx.diags.push({
|
|
1022
|
+
severity: "error",
|
|
1023
|
+
code: "media-target-is-document",
|
|
1024
|
+
// `!` projects, so a GEML target here is a near-miss an author will reach for
|
|
1025
|
+
// once that reading is established. Name both forms it could have meant.
|
|
1026
|
+
message: `\`\` projects a GEML document, which is not media: for block content use \`=== embed {src=${m.src}}\`, for a phrase use \`![[${m.src}]]\``,
|
|
1027
|
+
line: m.line,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
550
1030
|
return { kind: "document", children, ids: [...ctx.ids.keys()], diagnostics: ctx.diags };
|
|
551
1031
|
}
|
|
552
1032
|
// The id that a fence/heading line defines, matching how scanBlocks derives it
|
|
553
1033
|
// (parseAttrs for the attribute object; heading text slug when no explicit id).
|
|
554
|
-
// The slug MUST come from the
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
// make the real one unaddressable. `ctx` is an inert context carrying the
|
|
558
|
-
// document's meta (diagnostics are discarded — spans never report).
|
|
1034
|
+
// The slug MUST come from the RAW text, before interpolation, so that changing
|
|
1035
|
+
// a meta variable does not silently change the block's addressable id.
|
|
1036
|
+
// `ctx` is passed just in case future features need context.
|
|
559
1037
|
function idOfHeading(braces, text, line, ctx) {
|
|
560
|
-
return (braces ? parseAttrs(braces).id : undefined) ?? slug(
|
|
1038
|
+
return (braces ? parseAttrs(braces).id : undefined) ?? slug(text);
|
|
561
1039
|
}
|
|
562
1040
|
// The matching close of the fence opened at lines[i] (equal-length run, or the
|
|
563
1041
|
// labeled `=== #id` close when the block carries an id): the index just past
|
|
@@ -594,7 +1072,7 @@ function sectionEnd(lines, i, level) {
|
|
|
594
1072
|
}
|
|
595
1073
|
// Walk `lines` exactly as scanBlocks does — same fence close rules (equal-length
|
|
596
1074
|
// or labeled `=== #id`), same flow-only recursion via REGISTRY — recording the
|
|
597
|
-
// source span of every addressable id (typed block, heading
|
|
1075
|
+
// source span of every addressable id (typed block, heading).
|
|
598
1076
|
// First definition wins, mirroring ctx.ids (a duplicate id is a build error, so
|
|
599
1077
|
// `get`/`set` operate on the one the parser actually registered). `base` is the
|
|
600
1078
|
// absolute line offset of this slice within the whole document.
|
|
@@ -662,8 +1140,8 @@ types) {
|
|
|
662
1140
|
export function blockSpans(source) {
|
|
663
1141
|
const out = new Map();
|
|
664
1142
|
const lines = normalizeSource(source).split("\n");
|
|
665
|
-
// Inert context: heading auto-ids slug the
|
|
666
|
-
//
|
|
1143
|
+
// Inert context: heading auto-ids slug the raw text, but parseDoc still
|
|
1144
|
+
// requires a valid context to parse the document.
|
|
667
1145
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: collectMeta(lines) };
|
|
668
1146
|
collectSpans(lines, 0, out, ctx);
|
|
669
1147
|
return out;
|
|
@@ -700,8 +1178,8 @@ function toNewline(text, nl) {
|
|
|
700
1178
|
return nl === "\n" ? lf : lf.replace(/\n/g, nl);
|
|
701
1179
|
}
|
|
702
1180
|
// `--head`: narrow any id's span to its HEAD line — the single declaring line
|
|
703
|
-
// (a heading's `# … {#id}` line, a typed block's opening fence
|
|
704
|
-
//
|
|
1181
|
+
// (a heading's `# … {#id}` line, or a typed block's opening fence). The head is
|
|
1182
|
+
// by construction the FIRST line of the span, so
|
|
705
1183
|
// the narrowing is parse-free and needs no type check. Main use: `set --head`
|
|
706
1184
|
// edits a block's attributes (caption/compute/lang/…) without re-sending its
|
|
707
1185
|
// body, or renames a heading without rewriting its section.
|
|
@@ -794,51 +1272,54 @@ export const PARSER_VERSION = (() => {
|
|
|
794
1272
|
}
|
|
795
1273
|
return "0.0.0";
|
|
796
1274
|
})();
|
|
797
|
-
const USAGE = `geml — GEML reference CLI
|
|
798
|
-
|
|
799
|
-
Usage:
|
|
800
|
-
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [-o out] transform a document (default: --to json)
|
|
801
|
-
--to
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
--to
|
|
806
|
-
|
|
807
|
-
geml
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
geml
|
|
818
|
-
(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
geml
|
|
823
|
-
(
|
|
824
|
-
geml
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
geml
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1275
|
+
const USAGE = `geml — GEML reference CLI
|
|
1276
|
+
|
|
1277
|
+
Usage:
|
|
1278
|
+
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [--root d] [-o out] transform a document (default: --to json)
|
|
1279
|
+
(--root widens cross-doc resolution to dir d, as on check — an
|
|
1280
|
+
=== embed whose target sits above the file's own directory
|
|
1281
|
+
needs it, or it renders unresolved)
|
|
1282
|
+
--to <output>: json | html | md | geml
|
|
1283
|
+
--to md -> Markdown (lossy)
|
|
1284
|
+
--to html -> self-contained HTML
|
|
1285
|
+
--to geml -> canonical re-format
|
|
1286
|
+
--to json -> document-model JSON (default)
|
|
1287
|
+
--from <input>: geml | md | json (overrides extension; html is output-only)
|
|
1288
|
+
geml notes.md -> GEML (md inferred from extension)
|
|
1289
|
+
geml model.json --to geml -> GEML (round-trips a prior --to json)
|
|
1290
|
+
geml - --from md read Markdown on stdin
|
|
1291
|
+
geml get <file.geml|-> [#id] [--json] [--head] with #id: print that block
|
|
1292
|
+
(a heading id = its whole section; --head = head line;
|
|
1293
|
+
--json = model node). Without #id: list all addressable
|
|
1294
|
+
ids (--json = array).
|
|
1295
|
+
geml set <file.geml|-> #id [--head|--body] [--in f[#src]|-] [-o f] replace ONE block by id
|
|
1296
|
+
(--in F takes F's block #id, F#src takes #src, else stdin raw;
|
|
1297
|
+
default = whole block · --head = head line · --body = body)
|
|
1298
|
+
geml add <file.geml|-> (--append | --before #id | --after #id) [--in f[#src]|-] [-o f] insert a fragment
|
|
1299
|
+
(1+ blocks and/or prose; content keeps its own ids, a clash is refused)
|
|
1300
|
+
geml delete <file.geml|-> #id [#id2 …] [-o f] remove one or more blocks
|
|
1301
|
+
(a missing id is skipped; a dangling reference is a warning, not a refusal)
|
|
1302
|
+
geml rename <file.geml|-> #old #new [-o f] rename an id and every reference to it (id-boundary safe)
|
|
1303
|
+
geml revert <file.geml> #id [--rev <sel>] [--head] undo one block to a past revision (splice / resurrect / remove)
|
|
1304
|
+
(sel: 0 | -N | id-prefix | changed; default -1)
|
|
1305
|
+
geml check <file.geml|-> [--root d] [--json] validate only: diagnostics + exit code
|
|
1306
|
+
(--root widens cross-doc refs to dir d, e.g. the repo root)
|
|
1307
|
+
geml history <commit|verify|show|restore|log> <file.geml> [...] .gemlhistory version sidecar
|
|
1308
|
+
geml codemap <build|verify|render|serve|refresh|find> [...] code-graph toolkit (alias: codegraph)
|
|
1309
|
+
geml mcp --root <dir> [--graph <dir>] [--no-history] serve documents (and the code graph) over MCP (stdio)
|
|
1310
|
+
(10 tools, each geml_ + its CLI verb: list/get/check/history/to +
|
|
1311
|
+
set/add/delete/rename/revert; every write is validated before it
|
|
1312
|
+
reaches disk. A code graph under --root adds four read-only
|
|
1313
|
+
geml_codemap_* tools to the same server)
|
|
1314
|
+
geml --help | --version [--json]
|
|
1315
|
+
|
|
1316
|
+
Use '-' as the file to read from stdin.
|
|
1317
|
+
Mutations (set/add/delete/rename) write the whole updated document in place for a
|
|
1318
|
+
file, or to stdout for '-' input; -o redirects it (-o - = stdout).
|
|
1319
|
+
Exit codes:
|
|
1320
|
+
0 ok
|
|
1321
|
+
1 document/operation error
|
|
1322
|
+
2 command usage error.
|
|
842
1323
|
`;
|
|
843
1324
|
// One-line usage for each subcommand — the single source for both the error
|
|
844
1325
|
// shown on misuse and the `<cmd> --help` text.
|
|
@@ -851,35 +1332,35 @@ const SUBHELP = {
|
|
|
851
1332
|
check: "usage: geml check <file.geml|-> [--root <dir>] [--json] (--root: resolve cross-doc refs within <dir> instead of the file's own directory)",
|
|
852
1333
|
revert: "usage: geml revert <file.geml> #id [--rev <sel>] [--append|--before #x|--after #x] [--head] [--dry-run] [-o out] (reconcile #id to a revision: splice / resurrect / remove; sel: 0 | -N | id-prefix | changed; default -1)",
|
|
853
1334
|
history: "usage: geml history <commit|verify|show|restore|log> <file.geml> [...]",
|
|
854
|
-
codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
|
|
855
|
-
geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
|
|
856
|
-
geml codemap verify [dir] geml check + profile reference checks
|
|
857
|
-
geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
|
|
858
|
-
geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
|
|
859
|
-
geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
|
|
860
|
-
geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
|
|
1335
|
+
codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
|
|
1336
|
+
geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
|
|
1337
|
+
geml codemap verify [dir] geml check + profile reference checks
|
|
1338
|
+
geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
|
|
1339
|
+
geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
|
|
1340
|
+
geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
|
|
1341
|
+
geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
|
|
861
1342
|
(<dir> for verify/render/serve/refresh/find defaults to ./.geml-code-graph; codegraph and code-graph are accepted as aliases of codemap)`,
|
|
862
|
-
mcp: `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
863
|
-
|
|
864
|
-
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).
|
|
865
|
-
Every tool is geml_ + its CLI verb, so the terminal and the assistant share
|
|
866
|
-
one vocabulary.
|
|
867
|
-
Ten tools: geml_list · geml_get · geml_check · geml_history · geml_to
|
|
868
|
-
geml_set · geml_add · geml_delete · geml_rename · geml_revert
|
|
869
|
-
With a code graph under --root, four more (read-only), so one client entry
|
|
870
|
-
covers both: geml_codemap_search · geml_codemap_callchain
|
|
871
|
-
geml_codemap_list · geml_codemap_node
|
|
872
|
-
|
|
873
|
-
--root <dir> REQUIRED. Root holding the .geml documents. Every path a
|
|
874
|
-
client names is confined here; a client cannot widen it.
|
|
875
|
-
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
876
|
-
<root>/.geml-code-graph when it holds an index.geml; with
|
|
877
|
-
no graph the four graph tools are not served at all.
|
|
878
|
-
--no-history Skip the .gemlhistory commit taken before each write
|
|
879
|
-
(default: commit, so geml_revert always has a revision to
|
|
880
|
-
undo to).
|
|
881
|
-
|
|
882
|
-
Register with a client:
|
|
1343
|
+
mcp: `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
1344
|
+
|
|
1345
|
+
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).
|
|
1346
|
+
Every tool is geml_ + its CLI verb, so the terminal and the assistant share
|
|
1347
|
+
one vocabulary.
|
|
1348
|
+
Ten tools: geml_list · geml_get · geml_check · geml_history · geml_to
|
|
1349
|
+
geml_set · geml_add · geml_delete · geml_rename · geml_revert
|
|
1350
|
+
With a code graph under --root, four more (read-only), so one client entry
|
|
1351
|
+
covers both: geml_codemap_search · geml_codemap_callchain
|
|
1352
|
+
geml_codemap_list · geml_codemap_node
|
|
1353
|
+
|
|
1354
|
+
--root <dir> REQUIRED. Root holding the .geml documents. Every path a
|
|
1355
|
+
client names is confined here; a client cannot widen it.
|
|
1356
|
+
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
1357
|
+
<root>/.geml-code-graph when it holds an index.geml; with
|
|
1358
|
+
no graph the four graph tools are not served at all.
|
|
1359
|
+
--no-history Skip the .gemlhistory commit taken before each write
|
|
1360
|
+
(default: commit, so geml_revert always has a revision to
|
|
1361
|
+
undo to).
|
|
1362
|
+
|
|
1363
|
+
Register with a client:
|
|
883
1364
|
claude mcp add geml -- geml mcp --root /abs/path/to/repo`,
|
|
884
1365
|
};
|
|
885
1366
|
// Set from argv at dispatch time; when true, errors are emitted as a JSON
|
|
@@ -1002,7 +1483,7 @@ function runCheck(args) {
|
|
|
1002
1483
|
if (!isDir)
|
|
1003
1484
|
fail(`--root ${root} is not a directory`);
|
|
1004
1485
|
}
|
|
1005
|
-
const doc = parse(readInput(file), { resolveDoc: resolverFor(file, root) });
|
|
1486
|
+
const doc = parse(readInput(file), { resolveDoc: resolverFor(file, root), self: file === "-" ? undefined : basename(file) });
|
|
1006
1487
|
if (json) {
|
|
1007
1488
|
console.log(JSON.stringify(doc.diagnostics, null, 2));
|
|
1008
1489
|
}
|
|
@@ -1090,7 +1571,15 @@ function runTransform(argv) {
|
|
|
1090
1571
|
const out = flag(argv, "-o") ?? flag(argv, "--out");
|
|
1091
1572
|
const fromRaw = flag(argv, "--from");
|
|
1092
1573
|
const toRaw = flag(argv, "--to");
|
|
1093
|
-
|
|
1574
|
+
// Same `--root` as `check`, and for the same reason: cross-document resolution is
|
|
1575
|
+
// fail-closed at the document's own directory, so a reference that climbs out of
|
|
1576
|
+
// it needs the tree's root named. Without this the transform silently ignored the
|
|
1577
|
+
// flag — a document whose embeds `check --root .` validated still rendered with
|
|
1578
|
+
// every one of them unresolved, which reads as "transclusion does not work".
|
|
1579
|
+
const root = flag(argv, "--root");
|
|
1580
|
+
if (argv.includes("--root") && root === undefined)
|
|
1581
|
+
fail("--root needs a directory", 2);
|
|
1582
|
+
const [file] = positionals(argv, ["-o", "--out", "--from", "--to", "--root"]);
|
|
1094
1583
|
if (!file)
|
|
1095
1584
|
fail("no input file (use '-' to read from stdin)", 2);
|
|
1096
1585
|
// A bare `--to`/`--from` (no following value) is a mistyped flag, not a
|
|
@@ -1149,10 +1638,10 @@ function runTransform(argv) {
|
|
|
1149
1638
|
else if (inFmt === "md") {
|
|
1150
1639
|
const conv = mdToGeml(src);
|
|
1151
1640
|
notes = conv.notes;
|
|
1152
|
-
doc = parse(conv.geml, { resolveDoc: resolverFor(file) });
|
|
1641
|
+
doc = parse(conv.geml, { resolveDoc: resolverFor(file, root), self: file === "-" ? undefined : basename(file) });
|
|
1153
1642
|
}
|
|
1154
1643
|
else {
|
|
1155
|
-
doc = parse(src, { resolveDoc: resolverFor(file) });
|
|
1644
|
+
doc = parse(src, { resolveDoc: resolverFor(file, root), self: file === "-" ? undefined : basename(file) });
|
|
1156
1645
|
}
|
|
1157
1646
|
let output;
|
|
1158
1647
|
switch (outFmt) {
|
|
@@ -1166,8 +1655,8 @@ function runTransform(argv) {
|
|
|
1166
1655
|
output = renderHtml(doc, {
|
|
1167
1656
|
source: file === "-" ? "stdin" : basename(file),
|
|
1168
1657
|
// geml-code-graph embeds load + parse sibling codemap docs on demand.
|
|
1169
|
-
loadDoc: resolverFor(file),
|
|
1170
|
-
parseDoc: (s) => parse(s),
|
|
1658
|
+
loadDoc: resolverFor(file, root),
|
|
1659
|
+
parseDoc: (s) => parse(s, { resolveDoc: resolverFor(file, root) }),
|
|
1171
1660
|
});
|
|
1172
1661
|
break;
|
|
1173
1662
|
case "md": {
|
|
@@ -1291,7 +1780,7 @@ function resolveSelector(source, file, raw) {
|
|
|
1291
1780
|
return bare;
|
|
1292
1781
|
const level = m[1].length;
|
|
1293
1782
|
const want = m[2];
|
|
1294
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
1783
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1295
1784
|
const heads = doc.ids.flatMap((id) => {
|
|
1296
1785
|
const site = findBlockSite(doc.children, id);
|
|
1297
1786
|
const b = site?.siblings[site.index];
|
|
@@ -1330,9 +1819,10 @@ function resolveSelector(source, file, raw) {
|
|
|
1330
1819
|
// a heading, its level and text); `--json` is a machine-readable array so an
|
|
1331
1820
|
// agent can pick its next `get #id` target. Ids are listed in document order
|
|
1332
1821
|
// (the registration order parse() records), covering the same set `get #id`
|
|
1333
|
-
// resolves against: typed blocks
|
|
1822
|
+
// resolves against: typed blocks and headings. A `[^id]` reference names one
|
|
1823
|
+
// of those (§5.2); the `[^id]: text` definition line was withdrawn.
|
|
1334
1824
|
function listIds(source, file, json) {
|
|
1335
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
1825
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1336
1826
|
const rows = doc.ids.map((id) => {
|
|
1337
1827
|
const site = findBlockSite(doc.children, id);
|
|
1338
1828
|
const b = site?.siblings[site.index];
|
|
@@ -1340,8 +1830,10 @@ function listIds(source, file, json) {
|
|
|
1340
1830
|
return { id, kind: "heading", level: b.level, text: b.text };
|
|
1341
1831
|
if (b?.kind === "block") {
|
|
1342
1832
|
const row = { id, kind: b.type };
|
|
1833
|
+
// `.footnote` is authored, not synthesized (the `[^id]: text` definition
|
|
1834
|
+
// line was withdrawn) — but it still marks a block meant as a footnote.
|
|
1343
1835
|
if (b.classes.includes("footnote"))
|
|
1344
|
-
row.footnote = true;
|
|
1836
|
+
row.footnote = true;
|
|
1345
1837
|
return row;
|
|
1346
1838
|
}
|
|
1347
1839
|
return { id, kind: b?.kind ?? "unknown" };
|
|
@@ -1391,7 +1883,7 @@ function getByType(source, file, type, json, headOnly) {
|
|
|
1391
1883
|
// The ONLY block of its type: locating it in the model needs no index, so
|
|
1392
1884
|
// --json can still answer with the parsed node (meta's key/values, a
|
|
1393
1885
|
// table's model) rather than a mere location.
|
|
1394
|
-
const node = onlyBlockOfType(parse(source, { resolveDoc: resolverFor(file) }).children, type);
|
|
1886
|
+
const node = onlyBlockOfType(parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) }).children, type);
|
|
1395
1887
|
if (node) {
|
|
1396
1888
|
console.log(JSON.stringify(node, null, 2));
|
|
1397
1889
|
return;
|
|
@@ -1457,7 +1949,7 @@ function runGet(args) {
|
|
|
1457
1949
|
if (json) {
|
|
1458
1950
|
// The model node(s) — same shapes `geml <file>` emits. Parsing is needed
|
|
1459
1951
|
// to resolve the tree (and nested-block ids), but only the target prints.
|
|
1460
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
1952
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1461
1953
|
const site = findBlockSite(doc.children, id);
|
|
1462
1954
|
if (!site)
|
|
1463
1955
|
fail(`no block with id \`${id}\``, 1);
|
|
@@ -1532,6 +2024,11 @@ function runSet(args) {
|
|
|
1532
2024
|
return;
|
|
1533
2025
|
}
|
|
1534
2026
|
// default / --head: content is a whole block (default) or a bare head line.
|
|
2027
|
+
// Does the target exist? Asked FIRST: the shape checks below name the id in
|
|
2028
|
+
// their advice ("use --body to set the body of #far"), which reads as though the
|
|
2029
|
+
// id were there. Whether the content is prose is the second question.
|
|
2030
|
+
if (!blockSpans(source).has(id))
|
|
2031
|
+
fail(`no block with id \`${id}\``, 1);
|
|
1535
2032
|
let content;
|
|
1536
2033
|
if (rawChannel) {
|
|
1537
2034
|
content = readInput("-");
|
|
@@ -1655,7 +2152,7 @@ function runAdd(args) {
|
|
|
1655
2152
|
// or duplicate id surfaces as an error diagnostic) and no pre-existing id may
|
|
1656
2153
|
// vanish. Returns the updated text; on any violation fail()s and writes nothing.
|
|
1657
2154
|
function insertFragment(source, lines, at, fragment, file) {
|
|
1658
|
-
const beforeIds = parse(source, { resolveDoc: resolverFor(file) }).ids;
|
|
2155
|
+
const beforeIds = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) }).ids;
|
|
1659
2156
|
const before = lines.slice(0, at);
|
|
1660
2157
|
const after = lines.slice(at);
|
|
1661
2158
|
const nl = newlineOf(source); // the fragment AND every separator we add
|
|
@@ -1672,7 +2169,7 @@ function insertFragment(source, lines, at, fragment, file) {
|
|
|
1672
2169
|
const sepBefore = before.length && !blank(before[before.length - 1]) ? nl : "";
|
|
1673
2170
|
const sepAfter = after.length && !blank(after[0]) ? nl : "";
|
|
1674
2171
|
const updated = before.join("") + sepBefore + frag + sepAfter + after.join("");
|
|
1675
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2172
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1676
2173
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1677
2174
|
if (errs.length) {
|
|
1678
2175
|
const first = errs[0];
|
|
@@ -1722,7 +2219,7 @@ function runDelete(args) {
|
|
|
1722
2219
|
const updated = splitLines(source).filter((_, i) => !toDelete.has(i)).join("");
|
|
1723
2220
|
// Lenient guard: surface any resulting error diagnostic (a reference now
|
|
1724
2221
|
// dangling) as a WARNING, but write regardless.
|
|
1725
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2222
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1726
2223
|
for (const d of reparsed.diagnostics.filter((x) => x.severity === "error")) {
|
|
1727
2224
|
console.error(`warning: ${d.message} (line ${d.line}) — left dangling by delete; run 'geml check' to see it as an error`);
|
|
1728
2225
|
}
|
|
@@ -1741,7 +2238,7 @@ function runRename(args) {
|
|
|
1741
2238
|
if (oldId === newId)
|
|
1742
2239
|
fail("#old and #new are the same id — nothing to rename", 2);
|
|
1743
2240
|
const source = readInput(file);
|
|
1744
|
-
const before = parse(source, { resolveDoc: resolverFor(file) });
|
|
2241
|
+
const before = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1745
2242
|
if (!before.ids.includes(oldId))
|
|
1746
2243
|
fail(`no block with id \`${oldId}\``, 1);
|
|
1747
2244
|
if (before.ids.includes(newId))
|
|
@@ -1761,7 +2258,7 @@ function runRename(args) {
|
|
|
1761
2258
|
}
|
|
1762
2259
|
}
|
|
1763
2260
|
const updated = rewriteId(source, oldId, newId, file);
|
|
1764
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2261
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1765
2262
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1766
2263
|
if (errs.length) {
|
|
1767
2264
|
const e = errs[0];
|
|
@@ -1792,7 +2289,7 @@ function runRename(args) {
|
|
|
1792
2289
|
// text, not a reference. (Known residual: id-less raw bodies and inline
|
|
1793
2290
|
// code/math spans in flow content — see design §8.)
|
|
1794
2291
|
function rewriteId(source, oldId, newId, file) {
|
|
1795
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
2292
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1796
2293
|
const spans = blockSpans(source);
|
|
1797
2294
|
const protectedLines = new Set();
|
|
1798
2295
|
for (const b of doc.children) {
|
|
@@ -1900,7 +2397,7 @@ function spliceBlock(source, id, replacement, file, headOnly = false, guardCount
|
|
|
1900
2397
|
const found = blockSpans(source).get(id);
|
|
1901
2398
|
if (!found)
|
|
1902
2399
|
fail(`no block with id \`${id}\``, 1);
|
|
1903
|
-
const beforeDoc = parse(source, { resolveDoc: resolverFor(file) });
|
|
2400
|
+
const beforeDoc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1904
2401
|
const beforeIds = beforeDoc.ids;
|
|
1905
2402
|
// Keep the bytes before and after the target span exactly; give the new block
|
|
1906
2403
|
// a single trailing newline so the following block still starts on its own
|
|
@@ -1924,7 +2421,7 @@ function spliceBlock(source, id, replacement, file, headOnly = false, guardCount
|
|
|
1924
2421
|
// surface as error diagnostics (registerId flags dups); one check covers both.
|
|
1925
2422
|
// Then require the target id to survive, and — because a malformed replacement
|
|
1926
2423
|
// can swallow a neighbour — that every other pre-existing id survives too.
|
|
1927
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2424
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1928
2425
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1929
2426
|
if (errs.length) {
|
|
1930
2427
|
const first = errs[0];
|
|
@@ -2107,9 +2604,9 @@ function runRevert(args) {
|
|
|
2107
2604
|
return;
|
|
2108
2605
|
}
|
|
2109
2606
|
const span = curFull;
|
|
2110
|
-
const beforeIds = parse(source, { resolveDoc: resolverFor(file) }).ids;
|
|
2607
|
+
const beforeIds = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) }).ids;
|
|
2111
2608
|
const updated = splitLines(source).filter((_, i) => i < span.start || i >= span.end).join("");
|
|
2112
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2609
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
2113
2610
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
2114
2611
|
if (errs.length) {
|
|
2115
2612
|
const first = errs[0];
|