vantage-md 0.5.7 → 0.5.9
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/dist/index.cjs +196 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +196 -20
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +261 -45
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts.map +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +262 -46
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -46,7 +46,31 @@ let unist_util_visit = require("unist-util-visit");
|
|
|
46
46
|
let yaml = require("yaml");
|
|
47
47
|
yaml = __toESM(yaml, 1);
|
|
48
48
|
let smol_toml = require("smol-toml");
|
|
49
|
+
//#region src/rehypeVantageAnchors.ts
|
|
50
|
+
/** What `rehypeVantageDirectives` stamps, in hast property form. */
|
|
51
|
+
const OQ_ID_PROPERTY$1 = "dataVantageOqId";
|
|
52
|
+
function rehypeVantageAnchors() {
|
|
53
|
+
return (tree) => {
|
|
54
|
+
(0, unist_util_visit.visit)(tree, "element", (node) => {
|
|
55
|
+
const carried = node.properties?.[OQ_ID_PROPERTY$1];
|
|
56
|
+
if (typeof carried !== "string" || carried === "") return;
|
|
57
|
+
delete node.properties[OQ_ID_PROPERTY$1];
|
|
58
|
+
if (typeof node.properties.id === "string" && node.properties.id !== "") return;
|
|
59
|
+
node.properties.id = carried;
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
49
64
|
//#region src/rehypeSourceLines.ts
|
|
65
|
+
/**
|
|
66
|
+
* Tags that get a `data-source-line`.
|
|
67
|
+
*
|
|
68
|
+
* `td`/`th` are here for review mode: a comment anchors to the cell it was
|
|
69
|
+
* written on, so the cell needs a line of its own to be found again. Every cell
|
|
70
|
+
* in a row reports the *row's* start line — a GFM row is one source line — so a
|
|
71
|
+
* line no longer names at most one anchorable element, and whatever resolves an
|
|
72
|
+
* anchor has to break the tie by block hash (`useReviewHighlights`).
|
|
73
|
+
*/
|
|
50
74
|
const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
51
75
|
"p",
|
|
52
76
|
"h1",
|
|
@@ -59,6 +83,8 @@ const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
|
59
83
|
"blockquote",
|
|
60
84
|
"pre",
|
|
61
85
|
"table",
|
|
86
|
+
"td",
|
|
87
|
+
"th",
|
|
62
88
|
"tr",
|
|
63
89
|
"ul",
|
|
64
90
|
"ol",
|
|
@@ -306,12 +332,17 @@ const VANTAGE_RUNS = [
|
|
|
306
332
|
"only"
|
|
307
333
|
];
|
|
308
334
|
/**
|
|
309
|
-
* The tags a `section`/`block` directive may
|
|
335
|
+
* The tags a `section`/`block` directive may **target**.
|
|
336
|
+
*
|
|
337
|
+
* Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a directive's target should
|
|
338
|
+
* also be a block with a `data-source-line`, so the styling surface and the
|
|
339
|
+
* anchor surface coincide. It also keeps an inline directive from stamping the
|
|
340
|
+
* `<em>` that happens to follow it inside a paragraph.
|
|
310
341
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
342
|
+
* It does **not** bound a `section`'s range. Every element in the span is
|
|
343
|
+
* stamped, on the tag list or not, because a member only has to be a box in the
|
|
344
|
+
* flow for the section's vertical rule to cross it — see `styleRange` in
|
|
345
|
+
* `rehypeVantageDirectives.ts` for the hole that restricting the range left.
|
|
315
346
|
*
|
|
316
347
|
* It lives here rather than in the plugin because the CLI checker has to answer
|
|
317
348
|
* "will this directive stamp anything?" from an mdast tree with no hast in
|
|
@@ -378,6 +409,23 @@ const VANTAGE_ANCHOR_TARGETS = [
|
|
|
378
409
|
* and said nothing, which is the D5 break this module exists to prevent.
|
|
379
410
|
*/
|
|
380
411
|
const VANTAGE_OQ_HOST_TARGETS = VANTAGE_ANCHOR_TARGETS.filter((tag) => tag !== "pre" && tag !== "table");
|
|
412
|
+
/**
|
|
413
|
+
* The shape of an `oq` directive's `id`: `OQ-` then an optional short uppercase
|
|
414
|
+
* prefix then digits. `OQ-9`, `OQ-TP6` and `OQ-A03` are ids; `OQ-foo`, `OQ-tp6`
|
|
415
|
+
* and a bare `OQ6` are not.
|
|
416
|
+
*
|
|
417
|
+
* The prefix is what keeps ids distinct once one document references another's
|
|
418
|
+
* questions — `trust-paths.md`'s `OQ-4` and a design sketch's `OQ-4` are
|
|
419
|
+
* different questions, and a bare number cannot say which one a cross-document
|
|
420
|
+
* reference means. It is optional because most documents never leave their own
|
|
421
|
+
* file, and requiring it everywhere would fire on every single-doc sketch.
|
|
422
|
+
*
|
|
423
|
+
* Three consumers read it from here and none of them may re-spell it: the
|
|
424
|
+
* plugin that stamps the anchor, the sanitiser that allowlists the value, and
|
|
425
|
+
* the checker's `vantage/oq-id-format`. A fourth copy is how the checker starts
|
|
426
|
+
* calling a working anchor malformed.
|
|
427
|
+
*/
|
|
428
|
+
const VANTAGE_OQ_ID = /^OQ-(?:[A-Z][A-Z0-9]{0,5})?[0-9]+$/;
|
|
381
429
|
const STYLE_KEYS = {
|
|
382
430
|
tone: VANTAGE_TONES,
|
|
383
431
|
emphasis: VANTAGE_EMPHASIS,
|
|
@@ -520,11 +568,13 @@ function parseVantageDirective(comment) {
|
|
|
520
568
|
//#endregion
|
|
521
569
|
//#region src/rehypeVantageDirectives.ts
|
|
522
570
|
/**
|
|
523
|
-
* What a `section`/`block` and an `oq` directive may
|
|
571
|
+
* What a `section`/`block` and an `oq` directive may **target**.
|
|
524
572
|
*
|
|
525
573
|
* Both lists live in `vantageDirectives.ts`, with the reasoning for each tag,
|
|
526
574
|
* because the CLI checker resolves the same question over mdast and must reach
|
|
527
575
|
* the same answer (D5).
|
|
576
|
+
*
|
|
577
|
+
* Neither list bounds a `section`'s range: see `styleRange`.
|
|
528
578
|
*/
|
|
529
579
|
const STYLE_TARGET_TAGS = new Set(VANTAGE_STYLE_TARGETS);
|
|
530
580
|
const ANCHOR_TARGET_TAGS = new Set(VANTAGE_ANCHOR_TARGETS);
|
|
@@ -571,6 +621,18 @@ const RUN_PROPERTY = "dataVantageRun";
|
|
|
571
621
|
const OQ_PROPERTY = "dataVantageOq";
|
|
572
622
|
const LEANING_PROPERTY = "dataVantageLeaning";
|
|
573
623
|
/**
|
|
624
|
+
* The id, carried as a `data-` attribute rather than written straight to `id`.
|
|
625
|
+
*
|
|
626
|
+
* This plugin runs *before* `rehypeSanitize` — it has to, it reads comments and
|
|
627
|
+
* the sanitiser deletes them — and the sanitiser's default schema clobbers `id`
|
|
628
|
+
* with the prefix `user-content-`. A bare `id` set here would reach the page as
|
|
629
|
+
* `user-content-OQ-4`, every `#OQ-4` link in every document would land nowhere,
|
|
630
|
+
* and nothing would error. `rehypeVantageAnchors` promotes this to a real `id`
|
|
631
|
+
* on the other side of the sanitiser, which is the same reason `rehypeSlug` is
|
|
632
|
+
* registered there (`pipeline.ts`).
|
|
633
|
+
*/
|
|
634
|
+
const OQ_ID_PROPERTY = "dataVantageOqId";
|
|
635
|
+
/**
|
|
574
636
|
* The three properties `collapsed=true` stamps across a section.
|
|
575
637
|
*
|
|
576
638
|
* The heading takes a *different* attribute from the blocks it hides, and that
|
|
@@ -643,6 +705,22 @@ function accepts(name, key, value) {
|
|
|
643
705
|
* `section` before anything else degrades to that one block, and `block` is
|
|
644
706
|
* always that one block. A heading nested inside a stamped `blockquote` or
|
|
645
707
|
* `li` does not end the section: the walk never descends.
|
|
708
|
+
*
|
|
709
|
+
* **Every element in the span, not only a `VANTAGE_STYLE_TARGETS` one.** That
|
|
710
|
+
* list gates the *target* and nothing else. Restricting the range to it as well
|
|
711
|
+
* used to leave a raw-HTML `<figure>`, `<dl>` or `<details>` unstamped between
|
|
712
|
+
* two stamped paragraphs — and the section's one continuous vertical rule is
|
|
713
|
+
* drawn per member, so an unstamped member is a hole the height of the block
|
|
714
|
+
* plus its margins. Measured over the real stylesheet: 44px for a one-line
|
|
715
|
+
* `<figure>`, against the 40px a neighbour can bleed upward, and arbitrarily
|
|
716
|
+
* large for anything taller. `collapsed=true` had the same shape of bug the
|
|
717
|
+
* other way round — it hid the paragraphs and left the figure on the page.
|
|
718
|
+
*
|
|
719
|
+
* The two lists answering different questions is the point, not an oversight:
|
|
720
|
+
* a *target* must be a block a review anchor can name, because a directive
|
|
721
|
+
* pointing at something unanchorable is a directive with no addressable effect.
|
|
722
|
+
* A *member* only has to be a box in the flow, because all it does is carry the
|
|
723
|
+
* run's tone across itself.
|
|
646
724
|
*/
|
|
647
725
|
function styleRange(children, targetIndex, name) {
|
|
648
726
|
const range = [targetIndex];
|
|
@@ -652,7 +730,7 @@ function styleRange(children, targetIndex, name) {
|
|
|
652
730
|
const node = children[i];
|
|
653
731
|
const nodeDepth = headingDepth(node);
|
|
654
732
|
if (nodeDepth !== void 0 && nodeDepth <= depth) break;
|
|
655
|
-
if (node.type === "element"
|
|
733
|
+
if (node.type === "element") range.push(i);
|
|
656
734
|
}
|
|
657
735
|
return range;
|
|
658
736
|
}
|
|
@@ -711,6 +789,8 @@ function stampStyle(children, targetIndex, name, pairs, state) {
|
|
|
711
789
|
}
|
|
712
790
|
function stampOq(target, pairs) {
|
|
713
791
|
setProperty(target, OQ_PROPERTY, "true");
|
|
792
|
+
const id = pairs.get("id");
|
|
793
|
+
if (id !== void 0 && id !== "") setProperty(target, OQ_ID_PROPERTY, id);
|
|
714
794
|
const leaning = pairs.get("leaning");
|
|
715
795
|
if (leaning === void 0) return;
|
|
716
796
|
const text = leaning.replace(/\s+/g, " ").trim().slice(0, MAX_LEANING);
|
|
@@ -1016,6 +1096,7 @@ const sanitizeSchema = {
|
|
|
1016
1096
|
["dataVantageCollapseToggle", COLLAPSE_GROUP_ID],
|
|
1017
1097
|
["dataVantageRun", ...VANTAGE_RUNS],
|
|
1018
1098
|
["dataVantageOq", "true"],
|
|
1099
|
+
["dataVantageOqId", VANTAGE_OQ_ID],
|
|
1019
1100
|
["dataVantageAlert", ...VANTAGE_ALERTS],
|
|
1020
1101
|
"dataVantageLeaning"
|
|
1021
1102
|
],
|
|
@@ -1065,6 +1146,7 @@ function buildRehypePlugins(options = {}) {
|
|
|
1065
1146
|
plugins.push(rehypeVantageAlerts);
|
|
1066
1147
|
plugins.push(rehypeVantageDirectives);
|
|
1067
1148
|
if (sanitize) plugins.push([rehype_sanitize.default, sanitizeSchema]);
|
|
1149
|
+
plugins.push(rehypeVantageAnchors);
|
|
1068
1150
|
plugins.push(rehype_slug.default);
|
|
1069
1151
|
if (highlight) plugins.push(rehype_highlight.default);
|
|
1070
1152
|
if (math) plugins.push(rehypeCaptureMathStamps, rehype_katex.default, rehypeRestoreMathStamps);
|
|
@@ -1458,27 +1540,92 @@ function readStatusChip(frontmatter, raw, issues) {
|
|
|
1458
1540
|
});
|
|
1459
1541
|
}
|
|
1460
1542
|
//#endregion
|
|
1543
|
+
//#region src/mermaidTheme.ts
|
|
1544
|
+
/** Whether the document is asking for the dark palette right now. */
|
|
1545
|
+
function currentMermaidTheme() {
|
|
1546
|
+
return typeof document !== "undefined" && document.documentElement.classList.contains("dark") ? "dark" : "default";
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Theme variables per theme. Mermaid derives most of its palette from these, so
|
|
1550
|
+
* the set is deliberately small: the surfaces, the ink, and the lines.
|
|
1551
|
+
*/
|
|
1552
|
+
const THEME_VARIABLES = {
|
|
1553
|
+
dark: {
|
|
1554
|
+
background: "#1d293d",
|
|
1555
|
+
mainBkg: "#314158",
|
|
1556
|
+
nodeBorder: "#90a1b9",
|
|
1557
|
+
nodeTextColor: "#f1f5f9",
|
|
1558
|
+
lineColor: "#90a1b9",
|
|
1559
|
+
textColor: "#e2e8f0",
|
|
1560
|
+
edgeLabelBackground: "#1d293d"
|
|
1561
|
+
},
|
|
1562
|
+
default: {
|
|
1563
|
+
background: "#f8fafc",
|
|
1564
|
+
mainBkg: "#f1f5f9",
|
|
1565
|
+
nodeBorder: "#62748e",
|
|
1566
|
+
nodeTextColor: "#0f172b",
|
|
1567
|
+
lineColor: "#62748e",
|
|
1568
|
+
textColor: "#1d293d",
|
|
1569
|
+
edgeLabelBackground: "#f8fafc"
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
function mermaidThemeVariables(theme) {
|
|
1573
|
+
return THEME_VARIABLES[theme];
|
|
1574
|
+
}
|
|
1575
|
+
//#endregion
|
|
1461
1576
|
//#region src/mermaidCache.ts
|
|
1462
1577
|
const svgCache = /* @__PURE__ */ new Map();
|
|
1578
|
+
const cacheKey = (code, theme) => `${theme}${code}`;
|
|
1579
|
+
/** The SVG for this fence in the theme the page is currently asking for. */
|
|
1580
|
+
function getCachedSvg(code, theme = currentMermaidTheme()) {
|
|
1581
|
+
return svgCache.get(cacheKey(code, theme));
|
|
1582
|
+
}
|
|
1583
|
+
function setCachedSvg(code, svg, theme = currentMermaidTheme()) {
|
|
1584
|
+
svgCache.set(cacheKey(code, theme), svg);
|
|
1585
|
+
}
|
|
1463
1586
|
//#endregion
|
|
1464
1587
|
//#region src/mermaidLoader.ts
|
|
1465
1588
|
let mermaidInstance = null;
|
|
1466
1589
|
let mermaidLoading = null;
|
|
1467
|
-
|
|
1590
|
+
/** The theme the loaded instance was last configured for, `null` until loaded. */
|
|
1591
|
+
let configuredTheme = null;
|
|
1592
|
+
function configure(m, theme) {
|
|
1593
|
+
m.initialize({
|
|
1594
|
+
startOnLoad: false,
|
|
1595
|
+
theme,
|
|
1596
|
+
themeVariables: mermaidThemeVariables(theme),
|
|
1597
|
+
securityLevel: "strict",
|
|
1598
|
+
suppressErrorRendering: true
|
|
1599
|
+
});
|
|
1600
|
+
configuredTheme = theme;
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* The mermaid module, configured for the theme the page is asking for *now*.
|
|
1604
|
+
*
|
|
1605
|
+
* Re-configuring on a theme change is the point. `initialize` used to run once,
|
|
1606
|
+
* on first import, so every diagram rendered after a light/dark switch still
|
|
1607
|
+
* came out in the palette the session started in — a white slab of a flowchart
|
|
1608
|
+
* on the dark page, or a black one on the light page. `initialize` merges into
|
|
1609
|
+
* mermaid's global config, so calling it again is how the next `render` picks
|
|
1610
|
+
* the new palette up; the cache is keyed by theme so the old SVGs are not
|
|
1611
|
+
* served instead (`mermaidCache.ts`).
|
|
1612
|
+
*/
|
|
1468
1613
|
async function getMermaid() {
|
|
1469
|
-
|
|
1614
|
+
const theme = currentMermaidTheme();
|
|
1615
|
+
if (mermaidInstance) {
|
|
1616
|
+
if (configuredTheme !== theme) configure(mermaidInstance, theme);
|
|
1617
|
+
return mermaidInstance;
|
|
1618
|
+
}
|
|
1470
1619
|
if (!mermaidLoading) mermaidLoading = import("mermaid").then((mod) => {
|
|
1471
1620
|
const m = mod.default;
|
|
1472
|
-
m
|
|
1473
|
-
startOnLoad: false,
|
|
1474
|
-
theme: isDark() ? "dark" : "default",
|
|
1475
|
-
securityLevel: "strict",
|
|
1476
|
-
suppressErrorRendering: true
|
|
1477
|
-
});
|
|
1621
|
+
configure(m, currentMermaidTheme());
|
|
1478
1622
|
mermaidInstance = m;
|
|
1479
1623
|
return m;
|
|
1480
1624
|
});
|
|
1481
|
-
|
|
1625
|
+
const loaded = await mermaidLoading;
|
|
1626
|
+
const wanted = currentMermaidTheme();
|
|
1627
|
+
if (configuredTheme !== wanted) configure(loaded, wanted);
|
|
1628
|
+
return loaded;
|
|
1482
1629
|
}
|
|
1483
1630
|
//#endregion
|
|
1484
1631
|
//#region src/renderMermaidBlocks.ts
|
|
@@ -1512,18 +1659,20 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
1512
1659
|
const { className = "mermaid", onError } = options;
|
|
1513
1660
|
const codeBlocks = container.querySelectorAll("pre > code.language-mermaid, pre > code[class*=\"language-mermaid\"]");
|
|
1514
1661
|
if (codeBlocks.length === 0) return;
|
|
1515
|
-
|
|
1662
|
+
let loading;
|
|
1663
|
+
const mermaidOnce = () => loading ??= getMermaid();
|
|
1516
1664
|
const renderPromises = Array.from(codeBlocks).map(async (codeEl) => {
|
|
1517
1665
|
const preEl = codeEl.parentElement;
|
|
1518
1666
|
if (!preEl) return;
|
|
1519
1667
|
const code = codeEl.textContent || "";
|
|
1520
1668
|
if (!code.trim()) return;
|
|
1521
|
-
const cached =
|
|
1669
|
+
const cached = getCachedSvg(code);
|
|
1522
1670
|
if (cached) {
|
|
1523
1671
|
replaceWithSvg(preEl, cached, className);
|
|
1524
1672
|
return;
|
|
1525
1673
|
}
|
|
1526
1674
|
try {
|
|
1675
|
+
const mermaid = await mermaidOnce();
|
|
1527
1676
|
let hash = 0;
|
|
1528
1677
|
for (let i = 0; i < code.length; i++) {
|
|
1529
1678
|
hash = (hash << 5) - hash + code.charCodeAt(i);
|
|
@@ -1531,7 +1680,7 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
1531
1680
|
}
|
|
1532
1681
|
const id = `mermaid-${Math.abs(hash).toString(36)}-${Date.now()}`;
|
|
1533
1682
|
const { svg } = await mermaid.render(id, code);
|
|
1534
|
-
|
|
1683
|
+
setCachedSvg(code, svg);
|
|
1535
1684
|
replaceWithSvg(preEl, svg, className);
|
|
1536
1685
|
} catch (err) {
|
|
1537
1686
|
if (onError) onError(code, err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1539,9 +1688,34 @@ async function renderMermaidBlocks(container, options = {}) {
|
|
|
1539
1688
|
});
|
|
1540
1689
|
await Promise.all(renderPromises);
|
|
1541
1690
|
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Attributes the wrapper inherits from the `<pre>` it replaces.
|
|
1693
|
+
*
|
|
1694
|
+
* The splice is the same shape of problem `rehypeVantageMathStamps` solves for
|
|
1695
|
+
* KaTeX: the pipeline stamped the fence, and swapping the element out throws
|
|
1696
|
+
* the stamps away. A mermaid diagram inside a toned section then drew no slice
|
|
1697
|
+
* of the section's vertical rule, leaving a hole as tall as the diagram; a
|
|
1698
|
+
* collapsed section left the diagram visible under a closed heading; and a
|
|
1699
|
+
* `#L` anchor pointing at the fence resolved to nothing.
|
|
1700
|
+
*
|
|
1701
|
+
* Named individually rather than copied wholesale: `class` is the caller's
|
|
1702
|
+
* (`className`), and `id` would be duplicated onto a second element.
|
|
1703
|
+
*/
|
|
1704
|
+
const CARRIED_ATTRIBUTES = [
|
|
1705
|
+
"data-source-line",
|
|
1706
|
+
"data-vantage-tone",
|
|
1707
|
+
"data-vantage-emphasis",
|
|
1708
|
+
"data-vantage-run",
|
|
1709
|
+
"data-vantage-collapsed",
|
|
1710
|
+
"data-vantage-collapse-group"
|
|
1711
|
+
];
|
|
1542
1712
|
function replaceWithSvg(preEl, svg, className) {
|
|
1543
1713
|
const wrapper = document.createElement("div");
|
|
1544
1714
|
wrapper.className = className;
|
|
1715
|
+
for (const name of CARRIED_ATTRIBUTES) {
|
|
1716
|
+
const value = preEl.getAttribute(name);
|
|
1717
|
+
if (value !== null) wrapper.setAttribute(name, value);
|
|
1718
|
+
}
|
|
1545
1719
|
wrapper.innerHTML = svg;
|
|
1546
1720
|
preEl.replaceWith(wrapper);
|
|
1547
1721
|
}
|
|
@@ -1708,6 +1882,8 @@ The steps below predate the rewrite.
|
|
|
1708
1882
|
- **Anything outside those sets is silently ignored** — nothing breaks, and nothing styles either. Run \`vantage-check\` on the document: the \`vantage/*\` rules are the only thing that will ever tell you a directive did nothing.
|
|
1709
1883
|
- **Always close the comment with \`-->\`.** Never \`--!>\`, and never leave it open: Markdown reads every line below an unclosed \`<!--\` as part of the comment, and the whole rest of the document vanishes from the page. For the same reason \`-->\` cannot appear *inside* a value — it ends the comment early and spills the remainder into the page as literal text.
|
|
1710
1884
|
- **In a list, indent the directive inside the item**, with blank lines around it (below). At the start of a line between two items it ends the list and starts a second one, which changes the numbering and the spacing in every renderer — the one thing a directive must never do.
|
|
1885
|
+
- **An open question's id is \`OQ-\` then an optional short uppercase prefix then digits** — \`OQ-9\`, \`OQ-TP6\`, \`OQ-A03\`. The prefix is what keeps ids distinct once one document references another's questions, so use one in both whenever they cross-reference. \`vantage-check\` reports anything outside that shape as \`vantage/oq-id-format\`, and the same id twice in one document as \`vantage/oq-id-duplicate\` — both are silent otherwise, because the id becomes the block's anchor and a refused or duplicated one simply goes nowhere.
|
|
1886
|
+
- **A reference is a link, or it is a lie.** An \`OQ-\` id, a \`\u00a7N\` section number and a filename all read like pointers, and written as bare prose none of them can be followed or checked — which is exactly why a stale one is never caught. Link the question to its anchor (\`[OQ-4](#OQ-4)\`, or the Decision Ledger once it is compacted), the section to its heading, the filename to the file. \`vantage-check\` reports all three (\`ref/*\`) as errors, and checks that the link points at the thing the reference names rather than merely at something. Writing a specimen rather than a reference? Put it in a fenced block, which the rules never read.
|
|
1711
1887
|
- **Every open question (\u{1F4AC}) with a stated leaning gets an \`oq\` directive.** The convention's prose — the emoji, the \`OQ-N\` id, the \`_Leaning:_\` line, the fill-in \`**Answer:**\` — produces no button on its own. Writing the convention and stopping there is the most common way this feature goes missing: the questions look complete, review mode is on, and there is nothing to click. **\`vantage-check\` reports it as an error** (\`vantage/oq-missing\`), because a question awaiting a ruling that the reviewer cannot file is not a style preference. Mark it \u{1F512} if it is blocked on something upstream and cannot be answered yet, or \u2705 once it is decided; either state needs no directive.
|
|
1712
1888
|
- **A \`leaning\` restates the leaning; it is never "yes".** The one-click button in review mode files that text as a review comment, and the comment is all the agent reading it has — nobody remembers which button was clicked. \`leaning="Yes"\` beside a two-branch question is a support ticket.
|
|
1713
1889
|
|
|
@@ -1753,6 +1929,7 @@ exports.parseVantageDirective = parseVantageDirective;
|
|
|
1753
1929
|
exports.readVantageFrontmatter = readVantageFrontmatter;
|
|
1754
1930
|
exports.rehypeSourceLines = rehypeSourceLines;
|
|
1755
1931
|
exports.rehypeVantageAlerts = rehypeVantageAlerts;
|
|
1932
|
+
exports.rehypeVantageAnchors = rehypeVantageAnchors;
|
|
1756
1933
|
exports.rehypeVantageDirectives = rehypeVantageDirectives;
|
|
1757
1934
|
exports.renderMarkdown = renderMarkdown;
|
|
1758
1935
|
exports.renderMermaidBlocks = renderMermaidBlocks;
|