@geml/geml 1.4.6 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codemap/mcp-server.mjs +1 -1
- package/codemap/render-all.mjs +16 -3
- package/codemap/serve.mjs +8 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +10 -0
- package/dist/geml.d.ts +4 -0
- package/dist/geml.js +515 -46
- package/dist/inline.d.ts +20 -0
- package/dist/inline.js +29 -2
- package/dist/mcp.js +13 -5
- package/dist/render.d.ts +19 -0
- package/dist/render.js +349 -24
- package/dist/serialize.js +9 -1
- package/dist/table.js +5 -3
- package/dist/to-md.js +13 -2
- package/package.json +1 -1
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",
|
|
@@ -319,12 +319,50 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
319
319
|
}
|
|
320
320
|
if (attrs.attrs["hidden"] === true)
|
|
321
321
|
block.hidden = true; // §4: not rendered, still in model
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
322
|
+
// Block transclusion: `src=` names the content this block stands for, and
|
|
323
|
+
// is registered as an ordinary reference so the existing §8 resolver
|
|
324
|
+
// validates the document and the id. Without that, an embed would be the
|
|
325
|
+
// one reference shape whose rot is silent.
|
|
326
|
+
if (type === "embed") {
|
|
327
|
+
const src = typeof attrs.attrs["src"] === "string" ? attrs.attrs["src"].trim() : "";
|
|
328
|
+
if (src === "") {
|
|
329
|
+
diags.push({ severity: "error", code: "embed-missing-src", message: "embed: missing `src=`", line: openLineNo });
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const hash = src.indexOf("#");
|
|
333
|
+
const docPath = hash < 0 ? src : src.slice(0, hash);
|
|
334
|
+
const anchor = hash < 0 ? undefined : src.slice(hash + 1);
|
|
335
|
+
// §9.5: a destination naming a scheme outside the allowlist MUST NOT be
|
|
336
|
+
// emitted as a navigable or loadable target, and the check belongs HERE —
|
|
337
|
+
// when the model is built — so no consumer of the model can reintroduce
|
|
338
|
+
// it. The attribute is blanked as well as reported, the same treatment a
|
|
339
|
+
// media `src` already gets: a diagnostic alone would still leave the
|
|
340
|
+
// string in `attrs` for a renderer to put in an href.
|
|
341
|
+
if (!isSafeUrl(src)) {
|
|
342
|
+
diags.push({ severity: "error", code: "unsafe-embed-scheme", message: `embed: \`src=${src}\` names a disallowed URL scheme`, line: openLineNo });
|
|
343
|
+
block.attrs = { ...block.attrs, src: "" };
|
|
344
|
+
}
|
|
345
|
+
else if (docPath !== "" && !/\.geml$/i.test(docPath)) {
|
|
346
|
+
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 });
|
|
347
|
+
}
|
|
348
|
+
else if (docPath === "") {
|
|
349
|
+
// Recorded with an empty doc so the self-cycle pass can see it.
|
|
350
|
+
if (anchor !== undefined)
|
|
351
|
+
(ctx.embeds ??= []).push({ doc: "", anchor, line: openLineNo });
|
|
352
|
+
// `src=#id`: a block of THIS document. Validated against local ids.
|
|
353
|
+
if (anchor !== undefined)
|
|
354
|
+
ctx.refs.push({ kind: "internal", anchor, line: openLineNo });
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
ctx.refs.push({ kind: "cross", doc: docPath, anchor, line: openLineNo });
|
|
358
|
+
// Kept apart from refs: a transclusion can pull in a document that
|
|
359
|
+
// transcludes further, so cycle detection has to walk the graph.
|
|
360
|
+
(ctx.embeds ??= []).push(anchor === undefined ? { doc: docPath, line: openLineNo } : { doc: docPath, anchor, line: openLineNo });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (body.some((l) => l.trim() !== "")) {
|
|
364
|
+
diags.push({ severity: "warning", code: "ignored-embed-body", message: "embed body is ignored; the target lives in `src=`", line: openLineNo });
|
|
365
|
+
}
|
|
328
366
|
}
|
|
329
367
|
if (mode === "flow") {
|
|
330
368
|
if (depth >= MAX_NESTING) {
|
|
@@ -344,8 +382,18 @@ function scanBlocks(lines, base, ctx, depth = 0) {
|
|
|
344
382
|
else {
|
|
345
383
|
block.raw = body;
|
|
346
384
|
if (type === "table") {
|
|
385
|
+
// `src=` and `data=` are one attribute in two spellings: where this
|
|
386
|
+
// table's data comes from. Recorded for the post-scan pass.
|
|
387
|
+
const srcAttr = typeof attrs.attrs["src"] === "string" ? attrs.attrs["src"].trim() : undefined;
|
|
388
|
+
const dataAttr = typeof attrs.attrs["data"] === "string" ? attrs.attrs["data"].trim() : undefined;
|
|
389
|
+
if (srcAttr !== undefined && dataAttr !== undefined) {
|
|
390
|
+
diags.push({ severity: "error", code: "source-attr-conflict", message: "table has both `src=` and `data=`; they mean the same thing — use one", line: openLineNo });
|
|
391
|
+
}
|
|
392
|
+
const target = srcAttr ?? dataAttr;
|
|
347
393
|
// §6: parse the raw body (visual or csv/tsv) into one table model.
|
|
348
|
-
const { model, diagnostics } = parseTable(body, attrs.attrs, openLineNo, ctx);
|
|
394
|
+
const { model, diagnostics } = parseTable(body, target === undefined ? attrs.attrs : { ...attrs.attrs, src: target }, openLineNo, ctx);
|
|
395
|
+
if (target !== undefined)
|
|
396
|
+
(ctx.tableSources ??= []).push({ block, line: openLineNo, target });
|
|
349
397
|
block.table = model;
|
|
350
398
|
for (const d of diagnostics)
|
|
351
399
|
diags.push({ ...d, line: openLineNo });
|
|
@@ -448,6 +496,343 @@ function parseData(lines) {
|
|
|
448
496
|
// ---------------------------------------------------------------------------
|
|
449
497
|
// Collect the block ids of a (cross-document) source, without validation, for
|
|
450
498
|
// resolving `other.geml#id` references.
|
|
499
|
+
// S5/S6: a transclusion may pull in a document that transcludes further, so a
|
|
500
|
+
// cycle is only visible by walking the graph. Reported at check time — before
|
|
501
|
+
// any rendering — so a build fails on the cycle rather than on a placeholder in
|
|
502
|
+
// the output. Paths compose the way the renderer composes them: a target inside
|
|
503
|
+
// a borrowed document is relative to THAT document.
|
|
504
|
+
// `data=rows.csv` on a chart, desugared: the anonymous table it stands for. Built
|
|
505
|
+
// by handing the loaded lines to the SAME body parser a `=== table {src=…}` uses,
|
|
506
|
+
// with the chart's own `format=`/`header=` carried over, so nothing about how the
|
|
507
|
+
// data is read is specific to charts. Returns null when the source could not be
|
|
508
|
+
// resolved — the diagnostic is already pushed, in the table rules' own words.
|
|
509
|
+
function chartSourceTable(ctx, opts, block, target, line) {
|
|
510
|
+
const scheme = schemeOf(target);
|
|
511
|
+
if (scheme === "http" || scheme === "https") {
|
|
512
|
+
// §9.4: fetched at render time, so there is nothing to chart at build time —
|
|
513
|
+
// the same state a remote-sourced table leaves behind.
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
if (!opts.resolveDoc) {
|
|
517
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `geml-chart: data source \`${target}\` not checked (no document resolver)`, line });
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const text = opts.resolveDoc(target);
|
|
521
|
+
if (text === null) {
|
|
522
|
+
ctx.diags.push({ severity: "error", code: "unresolvable-table-source", message: `geml-chart: cannot resolve data source \`${target}\``, line });
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const attrs = {
|
|
526
|
+
format: typeof block.attrs["format-data"] === "string" ? block.attrs["format-data"] : inferDataFormat(target),
|
|
527
|
+
header: block.attrs["header"] === undefined ? true : block.attrs["header"],
|
|
528
|
+
};
|
|
529
|
+
const { model, diagnostics } = parseTable(normalizeSource(text).split("\n"), attrs, line, ctx);
|
|
530
|
+
for (const d of diagnostics)
|
|
531
|
+
ctx.diags.push({ ...d, line });
|
|
532
|
+
model.src = target;
|
|
533
|
+
return model;
|
|
534
|
+
}
|
|
535
|
+
const inferDataFormat = (target) => (/\.tsv$/i.test(target) ? "tsv" : "csv");
|
|
536
|
+
// The renderer's own cap (render.ts EMBED_DEPTH_CAP). Kept in step here so the
|
|
537
|
+
// check and the render agree on which documents are reachable at all.
|
|
538
|
+
const EMBED_DEPTH_LIMIT = 8;
|
|
539
|
+
function detectTransclusionCycles(ctx, opts) {
|
|
540
|
+
if (!opts.resolveDoc || ctx.embeds === undefined || ctx.embeds.length === 0)
|
|
541
|
+
return;
|
|
542
|
+
const resolve = opts.resolveDoc;
|
|
543
|
+
const embedsOf = new Map(); // memoized per path
|
|
544
|
+
const reported = new Set();
|
|
545
|
+
// A three-colour DFS over DOCUMENTS, not over paths. Enumerating every path
|
|
546
|
+
// through the graph is exponential in its fan-out: a chain of 21 tiny files,
|
|
547
|
+
// each embedding the next three times, took over two minutes — and `check` is
|
|
548
|
+
// the CI gate and the validator every MCP write runs twice. Grey means "on the
|
|
549
|
+
// current stack" and is the cycle; black means already fully explored, so each
|
|
550
|
+
// edge is walked once and the whole traversal is O(V+E).
|
|
551
|
+
const colour = new Map();
|
|
552
|
+
const walk = (path, base, stack, line) => {
|
|
553
|
+
const rel = relJoinPath(base, path);
|
|
554
|
+
if (colour.get(rel) === "grey") {
|
|
555
|
+
const chain = [...stack, rel].join(" → ");
|
|
556
|
+
if (reported.has(chain))
|
|
557
|
+
return;
|
|
558
|
+
reported.add(chain);
|
|
559
|
+
ctx.diags.push({ severity: "error", code: "transclusion-cycle", message: `transclusion cycle: ${chain}`, line });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (colour.get(rel) === "black")
|
|
563
|
+
return;
|
|
564
|
+
// Agree with the renderer about what is even reachable, instead of exploring
|
|
565
|
+
// eight times deeper than it will ever expand.
|
|
566
|
+
if (stack.length >= EMBED_DEPTH_LIMIT)
|
|
567
|
+
return;
|
|
568
|
+
colour.set(rel, "grey");
|
|
569
|
+
let inner = embedsOf.get(rel);
|
|
570
|
+
if (inner === undefined) {
|
|
571
|
+
const src = resolve(rel);
|
|
572
|
+
inner = src === null ? [] : gatherEmbeds(src); // an unresolvable doc is already an error
|
|
573
|
+
embedsOf.set(rel, inner);
|
|
574
|
+
}
|
|
575
|
+
for (const e of inner)
|
|
576
|
+
walk(e.doc, relDirPath(rel), [...stack, rel], line);
|
|
577
|
+
colour.set(rel, "black");
|
|
578
|
+
};
|
|
579
|
+
// The root is named so a chain can be seen returning to it. Falling back to ""
|
|
580
|
+
// only loses the A→…→A case, which is what happened before `self` existed.
|
|
581
|
+
const root = opts.self ?? "";
|
|
582
|
+
for (const e of ctx.embeds)
|
|
583
|
+
walk(e.doc, relDirPath(root), [root], e.line);
|
|
584
|
+
}
|
|
585
|
+
// The smallest cycle of all, and the one the cross-document walk above cannot
|
|
586
|
+
// see: `=== embed {src=#sec}` written INSIDE the section `#sec` selects the slice
|
|
587
|
+
// that contains it. Decided on spans, so the boundary is exactly the one `geml
|
|
588
|
+
// get` uses — a heading id spans its whole section, so an embed anywhere in that
|
|
589
|
+
// section is inside its own target.
|
|
590
|
+
function detectSelfEmbedCycles(source, ctx) {
|
|
591
|
+
const selfEmbeds = (ctx.embeds ?? []).filter((e) => e.doc === "" && e.anchor !== undefined);
|
|
592
|
+
if (selfEmbeds.length === 0)
|
|
593
|
+
return;
|
|
594
|
+
const spans = blockSpans(source);
|
|
595
|
+
for (const e of selfEmbeds) {
|
|
596
|
+
const span = spans.get(e.anchor);
|
|
597
|
+
if (span === undefined)
|
|
598
|
+
continue; // a missing id is already an unresolved reference
|
|
599
|
+
const line = e.line - 1; // spans are 0-based line indices
|
|
600
|
+
if (line >= span.start && line <= span.end) {
|
|
601
|
+
ctx.diags.push({
|
|
602
|
+
severity: "error",
|
|
603
|
+
code: "transclusion-cycle",
|
|
604
|
+
message: `transclusion cycle: \`#${e.anchor}\` selects the content this embed is part of`,
|
|
605
|
+
line: e.line,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// A phrase that projects itself. The same shape as detectSelfEmbedCycles, and
|
|
611
|
+
// deliberately the same machinery rather than a second parallel one: decided on
|
|
612
|
+
// spans, so a projection written anywhere inside its own target is caught.
|
|
613
|
+
function detectSelfProjectionCycles(source, ctx) {
|
|
614
|
+
const local = (ctx.projections ?? []).filter((p) => p.doc === undefined);
|
|
615
|
+
if (local.length === 0)
|
|
616
|
+
return;
|
|
617
|
+
const spans = blockSpans(source);
|
|
618
|
+
for (const p of local) {
|
|
619
|
+
const span = spans.get(p.anchor);
|
|
620
|
+
if (span === undefined)
|
|
621
|
+
continue;
|
|
622
|
+
const line = p.line - 1;
|
|
623
|
+
if (line >= span.start && line <= span.end) {
|
|
624
|
+
ctx.diags.push({
|
|
625
|
+
severity: "error",
|
|
626
|
+
code: "transclusion-cycle",
|
|
627
|
+
message: `transclusion cycle: \`![[#${p.anchor}]]\` projects the content it is part of`,
|
|
628
|
+
line: p.line,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Inline content that a projection may stand for: a `text` block whose body is a
|
|
634
|
+
// single paragraph. Returned so the renderer and this validator agree on one
|
|
635
|
+
// definition. Anything else — a heading (and so a whole section), a table, a
|
|
636
|
+
// diagram, a multi-paragraph body — is block content, and no amount of syntax
|
|
637
|
+
// makes it fit inside a sentence.
|
|
638
|
+
export function projectableInlines(blocks, id) {
|
|
639
|
+
const found = (function find(bs) {
|
|
640
|
+
for (const b of bs) {
|
|
641
|
+
if ((b.kind === "block" || b.kind === "heading") && b.id === id)
|
|
642
|
+
return b;
|
|
643
|
+
if (b.kind === "block" && b.children) {
|
|
644
|
+
const inner = find(b.children);
|
|
645
|
+
if (inner)
|
|
646
|
+
return inner;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return undefined;
|
|
650
|
+
})(blocks);
|
|
651
|
+
if (found === undefined)
|
|
652
|
+
return null;
|
|
653
|
+
if (found.kind !== "block" || found.type !== "text")
|
|
654
|
+
return "not-inline";
|
|
655
|
+
const kids = (found.children ?? []).filter((c) => !(c.kind === "paragraph" && c.text.trim() === ""));
|
|
656
|
+
if (kids.length !== 1 || kids[0].kind !== "paragraph")
|
|
657
|
+
return "not-inline";
|
|
658
|
+
return { inlines: kids[0].inlines };
|
|
659
|
+
}
|
|
660
|
+
// A projection may only stand for inline content, and the target decides — the
|
|
661
|
+
// same shape of rule as `table-source-not-a-table`, not a rule about where the
|
|
662
|
+
// reference was written.
|
|
663
|
+
function validateProjections(children, ctx, opts) {
|
|
664
|
+
for (const p of ctx.projections ?? []) {
|
|
665
|
+
let blocks = null;
|
|
666
|
+
if (p.doc === undefined)
|
|
667
|
+
blocks = children;
|
|
668
|
+
else if (opts.resolveDoc) {
|
|
669
|
+
const src = opts.resolveDoc(p.doc);
|
|
670
|
+
if (src === null)
|
|
671
|
+
continue; // already an unresolvable-document error
|
|
672
|
+
blocks = parse(src).children;
|
|
673
|
+
}
|
|
674
|
+
if (blocks === null)
|
|
675
|
+
continue; // unchecked without a resolver, like any cross-doc ref
|
|
676
|
+
const got = projectableInlines(blocks, p.anchor);
|
|
677
|
+
if (got === null)
|
|
678
|
+
continue; // already an unresolved-reference error
|
|
679
|
+
if (got === "not-inline") {
|
|
680
|
+
const target = p.doc === undefined ? `#${p.anchor}` : `${p.doc}#${p.anchor}`;
|
|
681
|
+
ctx.diags.push({
|
|
682
|
+
severity: "error",
|
|
683
|
+
code: "inline-transclusion-not-inline",
|
|
684
|
+
message: `\`![[${target}]]\` projects inline content, but the target is not a single-paragraph \`text\` block; for block content use \`=== embed {src=${target}}\``,
|
|
685
|
+
line: p.line,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// Same pure-string path composition the renderer uses (relJoin/relDir there).
|
|
691
|
+
function relJoinPath(base, target) {
|
|
692
|
+
if (base === "" || target === "" || target.startsWith("/") || /^[a-z][a-z0-9+.-]*:/i.test(target))
|
|
693
|
+
return target;
|
|
694
|
+
const out = [];
|
|
695
|
+
for (const s of (base + "/" + target).split("/")) {
|
|
696
|
+
if (s === "" || s === ".")
|
|
697
|
+
continue;
|
|
698
|
+
if (s === ".." && out.length > 0 && out[out.length - 1] !== "..")
|
|
699
|
+
out.pop();
|
|
700
|
+
else
|
|
701
|
+
out.push(s);
|
|
702
|
+
}
|
|
703
|
+
return out.join("/");
|
|
704
|
+
}
|
|
705
|
+
function relDirPath(p) {
|
|
706
|
+
const i = p.lastIndexOf("/");
|
|
707
|
+
return i < 0 ? "" : p.slice(0, i);
|
|
708
|
+
}
|
|
709
|
+
function gatherEmbeds(source) {
|
|
710
|
+
const ctx = { diags: [], ids: new Map(), refs: [], meta: new Map(), embeds: [] };
|
|
711
|
+
scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
|
|
712
|
+
return (ctx.embeds ?? []).map((e) => (e.anchor === undefined ? { doc: e.doc } : { doc: e.doc, anchor: e.anchor }));
|
|
713
|
+
}
|
|
714
|
+
// One rule for "where this data comes from", shared by a table's `src=`/`data=`
|
|
715
|
+
// and a chart's `data=`. Three target forms: a data file, `#id` naming a table
|
|
716
|
+
// block in this document, or `doc.geml#id` naming one in another document. An
|
|
717
|
+
// unresolvable target is an error — a table whose source silently produced no
|
|
718
|
+
// rows used to render as an empty table with no diagnostic at all.
|
|
719
|
+
function tableFromDocument(source, id) {
|
|
720
|
+
const ctx = { diags: [], ids: new Map(), refs: [], meta: new Map() };
|
|
721
|
+
const blocks = scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
|
|
722
|
+
const found = ctx.tables?.get(id);
|
|
723
|
+
if (found !== undefined)
|
|
724
|
+
return found;
|
|
725
|
+
const anyBlock = (function find(bs) {
|
|
726
|
+
for (const b of bs) {
|
|
727
|
+
if ((b.kind === "block" || b.kind === "heading") && b.id === id)
|
|
728
|
+
return b;
|
|
729
|
+
if (b.kind === "block" && b.children) {
|
|
730
|
+
const inner = find(b.children);
|
|
731
|
+
if (inner)
|
|
732
|
+
return inner;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return undefined;
|
|
736
|
+
})(blocks);
|
|
737
|
+
return anyBlock === undefined ? null : "not-a-table";
|
|
738
|
+
}
|
|
739
|
+
function resolveTableSources(ctx, opts) {
|
|
740
|
+
const pending = ctx.tableSources ?? [];
|
|
741
|
+
if (pending.length === 0)
|
|
742
|
+
return;
|
|
743
|
+
const err = (line, code, message) => void ctx.diags.push({ severity: "error", code, message, line });
|
|
744
|
+
// Data files first: a `#id` target may point at a table whose OWN rows come
|
|
745
|
+
// from a file, and this way that table is already populated when it is read.
|
|
746
|
+
for (const { block, line, target } of pending) {
|
|
747
|
+
if (target.includes("#"))
|
|
748
|
+
continue;
|
|
749
|
+
// §9.4: a remote source is fetched by the RENDERER, not the parser. Leaving
|
|
750
|
+
// `model.src` set with no columns is the state resolveCharts already handles,
|
|
751
|
+
// so a chart over it defers too. Passing it to resolveDoc treated a URL as a
|
|
752
|
+
// filesystem path and failed a spec-conformant document.
|
|
753
|
+
const scheme = schemeOf(target);
|
|
754
|
+
if (scheme === "http" || scheme === "https")
|
|
755
|
+
continue;
|
|
756
|
+
if (scheme !== null) {
|
|
757
|
+
err(line, "unresolvable-table-source", `table source \`${target}\` names a disallowed URL scheme`);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
// A data source is data. Without this the loader read any file under the base
|
|
761
|
+
// — a `.env`, a private key — split it into rows, and put it in the model and
|
|
762
|
+
// the page, with no diagnostic. `embed` already applies the same shape of rule.
|
|
763
|
+
if (!/\.(csv|tsv)$/i.test(target)) {
|
|
764
|
+
err(line, "unresolvable-table-source", `table source \`${target}\` is not a \`.csv\`/\`.tsv\` data file`);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (!opts.resolveDoc) {
|
|
768
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `table source \`${target}\` not checked (no document resolver)`, line });
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const text = opts.resolveDoc(target);
|
|
772
|
+
if (text === null) {
|
|
773
|
+
err(line, "unresolvable-table-source", `cannot resolve table source \`${target}\``);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
// Reuse the body parser: with `src`/`data` dropped, the file's lines are just
|
|
777
|
+
// this table's body, so format/header/compute/summary all behave identically.
|
|
778
|
+
const attrs = { ...block.attrs };
|
|
779
|
+
delete attrs["src"];
|
|
780
|
+
delete attrs["data"];
|
|
781
|
+
const { model, diagnostics } = parseTable(normalizeSource(text).split("\n"), attrs, line, ctx);
|
|
782
|
+
model.src = target;
|
|
783
|
+
block.table = model;
|
|
784
|
+
for (const d of diagnostics)
|
|
785
|
+
ctx.diags.push({ ...d, line });
|
|
786
|
+
if (block.id !== undefined)
|
|
787
|
+
(ctx.tables ??= new Map()).set(block.id, model);
|
|
788
|
+
}
|
|
789
|
+
for (const { block, line, target } of pending) {
|
|
790
|
+
const hash = target.indexOf("#");
|
|
791
|
+
if (hash < 0)
|
|
792
|
+
continue;
|
|
793
|
+
const docPath = target.slice(0, hash);
|
|
794
|
+
const id = target.slice(hash + 1);
|
|
795
|
+
let model;
|
|
796
|
+
if (docPath === "") {
|
|
797
|
+
const local = ctx.tables?.get(id);
|
|
798
|
+
if (local === undefined) {
|
|
799
|
+
if (ctx.ids.has(id))
|
|
800
|
+
err(line, "table-source-not-a-table", `table source \`#${id}\` is not a table`);
|
|
801
|
+
else
|
|
802
|
+
err(line, "unresolved-reference", `unresolved reference \`#${id}\``);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
model = local;
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
if (!opts.resolveDoc) {
|
|
809
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `table source \`${target}\` not checked (no document resolver)`, line });
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const text = opts.resolveDoc(docPath);
|
|
813
|
+
if (text === null) {
|
|
814
|
+
err(line, "unresolvable-document", `cannot resolve document \`${docPath}\``);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const remote = tableFromDocument(text, id);
|
|
818
|
+
if (remote === null) {
|
|
819
|
+
err(line, "unresolved-cross-document-reference", `unresolved reference \`${target}\``);
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (remote === "not-a-table") {
|
|
823
|
+
err(line, "table-source-not-a-table", `table source \`${target}\` is not a table`);
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
model = remote;
|
|
827
|
+
}
|
|
828
|
+
// Borrowed, not copied in the source: the model is shared, so the borrowing
|
|
829
|
+
// table means exactly what the original means. Its own caption still wins.
|
|
830
|
+
const caption = block.table?.caption;
|
|
831
|
+
block.table = caption === undefined ? model : { ...model, caption };
|
|
832
|
+
if (block.id !== undefined)
|
|
833
|
+
(ctx.tables ??= new Map()).set(block.id, block.table);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
451
836
|
function gatherIds(source) {
|
|
452
837
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: new Map() };
|
|
453
838
|
scanBlocks(normalizeSource(source).split("\n"), 0, ctx);
|
|
@@ -512,26 +897,77 @@ function validateRefs(ctx, opts) {
|
|
|
512
897
|
}
|
|
513
898
|
// §7: resolve every geml-chart against its referenced table. Runs after the
|
|
514
899
|
// scan so that `data=#id` may point at a table defined anywhere in the doc.
|
|
515
|
-
function resolveCharts(ctx) {
|
|
900
|
+
function resolveCharts(ctx, opts) {
|
|
516
901
|
for (const { block, line } of ctx.charts ?? []) {
|
|
517
|
-
const ref = typeof block.attrs["data"] === "string" ? block.attrs["data"] : "";
|
|
518
|
-
|
|
519
|
-
if (id === "") {
|
|
902
|
+
const ref = typeof block.attrs["data"] === "string" ? block.attrs["data"].trim() : "";
|
|
903
|
+
if (ref === "" || ref === "#") {
|
|
520
904
|
ctx.diags.push({ severity: "error", code: "chart-missing-data", message: "geml-chart: missing `data=#id`", line });
|
|
521
905
|
continue;
|
|
522
906
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
907
|
+
// `data=` resolves by the same rule as a table's source: `#id` (or a bare id)
|
|
908
|
+
// names a table in THIS document, `doc.geml#id` one in another. Splitting on
|
|
909
|
+
// the LAST `#` is what the old code got wrong — it stripped the leading one
|
|
910
|
+
// and reported `#other.geml#fy25`, a target that never existed.
|
|
911
|
+
const hash = ref.indexOf("#");
|
|
912
|
+
const docPath = hash <= 0 ? "" : ref.slice(0, hash);
|
|
913
|
+
const id = hash < 0 ? ref : ref.slice(hash + 1);
|
|
914
|
+
let table;
|
|
915
|
+
if (docPath === "") {
|
|
916
|
+
table = ctx.tables?.get(id);
|
|
917
|
+
if (!table) {
|
|
918
|
+
// A chart is a view of a table, and a data file is one of the three ways
|
|
919
|
+
// §6 lets a table name its content. So `data=rows.csv` desugars: it is an
|
|
920
|
+
// anonymous table with that source, feeding this chart. Nothing new is
|
|
921
|
+
// invented — the resolution, the `.csv`/`.tsv` gate, the §9.4 remote rule
|
|
922
|
+
// and `format=` all come from the table rules, which is what makes the one
|
|
923
|
+
// source rule hold for charts too instead of charts being its exception.
|
|
924
|
+
if (hash < 0 && /\.(csv|tsv)$/i.test(id)) {
|
|
925
|
+
const sugar = chartSourceTable(ctx, opts, block, id, line);
|
|
926
|
+
if (sugar === null)
|
|
927
|
+
continue; // already reported by the table rules
|
|
928
|
+
table = sugar;
|
|
929
|
+
}
|
|
930
|
+
else if (hash < 0 && /\.[a-z0-9]+$/i.test(id)) {
|
|
931
|
+
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 });
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
const known = ctx.ids.has(id);
|
|
936
|
+
const what = known ? `data target \`#${id}\` is not a table` : `unresolved reference \`#${id}\``;
|
|
937
|
+
const code = known ? "chart-data-not-a-table" : "unresolved-reference";
|
|
938
|
+
ctx.diags.push({ severity: "error", code, message: `geml-chart: ${what}`, line });
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
530
942
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
943
|
+
else {
|
|
944
|
+
if (!opts.resolveDoc) {
|
|
945
|
+
ctx.diags.push({ severity: "warning", code: "unchecked-cross-document-reference", message: `geml-chart: data target \`${ref}\` not checked (no document resolver)`, line });
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const text = opts.resolveDoc(docPath);
|
|
949
|
+
if (text === null) {
|
|
950
|
+
ctx.diags.push({ severity: "error", code: "unresolvable-document", message: `geml-chart: cannot resolve document \`${docPath}\``, line });
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const remote = tableFromDocument(text, id);
|
|
954
|
+
if (remote === null) {
|
|
955
|
+
ctx.diags.push({ severity: "error", code: "unresolved-cross-document-reference", message: `geml-chart: unresolved reference \`${ref}\``, line });
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (remote === "not-a-table") {
|
|
959
|
+
ctx.diags.push({ severity: "error", code: "chart-data-not-a-table", message: `geml-chart: data target \`${ref}\` is not a table`, line });
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
table = remote;
|
|
963
|
+
}
|
|
964
|
+
if (table.src !== undefined && table.columns.length === 0) {
|
|
965
|
+
// §6: the table names a source whose data did not arrive at build time — a
|
|
966
|
+
// remote URL, or any source with no document resolver supplied. The chart is
|
|
967
|
+
// therefore resolved at render time, and its column names are checked there.
|
|
968
|
+
// The test is whether the data is actually here, not what the source looks
|
|
969
|
+
// like: skipping every `src` table unconditionally is what left a chart
|
|
970
|
+
// unbuilt with no diagnostic while the page said to go and read one.
|
|
535
971
|
continue;
|
|
536
972
|
}
|
|
537
973
|
const { model, diagnostics } = buildChart(block.attrs, table);
|
|
@@ -545,8 +981,25 @@ export function parse(source, opts = {}) {
|
|
|
545
981
|
const lines = normalizeSource(source).split("\n");
|
|
546
982
|
const ctx = { diags: [], ids: new Map(), refs: [], meta: collectMeta(lines), resolveDoc: opts.resolveDoc };
|
|
547
983
|
const children = scanBlocks(lines, 0, ctx);
|
|
548
|
-
|
|
984
|
+
// Table sources first: a chart reads the build-time model of the table it
|
|
985
|
+
// charts, so that model has to be filled before charts are resolved.
|
|
986
|
+
resolveTableSources(ctx, opts);
|
|
987
|
+
resolveCharts(ctx, opts);
|
|
549
988
|
validateRefs(ctx, opts);
|
|
989
|
+
detectTransclusionCycles(ctx, opts);
|
|
990
|
+
detectSelfEmbedCycles(source, ctx);
|
|
991
|
+
validateProjections(children, ctx, opts);
|
|
992
|
+
detectSelfProjectionCycles(source, ctx);
|
|
993
|
+
for (const m of ctx.mediaDocTargets ?? []) {
|
|
994
|
+
ctx.diags.push({
|
|
995
|
+
severity: "error",
|
|
996
|
+
code: "media-target-is-document",
|
|
997
|
+
// `!` projects, so a GEML target here is a near-miss an author will reach for
|
|
998
|
+
// once that reading is established. Name both forms it could have meant.
|
|
999
|
+
message: `\`\` projects a GEML document, which is not media: for block content use \`=== embed {src=${m.src}}\`, for a phrase use \`![[${m.src}]]\``,
|
|
1000
|
+
line: m.line,
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
550
1003
|
return { kind: "document", children, ids: [...ctx.ids.keys()], diagnostics: ctx.diags };
|
|
551
1004
|
}
|
|
552
1005
|
// The id that a fence/heading line defines, matching how scanBlocks derives it
|
|
@@ -797,7 +1250,10 @@ export const PARSER_VERSION = (() => {
|
|
|
797
1250
|
const USAGE = `geml — GEML reference CLI
|
|
798
1251
|
|
|
799
1252
|
Usage:
|
|
800
|
-
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [-o out] transform a document (default: --to json)
|
|
1253
|
+
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [--root d] [-o out] transform a document (default: --to json)
|
|
1254
|
+
(--root widens cross-doc resolution to dir d, as on check — an
|
|
1255
|
+
=== embed whose target sits above the file's own directory
|
|
1256
|
+
needs it, or it renders unresolved)
|
|
801
1257
|
--to <output>: json | html | md | geml
|
|
802
1258
|
--to md -> Markdown (lossy)
|
|
803
1259
|
--to html -> self-contained HTML
|
|
@@ -1002,7 +1458,7 @@ function runCheck(args) {
|
|
|
1002
1458
|
if (!isDir)
|
|
1003
1459
|
fail(`--root ${root} is not a directory`);
|
|
1004
1460
|
}
|
|
1005
|
-
const doc = parse(readInput(file), { resolveDoc: resolverFor(file, root) });
|
|
1461
|
+
const doc = parse(readInput(file), { resolveDoc: resolverFor(file, root), self: file === "-" ? undefined : basename(file) });
|
|
1006
1462
|
if (json) {
|
|
1007
1463
|
console.log(JSON.stringify(doc.diagnostics, null, 2));
|
|
1008
1464
|
}
|
|
@@ -1090,7 +1546,15 @@ function runTransform(argv) {
|
|
|
1090
1546
|
const out = flag(argv, "-o") ?? flag(argv, "--out");
|
|
1091
1547
|
const fromRaw = flag(argv, "--from");
|
|
1092
1548
|
const toRaw = flag(argv, "--to");
|
|
1093
|
-
|
|
1549
|
+
// Same `--root` as `check`, and for the same reason: cross-document resolution is
|
|
1550
|
+
// fail-closed at the document's own directory, so a reference that climbs out of
|
|
1551
|
+
// it needs the tree's root named. Without this the transform silently ignored the
|
|
1552
|
+
// flag — a document whose embeds `check --root .` validated still rendered with
|
|
1553
|
+
// every one of them unresolved, which reads as "transclusion does not work".
|
|
1554
|
+
const root = flag(argv, "--root");
|
|
1555
|
+
if (argv.includes("--root") && root === undefined)
|
|
1556
|
+
fail("--root needs a directory", 2);
|
|
1557
|
+
const [file] = positionals(argv, ["-o", "--out", "--from", "--to", "--root"]);
|
|
1094
1558
|
if (!file)
|
|
1095
1559
|
fail("no input file (use '-' to read from stdin)", 2);
|
|
1096
1560
|
// A bare `--to`/`--from` (no following value) is a mistyped flag, not a
|
|
@@ -1149,10 +1613,10 @@ function runTransform(argv) {
|
|
|
1149
1613
|
else if (inFmt === "md") {
|
|
1150
1614
|
const conv = mdToGeml(src);
|
|
1151
1615
|
notes = conv.notes;
|
|
1152
|
-
doc = parse(conv.geml, { resolveDoc: resolverFor(file) });
|
|
1616
|
+
doc = parse(conv.geml, { resolveDoc: resolverFor(file, root), self: file === "-" ? undefined : basename(file) });
|
|
1153
1617
|
}
|
|
1154
1618
|
else {
|
|
1155
|
-
doc = parse(src, { resolveDoc: resolverFor(file) });
|
|
1619
|
+
doc = parse(src, { resolveDoc: resolverFor(file, root), self: file === "-" ? undefined : basename(file) });
|
|
1156
1620
|
}
|
|
1157
1621
|
let output;
|
|
1158
1622
|
switch (outFmt) {
|
|
@@ -1166,8 +1630,8 @@ function runTransform(argv) {
|
|
|
1166
1630
|
output = renderHtml(doc, {
|
|
1167
1631
|
source: file === "-" ? "stdin" : basename(file),
|
|
1168
1632
|
// geml-code-graph embeds load + parse sibling codemap docs on demand.
|
|
1169
|
-
loadDoc: resolverFor(file),
|
|
1170
|
-
parseDoc: (s) => parse(s),
|
|
1633
|
+
loadDoc: resolverFor(file, root),
|
|
1634
|
+
parseDoc: (s) => parse(s, { resolveDoc: resolverFor(file, root) }),
|
|
1171
1635
|
});
|
|
1172
1636
|
break;
|
|
1173
1637
|
case "md": {
|
|
@@ -1291,7 +1755,7 @@ function resolveSelector(source, file, raw) {
|
|
|
1291
1755
|
return bare;
|
|
1292
1756
|
const level = m[1].length;
|
|
1293
1757
|
const want = m[2];
|
|
1294
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
1758
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1295
1759
|
const heads = doc.ids.flatMap((id) => {
|
|
1296
1760
|
const site = findBlockSite(doc.children, id);
|
|
1297
1761
|
const b = site?.siblings[site.index];
|
|
@@ -1332,7 +1796,7 @@ function resolveSelector(source, file, raw) {
|
|
|
1332
1796
|
// (the registration order parse() records), covering the same set `get #id`
|
|
1333
1797
|
// resolves against: typed blocks, headings, and footnote definitions.
|
|
1334
1798
|
function listIds(source, file, json) {
|
|
1335
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
1799
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1336
1800
|
const rows = doc.ids.map((id) => {
|
|
1337
1801
|
const site = findBlockSite(doc.children, id);
|
|
1338
1802
|
const b = site?.siblings[site.index];
|
|
@@ -1391,7 +1855,7 @@ function getByType(source, file, type, json, headOnly) {
|
|
|
1391
1855
|
// The ONLY block of its type: locating it in the model needs no index, so
|
|
1392
1856
|
// --json can still answer with the parsed node (meta's key/values, a
|
|
1393
1857
|
// table's model) rather than a mere location.
|
|
1394
|
-
const node = onlyBlockOfType(parse(source, { resolveDoc: resolverFor(file) }).children, type);
|
|
1858
|
+
const node = onlyBlockOfType(parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) }).children, type);
|
|
1395
1859
|
if (node) {
|
|
1396
1860
|
console.log(JSON.stringify(node, null, 2));
|
|
1397
1861
|
return;
|
|
@@ -1457,7 +1921,7 @@ function runGet(args) {
|
|
|
1457
1921
|
if (json) {
|
|
1458
1922
|
// The model node(s) — same shapes `geml <file>` emits. Parsing is needed
|
|
1459
1923
|
// to resolve the tree (and nested-block ids), but only the target prints.
|
|
1460
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
1924
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1461
1925
|
const site = findBlockSite(doc.children, id);
|
|
1462
1926
|
if (!site)
|
|
1463
1927
|
fail(`no block with id \`${id}\``, 1);
|
|
@@ -1532,6 +1996,11 @@ function runSet(args) {
|
|
|
1532
1996
|
return;
|
|
1533
1997
|
}
|
|
1534
1998
|
// default / --head: content is a whole block (default) or a bare head line.
|
|
1999
|
+
// Does the target exist? Asked FIRST: the shape checks below name the id in
|
|
2000
|
+
// their advice ("use --body to set the body of #far"), which reads as though the
|
|
2001
|
+
// id were there. Whether the content is prose is the second question.
|
|
2002
|
+
if (!blockSpans(source).has(id))
|
|
2003
|
+
fail(`no block with id \`${id}\``, 1);
|
|
1535
2004
|
let content;
|
|
1536
2005
|
if (rawChannel) {
|
|
1537
2006
|
content = readInput("-");
|
|
@@ -1655,7 +2124,7 @@ function runAdd(args) {
|
|
|
1655
2124
|
// or duplicate id surfaces as an error diagnostic) and no pre-existing id may
|
|
1656
2125
|
// vanish. Returns the updated text; on any violation fail()s and writes nothing.
|
|
1657
2126
|
function insertFragment(source, lines, at, fragment, file) {
|
|
1658
|
-
const beforeIds = parse(source, { resolveDoc: resolverFor(file) }).ids;
|
|
2127
|
+
const beforeIds = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) }).ids;
|
|
1659
2128
|
const before = lines.slice(0, at);
|
|
1660
2129
|
const after = lines.slice(at);
|
|
1661
2130
|
const nl = newlineOf(source); // the fragment AND every separator we add
|
|
@@ -1672,7 +2141,7 @@ function insertFragment(source, lines, at, fragment, file) {
|
|
|
1672
2141
|
const sepBefore = before.length && !blank(before[before.length - 1]) ? nl : "";
|
|
1673
2142
|
const sepAfter = after.length && !blank(after[0]) ? nl : "";
|
|
1674
2143
|
const updated = before.join("") + sepBefore + frag + sepAfter + after.join("");
|
|
1675
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2144
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1676
2145
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1677
2146
|
if (errs.length) {
|
|
1678
2147
|
const first = errs[0];
|
|
@@ -1722,7 +2191,7 @@ function runDelete(args) {
|
|
|
1722
2191
|
const updated = splitLines(source).filter((_, i) => !toDelete.has(i)).join("");
|
|
1723
2192
|
// Lenient guard: surface any resulting error diagnostic (a reference now
|
|
1724
2193
|
// dangling) as a WARNING, but write regardless.
|
|
1725
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2194
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1726
2195
|
for (const d of reparsed.diagnostics.filter((x) => x.severity === "error")) {
|
|
1727
2196
|
console.error(`warning: ${d.message} (line ${d.line}) — left dangling by delete; run 'geml check' to see it as an error`);
|
|
1728
2197
|
}
|
|
@@ -1741,7 +2210,7 @@ function runRename(args) {
|
|
|
1741
2210
|
if (oldId === newId)
|
|
1742
2211
|
fail("#old and #new are the same id — nothing to rename", 2);
|
|
1743
2212
|
const source = readInput(file);
|
|
1744
|
-
const before = parse(source, { resolveDoc: resolverFor(file) });
|
|
2213
|
+
const before = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1745
2214
|
if (!before.ids.includes(oldId))
|
|
1746
2215
|
fail(`no block with id \`${oldId}\``, 1);
|
|
1747
2216
|
if (before.ids.includes(newId))
|
|
@@ -1761,7 +2230,7 @@ function runRename(args) {
|
|
|
1761
2230
|
}
|
|
1762
2231
|
}
|
|
1763
2232
|
const updated = rewriteId(source, oldId, newId, file);
|
|
1764
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2233
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1765
2234
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1766
2235
|
if (errs.length) {
|
|
1767
2236
|
const e = errs[0];
|
|
@@ -1792,7 +2261,7 @@ function runRename(args) {
|
|
|
1792
2261
|
// text, not a reference. (Known residual: id-less raw bodies and inline
|
|
1793
2262
|
// code/math spans in flow content — see design §8.)
|
|
1794
2263
|
function rewriteId(source, oldId, newId, file) {
|
|
1795
|
-
const doc = parse(source, { resolveDoc: resolverFor(file) });
|
|
2264
|
+
const doc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1796
2265
|
const spans = blockSpans(source);
|
|
1797
2266
|
const protectedLines = new Set();
|
|
1798
2267
|
for (const b of doc.children) {
|
|
@@ -1900,7 +2369,7 @@ function spliceBlock(source, id, replacement, file, headOnly = false, guardCount
|
|
|
1900
2369
|
const found = blockSpans(source).get(id);
|
|
1901
2370
|
if (!found)
|
|
1902
2371
|
fail(`no block with id \`${id}\``, 1);
|
|
1903
|
-
const beforeDoc = parse(source, { resolveDoc: resolverFor(file) });
|
|
2372
|
+
const beforeDoc = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1904
2373
|
const beforeIds = beforeDoc.ids;
|
|
1905
2374
|
// Keep the bytes before and after the target span exactly; give the new block
|
|
1906
2375
|
// a single trailing newline so the following block still starts on its own
|
|
@@ -1924,7 +2393,7 @@ function spliceBlock(source, id, replacement, file, headOnly = false, guardCount
|
|
|
1924
2393
|
// surface as error diagnostics (registerId flags dups); one check covers both.
|
|
1925
2394
|
// Then require the target id to survive, and — because a malformed replacement
|
|
1926
2395
|
// can swallow a neighbour — that every other pre-existing id survives too.
|
|
1927
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2396
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
1928
2397
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
1929
2398
|
if (errs.length) {
|
|
1930
2399
|
const first = errs[0];
|
|
@@ -2107,9 +2576,9 @@ function runRevert(args) {
|
|
|
2107
2576
|
return;
|
|
2108
2577
|
}
|
|
2109
2578
|
const span = curFull;
|
|
2110
|
-
const beforeIds = parse(source, { resolveDoc: resolverFor(file) }).ids;
|
|
2579
|
+
const beforeIds = parse(source, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) }).ids;
|
|
2111
2580
|
const updated = splitLines(source).filter((_, i) => i < span.start || i >= span.end).join("");
|
|
2112
|
-
const reparsed = parse(updated, { resolveDoc: resolverFor(file) });
|
|
2581
|
+
const reparsed = parse(updated, { resolveDoc: resolverFor(file), self: file === "-" ? undefined : basename(file) });
|
|
2113
2582
|
const errs = reparsed.diagnostics.filter((d) => d.severity === "error");
|
|
2114
2583
|
if (errs.length) {
|
|
2115
2584
|
const first = errs[0];
|