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 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 stamp.
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
- * Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a stamped block should also
312
- * be a block with a `data-source-line`, so the styling surface and the anchor
313
- * surface coincide. It also keeps an inline directive from stamping the `<em>`
314
- * that happens to follow it inside a paragraph.
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 stamp.
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" && STYLE_TARGET_TAGS.has(node.tagName)) range.push(i);
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
- const isDark = () => typeof document !== "undefined" && document.documentElement.classList.contains("dark");
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
- if (mermaidInstance) return mermaidInstance;
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.initialize({
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
- return mermaidLoading;
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
- const mermaid = await getMermaid();
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 = svgCache.get(code);
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
- svgCache.set(code, svg);
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;