vantage-md 0.8.0 → 0.8.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/CHANGELOG.md +11 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["OQ_ID_PROPERTY","visit","defaultSchema","remarkGfm","remarkMath","rehypeRaw","rehypeSanitize","rehypeSlug","rehypeHighlight","rehypeKatex","parseTOML","YAML","remarkParse","remarkRehype","rehypeStringify","remarkParse"],"sources":["../src/rehypeVantageAnchors.ts","../src/rehypeSourceLines.ts","../src/rehypeVantageAlerts.ts","../src/vantageDirectives.ts","../src/rehypeVantageDirectives.ts","../src/rehypeVantageMathStamps.ts","../src/sanitize.ts","../src/remarkPlugins.ts","../src/pipeline.ts","../src/frontmatter.ts","../src/renderMarkdown.ts","../src/lineAnchor.ts","../src/scrollToLineAnchor.ts","../src/vantageFrontmatter.ts","../src/mermaidTheme.ts","../src/mermaidCache.ts","../src/mermaidLoader.ts","../src/renderMermaidBlocks.ts","../src/resolveLinks.ts","../src/styleGuide.ts"],"sourcesContent":["/**\n * Promote a carried Open Question id to a real `id`, on the safe side of the\n * sanitizer.\n *\n * `rehypeVantageDirectives` cannot write `id` itself. It runs before\n * `rehypeSanitize` — it must, because it reads HTML comments and the sanitizer\n * deletes them — and the sanitizer's default schema clobbers `id` with the\n * prefix `user-content-`. So the directive plugin stamps `data-vantage-oq-id`,\n * the sanitizer validates that value against the id grammar, and this plugin\n * moves it onto `id` afterwards.\n *\n * That is the same reason `rehypeSlug` is registered after the sanitizer\n * (`pipeline.ts`), and this plugin has to run **before** `rehypeSlug`:\n * `rehype-slug` leaves an element that already has an `id` alone, so promoting\n * first is what lets a question written as a heading keep `OQ-4` instead of\n * acquiring a heading slug. Where both could apply, the Open Question id wins.\n *\n * The data attribute is removed on the way through. Nothing downstream reads\n * it — every reader finds a question by `[data-vantage-question]`\n * (`VANTAGE_QUESTION_SELECTOR`) — and leaving a second copy of the id in the\n * markup invites a future reader to use the wrong one.\n *\n * Architecture and invariants: docs/reference/linked-references.md\n */\n\nimport type { Root, Element } from \"hast\";\nimport { visit } from \"unist-util-visit\";\n\n/** What `rehypeVantageDirectives` stamps, in hast property form. */\nconst OQ_ID_PROPERTY = \"dataVantageOqId\";\n\nexport default function rehypeVantageAnchors() {\n return (tree: Root) => {\n visit(tree, \"element\", (node: Element) => {\n const carried = node.properties?.[OQ_ID_PROPERTY];\n if (typeof carried !== \"string\" || carried === \"\") return;\n\n delete node.properties[OQ_ID_PROPERTY];\n\n // An element that already carries an `id` keeps it. Nothing in the\n // pipeline produces one this early, but a document can hand-write raw\n // HTML, and silently overwriting an author's own id would be a surprise\n // with no error attached.\n if (typeof node.properties.id === \"string\" && node.properties.id !== \"\") {\n return;\n }\n node.properties.id = carried;\n });\n };\n}\n","/**\n * Rehype plugin that adds `data-source-line` attributes to block-level\n * elements based on their position in the original markdown source.\n *\n * This enables GitHub-style line anchors (#L42, #L42-L50) by giving\n * each rendered block a traceable line number from the source.\n */\n\nimport type { Root, Element } from \"hast\";\nimport type { Plugin } from \"unified\";\n\n/**\n * Tags that get a `data-source-line`.\n *\n * `td`/`th` are here for review mode: a comment anchors to the cell it was\n * written on, so the cell needs a line of its own to be found again. Every cell\n * in a row reports the *row's* start line — a GFM row is one source line — so a\n * line no longer names at most one anchorable element, and whatever resolves an\n * anchor has to break the tie by block hash (`useReviewHighlights`).\n */\nconst BLOCK_TAGS = new Set([\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"li\",\n \"blockquote\",\n \"pre\",\n \"table\",\n \"td\",\n \"th\",\n \"tr\",\n \"ul\",\n \"ol\",\n \"hr\",\n \"div\",\n]);\n\nexport interface RehypeSourceLinesOptions {\n /**\n * Lines stripped off the front of the file before parsing — frontmatter,\n * essentially. Added to every emitted line number so `data-source-line`\n * names a line in the *file* rather than in the parsed body, which is what\n * a `#L42` link written against the file means. Defaults to 0.\n */\n offset?: number;\n}\n\nfunction visit(node: Root | Element, offset: number) {\n if (\"children\" in node) {\n for (const child of node.children) {\n if (child.type === \"element\") {\n if (BLOCK_TAGS.has(child.tagName) && child.position?.start?.line) {\n child.properties = child.properties || {};\n child.properties[\"dataSourceLine\"] =\n child.position.start.line + offset;\n }\n visit(child, offset);\n }\n }\n }\n}\n\nconst rehypeSourceLines: Plugin<[RehypeSourceLinesOptions?], Root> = (\n options,\n) => {\n const offset = options?.offset ?? 0;\n return (tree: Root) => {\n visit(tree, offset);\n };\n};\n\nexport default rehypeSourceLines;\n","/**\n * GFM alerts — `> [!WARNING]` — compiled into `data-vantage-alert`.\n *\n * `remark-gfm` does not implement alerts, so until this plugin existed a\n * `> [!WARNING]` rendered as an ordinary blockquote with the literal marker\n * visible as its first words. Worse than merely unstyled: `@tailwindcss/typography`\n * italicizes blockquotes and draws `open-quote`/`close-quote` around the first\n * paragraph, so a callout came out as an italic *quotation* whose opening words\n * were `\"[!WARNING]`. That was the \"Known gaps\" entry in\n * `docs/reference/inline-markup.md` and OQ-10, filed rather than fixed, while\n * `styleGuide.ts` went on telling every agent to write them.\n *\n * The tokens are deliberately the ones the `tone` vocabulary already resolves —\n * an alert *is* the six-color light/dark treatment `tone` shipped, which is\n * exactly what the gap entry said whoever fixed this should do rather than\n * building a second palette. `[!WARNING]` and `<!-- vantage: block tone=warning -->`\n * therefore agree by construction, and adding a theme still touches one\n * custom-property block.\n *\n * **This runs in the shared pipeline, so all four renderers get it** — the live\n * viewer, the package's exported viewer, the static export and the CLI checker's\n * `renderMarkdown`. That is what makes an injected title element acceptable here\n * where the collapse caret's glyph had to be drawn in CSS: the caret is injected\n * by app JS that may never run, and this is not (D5).\n *\n * ## What it does not do\n *\n * It does not touch a blockquote that carries no marker, and an unrecognized\n * marker (`[!HINT]`) is left exactly as it was — visible literal text, which is\n * the honest rendering of something GitHub also would not style. Silently\n * swallowing it would hide a typo that reads as a callout on neither renderer.\n */\n\nimport { visit } from \"unist-util-visit\";\nimport type { Element, Root, Text } from \"hast\";\n\n/**\n * The five GFM alert kinds, lowercased.\n *\n * Deliberately *not* re-derived from `VANTAGE_TONES`: that list carries a sixth\n * token, `muted`, which is ours and is not an alert word. The overlap is the\n * point — the five that coincide share a palette — but the two vocabularies are\n * closed by different authorities and a change to one must not silently move the\n * other. A test asserts the five are a subset of the tones.\n */\nexport const VANTAGE_ALERTS = [\n \"note\",\n \"tip\",\n \"important\",\n \"warning\",\n \"caution\",\n] as const;\n\nexport type VantageAlert = (typeof VANTAGE_ALERTS)[number];\n\n/**\n * The class on the title element this plugin injects, which\n * `styles/directives.css` styles. Exported because the sanitizer runs after\n * this plugin and admits a `div`'s class only when it is this one.\n */\nexport const ALERT_TITLE_CLASS = \"vantage-alert-title\";\n\n/** The visible label per kind. Title case, as GitHub renders it. */\nexport const ALERT_TITLES: Readonly<Record<VantageAlert, string>> = {\n note: \"Note\",\n tip: \"Tip\",\n important: \"Important\",\n warning: \"Warning\",\n caution: \"Caution\",\n};\n\n/**\n * The marker, anchored and requiring the rest of its line to be empty.\n *\n * GFM puts the marker alone on the blockquote's first line, and holding to that\n * is what keeps a paragraph that merely *begins* with bracketed text from being\n * eaten. The trailing newline is optional only for the degenerate blockquote\n * whose entire content is the marker.\n *\n * Measured against the real chain rather than assumed: `remark-parse` reads\n * `[!TIP]` as a shortcut link reference, and because no definition matches,\n * `mdast-util-to-hast` puts it back as **one** leading text node —\n * `\"[!TIP]\\nThe generalization: \"` — not as a `[`/label/`]` triple. So a single\n * anchored test on the first text node is enough, and the plugin does not have\n * to reassemble the marker across siblings.\n */\nexport const ALERT_MARKER =\n /^\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\][ \\t]*(?:\\r?\\n|$)/;\n\n/** The first child, if it is an element. */\nfunction firstElement(node: Element): Element | undefined {\n const child = node.children.find(\n (c) => c.type === \"element\" || (c.type === \"text\" && c.value.trim() !== \"\"),\n );\n return child?.type === \"element\" ? child : undefined;\n}\n\n/**\n * Compile `> [!KIND]` blockquotes into `data-vantage-alert=\"kind\"`.\n *\n * Order in the chain matters twice, and both are stated in `pipeline.ts`:\n *\n * - **after `rehypeSourceLines`**, so the injected title carries no\n * `data-source-line`. That is what keeps it out of `anchorBlockWithin`, which\n * filters candidates to those with a finite line — otherwise a review comment\n * on an alert would anchor to the word \"Warning\" instead of to the prose.\n * - **before `rehypeSanitize`**, so nothing reaches the DOM the schema has not\n * passed. `dataVantageAlert` is allowlisted there by name *and* value, like\n * every other `data-vantage-*` attribute.\n */\nexport function rehypeVantageAlerts() {\n return (tree: Root): void => {\n visit(tree, \"element\", (node: Element) => {\n if (node.tagName !== \"blockquote\") return;\n\n const paragraph = firstElement(node);\n if (paragraph === undefined || paragraph.tagName !== \"p\") return;\n\n const lead = paragraph.children[0];\n if (lead === undefined || lead.type !== \"text\") return;\n\n const match = ALERT_MARKER.exec(lead.value);\n if (match === null) return;\n\n const kind = match[1].toLowerCase() as VantageAlert;\n lead.value = lead.value.slice(match[0].length);\n\n // A paragraph holding nothing but the marker leaves an empty <p> that\n // typography still gives a margin to, so the callout opens with a blank\n // line. Drop it — but only when it is genuinely empty, since\n // `> [!NOTE]\\n> text` puts the text in this same node.\n if (lead.value === \"\" && paragraph.children.length === 1) {\n node.children = node.children.filter((c) => c !== paragraph);\n }\n\n node.properties = { ...node.properties, dataVantageAlert: kind };\n node.children.unshift({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [ALERT_TITLE_CLASS] },\n children: [{ type: \"text\", value: ALERT_TITLES[kind] } as Text],\n } as Element);\n });\n };\n}\n","/**\n * The directive grammar and the closed vocabulary — one parser, no renderer.\n *\n * A Vantage directive is an ordinary HTML comment carrying a `vantage:`\n * sentinel: `<!-- vantage: section tone=warning -->`. GitHub drops it, every\n * other Markdown renderer drops it, and Vantage compiles it into\n * `data-vantage-*` attributes on the block that follows\n * (`rehypeVantageDirectives`). See `docs/reference/inline-markup.md`, \"The carrier and the grammar\".\n *\n * This module is deliberately **zero-dependency — not even a type import**, and\n * it knows nothing about hast. Two callers need it and only one of them has a\n * tree: the rehype plugin stamps attributes, and the `vantage-check` CLI\n * validates directives with no rendering at all, importing this file by\n * relative path. A checker with its own copy of the grammar is a checker that\n * disagrees with the renderer, which is the failure D5 names.\n *\n * Everything here is a pure function of a string. Nothing throws, nothing logs\n * (P3): a comment that is not a directive is `null`, and a comment that carries\n * the sentinel but does not parse is `malformed` with a reason only the checker\n * reads.\n */\n\n/**\n * The mandatory sentinel — the full word, never a terser `v:`.\n *\n * It is what keeps an ordinary `<!-- TODO: rewrite this -->` from being parsed\n * as markup, and it makes the common case a prefix test rather than a grammar\n * attempt (Ledger OQ-1).\n */\nexport const VANTAGE_SENTINEL = \"vantage:\";\n\n/**\n * The closed name set. An unknown name drops the **whole** directive: there is\n * no target semantics without a name. An unknown key or value drops only that\n * pair (D2 is per-key).\n *\n * Position picks the target; the name picks the extent. `section` before a\n * heading reaches the heading's whole section, `block` reaches one block, and\n * `question` declares one question in any state, as `oq`, the name it\n * replaces, still does (`VANTAGE_QUESTION_NAMES`). The name cannot disagree\n * with position — it only says how far the stamp reaches — so §4.2's refusal\n * of a `scope=` key stands.\n *\n * `fallback` is the one name that stamps nothing: it withholds the block after\n * it, which is written for the renderers that cannot show something Vantage\n * shows — an inline `<svg>`, today. Every renderer that does not know the name\n * drops the comment and shows the block, a 0.7.x Vantage included, which is\n * the whole mechanism: the name is new, so no older reader gives it a meaning\n * (`docs/reference/inline-markup.md`, \"Fallback blocks\").\n */\nexport const DIRECTIVE_NAMES = [\n \"section\",\n \"block\",\n \"oq\",\n \"question\",\n \"fallback\",\n] as const;\n\n/**\n * The two names that declare a question: `question`, the one to write, and\n * `oq`, which it replaces and which every Vantage keeps reading.\n *\n * - **`question`** declares a question in any state: open (💬, 💬 🤷 or no\n * marker at all), 🔒 blocked or ✅ answered. The state comes from the marker\n * and never from the name, so a question that changes state changes its\n * marker and nothing else. A viewer from 0.8 on offers Take this leaning on\n * an open one, and never on a 🔒 or ✅ one.\n * - **`oq`** is deprecated, never removed. It keeps the meaning it shipped\n * with in 0.7, an open question to answer in one click, because documents\n * outlive releases and a release never gives existing notation a new\n * meaning (`docs/design/checker-version-skew.md`, P0). Every viewer before\n * 0.8 offers the button on every `oq` it meets, whatever its marker says,\n * so `vantage-check` reports one on a 🔒 or ✅ question as an error and any\n * other as a warning that names the `question` to write.\n *\n * The two take the same keys, `id` and `leaning`, with one grammar. A viewer\n * that predates `question` drops it whole, as it drops every name it does not\n * know, so on a question written with it that viewer offers no button and no\n * anchor: it does less, and misreads nothing. A run holding both names is one\n * question, read as such a viewer reads it: where both set a key, the `oq`'s\n * value wins.\n */\nexport const VANTAGE_QUESTION_NAMES = [\"oq\", \"question\"] as const;\n\n/** `oq` or `question`: a directive that declares a question. */\nexport type VantageQuestionName = (typeof VANTAGE_QUESTION_NAMES)[number];\n\n/** Whether `name` declares a question, `question` or the deprecated `oq`. */\nexport function isQuestionDirective(name: string): name is VantageQuestionName {\n return name === \"oq\" || name === \"question\";\n}\n\n/**\n * The attribute every question carries in the rendered page, whichever name\n * declared it, with the value `\"true\"`. It is how a reader of this package's\n * markup finds every question: `[data-vantage-question]`\n * (`VANTAGE_QUESTION_SELECTOR`).\n */\nexport const VANTAGE_QUESTION_ATTRIBUTE = \"data-vantage-question\";\n\n/** `[data-vantage-question]`: every question in a rendered page. */\nexport const VANTAGE_QUESTION_SELECTOR = `[${VANTAGE_QUESTION_ATTRIBUTE}]`;\n\n/**\n * The attribute a question declared with `oq` carries besides\n * `VANTAGE_QUESTION_ATTRIBUTE`, with the value `\"true\"`. It means what it has\n * meant since 0.7, \"declared with `oq`\", and nothing more: whether a question\n * offers Take this leaning is read from its state (`questionOffersTake`),\n * never from this attribute.\n */\nexport const VANTAGE_OQ_ATTRIBUTE = \"data-vantage-oq\";\n\n/**\n * The attribute that carries a question's `leaning=`, normalized\n * (`normalizeLeaning`), on the element `VANTAGE_QUESTION_ATTRIBUTE` is on,\n * whichever name declared it. Absent when the directive states none.\n */\nexport const VANTAGE_LEANING_ATTRIBUTE = \"data-vantage-leaning\";\n\n/**\n * The `tone` vocabulary: GitHub's alert words plus `muted`.\n *\n * Semantic, never chromatic (P2, Ledger OQ-3). A document says what a section\n * *is*; the theme decides what that looks like, which is what lets one document\n * render correctly in light, in dark, and in themes that do not exist yet.\n */\nexport const VANTAGE_TONES = [\n \"note\",\n \"tip\",\n \"important\",\n \"warning\",\n \"caution\",\n \"muted\",\n] as const;\n\n/** How much the block should pull the eye — separate from `tone` on purpose. */\nexport const VANTAGE_EMPHASIS = [\"strong\", \"normal\", \"quiet\"] as const;\n\n/** A small chip beside the heading. */\nexport const VANTAGE_BADGES = [\n \"draft\",\n \"stale\",\n \"blocked\",\n \"done\",\n \"wip\",\n] as const;\n\n/**\n * `collapsed` is a token, not a flag: `false` is the default written down.\n *\n * It stamps nothing on its own. Its one real effect is overriding a\n * `collapsed=true` earlier in the same merged directive run — last key wins — so\n * it is in the vocabulary rather than being an unknown value that drops. It\n * cannot cancel an *enclosing* collapsed section: a nested heading is a hidden\n * member of the outer group by design (A3), and the outer run is stamped before\n * any inner directive has been resolved.\n */\nexport const VANTAGE_COLLAPSED = [\"true\", \"false\"] as const;\n\n/**\n * Where a block sits in a stamped run, so section-wide CSS can join its members\n * without an adjacent-sibling combinator.\n *\n * Not cosmetic. Review mode inserts comment cards as siblings *inside* a\n * stamped run (`useReviewHighlights`), so `[tone] + [tone]` severs at every\n * commented paragraph and bleeds across the boundary between two adjacent runs\n * of different tone. An attribute survives both.\n */\nexport const VANTAGE_RUNS = [\"start\", \"middle\", \"end\", \"only\"] as const;\n\n/**\n * The tags a `section`/`block` directive may **target**.\n *\n * Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a directive's target should\n * also be a block with a `data-source-line`, so the styling surface and the\n * anchor surface coincide. It also keeps an inline directive from stamping the\n * `<em>` that happens to follow it inside a paragraph.\n *\n * It does **not** bound a `section`'s range. Every element in the span is\n * stamped, on the tag list or not, because a member only has to be a box in the\n * flow for the section's vertical rule to cross it — see `styleRange` in\n * `rehypeVantageDirectives.ts` for the hole that restricting the range left.\n *\n * It lives here rather than in the plugin because the CLI checker has to answer\n * \"will this directive stamp anything?\" from an mdast tree with no hast in\n * sight. A checker with its own copy of this list is a checker that calls a\n * working directive an orphan, or stays silent about a dead one (D5).\n */\nexport const VANTAGE_STYLE_TARGETS = [\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"li\",\n \"blockquote\",\n \"pre\",\n \"table\",\n \"tr\",\n \"ul\",\n \"ol\",\n \"hr\",\n \"div\",\n] as const;\n\n/**\n * The tags a `fallback` directive may **withhold**: `VANTAGE_STYLE_TARGETS`\n * minus the six headings, written as a subtraction so the narrowing stays\n * visible.\n *\n * A heading is refused because it is the document's structure rather than a\n * stand-in for anything: the contents column lists it, other documents link to\n * its slug, and the planning index reads sections under it. A fallback heading\n * would be an anchor that exists on GitHub and nowhere in Vantage. What a\n * fallback holds — a sentence naming the release a drawing needs, a link to\n * the drawing as a file, an ASCII sketch in a fence — is a paragraph, a list,\n * a quote, a code block, a table or a `<div>`, and those are all here.\n *\n * Three callers read it, and they must agree: `rehypeVantageDirectives`, which\n * withholds the block; the planning index's scan, which must not read a\n * question or a link from a block the page never shows; and the checker's\n * `vantage/orphan`, which reports a fallback that withholds nothing.\n */\nexport const VANTAGE_FALLBACK_TARGETS = VANTAGE_STYLE_TARGETS.filter(\n (tag) => !/^h[1-6]$/.test(tag),\n);\n\n/**\n * The tags a question directive, `oq` or `question`, may stamp — strictly the\n * tags the review system can resolve an anchor on (`ANCHOR_TAGS` in the app's\n * `MarkdownViewer`, and the block map in `useReviewHighlights`). `ul`, `ol`,\n * `tr`, `hr` and `div` are in neither, so a button on one of them would build\n * an anchor no review pass can find — the \"mis-wired button\" D6 forbids.\n *\n * The gap between this list and `VANTAGE_STYLE_TARGETS` is why a question\n * directive at column 0 above a list silently does nothing: the target is the\n * `<ul>`, not the `<li>`. The checker says so.\n */\nexport const VANTAGE_ANCHOR_TARGETS = [\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"li\",\n \"blockquote\",\n \"pre\",\n \"table\",\n] as const;\n\n/**\n * The tags a question directive declares a question on, and so the only ones\n * an open question's *button* can sit on — `VANTAGE_ANCHOR_TARGETS` minus `pre`\n * and `table`, written as an explicit subtraction so the narrowing stays\n * visible. One list for both names and every state, so a question that changes\n * state is declared in exactly the same place.\n *\n * Anchorable and button-hosting are different questions, and this is the second\n * one. A comment *can* be anchored on a `<pre>` or a `<table>` — both are in\n * `ANCHOR_TAGS` — but neither can hold the affordance: inside a `<pre>` the\n * button renders as part of the code, and a `<button>` child of `<table>` is not\n * valid HTML at all, so the parser hoists it out.\n *\n * Every consumer reads it from here: `OQ_HOST_TAGS` in the app's\n * `useOpenQuestionButtons`, the planning index's scan, and the question branch\n * of the checker's `vantage/orphan`. They were two hand-written lists that\n * disagreed — the checker called an `oq` above a fence fine while the app\n * rendered no button and said nothing, which is the D5 break this module\n * exists to prevent.\n */\nexport const VANTAGE_OQ_HOST_TARGETS = VANTAGE_ANCHOR_TARGETS.filter(\n (tag) => tag !== \"pre\" && tag !== \"table\",\n);\n\n/**\n * The shape of a question directive's `id`, on `oq` and `question` alike:\n * `OQ-` then an optional short uppercase prefix then digits. `OQ-9`, `OQ-TP6`\n * and `OQ-A03` are ids; `OQ-foo`, `OQ-tp6` and a bare `OQ6` are not.\n *\n * The prefix is what keeps ids distinct once one document references another's\n * questions — `trust-paths.md`'s `OQ-4` and a design sketch's `OQ-4` are\n * different questions, and a bare number cannot say which one a cross-document\n * reference means. It is optional because most documents never leave their own\n * file, and requiring it everywhere would fire on every single-doc sketch.\n *\n * Read from here, never re-spelled, by the sanitizer that allowlists the value,\n * by the checker's `collectOqIds` (which feeds `vantage/oq-id-format` and the\n * anchors the checker accepts) and by the planning index. The directive plugin\n * that stamps the anchor deliberately does not test it: it stamps any non-empty\n * id and leaves refusing a malformed one to the sanitizer. A re-spelled copy is\n * how the checker starts calling a working anchor malformed — and the patterns\n * that look for an id in text (`OQ_REFERENCE` and `OQ_EXACT` in the checker's\n * `rules/references.ts`, `OQ_TOKEN` in `planning/scan.ts` and in\n * `FrontmatterDisplay.tsx`) restate this shape, so they change with it.\n */\nexport const VANTAGE_OQ_ID = /^OQ-(?:[A-Z][A-Z0-9]{0,5})?[0-9]+$/;\n\n/**\n * The status emoji the documentation convention marks an Open Question with, and\n * what each one means to a reader deciding whether the question wants them.\n *\n * These are *prose* — ordinary characters in the question's title, not part of\n * any directive — which is exactly why they need a home that both readers of\n * them can import. Two consumers ask what a marker means and they must not\n * answer differently: the checker's `vantage/oq-missing`, which demands a\n * directive on an open question and must never demand one on a blocked one, and\n * the viewer's contents column, which shows the marker and needs a word for it\n * that a screen reader can say. They were private constants in the checker until\n * the second consumer arrived.\n *\n * `open` is the only state that wants a one-click answer, so it is the only one\n * a viewer offers Take this leaning on (`questionOffersTake`). A `settled` or\n * `blocked` question carries no control — a control offering to answer a\n * question that is already decided, or that cannot be answered yet, is a lie.\n * The checker keys on the distinction rather than on the word \"Leaning:\"\n * alone. Either non-open marker wins when both appear on one item.\n */\nexport const VANTAGE_OQ_STATUS = {\n /** 💬 — an active decision awaiting a ruling. */\n open: \"\\u{1F4AC}\",\n /** ✅ — decided, awaiting compaction into a Decision Ledger. */\n settled: \"\\u2705\",\n /** 🔒 — blocked on an upstream decision or experiment. */\n blocked: \"\\u{1F512}\",\n} as const;\n\n/** One of the convention's three states, as [VANTAGE_OQ_STATUS] names them. */\nexport type VantageOqStatus = keyof typeof VANTAGE_OQ_STATUS;\n\n/**\n * What a marker says, in words, for somewhere an emoji cannot go.\n *\n * An accessible name is the reason this exists: a contents entry whose whole\n * status is one glyph says nothing to a screen reader, and \"speech bubble\" —\n * which is what it would otherwise read out — is worse than nothing.\n */\nexport const VANTAGE_OQ_STATUS_LABEL: Readonly<\n Record<VantageOqStatus, string>\n> = {\n open: \"Open question\",\n settled: \"Answered question\",\n blocked: \"Blocked question\",\n};\n\n/**\n * The state `text` is marked with, or `null` when it carries no marker.\n *\n * Non-open wins over open, the resolution `vantage/oq-missing` has always made:\n * a question marked both 💬 and ✅ has been answered and the stale marker simply\n * has not been cleared yet, so treating it as open would re-open a ruling.\n */\nexport function vantageOqStatus(text: string): VantageOqStatus | null {\n if (text.includes(VANTAGE_OQ_STATUS.settled)) return \"settled\";\n if (text.includes(VANTAGE_OQ_STATUS.blocked)) return \"blocked\";\n if (text.includes(VANTAGE_OQ_STATUS.open)) return \"open\";\n return null;\n}\n\n/**\n * Whether a question in this state offers Take this leaning in review mode: an\n * open one, marked 💬 or 💬 🤷, or one carrying no marker, which counts as\n * open. A 🔒 question cannot be answered yet and a ✅ one has been ruled, so\n * neither has a leaning left to take, whatever its directive's name.\n *\n * One home for the rule, because every surface that answers it must agree:\n * the page's button and the planning card, the counts of questions waiting on\n * a human, and `vantage-check`, which reports an `oq` on a question this says\n * no to, since every viewer before 0.8 offers the button there anyway.\n */\nexport function questionOffersTake(status: VantageOqStatus | null): boolean {\n return status === null || status === \"open\";\n}\n\n/** `null` for a key the grammar accepts but no closed set covers. */\nexport type KeyVocabulary = readonly string[] | null;\n\n/** The keys one directive name accepts. `undefined` for an unknown key. */\nexport type KeyTable = Readonly<Record<string, KeyVocabulary | undefined>>;\n\n/** The whole vocabulary. `undefined` for an unknown directive name. */\nexport type DirectiveVocabulary = Readonly<\n Record<string, KeyTable | undefined>\n>;\n\nconst STYLE_KEYS: KeyTable = {\n tone: VANTAGE_TONES,\n emphasis: VANTAGE_EMPHASIS,\n badge: VANTAGE_BADGES,\n collapsed: VANTAGE_COLLAPSED,\n};\n\n/**\n * Name → key → the closed value set for that key.\n *\n * `section` and `block` share their keys: they differ in *extent*, not in what\n * they can say. The two question names share theirs too, and they are the\n * design's only values with no closed set — `id` is a token an author chose\n * and `leaning` is a sentence (§8.3) — so neither can be value-allowlisted,\n * which is recorded here as `null` rather than left to a caller to guess.\n *\n * `question` takes exactly `oq`'s keys and nothing more. Starting narrow is\n * the forward-compatible choice: a key added later is one older viewers drop\n * pair by pair, while a key accepted now could never be given a meaning (P0).\n * A `leaning` on a 🔒 or ✅ question is allowed and simply not offered\n * (`questionOffersTake`), so changing a question's state changes only its\n * marker.\n *\n * `fallback` takes no keys: whether a block is withheld is the whole of what\n * it says, and a key added later is dropped pair by pair by this release, as\n * D2 drops any other.\n */\nexport const DIRECTIVE_VOCABULARY: DirectiveVocabulary = {\n section: STYLE_KEYS,\n block: STYLE_KEYS,\n oq: { id: null, leaning: null },\n question: { id: null, leaning: null },\n fallback: {},\n};\n\n/**\n * The one question a run of directives declares, merged: the name that won,\n * and its keys.\n */\nexport interface QuestionRun {\n /** `oq` when the run holds one, otherwise `question`. */\n name: VantageQuestionName;\n /** Every key the winning name's directives set that it accepts, merged. */\n keys: Map<string, string>;\n /** Which directive of the run, by index, each merged key's value came from. */\n sources: Map<string, number>;\n}\n\n/**\n * Merge the question directives of one run — the comments up to the block they\n * land on — into the one question they declare, or `undefined` when the run\n * holds none. Directives of any other name are skipped.\n *\n * A run holding an `oq` is read exactly as a viewer that predates `question`\n * reads it: that viewer drops every `question` and reads the `oq` alone, so\n * the `oq` wins wherever it appears in the run, and only its keys apply — a\n * key only a `question` in the same run sets applies nowhere, in no release.\n * That is the one reading every viewer agrees on (`VANTAGE_QUESTION_NAMES`),\n * so the same bytes file the same Take in 0.7 and in 0.8. Within one name, the\n * last value of a key wins, as `section` and `block` resolve theirs. A pair\n * its name does not accept, or a value outside a closed set, is dropped (D2).\n *\n * One function for every reader of a run: the plugin that stamps the question\n * and the planning index that reads it, which the checker reads a question\n * through, so none can disagree about which leaning or which id a run carries\n * (D5).\n */\nexport function mergeQuestionRun(\n run: readonly {\n name: string;\n pairs: readonly { key: string; value: string }[];\n }[],\n): QuestionRun | undefined {\n let name: VantageQuestionName | undefined;\n for (const directive of run) {\n if (!isQuestionDirective(directive.name)) continue;\n if (name !== \"oq\") name = directive.name;\n }\n if (name === undefined) return undefined;\n const accepted = DIRECTIVE_VOCABULARY[name] ?? {};\n const keys = new Map<string, string>();\n const sources = new Map<string, number>();\n run.forEach((directive, index) => {\n if (directive.name !== name) return;\n for (const pair of directive.pairs) {\n const values = accepted[pair.key];\n if (values === undefined) continue;\n if (values !== null && !values.includes(pair.value)) continue;\n keys.set(pair.key, pair.value);\n sources.set(pair.key, index);\n }\n });\n return { name, keys, sources };\n}\n\nexport interface DirectivePair {\n key: string;\n /** The value with quotes stripped, if it was quoted. */\n value: string;\n /** Offset of `key` within the comment's inner text. */\n keyOffset: number;\n /** Offset of the value token — opening quote included — within it. */\n valueOffset: number;\n quoted: boolean;\n}\n\nexport interface ParsedDirective {\n kind: \"directive\";\n name: string;\n /** Offset of `name` within the comment's inner text. */\n nameOffset: number;\n /** In written order, duplicates included: a checker reports them, the\n * renderer resolves them last-one-wins. */\n pairs: DirectivePair[];\n}\n\n/** Sentinel present, grammar not satisfied. The renderer ignores `reason`. */\nexport interface MalformedDirective {\n kind: \"malformed\";\n /** One clause a checker can quote verbatim, lowercase and unpunctuated. */\n reason: string;\n /** Offset of the first character the parse could not use. */\n offset: number;\n}\n\nexport type DirectiveParse = ParsedDirective | MalformedDirective | null;\n\n/**\n * `ws` is `[ \\t\\r\\n]` — the design's grammar leaves it undefined, and `\\n` has\n * to be in the set because a directive may legally wrap: a multi-line comment\n * is one node whose value contains the newlines.\n */\nconst WS = /[ \\t\\r\\n]*/y;\nconst SENTINEL_PREFIX = /^[ \\t\\r\\n]*vantage:/;\nconst NAME = /[a-z][a-z0-9-]*/y;\nconst UNQUOTED = /[A-Za-z0-9_.:#-]+/y;\n/**\n * A quoted value holds anything but a `\"`, `--` included: measured through the\n * real chain, `leaning=\"a--b\"` reaches the tree intact, because HTML5 closes a\n * comment on `-->` or `--!>` and on nothing else. There is deliberately **no**\n * `--` restriction here. What a quoted value cannot hold is a terminator: a\n * `-->` inside one ends the comment early and spills the tail into the document\n * as literal text, which is a finding for the checker rather than a rule here —\n * by the time this function runs, the truncation has already happened.\n */\nconst QUOTED = /\"[^\"]*\"/y;\n\n/**\n * The cheap prefix test. Runs first on every comment in every document, so an\n * ordinary editorial comment never reaches the tokenizer.\n *\n * Note `<!--- vantage: x -->` is *not* a directive: its inner text begins with\n * the extra `-`, and the sentinel must be the first thing in the comment.\n */\nexport function hasVantageSentinel(comment: string): boolean {\n return SENTINEL_PREFIX.test(comment);\n}\n\n/** The whole non-whitespace run at `offset`, capped, for a quotable message. */\nfunction token(comment: string, offset: number): string {\n const rest = comment.slice(offset);\n const end = rest.search(/[ \\t\\r\\n]/);\n const word = end === -1 ? rest : rest.slice(0, end);\n return word.length > 24 ? `${word.slice(0, 24)}…` : word;\n}\n\n/** The sticky match at `offset`, or `null` if the pattern does not apply. */\nfunction matchAt(\n pattern: RegExp,\n comment: string,\n offset: number,\n): string | null {\n pattern.lastIndex = offset;\n const match = pattern.exec(comment);\n return match === null ? null : match[0];\n}\n\n/** How much whitespace sits at `offset`. `WS` matches everywhere, empty. */\nfunction skipWhitespace(comment: string, offset: number): number {\n return matchAt(WS, comment, offset)?.length ?? 0;\n}\n\nfunction malformed(reason: string, offset: number): MalformedDirective {\n return { kind: \"malformed\", reason, offset };\n}\n\n/**\n * Parse one comment's **inner** text — the value of a hast `comment` node, with\n * `<!--` and `-->` already stripped. `null` means \"no sentinel, not ours\".\n *\n * Hand-rolled rather than one regular expression, because a repeated capture\n * group keeps only its last match and the checker needs an offset per token to\n * point at the character that broke.\n */\nexport function parseVantageDirective(comment: string): DirectiveParse {\n const sentinel = SENTINEL_PREFIX.exec(comment);\n if (sentinel === null) return null;\n\n let at = sentinel[0].length;\n at += skipWhitespace(comment, at);\n\n const nameOffset = at;\n const name = matchAt(NAME, comment, at);\n if (name === null) {\n return malformed(\"no directive name after `vantage:`\", at);\n }\n at += name.length;\n\n const pairs: DirectivePair[] = [];\n while (at < comment.length) {\n const gap = skipWhitespace(comment, at);\n at += gap;\n if (at >= comment.length) break;\n if (gap === 0) {\n return malformed(`\\`${token(comment, at)}\\` needs a space before it`, at);\n }\n\n const keyOffset = at;\n const key = matchAt(NAME, comment, at);\n if (key === null) {\n return malformed(\n `\\`${token(comment, at)}\\` is not a \\`key=value\\` pair`,\n at,\n );\n }\n at += key.length;\n\n if (comment[at] !== \"=\") {\n return malformed(`\\`${key}\\` is not followed by \\`=value\\``, at);\n }\n at += 1;\n\n const valueOffset = at;\n const quoted = matchAt(QUOTED, comment, at);\n if (quoted !== null) {\n at += quoted.length;\n pairs.push({\n key,\n value: quoted.slice(1, -1),\n keyOffset,\n valueOffset,\n quoted: true,\n });\n continue;\n }\n\n const unquoted = matchAt(UNQUOTED, comment, at);\n if (unquoted === null) {\n const found = token(comment, at);\n return malformed(\n found === \"\"\n ? `\\`${key}=\\` has no value`\n : `\\`${found}\\` is not a valid value for \\`${key}\\``,\n at,\n );\n }\n at += unquoted.length;\n pairs.push({ key, value: unquoted, keyOffset, valueOffset, quoted: false });\n }\n\n return { kind: \"directive\", name, nameOffset, pairs };\n}\n\n/**\n * 🤷 — the second glyph of `💬 🤷`, which marks an open question the author\n * flags as a matter of preference rather than a decision with stakes.\n *\n * Not a state: a question marked `💬 🤷` is open, and `vantageOqStatus` says so.\n * The planning index records the flag beside the state, so this lives with the\n * three state glyphs rather than in the index, where a second spelling of the\n * convention's markers would start.\n */\nexport const VANTAGE_OQ_PREFERENCE = \"\\u{1F937}\";\n\n/**\n * The longest leaning a question directive carries into the page, in\n * characters.\n *\n * A leaning becomes the body of a review comment, not prose, so it is bounded;\n * 500 is generous for the one sentence the convention asks for.\n */\nexport const MAX_LEANING = 500;\n\n/**\n * A question directive's `leaning=` value as the page carries it: whitespace\n * runs collapsed to one space, trimmed, and capped at `MAX_LEANING`.\n *\n * A wrapped directive puts newlines and indentation into the value. Two readers\n * need the result and must agree on it: `rehypeVantageDirectives`, which stamps\n * it as `data-vantage-leaning` for the one-click answer, and the planning index,\n * which reports the leaning a question carries. An empty result means the\n * question states no leaning.\n */\nexport function normalizeLeaning(raw: string): string {\n return raw.replace(/\\s+/g, \" \").trim().slice(0, MAX_LEANING);\n}\n","/**\n * Rehype plugin that compiles `<!-- vantage: … -->` directives into\n * `data-vantage-*` attributes on the block that follows them.\n *\n * It has to run between `rehype-raw` — which turns the comment into a hast node\n * — and `rehype-sanitize`, which deletes every comment node. That is the only\n * window in which the information exists (`docs/reference/inline-markup.md`, \"Where the plugin runs\"),\n * and `pipeline.ts` is where the slot is spelled out.\n *\n * The grammar and the vocabulary live in `./vantageDirectives.js`, which the\n * CLI checker imports too: one parser, two callers, so a directive cannot mean\n * one thing in the viewer and another in the tool that validates it (D5).\n *\n * Nothing here throws and nothing logs. An unknown name drops the whole\n * directive, an unknown key or value drops that pair only, and a directive with\n * no block after it does nothing at all (P3/D2/D6). The comment node is left\n * where it is: the sanitizer removes it, which is why no Vantage-specific\n * markup other than these attributes ever reaches the DOM.\n *\n * A `fallback` run is the one exception, and it stamps nothing: the run and\n * the block it lands on are removed from the tree, in a pass of their own that\n * runs before any stamping (`withholdFallbacks`). Removing the run's comments\n * with the block is what keeps a `block tone=…` written in the same run from\n * re-targeting the block after it, and nothing is lost, because the sanitizer\n * would have deleted those comments a few plugins later.\n */\n\nimport type { Element, Parents, Properties, RootContent, Root } from \"hast\";\nimport type { Plugin } from \"unified\";\nimport {\n DIRECTIVE_VOCABULARY,\n mergeQuestionRun,\n normalizeLeaning,\n parseVantageDirective,\n VANTAGE_ANCHOR_TARGETS,\n VANTAGE_FALLBACK_TARGETS,\n VANTAGE_STYLE_TARGETS,\n} from \"./vantageDirectives.js\";\nimport type {\n KeyVocabulary,\n ParsedDirective,\n VantageQuestionName,\n} from \"./vantageDirectives.js\";\n\n/**\n * What a `section`/`block` and a question directive (`oq`, `question`) may\n * **target**.\n *\n * Both lists live in `vantageDirectives.ts`, with the reasoning for each tag,\n * because the CLI checker resolves the same question over mdast and must reach\n * the same answer (D5).\n *\n * Neither list bounds a `section`'s range: see `styleRange`.\n */\nconst STYLE_TARGET_TAGS = new Set<string>(VANTAGE_STYLE_TARGETS);\nconst ANCHOR_TARGET_TAGS = new Set<string>(VANTAGE_ANCHOR_TARGETS);\n/** What a `fallback` may withhold: every style target but a heading. */\nconst FALLBACK_TARGET_TAGS = new Set<string>(VANTAGE_FALLBACK_TARGETS);\n\nconst HEADING_DEPTHS = new Map([\n [\"h1\", 1],\n [\"h2\", 2],\n [\"h3\", 3],\n [\"h4\", 4],\n [\"h5\", 5],\n [\"h6\", 6],\n]);\n\n/**\n * Key → hast property, for the keys that treat a whole run.\n *\n * A camelCase hast property serializes to the kebab-case attribute, so\n * `dataVantageTone` is `data-vantage-tone` in every renderer.\n *\n * `tone` and `emphasis` describe what a section *is* and how loud it is, so\n * every block in the range wears them: the tone rule is a slice of one\n * continuous line down the section, and the weight applies to all of its prose.\n *\n * `collapsed` is not here because it is not one property on one block: it puts a\n * toggle on the heading and a collapsed flag plus a group id on every block the\n * heading hides, which `stampStyle` does with the three properties below.\n */\nconst RANGE_PROPERTIES = new Map([\n [\"tone\", \"dataVantageTone\"],\n [\"emphasis\", \"dataVantageEmphasis\"],\n]);\n\n/**\n * Key → hast property, for the keys that mark one block: the directive's target.\n *\n * `badge` is the asymmetry in the vocabulary and the reason this second map\n * exists. It is not a treatment of a run but a single chip — \"a small chip after\n * the heading text\" (§4.3), drawn as `[data-vantage-badge]::after` — so a\n * section-wide stamp paints the word once per paragraph, list, table and fence\n * under the heading instead of once beside it.\n *\n * The chip is fixed here rather than in the stylesheet, because narrowing the\n * CSS to `:is(h1, …, h6)` would silently draw nothing for the two placements\n * that legitimately badge a non-heading — `block badge=…` on a paragraph, and a\n * `section` that degraded onto one (A1) — and would leave an attribute stamped\n * on every block that says something untrue about it.\n */\nconst POINT_PROPERTIES = new Map([[\"badge\", \"dataVantageBadge\"]]);\n\nconst RUN_PROPERTY = \"dataVantageRun\";\n/**\n * What a question stamps, whichever name declared it: `data-vantage-question`\n * on every question, and `data-vantage-oq` besides on one an `oq` declared\n * (`VANTAGE_QUESTION_ATTRIBUTE`, `VANTAGE_OQ_ATTRIBUTE`).\n *\n * Two attributes, and neither one ever means more than it says. Every release\n * since 0.7 stamps `data-vantage-oq` on an `oq` and on nothing else, so a\n * reader of this package's markup that has ever read it keeps reading exactly\n * what it read; `data-vantage-question` is the one to read for \"a question\",\n * and the question's state, from its marker, is what decides whether anything\n * offers to answer it (`questionOffersTake`).\n */\nconst QUESTION_PROPERTY = \"dataVantageQuestion\";\nconst OQ_PROPERTY = \"dataVantageOq\";\nconst LEANING_PROPERTY = \"dataVantageLeaning\";\n/**\n * The id, carried as a `data-` attribute rather than written straight to `id`.\n *\n * This plugin runs *before* `rehypeSanitize` — it has to, it reads comments and\n * the sanitizer deletes them — and the sanitizer's default schema clobbers `id`\n * with the prefix `user-content-`. A bare `id` set here would reach the page as\n * `user-content-OQ-4`, every `#OQ-4` link in every document would land nowhere,\n * and nothing would error. `rehypeVantageAnchors` promotes this to a real `id`\n * on the other side of the sanitizer, which is the same reason `rehypeSlug` is\n * registered there (`pipeline.ts`).\n */\nconst OQ_ID_PROPERTY = \"dataVantageOqId\";\n\n/**\n * The three properties `collapsed=true` stamps across a section.\n *\n * The heading takes a *different* attribute from the blocks it hides, and that\n * asymmetry is the whole design (A3): a nested `###` inside a collapsed `##` is\n * both a hidden member of the outer group and the toggle for its own, so one\n * shared attribute would make it permanently invisible and unreachable by\n * either toggle. There is no `<details>` and no wrapper — the run stays a flat\n * list of siblings, which is what keeps review comment cards, the typography\n * plugin's `h2 + *` margin resets and the anchor surface working.\n *\n * Hiding is CSS, and that CSS is gated on two markers the toggle JS sets — the\n * prose container's readiness, and an armed marker on each block whose group it\n * gave a caret (`docs/reference/inline-markup.md`, \"Collapse without a wrapper\"). A renderer without the JS\n * — the CLI checker's HTML, an external consumer of this package — shows every\n * block, and so does any block that ended up with no control.\n */\nconst COLLAPSED_PROPERTY = \"dataVantageCollapsed\";\nconst COLLAPSE_GROUP_PROPERTY = \"dataVantageCollapseGroup\";\nconst COLLAPSE_TOGGLE_PROPERTY = \"dataVantageCollapseToggle\";\n\n/**\n * Per-tree state. Group ids are `1`, `2`, `3`… in the document order of the\n * headings that own them, so the same document always numbers the same way and\n * an inner section always draws a higher number than the section enclosing it.\n *\n * It lives in the transformer's closure rather than at module scope: a counter\n * shared between trees would renumber a document because another one rendered\n * first, and `renderMarkdown` running twice in one process has to produce\n * byte-identical HTML.\n */\ninterface CollapseState {\n nextGroup: number;\n}\n\n/**\n * Nodes that may sit between a directive and its target.\n *\n * A whitespace-only `text` node always does — measured, with or without a blank\n * line in the source. Comments do too, and an unrelated `<!-- TODO -->` must not\n * break the chain: it is invisible in every renderer and deleted by the\n * sanitizer, so letting it change a directive's meaning would make behavior\n * depend on something no reader can see.\n */\nfunction isSkippable(node: RootContent): boolean {\n if (node.type === \"comment\" || node.type === \"doctype\") return true;\n if (node.type === \"text\") return node.value.trim() === \"\";\n return false;\n}\n\nfunction headingDepth(node: RootContent): number | undefined {\n if (node.type !== \"element\") return undefined;\n return HEADING_DEPTHS.get(node.tagName);\n}\n\nfunction setProperty(element: Element, property: string, value: string) {\n element.properties = element.properties ?? ({} as Properties);\n element.properties[property] = value;\n}\n\n/**\n * Where a member sits in a stamped run: `only` for a lone block, otherwise\n * `start`, `middle`, `end`. See `VANTAGE_RUNS`.\n */\nfunction runValue(index: number, length: number): string {\n if (length === 1) return \"only\";\n if (index === 0) return \"start\";\n return index === length - 1 ? \"end\" : \"middle\";\n}\n\n/** The closed value set for one key, or `undefined` when the key is unknown. */\nfunction vocabularyOf(name: string, key: string): KeyVocabulary | undefined {\n return DIRECTIVE_VOCABULARY[name]?.[key];\n}\n\nfunction accepts(name: string, key: string, value: string): boolean {\n const values = vocabularyOf(name, key);\n if (values === undefined) return false;\n return values === null || values.includes(value);\n}\n\n/**\n * The nodes one style directive reaches, as indexes into its own parent's\n * children — never outside that array, so a directive inside a blockquote or a\n * list item cannot stamp past it.\n *\n * Position picks the target (the next sibling element); the name picks how far\n * the stamp goes. `section` before a heading takes the heading and every\n * following sibling until the first heading of the same or shallower depth;\n * `section` before anything else degrades to that one block, and `block` is\n * always that one block. A heading nested inside a stamped `blockquote` or\n * `li` does not end the section: the walk never descends.\n *\n * **Every element in the span, not only a `VANTAGE_STYLE_TARGETS` one.** That\n * list gates the *target* and nothing else. Restricting the range to it as well\n * used to leave a raw-HTML `<figure>`, `<dl>` or `<details>` unstamped between\n * two stamped paragraphs — and the section's one continuous vertical rule is\n * drawn per member, so an unstamped member is a hole the height of the block\n * plus its margins. Measured over the real stylesheet: 44px for a one-line\n * `<figure>`, against the 40px a neighbor can bleed upward, and arbitrarily\n * large for anything taller. `collapsed=true` had the same shape of bug the\n * other way round — it hid the paragraphs and left the figure on the page.\n *\n * The two lists answering different questions is the point, not an oversight:\n * a *target* must be a block a review anchor can name, because a directive\n * pointing at something unanchorable is a directive with no addressable effect.\n * A *member* only has to be a box in the flow, because all it does is carry the\n * run's tone across itself.\n */\nfunction styleRange(\n children: RootContent[],\n targetIndex: number,\n name: string,\n): number[] {\n const range = [targetIndex];\n const depth =\n name === \"section\" ? headingDepth(children[targetIndex]) : undefined;\n if (depth === undefined) return range;\n\n for (let i = targetIndex + 1; i < children.length; i++) {\n const node = children[i];\n const nodeDepth = headingDepth(node);\n if (nodeDepth !== undefined && nodeDepth <= depth) break;\n if (node.type === \"element\") range.push(i);\n }\n return range;\n}\n\n/**\n * Whether this directive collapses its section — three ways to say no.\n *\n * `collapsed=false` stamps nothing: it is the default written down, and \"not\n * collapsed\" is not a thing an attribute can usefully say. Its only effect is\n * upstream of here — `stampRun` merges a run of comments last-key-wins, so a\n * `false` cancels a `true` in the *same* run. It does not cancel an *enclosing*\n * section: `styleRange` walks the outer heading's whole sibling span before any\n * inner directive is resolved, and a nested heading being a hidden member of the\n * outer group is the design (A3), not an oversight.\n *\n * A `block` scope is **dropped**, and so is a `section` that degraded onto\n * a non-heading, because both would hide a lone paragraph with nothing left\n * behind to reveal it — content that is simply gone, which is the P1/D8 failure\n * the readiness gate exists to prevent. Only a heading can be a summary.\n */\nfunction collapsesSection(\n name: string,\n pairs: Map<string, string>,\n target: RootContent,\n): boolean {\n if (name !== \"section\") return false;\n if (pairs.get(\"collapsed\") !== \"true\") return false;\n return headingDepth(target) !== undefined;\n}\n\nfunction stampStyle(\n children: RootContent[],\n targetIndex: number,\n name: string,\n pairs: Map<string, string>,\n state: CollapseState,\n) {\n const target = children[targetIndex] as Element;\n if (!STYLE_TARGET_TAGS.has(target.tagName)) return;\n\n // Resolve before stamping, and resolve the two reaches apart: a directive\n // whose every key was dropped stamps nothing at all, not even a run marker, so\n // `<!-- vantage: section -->` and `<!-- vantage: section tone=chartreuse -->`\n // are both plain documents.\n const rangeStamps: [string, string][] = [];\n const targetStamps: [string, string][] = [];\n for (const [key, value] of pairs) {\n if (!accepts(name, key, value)) continue;\n const rangeProperty = RANGE_PROPERTIES.get(key);\n if (rangeProperty !== undefined) {\n rangeStamps.push([rangeProperty, value]);\n continue;\n }\n const pointProperty = POINT_PROPERTIES.get(key);\n if (pointProperty !== undefined) targetStamps.push([pointProperty, value]);\n }\n const collapses = collapsesSection(name, pairs, target);\n if (rangeStamps.length === 0 && targetStamps.length === 0 && !collapses) {\n return;\n }\n\n const range = styleRange(children, targetIndex, name);\n // A heading with no body blocks gets no toggle: a caret that hides nothing is\n // an affordance that lies. The counter only advances for a group that exists,\n // so the ids stay dense.\n const group =\n collapses && range.length > 1 ? String(state.nextGroup++) : undefined;\n\n for (let i = 0; i < range.length; i++) {\n const element = children[range[i]] as Element;\n for (const [property, value] of rangeStamps) {\n setProperty(element, property, value);\n }\n // `range[0]` is the target, always: `styleRange` starts there and only ever\n // walks forward. A point marker stops here, and it is stamped before the run\n // marker so the attribute order of a badged heading is the order written.\n if (i === 0) {\n for (const [property, value] of targetStamps) {\n setProperty(element, property, value);\n }\n }\n // Only where a run treatment was stamped to join up: `run` describes the\n // extent of a tone's rule, and a collapse-only or badge-only section has no\n // rule to draw.\n if (rangeStamps.length > 0) {\n setProperty(element, RUN_PROPERTY, runValue(i, range.length));\n }\n if (group === undefined) continue;\n if (i === 0) {\n setProperty(element, COLLAPSE_TOGGLE_PROPERTY, group);\n } else {\n setProperty(element, COLLAPSED_PROPERTY, \"true\");\n setProperty(element, COLLAPSE_GROUP_PROPERTY, group);\n }\n }\n}\n\nfunction stampQuestion(\n target: Element,\n name: VantageQuestionName,\n pairs: Map<string, string>,\n) {\n // The string, never the boolean: `rehype-stringify` emits a bare\n // `data-vantage-oq` for `true` while react-markdown emits `=\"true\"`, and D5\n // requires every renderer to emit the same markup.\n setProperty(target, QUESTION_PROPERTY, \"true\");\n if (name === \"oq\") setProperty(target, OQ_PROPERTY, \"true\");\n\n // `id` becomes the block's anchor, so `[OQ-4](#OQ-4)` scrolls to the question.\n // Verbatim, case intact: heading slugs are lowercased by `github-slugger` and\n // these are author-chosen tokens that already appear verbatim in prose, so\n // the two id namespaces cannot collide by accident. A malformed id is stamped\n // anyway — the grammar is the checker's to enforce (`vantage/oq-id-format`),\n // and a renderer that silently dropped one would hide the mistake it is the\n // checker's job to name. The sanitizer's value allowlist is the backstop.\n const id = pairs.get(\"id\");\n if (id !== undefined && id !== \"\") setProperty(target, OQ_ID_PROPERTY, id);\n\n // On either name, in every state: whether the leaning is offered is the\n // viewer's reading of the question's state, never the renderer's.\n const leaning = pairs.get(\"leaning\");\n if (leaning === undefined) return;\n // A wrapped directive puts newlines and indentation in the value, and this is\n // about to become the body of a review comment, so collapse and cap it — the\n // same normalization the planning index reports a question's leaning with.\n const text = normalizeLeaning(leaning);\n if (text !== \"\") setProperty(target, LEANING_PROPERTY, text);\n}\n\n/**\n * Merge one run of directives onto one target, then stamp.\n *\n * Merging is defined on the tree, not on the source: every directive comment up\n * to the target merges, last-key-wins, whether or not blank lines separate\n * them. Measured — adjacent comments and comments separated by a blank line\n * produce byte-identical trees, so a rule that told them apart would have to\n * re-read line numbers to do it.\n */\nfunction stampRun(\n children: RootContent[],\n targetIndex: number,\n run: ParsedDirective[],\n state: CollapseState,\n) {\n const target = children[targetIndex] as Element;\n const style = new Map<string, string>();\n // The last style directive in the run decides the extent, on the same\n // last-one-wins principle that resolves a repeated key.\n let styleName: string | undefined;\n\n for (const directive of run) {\n if (directive.name === \"section\" || directive.name === \"block\") {\n styleName = directive.name;\n for (const pair of directive.pairs) style.set(pair.key, pair.value);\n }\n // A question directive is merged below, and any other name drops the\n // whole directive: there is no target semantics without a name. A\n // `fallback` reaching this point is one whose target is not a block it may\n // withhold — a heading — so it is inert here too, and the rest of its run\n // stamps as if it were not there.\n }\n\n if (styleName !== undefined) {\n stampStyle(children, targetIndex, styleName, style, state);\n }\n // One question per run, whichever names declared it, merged as every reader\n // of a run merges it (`mergeQuestionRun`).\n const question = mergeQuestionRun(run);\n if (question !== undefined && ANCHOR_TARGET_TAGS.has(target.tagName)) {\n stampQuestion(target, question.name, question.keys);\n }\n}\n\n/** A directive comment, or `undefined` for anything else — malformed included. */\nfunction directiveOf(node: RootContent): ParsedDirective | undefined {\n if (node.type !== \"comment\") return undefined;\n const parsed = parseVantageDirective(node.value);\n return parsed !== null && parsed.kind === \"directive\" ? parsed : undefined;\n}\n\n/**\n * One left-to-right pass over a parent's children, recursing into elements.\n *\n * The whole tree, not just the root: `rehype-raw` leaves comment nodes inside\n * `blockquote`, inside `li`, inside `td` and inline inside `p`, and the real\n * Open Questions layout puts the question directive inside a list item — so a\n * root-only walk finds none of them.\n *\n * Pass order is also what resolves a nested section: an inner heading's\n * directive necessarily sits at a higher child index than the outer directive\n * that ranged over it, so each property is simply last-write-wins.\n */\nfunction processChildren(parent: Parents, state: CollapseState) {\n const children = parent.children;\n let i = 0;\n while (i < children.length) {\n const node = children[i];\n if (node.type === \"element\") {\n processChildren(node, state);\n i++;\n continue;\n }\n\n const first = directiveOf(node);\n if (first === undefined) {\n i++;\n continue;\n }\n\n // Consume the run forward to the first element (the target) or the first\n // non-whitespace text (no target — the directive is inert). One pass, so a\n // run is never processed twice and document order is preserved.\n const run = [first];\n let j = i + 1;\n let targetIndex = -1;\n for (; j < children.length; j++) {\n const next = children[j];\n if (next.type === \"element\") {\n targetIndex = j;\n break;\n }\n if (!isSkippable(next)) break;\n const directive = directiveOf(next);\n if (directive !== undefined) run.push(directive);\n }\n\n if (targetIndex >= 0) stampRun(children, targetIndex, run, state);\n i = j; // resume at the target, or at the blocker — never inside the run\n }\n}\n\n/**\n * Remove every block a `fallback` run lands on, with the run itself.\n *\n * A fallback block is written for the renderers that cannot show something\n * Vantage shows — \"This drawing needs Vantage 0.8 or later\", beside an inline\n * `<svg>`. Every renderer that does not know the name drops the comment and\n * shows the block; this one never renders it, in any mode, in print, or\n * through any of the renderers that share this plugin (D5).\n *\n * The run is found exactly as `processChildren` finds one — the same skip\n * rules, the same first-element target, the same whole-tree walk — and it\n * withholds when any directive in it is a `fallback` and the target is on\n * `VANTAGE_FALLBACK_TARGETS`. Anything else is left for the stamping pass,\n * where a `fallback` is inert.\n *\n * A pass of its own, and first, because a `section` reaches *forward* over its\n * siblings: stamped in one pass, a toned section would count a fallback block\n * after it among its members, give it the run's last `end` marker, and leave\n * the last block anyone sees marked `middle`. Withheld first, the block was\n * never there for any later decision — the section's extent, its run markers,\n * a collapse group's members, a review anchor or an outline entry.\n */\nfunction withholdFallbacks(parent: Parents) {\n const children = parent.children;\n let i = 0;\n while (i < children.length) {\n const node = children[i];\n if (node.type === \"element\") {\n withholdFallbacks(node);\n i++;\n continue;\n }\n\n const first = directiveOf(node);\n if (first === undefined) {\n i++;\n continue;\n }\n\n let withholds = first.name === \"fallback\";\n let j = i + 1;\n let targetIndex = -1;\n for (; j < children.length; j++) {\n const next = children[j];\n if (next.type === \"element\") {\n targetIndex = j;\n break;\n }\n if (!isSkippable(next)) break;\n if (directiveOf(next)?.name === \"fallback\") withholds = true;\n }\n\n const target = targetIndex >= 0 ? children[targetIndex] : undefined;\n if (\n withholds &&\n target?.type === \"element\" &&\n FALLBACK_TARGET_TAGS.has(target.tagName)\n ) {\n // `i` now indexes whatever followed the block, which may start a run of\n // its own.\n children.splice(i, targetIndex - i + 1);\n continue;\n }\n i = j; // resume at the target, or at the blocker — never inside the run\n }\n}\n\nconst rehypeVantageDirectives: Plugin<[], Root> = () => {\n return (tree: Root) => {\n withholdFallbacks(tree);\n processChildren(tree, { nextGroup: 1 });\n };\n};\n\nexport default rehypeVantageDirectives;\n","/**\n * The two halves of one repair: carry a display-math block's own attributes\n * across the element swap `rehype-katex` performs.\n *\n * `$$…$$` (and a ` ```math ` fence) reaches rehype as `<pre><code\n * class=\"language-math\">`. `pre` is in `VANTAGE_STYLE_TARGETS`, so\n * `rehypeVantageDirectives` stamps it like any other block — it becomes a real\n * member of a toned section's run, and `rehypeSourceLines` has already given it\n * a `data-source-line`. Then `rehype-katex` reaches the same node, and for a\n * `code.language-math` inside a `pre` it takes the **`pre`** as its scope and\n * does `parent.children.splice(index, 1, …result)`: the stamped element is\n * *replaced* by a fresh `<span class=\"katex-display\">`, and every attribute on\n * it dies with it.\n *\n * Measured consequences, all three of them silent:\n *\n * - the section's vertical rule breaks across the formula. The rule is drawn\n * per member and bled upward by a fixed 40px, so the void is about the\n * formula's own height — 58px for a one-line fraction over the real\n * Tailwind build, more for a matrix — and the section reads as two;\n * - `#L` line anchors and review highlights stop resolving to the formula,\n * because `data-source-line` went with it;\n * - `collapsed=true` over a heading whose body includes a formula hides the\n * prose and leaves the formula on screen, since `data-vantage-collapsed`\n * never reached the span the toggle JS can see.\n *\n * The fix is to snapshot before and re-apply after, which is why this is a pair\n * and why the pair must bracket `rehype-katex` in `pipeline.ts`. Registering\n * only one half is inert, not wrong: capture alone writes to `file.data` and\n * nothing reads it, restore alone finds nothing to restore.\n *\n * Both halves run *after* `rehype-sanitize` — which is not a detail, twice\n * over. `rehype-sanitize` rebuilds the tree, so node identities taken before it\n * would all be stale; and every attribute carried here is one the sanitizer\n * already passed on the node it came from, so nothing here can reintroduce\n * markup the schema rejects.\n */\n\nimport type { Element, Parents, Properties, Root, RootContent } from \"hast\";\nimport type { Plugin } from \"unified\";\n\n/**\n * Where the snapshot lives between the two halves.\n *\n * `file.data` rather than a closure or a module-level map: `buildPipeline` is\n * allowed to be built once and run over many documents, and per-file state is\n * the only kind that cannot leak from one of those to the next.\n */\nconst CARRIED_KEY = \"vantageDisplayMathStamps\";\n\n/** The narrowest shape of the VFile these two need. */\ninterface StampFile {\n data: Record<string, unknown>;\n}\n\ninterface CarriedStamp {\n /** The stamped `<pre>`'s parent, which `rehype-katex` never replaces. */\n parent: Parents;\n /**\n * The sibling immediately before the `<pre>`, or `undefined` when it was the\n * first child. This is how the replacement is found again: surviving nodes\n * keep their identity across the splice, so the node one past the anchor is\n * whatever took the `<pre>`'s place, however many other blocks were rewritten\n * elsewhere in the tree.\n */\n anchor: RootContent | undefined;\n properties: Properties;\n}\n\nfunction classNames(node: Element): string[] {\n const value = node.properties?.className;\n return Array.isArray(value) ? value.map(String) : [];\n}\n\n/**\n * A `<pre>` `rehype-katex` will replace — its own condition, restated.\n *\n * `language-math` is the only class to test: `rehype-katex` keys the\n * pre-as-scope branch on it, and the sanitizer strips the `math-display` that\n * `remark-math` also emits (measured — a stamped fence arrives here with\n * `className: [\"language-math\"]` alone).\n */\nfunction isDisplayMath(node: RootContent): node is Element {\n if (node.type !== \"element\" || node.tagName !== \"pre\") return false;\n return node.children.some(\n (child) =>\n child.type === \"element\" &&\n child.tagName === \"code\" &&\n classNames(child).includes(\"language-math\"),\n );\n}\n\n/**\n * What is worth carrying: everything this pipeline stamped itself.\n *\n * Deliberately not the whole property bag. `className`, `style` and `id` belong\n * to the element KaTeX is about to build, and copying a `<pre>`'s onto a\n * `<span class=\"katex-display\">` would fight it.\n */\nfunction carriedProperties(properties: Properties | undefined): Properties {\n const carried: Properties = {};\n for (const [key, value] of Object.entries(properties ?? {})) {\n if (key === \"dataSourceLine\" || key.startsWith(\"dataVantage\")) {\n carried[key] = value;\n }\n }\n return carried;\n}\n\nfunction collect(parent: Parents, out: CarriedStamp[]) {\n const children: RootContent[] = parent.children;\n for (let i = 0; i < children.length; i++) {\n const node = children[i];\n if (node.type !== \"element\") continue;\n if (isDisplayMath(node)) {\n const properties = carriedProperties(node.properties);\n // An unstamped formula needs nothing carried, and recording it would only\n // give the restore pass a node to touch for no reason.\n if (Object.keys(properties).length > 0) {\n out.push({\n parent,\n anchor: i === 0 ? undefined : children[i - 1],\n properties,\n });\n }\n continue;\n }\n collect(node, out);\n }\n}\n\nfunction reapply(carried: CarriedStamp[]) {\n for (const { parent, anchor, properties } of carried) {\n // One annotation, because `Parents[\"children\"]` is a union of two array\n // types and `indexOf` on a union has no callable signature.\n const siblings: RootContent[] = parent.children;\n let index = 0;\n if (anchor !== undefined) {\n const at = siblings.indexOf(anchor);\n // The anchor was itself rewritten — two formulae with no node between\n // them, which mdast-to-hast does not produce (it separates siblings with\n // newline text nodes) but raw HTML could. Give up on this one rather than\n // guess: the result is the unrepaired gap, never a stamp on the wrong\n // block.\n if (at === -1) continue;\n index = at + 1;\n }\n const replacement = siblings[index];\n if (replacement === undefined || replacement.type !== \"element\") continue;\n // KaTeX emits `katex-display` normally and `katex-error` when the formula\n // does not parse; both are the block that took the `<pre>`'s place, and\n // anything else means the tree is not the shape this assumed.\n if (!classNames(replacement).some((name) => name.startsWith(\"katex\"))) {\n continue;\n }\n replacement.properties ??= {};\n for (const [key, value] of Object.entries(properties)) {\n replacement.properties[key] ??= value;\n }\n }\n}\n\n/** Snapshot every stamped display-math block. Register before `rehypeKatex`. */\nexport const rehypeCaptureMathStamps: Plugin<[], Root> = () => {\n return (tree: Root, file: StampFile) => {\n const carried: CarriedStamp[] = [];\n collect(tree, carried);\n file.data[CARRIED_KEY] = carried;\n };\n};\n\n/** Re-apply the snapshot. Register immediately after `rehypeKatex`. */\nexport const rehypeRestoreMathStamps: Plugin<[], Root> = () => {\n return (_tree: Root, file: StampFile) => {\n const carried = file.data[CARRIED_KEY];\n delete file.data[CARRIED_KEY];\n if (Array.isArray(carried)) reapply(carried as CarriedStamp[]);\n };\n};\n","/**\n * Sanitization schema for the rendering pipeline.\n * Allows GFM, KaTeX MathML, `data-source-line` attributes, a filtered inline\n * `style` on every element but `input` (see `UNSTYLED_TAGS`), a `class` that\n * carries only the names the pipeline itself emits (see `PIPELINE_CLASSES`),\n * and inline SVG as static drawing (see `SVG_CHILD_TAGS`), while blocking XSS\n * vectors and anything that lays a document over the app.\n */\n\nimport type { Element, Root } from \"hast\";\nimport { defaultSchema } from \"rehype-sanitize\";\nimport {\n VANTAGE_BADGES,\n VANTAGE_COLLAPSED,\n VANTAGE_EMPHASIS,\n VANTAGE_RUNS,\n VANTAGE_TONES,\n VANTAGE_OQ_ID,\n} from \"./vantageDirectives.js\";\nimport { ALERT_TITLE_CLASS, VANTAGE_ALERTS } from \"./rehypeVantageAlerts.js\";\n\ntype Schema = typeof defaultSchema;\ntype AttributeList = NonNullable<Schema[\"attributes\"]>[string];\ntype ClassValue = string | RegExp;\n\n/** Whether an attribute definition is for `className`. */\nconst isClassName = (definition: AttributeList[number]) =>\n (typeof definition === \"string\" ? definition : definition[0]) === \"className\";\n\n/**\n * The value list the default schema gives `className` on `tag`, or an empty\n * list if it gives none.\n */\nfunction defaultClasses(tag: string): ClassValue[] {\n const entry = defaultSchema.attributes?.[tag]?.find(isClassName);\n return Array.isArray(entry) ? (entry.slice(1) as ClassValue[]) : [];\n}\n\n/**\n * The only classes an element keeps: the ones the pipeline itself emits on it\n * before `rehypeSanitize` runs. Every other element keeps none.\n *\n * **A class is a style by another name.** The app ships Tailwind's utility CSS\n * for its own markup, so `<div class=\"fixed inset-0 z-50 bg-white\">` in a\n * document laid a white sheet over the whole Vantage window, header and\n * sidebar included. That is the overlay `SAFE_STYLE` bans `position` to\n * prevent, spelled as a class instead of a declaration, and `className` used\n * to be admitted with any value on every element. A document could also\n * borrow a class the app's own code looks for: `.overflow-y-auto` is how an\n * anchor link finds the element to scroll when no `data-content-scroll` is in\n * reach, and `.heading-anchor` is what the outline strips from a heading's\n * text. The comment-body sanitizer (`frontend/src/lib/commentMarkdown.ts`)\n * already refused `class` for the same reason.\n *\n * What each entry is for, and who emits it:\n *\n * - `code`: `language-*`, from a fence's info string (`mdast-util-to-hast`).\n * `rehype-highlight` reads it, `rehype-katex` keys on `language-math`, and\n * the viewers render `language-mermaid` as a diagram.\n * - `ul`, `ol` and `li`: `contains-task-list` and `task-list-item`, from GFM\n * task lists. `styles/task-list.css` draws the checkbox from them.\n * - `section`, `h2` and `a`: GFM footnotes (`mdast-util-to-hast`), on the\n * section, its label and each back-reference.\n * - `div`: `ALERT_TITLE_CLASS`, on the title `rehypeVantageAlerts` injects.\n *\n * `remark-math` also emits `math-display` and `math-inline` on its `code`.\n * They were never kept, because the default schema's `code` entry takes only\n * `language-*`, and nothing needs them: `rehype-katex` keys on `language-math`\n * and tells display from inline by whether the `code` sits in a `pre`.\n *\n * **A class added after the sanitizer needs no entry**, and that is most of\n * the classes the page styles: `rehype-highlight`'s `hljs-*`, KaTeX's output,\n * Mermaid's diagrams, and everything the app's components and hooks add. A\n * document that writes one of those on an element keeps nothing.\n *\n * **Mermaid source is the route this list cannot close.** Mermaid renders\n * after the sanitizer and copies the names a diagram's source gives a node\n * (`:::name`, `class A name`, `classDef`) onto that node, so a diagram can\n * carry any of the app's utilities on its own nodes. The diagram bounds it:\n * `position` does nothing on an SVG group, and the diagram's `svg` clips what\n * is inside it. `frontend/e2e/mermaid.spec.ts` measures that bound. The\n * stylesheet a diagram's source could write did reach past the diagram, and\n * `MERMAID_SECURE_KEYS` in `mermaidLoader.ts` refuses the settings that write\n * it.\n *\n * A document may still write these names on these elements. By the time the\n * sanitizer runs, a hand-written `<li class=\"task-list-item\">` and the one GFM\n * emitted are the same node, and none of the names can lay an element over\n * anything outside the document's scroll container. `styles/task-list.css`\n * floats the checkbox of a `task-list-item` instead of positioning it\n * absolutely, so it stays in the document's flow whatever the item's own\n * `style` does, and the checkbox takes no `style` itself (see\n * `UNSTYLED_TAGS`). The footnote label's class, where a stylesheet defines it,\n * clips its element to a single pixel.\n *\n * **The footnote label's class is read from the default schema, never\n * spelled here, and the tests spell it in halves.** It is a Tailwind utility,\n * and Tailwind generates a utility for every class name it finds in the files\n * it scans, comments included — this directory and all of `frontend/`. The app\n * renders the label as a visible heading only because nothing it scans spells\n * that name. Spelled whole in a test, it generated the rule, and the label\n * would have been hidden from sight as it is on GitHub: a change of rendering,\n * not a sanitizer fix.\n *\n * The list is measured, not remembered. The test \"takes no class from\n * anything the pipeline emits\" in `frontend/src/lib/sanitize.test.ts` records\n * every class on the tree on both sides of `rehypeSanitize`, over a document\n * that uses every feature that emits one, so a plugin that starts emitting a\n * new class fails there instead of losing it in every renderer.\n */\nconst PIPELINE_CLASSES: Readonly<Record<string, readonly ClassValue[]>> = {\n code: [/^language-./],\n ul: [\"contains-task-list\"],\n ol: [\"contains-task-list\"],\n li: [\"task-list-item\"],\n section: [\"footnotes\"],\n // The footnote label. Read, not spelled: see above.\n h2: defaultClasses(\"h2\"),\n a: [\"data-footnote-backref\"],\n div: [ALERT_TITLE_CLASS],\n};\n\n/**\n * `tag`'s attributes: the default schema's, less any `className`, then its\n * `PIPELINE_CLASSES` entry, then `extra`.\n *\n * **Always a value list, never a bare `\"className\"`.** `hast-util-sanitize`\n * reads a bare name, or a tuple holding no values, as \"any value\", and an\n * element's own entry is consulted before `*`'s — the first match wins, and\n * `*` is not consulted when that element's own entry filters a class list down\n * to nothing. An element with no entry has none added.\n */\nfunction withClasses(tag: string, ...extra: AttributeList): AttributeList {\n const classes = PIPELINE_CLASSES[tag] ?? [];\n const own: AttributeList = (defaultSchema.attributes?.[tag] ?? []).filter(\n (definition) => !isClassName(definition),\n );\n if (classes.length > 0) own.push([\"className\", ...classes]);\n return [...own, ...extra];\n}\n\n/**\n * CSS properties an inline `style` may set.\n *\n * **The only `style` this list ever filters is one a document wrote by hand.**\n * Nothing the pipeline generates reaches it: `rehypeKatex` and `rehypeHighlight`\n * both run *after* `rehypeSanitize` (`pipeline.ts`), so their output is trusted\n * rather than filtered, and `remark-gfm` emits table alignment as an `align`\n * attribute rather than as CSS. So this is a filter on untrusted author HTML and\n * nothing else — which is exactly the hole that made it necessary: `<div\n * style=\"position:fixed;inset:0\">` covered the viewport and\n * `style=\"background:url(https://…)\"` called home on render, both verbatim,\n * because `rehype-sanitize` does not parse CSS. Scripts were never the risk\n * here; layout and network were.\n *\n * The list is therefore deliberately typographic: the styling a prose document\n * has any business asking for. It is *not* sized to KaTeX, and a KaTeX release\n * that starts using a new property is a non-event here — `\\pmb` already emits\n * `text-shadow`, which is not on this list and renders anyway.\n *\n * **The design doc used to argue the opposite — that `style` had to be allowed\n * and `position` enumerated because KaTeX needs them — and it was wrong.** The\n * measurement behind it was real (KaTeX does emit `position:relative` on every\n * integral) but the inference was not, because the sanitizer has finished before\n * the first KaTeX span exists. Rebuilding the shipped rehype order with a filter\n * that rejects *every* value leaves all ten of the integral's style attributes\n * untouched. The \"Security\" section of `docs/reference/inline-markup.md`\n * records the correction; the test that would catch a reordering is in\n * `frontend/src/lib/sanitize.test.ts`.\n */\nconst SAFE_STYLE_PROPERTIES = [\n // Box metrics. `top`/`right`/`bottom`/`left` do nothing on an element no\n // stylesheet positions, and a document cannot position one, since `position`\n // is banned. The app positions headings and toned blocks relatively, and on\n // those they move the element no further than a negative `margin` would,\n // inside the scroll container that clips it. The one other element a\n // stylesheet positions, the task-list checkbox, takes no `style` at all (see\n // `UNSTYLED_TAGS`). They stay only because dropping them would fail the\n // whole attribute for a document that writes one — the all-or-nothing rule\n // below makes every removal a behavior change.\n \"height\",\n \"min-height\",\n \"max-height\",\n \"width\",\n \"min-width\",\n \"max-width\",\n \"top\",\n \"bottom\",\n \"left\",\n \"right\",\n \"margin\",\n \"margin-top\",\n \"margin-right\",\n \"margin-bottom\",\n \"margin-left\",\n \"padding\",\n \"padding-top\",\n \"padding-right\",\n \"padding-bottom\",\n \"padding-left\",\n // Rules and boxes.\n \"border\",\n \"border-style\",\n \"border-color\",\n \"border-width\",\n \"border-top-width\",\n \"border-right-width\",\n \"border-bottom-width\",\n \"border-left-width\",\n \"border-top-style\",\n \"border-right-style\",\n \"border-bottom-style\",\n \"border-left-style\",\n \"border-top-color\",\n \"border-right-color\",\n \"border-bottom-color\",\n \"border-left-color\",\n \"border-radius\",\n // Typography.\n \"color\",\n \"background-color\",\n \"font\",\n \"font-size\",\n \"font-style\",\n \"font-weight\",\n \"font-family\",\n \"font-variant\",\n \"line-height\",\n \"letter-spacing\",\n \"word-spacing\",\n \"text-align\",\n \"text-decoration\",\n \"text-indent\",\n \"white-space\",\n \"vertical-align\",\n \"list-style-type\",\n // Flow. `display` is not here: it takes only the values in `DISPLAY_VALUES`.\n \"float\",\n \"clear\",\n \"opacity\",\n \"overflow\",\n];\n\n/**\n * A `style` value we will keep, as a whole.\n *\n * One rule beyond the property list does the work: **no parentheses anywhere**,\n * which closes `url(…)` and `expression(…)` in one stroke — the network and\n * legacy-script vectors. Its cost is borne entirely by authors, who lose\n * `calc()`, `rgb()` and `var()` along with them; that is the trade, and it is\n * worth it for a filter this small.\n *\n * **`position` is not on the property list at all**, so every value of it is\n * refused — `static` and `relative` along with `fixed` and `sticky`. It used to\n * be enumerated, on the belief that KaTeX needed `relative`; KaTeX renders after\n * the sanitizer and never meets this regex, so the enumeration was buying\n * nothing but the residual it conceded. Banning the property closes that\n * residual, and it is bigger than \"overlaps its neighbors\" made it sound:\n * measured in Chrome against the viewer's real ancestor chain, an author's\n * `position:absolute;top:0;left:0;width:100%;height:100%` is sized to the whole\n * content pane (the nearest positioned ancestor is outside the scroll\n * container), is not clipped by the scroller, and survives scrolling to the end\n * of the document. It was `position:fixed` in all but the keyword.\n *\n * Matching is all-or-nothing: one unrecognized declaration drops the whole\n * attribute, and the element renders unstyled rather than partly styled. That\n * is the safe direction to fail, and it degrades to plain text rather than to a\n * broken page.\n *\n * **The grammar must stay unambiguous, and `;` is what keeps it so.** The value\n * class is \"anything but the delimiters\", which includes whitespace — a value\n * legitimately contains it (`margin: 0 auto`). So if whitespace could *also*\n * end a declaration, both constructs would compete for the same characters and\n * the match would fork at every declaration; on a value that ultimately fails,\n * the engine explores every fork. An earlier form of this regex separated\n * declarations with `\\s*;?\\s*`, and 200 document-controlled characters took the\n * renderer — and the CLI checker, and therefore CI — 94 seconds. Requiring `;`\n * pins each declaration's extent to the delimiter positions, so there is exactly\n * one way to parse any input and rejection is linear. `VALUE` absorbs the\n * padding on both sides for the same reason: a separate `\\\\s*` next to it would\n * put the ambiguity straight back. Pinned by the flat-time test in\n * `frontend/src/lib/sanitize.test.ts` — do not loosen the separator.\n *\n * Residual, stated plainly and now genuinely small: negative `margin` still lets\n * an element overlap its neighbors *inside the flow*. That one scrolls with the\n * content and is clipped by the scroll container, and closing it means giving up\n * margins, which prose actually uses. Containment in the stylesheet, not another\n * rule here, is what would close it. `overflow: visible` on an inline `<svg>` is\n * the same residual by another route — see `SVG_ROOT_ATTRIBUTES`.\n *\n * **The app's table of contents is inside that scroll container too**, beside\n * the document column, so a block carried over it took its clicks whenever the\n * block painted in the positioned layer: one with `opacity`, a toned block, a\n * heading, or a link to anywhere. The contents sit at `z-10` for that\n * (`frontend/src/components/TableOfContents.tsx`), and a document cannot climb\n * back over them only because `z-index` is not on the property list.\n */\nconst VALUE = `[^;:()\"'\\\\\\\\]*`;\n// Wrapped in its own group, and the trailing `?` below applies to that group.\n// Interpolating the declaration bare would attach the `?` to `VALUE`'s `*`,\n// making the last declaration's value *lazy* instead of the whole declaration\n// optional — which rejects a trailing `;` (`color:red;`). The semicolon test in\n// `frontend/src/lib/sanitize.test.ts` is what catches that.\n/**\n * The values `display` may take: every keyword that leaves the element a box of\n * its own, and not `contents`, the one that takes it away.\n *\n * **An element with `display: contents` generates no box, so it is no\n * containing block** for anything a stylesheet positions inside it, and the\n * app positions two things inside a document's elements. A toned block's rule\n * is a `::before` placed absolutely inside the block (`styles/directives.css`),\n * and a heading's link anchor is placed absolutely inside the heading\n * (`frontend/src/index.css`). Written on such an element, `display: contents`\n * placed them against the nearest positioned ancestor instead, which is outside\n * the scroll container, so neither was clipped by it nor scrolled with the\n * document. Measured in Chromium at 1280x800: a hand-written\n * `data-vantage-tone` on a paragraph drew its rule the full height of the\n * content pane, and on a heading the heading's `font-size` moved that rule, and\n * the anchor, which takes clicks, anywhere across the pane. The same held for\n * the task-list checkbox until `styles/task-list.css` floated it.\n *\n * So the values are listed rather than `contents` refused, for two reasons. A\n * CSS comment is legal between the colon and the keyword, and `VALUE` admits\n * the `/` and `*` that spell one, so a pattern that refused the word would keep\n * it behind an empty comment. And a CSS-wide keyword (`inherit`, `unset`,\n * `revert`) names no value of its own. Measured in Chromium, every keyword\n * below gives a toned block a rule as tall as the block, or, for `none` and\n * the two column values, generates none. The multi-keyword forms (`block\n * flow`) are refused along with them: nothing a prose document writes needs\n * them, and each has a single-keyword spelling here.\n *\n * Ordered longest first within each prefix, which the match does not need —\n * the `;`, `!` or end that must follow decides — but a reader checking the\n * list does.\n */\nconst DISPLAY_VALUES = [\n \"none\",\n \"block\",\n \"inline-block\",\n \"inline-flex\",\n \"inline-grid\",\n \"inline-table\",\n \"inline\",\n \"flow-root\",\n \"flex\",\n \"grid\",\n \"table-row-group\",\n \"table-row\",\n \"table-header-group\",\n \"table-footer-group\",\n \"table-cell\",\n \"table-column-group\",\n \"table-column\",\n \"table-caption\",\n \"table\",\n \"list-item\",\n];\n// Its own alternative, never folded into `VALUE`: a keyword, then an optional\n// `!important`, each padded by whitespace that no neighbor can also claim, so\n// the grammar keeps its one parse of any input. No other property's name\n// begins with `display`, so the two alternatives never compete either.\nconst DISPLAY_DECLARATION = `display\\\\s*:\\\\s*(?:${DISPLAY_VALUES.join(\"|\")})\\\\s*(?:!\\\\s*important\\\\s*)?`;\nconst DECLARATION = `(?:(?:${SAFE_STYLE_PROPERTIES.join(\"|\")})\\\\s*:${VALUE}|${DISPLAY_DECLARATION})`;\n\nexport const SAFE_STYLE = new RegExp(\n `^\\\\s*(?:${DECLARATION};\\\\s*)*${DECLARATION}?$`,\n \"i\",\n);\n\n/**\n * A collapse group id: one or more digits, anchored.\n *\n * The plugin mints these as a per-document counter, so there is no vocabulary to\n * list. Keeping the shape narrow matters anyway — the toggle JS builds a\n * `[data-vantage-collapse-group=\"…\"]` selector out of the value, and a document\n * that hand-wrote raw HTML is the only way a non-numeric one could ever appear.\n */\nconst COLLAPSE_GROUP_ID = /^[0-9]+$/;\n\n/**\n * Inline SVG, admitted as static drawing and nothing else.\n *\n * The allowlist is shapes, text and grouping. Everything in SVG that can run\n * code, fetch, or reach outside the element is refused: `script`,\n * `foreignObject`, `use` and `image` (both take an `href`), `xlink:href` on\n * `a`, the `animate`/`set` family (which can rewrite an `href` after the\n * sanitizer has looked), `style` elements, and every attribute that takes a\n * `url(…)` reference — `filter`, `mask`, `clip-path`, `marker-*`, `cursor`.\n *\n * Gradients, patterns and `<defs>` are out for a second reason: they are only\n * reachable through `url(#id)`, and the sanitizer prefixes every `id` with\n * `user-content-`, so the reference would dangle even if it were allowed.\n *\n * **Refusing an element is not the same as removing it.** `hast-util-sanitize`\n * *unwraps* an element it does not allow — drops the tag and keeps its\n * children — unless the tag is on `strip`. See `SVG_STRIPPED` for why that\n * matters here, and for the pass that removes those containers instead.\n *\n * `switch` is admitted, as a plain container, because draw.io wraps every label\n * in one: `<switch><foreignObject>…</foreignObject><text>…</text></switch>`. A\n * `switch` renders only its first child whose conditions hold, and with the\n * `foreignObject` removed and the conditional attributes (`requiredFeatures`,\n * `systemLanguage`) refused, that is the `<text>` fallback. Unwrapped instead,\n * it would render *every* branch — including the \"Text is not SVG - cannot\n * display\" notice draw.io appends for viewers without `foreignObject`.\n *\n * Every child requires an `svg` ancestor, so no shape or text element is\n * admitted into HTML flow on its own.\n *\n * `title` and `desc` are not children at all. They are admitted nowhere, inside\n * a drawing they are removed with their contents (see `SVG_STRIPPED`), and\n * `aria-label` on the root is the drawing's accessible name. Both are HTML\n * integration points — the parser reads what is inside them as HTML — and an\n * `svg` ancestor turned out not to be enough to keep a `<title>` in SVG\n * context. `<math><svg><title>` keeps MathML context, and React hoists any\n * `title` outside SVG context into the page's `<head>`, so the document set the\n * tab title. In `renderMarkdown`'s string output, `<svg><desc><title>`,\n * `<svg><title><title>` and a `title` inside a `foreignObject` all re-parse into\n * an HTML `title`, which is what `document.title` reads on a page that has none\n * of its own.\n */\nconst SVG_CHILD_TAGS = [\n \"g\",\n \"path\",\n \"rect\",\n \"circle\",\n \"ellipse\",\n \"line\",\n \"polyline\",\n \"polygon\",\n \"text\",\n \"tspan\",\n \"switch\",\n];\n\n/**\n * Refused SVG elements removed *with their contents* when they sit inside an\n * `<svg>`, rather than unwrapped.\n *\n * Each of these holds children that are never meant to be painted where they\n * stand: a clip region, a mask, a gradient's stops, a marker's arrowhead, a\n * symbol's body, a filter's primitives, an exporter's RDF, the text of a\n * `title` or `desc` (see `SVG_CHILD_TAGS`), or — for `foreignObject` — HTML.\n * Unwrapped, those children land in the drawing as ordinary shapes. A default\n * Figma export ends in `<defs><clipPath><rect fill=\"white\"/>`, and unwrapping\n * it painted that white rect over the whole drawing; a `<marker>` arrowhead\n * became a stray triangle at the origin. The HTML inside a `foreignObject`\n * survived as descendants of `svg`, which React then created as invisible\n * SVG-namespace `div`s and `p`s — the anchor an Open Question button looks for\n * — and which a browser re-parsing `renderMarkdown`'s string output breaks out\n * of the `svg` at, spilling the rest of the drawing into the page.\n *\n * **Only inside an `<svg>`, which is why this is a pass and not the schema's\n * `strip`.** `strip` removes a tag wherever it stands, and most of these names\n * are lowercase in prose too. There a bare `<pattern>` on a line of its own is\n * an HTML element nothing closes, so every Markdown block after it was parsed\n * into it, and stripping it removed the rest of the document — in every\n * renderer, with `vantage-check` silent. Inside a drawing the same container is\n * bounded by the `svg`, because a Markdown paragraph or heading breaks out of\n * SVG. Outside one the sanitizer unwraps these like any tag it does not know:\n * `title` and `desc` are not admitted anywhere, so no `title` element survives\n * to be hoisted, and what they held is text or ordinary sanitized HTML.\n *\n * Names compare case-insensitively. The camel-cased ones are how the HTML\n * parser spells these elements in SVG, but under `<math><svg>` the parser stays\n * in MathML and leaves them lowercase.\n */\nconst SVG_STRIPPED = [\n \"foreignObject\",\n \"defs\",\n \"clipPath\",\n \"mask\",\n \"pattern\",\n \"marker\",\n \"symbol\",\n \"linearGradient\",\n \"radialGradient\",\n \"filter\",\n \"metadata\",\n \"title\",\n \"desc\",\n];\n\nconst SVG_STRIPPED_NAMES = new Set(\n SVG_STRIPPED.map((name) => name.toLowerCase()),\n);\n\n/**\n * The camel-cased `SVG_STRIPPED` names, which the schema strips on its own.\n *\n * Only an SVG-namespace element carries one of these spellings, so stripping\n * them everywhere costs prose nothing. It keeps the worst of the list — the\n * HTML inside a `foreignObject` — out of a drawing for a consumer who hands\n * `sanitizeSchema` to `rehype-sanitize` without the rest of `buildPipeline`.\n */\nconst SVG_STRIPPED_EVERYWHERE = SVG_STRIPPED.filter(\n (name) => name !== name.toLowerCase(),\n);\n\nfunction stripSvgContainers(parent: Root | Element, inSvg: boolean): void {\n if (inSvg) {\n parent.children = parent.children.filter(\n (child) =>\n child.type !== \"element\" ||\n !SVG_STRIPPED_NAMES.has(child.tagName.toLowerCase()),\n );\n }\n for (const child of parent.children) {\n if (child.type === \"element\") {\n stripSvgContainers(child, inSvg || child.tagName.toLowerCase() === \"svg\");\n }\n }\n}\n\n/**\n * Remove every `SVG_STRIPPED` element below an `svg`, with its contents.\n *\n * Runs immediately before `rehypeSanitize` (`pipeline.ts`), and only when the\n * pipeline sanitizes: with the sanitizer off these elements are left to work\n * as SVG defines them.\n */\nexport function rehypeStripSvgContainers() {\n return (tree: Root) => {\n stripSvgContainers(tree, false);\n };\n}\n\n/**\n * A paint value: a keyword, a named color, or a hex color. No parentheses, for\n * the same reason `SAFE_STYLE` refuses them — `fill=\"url(https://…)\"` is a\n * render-time fetch, and `rgb()` is the price of closing it.\n */\nconst SVG_PAINT = /^(?:#[0-9a-f]{3,8}|[a-z]+)$/i;\n\n/**\n * Attributes any SVG element may carry, drawing and typography alike.\n *\n * **Each entry is a hast property name, which is not always the camel case of\n * the attribute.** `property-information` decides the spelling, and it has\n * `strokeLineCap` for `stroke-linecap` — a naive `strokeLinecap` matches\n * nothing, and the attribute is dropped in every renderer without a word. The\n * table-driven test in `frontend/src/lib/sanitize.test.ts` renders every entry\n * in its written spelling, so a misspelled one fails there.\n *\n * `stroke-dasharray` (and with it `stroke-dashoffset`, which does nothing\n * alone) is refused, because it lets a few bytes buy an unbounded paint. The\n * number of dashes is the path's length over the dash period, both in user\n * units the document chooses. Measured in headless Chromium, fifty 90-byte\n * paths with a 0.0011 dash took 21 s to paint and a hundred took 37 s. A value\n * grammar cannot bound it: refusing sub-unit dashes is undone by scaling the\n * user units, and the same fifty paths with `stroke-dasharray=\"1.1\"` under a\n * `viewBox` a thousand times larger still took 15 s.\n */\nconst SVG_ATTRIBUTES: NonNullable<Schema[\"attributes\"]>[string] = [\n [\"fill\", SVG_PAINT],\n [\"stroke\", SVG_PAINT],\n \"fillOpacity\",\n \"fillRule\",\n \"strokeOpacity\",\n \"strokeWidth\",\n \"strokeLineCap\",\n \"strokeLineJoin\",\n \"opacity\",\n \"transform\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"fontStyle\",\n \"letterSpacing\",\n \"textAnchor\",\n \"dominantBaseline\",\n // Geometry. Names are shared across shapes, so one list serves them all.\n \"x\",\n \"y\",\n \"x1\",\n \"y1\",\n \"x2\",\n \"y2\",\n \"cx\",\n \"cy\",\n \"r\",\n \"rx\",\n \"ry\",\n \"dx\",\n \"dy\",\n \"d\",\n \"points\",\n];\n\n/**\n * The root `<svg>`'s attributes: `SVG_ATTRIBUTES` less `transform`, plus the\n * viewport, a `role` from a closed list, and the accessible name.\n *\n * `transform` on the root is not an SVG transform at all. The root is an\n * in-flow CSS box, and the attribute becomes a CSS transform of that box, so\n * `<svg width=\"10\" height=\"10\" transform=\"translate(-300 -300) scale(80)\">`\n * laid out as a 10px box and painted as an 800px one over the paragraphs\n * around it. On a child it moves shapes inside the viewport, which clips them.\n *\n * Residual: that clip is the root's `overflow`, which `SAFE_STYLE` lets a\n * document set. `style=\"overflow:visible\"` on the root lets a child with a\n * large `transform` or large coordinates paint over its neighbors — measured in\n * Chromium, the same overlap. It is the negative-`margin` residual described at\n * `SAFE_STYLE` by another route: it stays in the flow, scrolls with the content,\n * and the scroll container clips it.\n */\nconst SVG_ROOT_ATTRIBUTES: NonNullable<Schema[\"attributes\"]>[string] = [\n ...SVG_ATTRIBUTES.filter((attribute) => attribute !== \"transform\"),\n \"xmlns\",\n \"viewBox\",\n \"preserveAspectRatio\",\n // The roles that describe a drawing: a picture, or a decoration. Any other\n // value let a document present its drawing to assistive technology as an\n // alert, a button or a dialog, with a label it wrote itself. The default\n // schema admits `role` nowhere, so this is the only place it gets through.\n [\"role\", \"img\", \"presentation\", \"none\"],\n \"ariaLabel\",\n];\n\n/** Every element the sanitizer admits. */\nconst TAG_NAMES = [\n ...(defaultSchema.tagNames || []),\n // KaTeX MathML elements\n \"math\",\n \"semantics\",\n \"mrow\",\n \"mi\",\n \"mo\",\n \"mn\",\n \"msup\",\n \"msub\",\n \"mfrac\",\n \"mover\",\n \"munder\",\n \"msqrt\",\n \"mroot\",\n \"mtable\",\n \"mtr\",\n \"mtd\",\n \"mtext\",\n \"mspace\",\n \"annotation\",\n // Other\n \"figure\",\n \"figcaption\",\n \"summary\",\n \"details\",\n \"svg\",\n ...SVG_CHILD_TAGS,\n];\n\n/**\n * The admitted elements that take no `style`: only `input`, which GFM emits as\n * a task list's checkbox, and never with a style.\n *\n * `styles/task-list.css` draws that checkbox and positions it, so on it `top`\n * and `left` are not the no-ops they are on an element nothing positions, and\n * with `width`, `height` and `background-color` beside them a document laid a\n * box of its own over the page. Measured in Chromium at 1280x800 while the\n * checkbox was positioned absolutely,\n * `top:-3000px;left:-3000px;width:9000px;height:9000px;background-color:white`\n * on it covered the whole content pane. It needed no class written:\n * GFM puts `task-list-item` on the item, and a checkbox typed into the item's\n * text is a child of that item too.\n *\n * **This is why `style` is on each element's own entry, never on `*`.**\n * `hast-util-sanitize` consults `*` whenever an element's own entry yields\n * nothing, so no entry on `input` could refuse a value that `*` admits. An\n * element takes `style` only by being in `TAG_NAMES` and not in this set.\n */\nconst UNSTYLED_TAGS: ReadonlySet<string> = new Set([\"input\"]);\n\ntype Attributes = NonNullable<Schema[\"attributes\"]>;\n\n/**\n * `attributes`, with `[\"style\", SAFE_STYLE]` added to the entry of every\n * element in `TAG_NAMES` but the `UNSTYLED_TAGS`.\n */\nfunction withStyle(attributes: Attributes): Attributes {\n const styled: Attributes = { ...attributes };\n for (const tag of new Set(TAG_NAMES)) {\n if (!UNSTYLED_TAGS.has(tag)) {\n styled[tag] = [...(styled[tag] ?? []), [\"style\", SAFE_STYLE]];\n }\n }\n return styled;\n}\n\n/**\n * Never set `allowComments` here.\n *\n * `hast-util-sanitize` drops comment nodes because that boolean defaults to\n * `false` — comments are not elements, so `tagNames` has nothing to do with it.\n * `rehypeVantageDirectives` relies on that deletion: it consumes a\n * `<!-- vantage: … -->` comment into attributes and deliberately leaves the node\n * for the sanitizer. Turning the switch on readmits every directive comment —\n * valid and malformed alike — into the rendered HTML, which breaks the carrier's\n * whole premise. `vantageDirectives.test.ts` (\"leaves no comment in the rendered\n * markup\") is the guard.\n */\nexport const sanitizeSchema: Schema = {\n ...defaultSchema,\n strip: [...(defaultSchema.strip || []), ...SVG_STRIPPED_EVERYWHERE],\n tagNames: TAG_NAMES,\n ancestors: {\n ...defaultSchema.ancestors,\n ...Object.fromEntries(SVG_CHILD_TAGS.map((tag) => [tag, [\"svg\"]])),\n },\n attributes: withStyle({\n ...defaultSchema.attributes,\n // No `className` here: see `PIPELINE_CLASSES`. No `style` either: see\n // `UNSTYLED_TAGS`.\n \"*\": [\n ...(defaultSchema.attributes?.[\"*\"] || []),\n \"dataSourceLine\",\n // What `rehypeVantageDirectives` compiles a `<!-- vantage: … -->` comment\n // into, named individually — never by a `data-vantage-*` wildcard, which\n // would readmit whatever a future bug emits and whatever a document\n // hand-writes as raw HTML.\n //\n // The value lists are the belt to the plugin's braces: the vocabulary is\n // closed in the plugin *and* here, imported from the one module that\n // defines it, so even if a refactor let an unvalidated value reach the\n // tree the sanitizer still refuses it.\n [\"dataVantageTone\", ...VANTAGE_TONES],\n [\"dataVantageEmphasis\", ...VANTAGE_EMPHASIS],\n [\"dataVantageBadge\", ...VANTAGE_BADGES],\n [\"dataVantageCollapsed\", ...VANTAGE_COLLAPSED],\n // The other half of `collapsed`: which group a hidden block belongs to,\n // and which group a heading toggles. Both are plugin-minted counters with\n // no vocabulary to allowlist, so they take a pattern instead —\n // `hast-util-sanitize` accepts a `RegExp` in place of a literal value.\n // A pattern rather than a bare name because the JS interpolates the value\n // into a selector: anything but digits has no business reaching it.\n [\"dataVantageCollapseGroup\", COLLAPSE_GROUP_ID],\n [\"dataVantageCollapseToggle\", COLLAPSE_GROUP_ID],\n [\"dataVantageRun\", ...VANTAGE_RUNS],\n // A question declared with `oq`, the name every release since 0.7 has\n // stamped this for, and a question under either name: the second is the\n // one to read for \"a question\" (`VANTAGE_QUESTION_ATTRIBUTE`).\n [\"dataVantageOq\", \"true\"],\n [\"dataVantageQuestion\", \"true\"],\n // The Open Question's id on its way to becoming a real `id`, pattern-\n // allowlisted like the collapse-group counters above rather than left\n // name-only: `rehypeVantageAnchors` writes this value straight into the\n // document's id namespace on the other side of this schema, so anything\n // a document could smuggle through here it would be smuggling into `id`.\n // The grammar is imported, never restated — see `VANTAGE_OQ_ID`.\n [\"dataVantageOqId\", VANTAGE_OQ_ID],\n // GFM alerts, compiled by `rehypeVantageAlerts`. Value-allowlisted like\n // the tone tokens it shares a palette with, so a document cannot forge a\n // sixth kind through raw HTML.\n [\"dataVantageAlert\", ...VANTAGE_ALERTS],\n // The design's one genuinely free-text value: the body of a review\n // comment, so it cannot be value-allowlisted and this entry is name-only.\n // Two defenses remain rather than three — `hast` escapes the value on\n // serialization and React sets it through the DOM property path, so it\n // cannot break out of the attribute — and the honest record of that is in\n // the design doc rather than a third layer implied here.\n \"dataVantageLeaning\",\n ],\n // Every element that keeps a class. `a` is built again below, by the same\n // helper, with the attribute it takes besides.\n ...Object.fromEntries(\n Object.keys(PIPELINE_CLASSES).map((tag) => [tag, withClasses(tag)]),\n ),\n a: withClasses(\"a\", \"id\"),\n math: [\"xmlns\"],\n annotation: [\"encoding\"],\n img: [...(defaultSchema.attributes?.img || []), \"loading\"],\n svg: SVG_ROOT_ATTRIBUTES,\n ...Object.fromEntries(SVG_CHILD_TAGS.map((tag) => [tag, SVG_ATTRIBUTES])),\n }),\n};\n","/**\n * The mdast half of the Vantage chain (`pipeline.ts`), in a module of its own\n * so that what imports it imports no rehype plugin.\n *\n * The planning scan parses with it, and the scan runs in a worker\n * (`docs/reference/planning-index.md` §10): `pipeline.ts` also imports\n * `rehype-katex`, whose browser build builds a `DOMParser` the moment it\n * loads, and a worker has none. A production bundle shakes those imports out,\n * but the dev server loads every module a module imports, so the worker would\n * die on its first import. `pipeline.ts` re-exports it, so every other caller\n * is unchanged.\n */\n\nimport type { PluggableList } from \"unified\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMath from \"remark-math\";\nimport type { PipelineOptions } from \"./pipeline.js\";\n\n/**\n * The mdast half of the chain. Exported on its own because there is a real\n * mdast-only consumer: the CLI checker parses documents without ever running\n * rehype (`packages/vantage-check/src/core/document.ts`), and it has to parse\n * them exactly the way the viewer does.\n */\nexport function buildRemarkPlugins(\n options: PipelineOptions = {},\n): PluggableList {\n const { gfm = true, math = true } = options;\n const plugins: PluggableList = [];\n // `singleTilde: false` — `~x~` is not strikethrough, so a lone tilde in\n // prose survives. `singleDollarTextMath: false` — `$` is not a math\n // delimiter, so `$HOME` and `$100` stay literal. Both are contracts the\n // style guide and the user guide state, not preferences.\n if (gfm) plugins.push([remarkGfm, { singleTilde: false }]);\n if (math) plugins.push([remarkMath, { singleDollarTextMath: false }]);\n return plugins;\n}\n","/**\n * The one definition of the Vantage remark/rehype chain.\n *\n * Three call sites render Markdown — `renderMarkdown` (string in, HTML out,\n * which is what the CLI checker runs), the app's `<MarkdownViewer>`, and this\n * package's exported `<MarkdownViewer>` — and each one used to hand-write the\n * same plugin list in the same order. Three copies kept in sync by hand is how\n * a plugin lands in the viewer and not in the checker: a document that styles\n * in the app and renders bare through the tool that is supposed to validate it,\n * with no error anywhere.\n *\n * The order is load-bearing, not incidental:\n *\n * - `rehypeRaw` first: `remark-rehype` runs with `allowDangerousHtml: true`,\n * so raw HTML is still a string until this plugin parses it.\n * - `rehypeSourceLines` before `rehypeSanitize`: `data-source-line` has to be\n * an allowlisted attribute on an element the sanitizer keeps.\n * - `rehypeVantageAnchors`, `rehypeSlug`, `rehypeHighlight` and `rehypeKatex`\n * after `rehypeSanitize`. For the first two this is not a preference: the\n * sanitizer's default schema clobbers `id` with the prefix `user-content-`,\n * so writing one before it turns every `#heading` and every `#OQ-4` link in\n * every document into a dead anchor. `rehypeVantageAnchors` precedes\n * `rehypeSlug` because `rehype-slug` leaves an element that already has an\n * `id` alone. For the other two, being here means their output is trusted\n * rather than filtered — KaTeX emits inline `style` on nearly every glyph.\n *\n * Anything that reads HTML comments must sit between `rehypeRaw` and\n * `rehypeSanitize`: before `rehypeRaw` there are no comment nodes, and\n * `rehypeSanitize` deletes them. `rehypeVantageDirectives` is what occupies\n * that slot, and it is registered unconditionally — a renderer that skipped it\n * would disagree with the others about what a document means.\n */\n\nimport type { PluggableList } from \"unified\";\nimport rehypeRaw from \"rehype-raw\";\nimport rehypeSanitize from \"rehype-sanitize\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeKatex from \"rehype-katex\";\nimport rehypeSlug from \"rehype-slug\";\nimport rehypeVantageAnchors from \"./rehypeVantageAnchors.js\";\nimport rehypeSourceLines from \"./rehypeSourceLines.js\";\nimport { rehypeVantageAlerts } from \"./rehypeVantageAlerts.js\";\nimport rehypeVantageDirectives from \"./rehypeVantageDirectives.js\";\nimport {\n rehypeCaptureMathStamps,\n rehypeRestoreMathStamps,\n} from \"./rehypeVantageMathStamps.js\";\nimport { rehypeStripSvgContainers, sanitizeSchema } from \"./sanitize.js\";\nimport { buildRemarkPlugins } from \"./remarkPlugins.js\";\n\nexport { buildRemarkPlugins };\n\nexport interface PipelineOptions {\n /** GFM tables, strikethrough, task lists (default: true) */\n gfm?: boolean;\n /** KaTeX math, `$$…$$` only (default: true) */\n math?: boolean;\n /** Syntax highlighting via highlight.js (default: true) */\n highlight?: boolean;\n /** `data-source-line` attributes for line anchors (default: true) */\n sourceLines?: boolean;\n /** XSS sanitization (default: true) */\n sanitize?: boolean;\n /**\n * Lines the frontmatter consumed, added to every emitted line number so\n * `data-source-line` names a line in the *file* rather than in the parsed\n * body — which is what a `#L42` link written against the file means.\n * Defaults to 0. Ignored when `sourceLines` is false.\n */\n bodyLineOffset?: number;\n}\n\nexport interface Pipeline {\n remarkPlugins: PluggableList;\n rehypePlugins: PluggableList;\n}\n\n/** The hast half. Deliberately not exported: see `buildPipeline`. */\nfunction buildRehypePlugins(options: PipelineOptions = {}): PluggableList {\n const {\n math = true,\n highlight = true,\n sourceLines = true,\n sanitize = true,\n bodyLineOffset = 0,\n } = options;\n\n const plugins: PluggableList = [rehypeRaw];\n if (sourceLines) {\n plugins.push([rehypeSourceLines, { offset: bodyLineOffset }]);\n }\n // ── The comment slot ──────────────────────────────────────────────────\n // `rehypeVantageDirectives` compiles `<!-- vantage: … -->` comments into\n // `data-vantage-*` attributes, and it can only do that here: before\n // `rehypeRaw` there are no comment nodes, and `rehypeSanitize` deletes them.\n // It gets no option of its own: every renderer has to agree about what a\n // document means, and a flag is a way for them to disagree.\n // GFM alerts. After `rehypeSourceLines` so the title it injects carries no\n // `data-source-line` and therefore cannot become a review anchor, and before\n // the sanitizer so its one attribute is allowlisted like every other\n // `data-vantage-*`. No option of its own, for the same reason the directives\n // plugin has none: a flag is a way for two renderers to disagree about what a\n // document means.\n plugins.push(rehypeVantageAlerts);\n plugins.push(rehypeVantageDirectives);\n // The SVG containers that must go with their contents go only inside a\n // drawing; the schema's `strip` would take them out of prose as well, and a\n // bare `<pattern>` there took the rest of the document with it. See\n // `SVG_STRIPPED`.\n if (sanitize) {\n plugins.push(rehypeStripSvgContainers, [rehypeSanitize, sanitizeSchema]);\n }\n // ── The id slot ───────────────────────────────────────────────────────\n // Both of these write `id`, and both are here rather than earlier for the\n // same reason: the sanitizer clobbers `id` with `user-content-`, so anything\n // that sets one upstream of it produces dead anchors in every document with\n // no error anywhere. `rehypeVantageAnchors` goes first — `rehypeSlug` skips\n // an element that already has an `id`, so promoting an Open Question's id\n // before slugging is what lets a question written as a heading keep it.\n plugins.push(rehypeVantageAnchors);\n plugins.push(rehypeSlug);\n if (highlight) plugins.push(rehypeHighlight);\n // ── The KaTeX bracket ─────────────────────────────────────────────────\n // `rehype-katex` does not decorate a display-math block, it *replaces* it:\n // `$$…$$` arrives as a `<pre>`, which `rehypeVantageDirectives` has already\n // stamped as a member of its section's run and `rehypeSourceLines` has already\n // given a `data-source-line`, and the splice throws all of that away. The two\n // plugins around it snapshot those attributes and put them back on the\n // `<span class=\"katex-display\">` that took the block's place — which is what\n // keeps a toned section's rule continuous across a formula, a `#L` anchor\n // pointing at one resolvable, and `collapsed=true` able to hide it. They are a\n // pair and they must bracket `rehypeKatex`; see `rehypeVantageMathStamps.ts`.\n if (math) {\n plugins.push(rehypeCaptureMathStamps, rehypeKatex, rehypeRestoreMathStamps);\n }\n return plugins;\n}\n\n/**\n * Both halves from one options object.\n *\n * This is what every renderer calls. It takes one object rather than exposing\n * the two builders because `math` spans both halves — `remark-math` parses the\n * delimiters, `rehype-katex` renders the result — and two calls are two places\n * to forget the second one.\n *\n * Returns fresh arrays on every call and reads no module-level state; keep it\n * that way, so a plugin in the chain cannot become a function of how many times\n * the chain has been built.\n */\nexport function buildPipeline(options: PipelineOptions = {}): Pipeline {\n return {\n remarkPlugins: buildRemarkPlugins(options),\n rehypePlugins: buildRehypePlugins(options),\n };\n}\n","/**\n * Frontmatter parser for YAML (---) and TOML (+++) delimited content.\n * Works in both browser and server environments.\n */\n\nimport YAML from \"yaml\";\nimport { parse as parseTOML } from \"smol-toml\";\n\nexport type FrontmatterFormat = \"yaml\" | \"toml\" | \"none\";\n\n/**\n * Why a document that *looks* like it has frontmatter ended up without any.\n *\n * The parser deliberately never throws: a document whose frontmatter is broken\n * still renders, with the block treated as body text. That is the right\n * behavior for a viewer and the wrong one for an author, who gets no signal\n * at all — so the reason is recorded here for anything that wants to report it\n * (`vantage-check` does; see its frontmatter rules).\n *\n * - `unterminated` — an opening delimiter with no closing one.\n * - `invalid` — the block did not parse; `message` is the parser's own words,\n * and `line`/`column` are 1-based *within the block* when it said.\n * - `not-a-mapping` — it parsed, but to a string or a list rather than a table\n * of fields, which is not something a metadata card can render.\n */\nexport interface FrontmatterProblem {\n kind: \"unterminated\" | \"invalid\" | \"not-a-mapping\";\n /** The delimiter the document opened with. */\n delimiter: string;\n message?: string;\n line?: number;\n column?: number;\n}\n\nexport interface ParsedFrontmatter {\n frontmatter: Record<string, unknown>;\n body: string;\n format: FrontmatterFormat;\n /**\n * How many source lines the frontmatter block consumed — the shift between a\n * line number in `body` and the same line in the original file:\n * `fileLine = bodyLine + bodyLineOffset`.\n *\n * Anything that renders `body` and reports line numbers (line anchors, review\n * comment anchors) has to add this back, or every number it produces points\n * `bodyLineOffset` lines short of the text it names.\n */\n bodyLineOffset: number;\n /**\n * Set when the document opens with a frontmatter delimiter that did not\n * yield a metadata table. Everything else in this result is unchanged —\n * this records *why*, it does not change what rendering does.\n */\n problem?: FrontmatterProblem;\n}\n\n/**\n * Parse frontmatter from markdown content.\n * Supports YAML (delimited by ---) and TOML (delimited by +++).\n */\nexport function parseFrontmatter(content: string): ParsedFrontmatter {\n if (content.startsWith(\"+++\")) {\n return parseFrontmatterWithDelimiter(content, \"+++\", \"toml\");\n }\n if (content.startsWith(\"---\")) {\n return parseFrontmatterWithDelimiter(content, \"---\", \"yaml\");\n }\n return withOffset(content, {\n frontmatter: {},\n body: content,\n format: \"none\",\n });\n}\n\n/**\n * Fill in `bodyLineOffset`. `body` is always a suffix of `content`, so the\n * newlines in the prefix that was stripped are exactly the shift — which also\n * accounts for the blank line consumed after the closing delimiter.\n */\nfunction withOffset(\n content: string,\n parsed: Omit<ParsedFrontmatter, \"bodyLineOffset\">,\n): ParsedFrontmatter {\n const stripped = content.slice(0, content.length - parsed.body.length);\n let bodyLineOffset = 0;\n for (const ch of stripped) {\n if (ch === \"\\n\") bodyLineOffset++;\n }\n return { ...parsed, bodyLineOffset };\n}\n\nfunction parseFrontmatterWithDelimiter(\n content: string,\n delimiter: string,\n format: \"yaml\" | \"toml\",\n): ParsedFrontmatter {\n const searchStart = delimiter.length;\n const endIndex = content.indexOf(`\\n${delimiter}`, searchStart);\n if (endIndex === -1) {\n return withOffset(content, {\n frontmatter: {},\n body: content,\n format: \"none\",\n problem: { kind: \"unterminated\", delimiter },\n });\n }\n\n const raw = content.slice(searchStart + 1, endIndex).trim();\n const bodyStart = endIndex + 1 + delimiter.length;\n const body = content.slice(bodyStart).replace(/^\\n/, \"\");\n\n try {\n const parsed: unknown =\n format === \"toml\" ? parseTOML(raw) : YAML.parse(raw);\n return withOffset(content, {\n frontmatter: (parsed as Record<string, unknown>) || {},\n body,\n format,\n ...(isMapping(parsed)\n ? {}\n : { problem: { kind: \"not-a-mapping\" as const, delimiter } }),\n });\n } catch (error) {\n return withOffset(content, {\n frontmatter: {},\n body: content,\n format: \"none\",\n problem: { kind: \"invalid\", delimiter, ...errorPosition(error) },\n });\n }\n}\n\n/** Empty frontmatter is fine; a scalar or a list where a table belongs is not. */\nfunction isMapping(value: unknown): boolean {\n return (\n value === null ||\n value === undefined ||\n (typeof value === \"object\" && !Array.isArray(value))\n );\n}\n\n/**\n * Pull the parser's message and, where it gave one, the position inside the\n * frontmatter block. `yaml` reports `linePos`; `smol-toml` reports `line` and\n * `column`. Both are optional and both are read defensively — a missing\n * position costs a less precise report, a wrong assumption costs a crash.\n */\nfunction errorPosition(error: unknown): {\n message: string;\n line?: number;\n column?: number;\n} {\n const message = error instanceof Error ? error.message : String(error);\n const source = error as {\n linePos?: Array<{ line?: number; col?: number }>;\n line?: number;\n column?: number;\n };\n\n const yamlPosition = source?.linePos?.[0];\n if (typeof yamlPosition?.line === \"number\") {\n return {\n message,\n line: yamlPosition.line,\n ...(typeof yamlPosition.col === \"number\"\n ? { column: yamlPosition.col }\n : {}),\n };\n }\n if (typeof source?.line === \"number\") {\n return {\n message,\n line: source.line,\n ...(typeof source.column === \"number\" ? { column: source.column } : {}),\n };\n }\n return { message };\n}\n","/**\n * Framework-agnostic markdown -> HTML rendering pipeline.\n * Uses the same remark/rehype chain as the Vantage viewer.\n */\n\nimport type { Root } from \"mdast\";\nimport { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkRehype from \"remark-rehype\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { buildPipeline } from \"./pipeline.js\";\nimport { parseFrontmatter } from \"./frontmatter.js\";\nimport type { ParsedFrontmatter } from \"./frontmatter.js\";\n\nexport interface RenderOptions {\n /** Enable GFM tables, strikethrough, task lists (default: true) */\n gfm?: boolean;\n /** Enable KaTeX math rendering (default: true) */\n math?: boolean;\n /** Enable syntax highlighting (default: true) */\n highlight?: boolean;\n /** Add data-source-line attributes for line anchors (default: true) */\n sourceLines?: boolean;\n /** Enable XSS sanitization (default: true) */\n sanitize?: boolean;\n /** Parse and strip frontmatter (default: true) */\n frontmatter?: boolean;\n /**\n * The body's mdast, already parsed — an optimization, not a second input.\n *\n * Parsing is the most expensive step in this function: measured over\n * `docs/design/`, `remark-parse` with GFM costs about twice what the whole\n * rehype half costs. A caller that already holds the tree — the CLI checker\n * does, because every `mdast` rule ran on it before the render rule got its\n * turn — was paying for the same parse twice.\n *\n * The tree must have been parsed by **this package's remark half with these\n * same options** (`buildRemarkPlugins`, which is exported for exactly that\n * reason). Anything parsed some other way renders through a chain the viewer\n * does not use, which is the one thing the shared pipeline exists to prevent,\n * and nothing here can detect it: a tree is a tree.\n *\n * `content` is still read, for its frontmatter. The two have to describe the\n * same document.\n */\n tree?: Root;\n}\n\nexport interface RenderResult {\n /** The rendered HTML string */\n html: string;\n /** Parsed frontmatter (empty object if none or disabled) */\n frontmatter: Record<string, unknown>;\n /** The markdown body with frontmatter stripped */\n body: string;\n}\n\n/**\n * Render a markdown string to HTML using the full Vantage pipeline.\n *\n * Features (all enabled by default):\n * - GitHub Flavored Markdown (tables, strikethrough, task lists)\n * - KaTeX math rendering, inline and block ($$...$$ only; single $ is not a delimiter)\n * - Syntax highlighting via highlight.js\n * - `data-source-line` attributes for line anchors\n * - XSS sanitization\n * - Heading slugs/anchors\n * - YAML/TOML frontmatter parsing\n *\n * Mermaid diagrams are NOT rendered server-side (they require a browser).\n * Mermaid code blocks are preserved as `<pre><code class=\"language-mermaid\">`.\n * Use the React `<MarkdownViewer>` component for client-side mermaid rendering.\n */\nexport async function renderMarkdown(\n content: string,\n options: RenderOptions = {},\n): Promise<RenderResult> {\n const {\n gfm = true,\n math = true,\n highlight = true,\n sourceLines = true,\n sanitize = true,\n frontmatter: parseFm = true,\n tree,\n } = options;\n\n // Parse frontmatter\n let parsed: ParsedFrontmatter;\n if (parseFm) {\n parsed = parseFrontmatter(content);\n } else {\n parsed = {\n frontmatter: {},\n body: content,\n format: \"none\",\n bodyLineOffset: 0,\n };\n }\n\n // One chain, defined in ./pipeline.ts and shared with both React viewers —\n // the checker must not render through a different pipeline than the app.\n const { remarkPlugins, rehypePlugins } = buildPipeline({\n gfm,\n math,\n highlight,\n sourceLines,\n sanitize,\n bodyLineOffset: parsed.bodyLineOffset,\n });\n\n // `allowDangerousHtml` is why raw HTML reaches `rehypeRaw` at all.\n const processor = unified()\n .use(remarkParse)\n .use(remarkPlugins)\n .use(remarkRehype, { allowDangerousHtml: true })\n .use(rehypePlugins)\n .use(rehypeStringify);\n\n // `process` is `parse` then `run` then `stringify`. With the tree in hand the\n // first of those is already done, so the other two are called directly —\n // same processor, same plugins, same order, one parse less. `parsed.body` is\n // handed to both as the file, which is what `process` does with it too.\n const result =\n tree === undefined\n ? String(await processor.process(parsed.body))\n : processor.stringify(\n await processor.run(tree, parsed.body),\n parsed.body,\n );\n\n return {\n html: String(result),\n frontmatter: parsed.frontmatter,\n body: parsed.body,\n };\n}\n","/**\n * Parsing for GitHub-style line anchors, with no DOM in sight.\n *\n * Split out from scrollToLineAnchor.ts so that non-browser consumers — the\n * `vantage-check` CLI, which validates `#L42` links against the file on disk —\n * can share the *same* syntax the viewer honors instead of reimplementing it\n * and drifting.\n */\n\n/**\n * Parse a GitHub-style line anchor hash.\n * Supports: #L42, #L42-L50, #L42-50\n * Returns null if the hash is not a line anchor.\n */\nexport function parseLineAnchor(\n hash: string,\n): { start: number; end: number } | null {\n if (!hash) return null;\n const frag = hash.startsWith(\"#\") ? hash.slice(1) : hash;\n const match = frag.match(/^L(\\d+)(?:-L?(\\d+))?$/);\n if (!match) return null;\n\n const start = parseInt(match[1], 10);\n const end = match[2] ? parseInt(match[2], 10) : start;\n return { start: Math.min(start, end), end: Math.max(start, end) };\n}\n","/**\n * Framework-agnostic line anchor utilities.\n * Scroll to and highlight the elements a GitHub-style line anchor\n * (#L42, #L42-L50) names. The parsing half lives in lineAnchor.ts, which has\n * no DOM dependency.\n */\n\nimport { parseLineAnchor } from \"./lineAnchor.js\";\n\nconst HIGHLIGHT_CLASS = \"line-anchor-highlight\";\n\n/**\n * Clear all line anchor highlights from a container.\n */\nexport function clearLineAnchorHighlights(container: HTMLElement): void {\n container.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((node) => {\n (node as HTMLElement).classList.remove(HIGHLIGHT_CLASS);\n });\n}\n\n/**\n * Scroll to and highlight line-anchored elements in a container.\n *\n * @param container - The DOM element containing rendered markdown\n * @param hash - The URL hash (e.g. \"#L42\" or \"#L42-L50\")\n * @returns A cleanup function that removes the highlights\n */\nexport function scrollToLineAnchor(\n container: HTMLElement,\n hash: string,\n): (() => void) | null {\n clearLineAnchorHighlights(container);\n\n const range = parseLineAnchor(hash);\n if (!range) return null;\n\n const blocks = container.querySelectorAll(\"[data-source-line]\");\n let firstMatch: HTMLElement | null = null;\n\n for (const block of blocks) {\n const line = parseInt((block as HTMLElement).dataset.sourceLine || \"0\", 10);\n if (line >= range.start && line <= range.end) {\n (block as HTMLElement).classList.add(HIGHLIGHT_CLASS);\n if (!firstMatch) firstMatch = block as HTMLElement;\n }\n }\n\n // If exact line not found, find the nearest block before the target line\n if (!firstMatch) {\n let closest: HTMLElement | null = null;\n let closestLine = 0;\n for (const block of blocks) {\n const line = parseInt(\n (block as HTMLElement).dataset.sourceLine || \"0\",\n 10,\n );\n if (line <= range.start && line > closestLine) {\n closestLine = line;\n closest = block as HTMLElement;\n }\n }\n if (closest) {\n closest.classList.add(HIGHLIGHT_CLASS);\n firstMatch = closest;\n }\n }\n\n // Scroll to the first highlighted element\n if (firstMatch) {\n requestAnimationFrame(() => {\n // Find the nearest scrollable ancestor\n const scrollParent = findScrollParent(container);\n if (scrollParent) {\n const offset =\n firstMatch!.getBoundingClientRect().top -\n scrollParent.getBoundingClientRect().top +\n scrollParent.scrollTop;\n scrollParent.scrollTo({ top: offset - 32, behavior: \"smooth\" });\n } else {\n firstMatch!.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n }\n });\n }\n\n return () => clearLineAnchorHighlights(container);\n}\n\nfunction findScrollParent(el: HTMLElement): HTMLElement | null {\n let node: HTMLElement | null = el;\n while (node) {\n const overflow = getComputedStyle(node).overflowY;\n if (overflow === \"auto\" || overflow === \"scroll\") return node;\n node = node.parentElement;\n }\n return null;\n}\n","/**\n * The `vantage:` frontmatter key — file-scoped chrome (`docs/reference/inline-markup.md`, \"File-scoped chrome\").\n *\n * One reserved key at the top level of a document's frontmatter, holding the\n * chrome that belongs to the *file* rather than to a section. Today that is one\n * thing: whether the document's lifecycle `status:` is shown as a chip above the\n * metadata card, instead of being buried as one row inside it.\n *\n * Read only at the top level, and **inert on every failure** (P3): an unknown\n * key, a value outside the closed set, or a `vantage:` that is not a table\n * produces no chrome, no throw and no console output. The reasons are returned\n * as data in `issues`, for anything that wants to report them — `vantage-check`\n * does, and it is the only signal an author gets. That split is exactly the one\n * `FrontmatterProblem` already uses in `frontmatter.ts`: the viewer reads the\n * value, the checker reads the reasons.\n *\n * Like `vantageDirectives.ts`, this module is imported by the CLI checker **by\n * relative path**, so it must stay a pure function of already-parsed data: no\n * hast, no React, no filesystem.\n */\n\nimport { VANTAGE_TONES } from \"./vantageDirectives.js\";\n\n/**\n * The document lifecycle vocabulary. Closed; extending it is a code change.\n *\n * This is the repo's own existing set, not a new one — `styleGuide.ts` tells\n * every agent to write `status: in-review # draft | in-review | accepted |\n * deprecated`, and every document under `docs/` follows it. It is deliberately\n * *not* the `badge` set (`draft stale blocked done wip`): `badge` is\n * section-scoped workflow state, `status` is document lifecycle state, and\n * `in-review` — the value the design doc's own only example renders — is not a\n * badge word at all. Only `draft` is a member of both, and a token set is per key.\n */\nexport const DOC_STATUSES = [\n \"draft\",\n \"in-review\",\n \"accepted\",\n \"deprecated\",\n] as const;\n\nexport type DocStatus = (typeof DOC_STATUSES)[number];\n\n/** Every key this build knows under `vantage:`. Closed. */\nexport const VANTAGE_FRONTMATTER_KEYS = [\"status-chip\"] as const;\n\n/**\n * Which tone each status borrows its colors from.\n *\n * The chip has no palette of its own: it reuses the tone chips\n * (`.vantage-chip--<tone>` in `styles/directives.css`), which is also what makes\n * a `draft` chip and a `badge=draft` chip the same visual object. A map rather\n * than a computed class name, so the whole status→tone relation is one readable\n * table and a test can assert it covers the vocabulary.\n */\nexport const DOC_STATUS_TONES: Readonly<\n Record<DocStatus, (typeof VANTAGE_TONES)[number]>\n> = {\n draft: \"muted\",\n \"in-review\": \"warning\",\n accepted: \"tip\",\n deprecated: \"caution\",\n};\n\n/**\n * Why something under `vantage:` produced no chrome.\n *\n * `status-chip-orphan` and `status-chip-disagrees` are not vocabulary errors —\n * both values are legal — but both are the markup rot R3 is about: a chip that\n * says something the document's own `status:` does not.\n */\nexport type VantageFrontmatterIssue =\n | { kind: \"not-a-table\"; value: unknown }\n | { kind: \"unknown-key\"; key: string }\n | { kind: \"bad-value\"; key: string; value: unknown; legal: readonly string[] }\n | { kind: \"status-chip-orphan\"; status: unknown }\n | { kind: \"status-chip-disagrees\"; chip: DocStatus; status: unknown };\n\nexport interface VantageFrontmatter {\n /** The chip's text, or `undefined` for no chip. */\n statusChip?: DocStatus;\n /** Why something was dropped. A viewer must never read this (P3). */\n issues: VantageFrontmatterIssue[];\n}\n\n/** The legal `status-chip` values, in the order a message should list them. */\nconst STATUS_CHIP_VALUES: readonly string[] = [\n ...DOC_STATUSES,\n \"true\",\n \"false\",\n];\n\n/** Narrowing helper the chip and the checker both use. */\nexport function isDocStatus(value: unknown): value is DocStatus {\n return (\n typeof value === \"string\" &&\n (DOC_STATUSES as readonly string[]).includes(value)\n );\n}\n\nfunction isTable(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\n/**\n * Read the `vantage:` key out of parsed frontmatter.\n *\n * Pure: no module state, no mutation of the input, no logging. The same object\n * in twice gives equal results out.\n */\nexport function readVantageFrontmatter(\n frontmatter: Record<string, unknown>,\n): VantageFrontmatter {\n const issues: VantageFrontmatterIssue[] = [];\n if (!Object.hasOwn(frontmatter, \"vantage\")) return { issues };\n\n const value = frontmatter[\"vantage\"];\n // A `Date` is an object and would otherwise pass for a table: `yaml` parses\n // `vantage: 2026-08-31` into one, which `Object.keys` reports as empty.\n if (!isTable(value)) {\n issues.push({ kind: \"not-a-table\", value });\n return { issues };\n }\n\n let statusChip: DocStatus | undefined;\n\n for (const key of Object.keys(value)) {\n // D2 is per key: an unknown key drops that key and nothing else, so a newer\n // document keeps working in an older build.\n if (!(VANTAGE_FRONTMATTER_KEYS as readonly string[]).includes(key)) {\n issues.push({ kind: \"unknown-key\", key });\n continue;\n }\n if (key === \"status-chip\") {\n statusChip = readStatusChip(frontmatter, value[key], issues);\n }\n }\n\n return { ...(statusChip === undefined ? {} : { statusChip }), issues };\n}\n\n/**\n * `status-chip` takes two shapes, and the boolean one is the recommended shape.\n *\n * `true` **inherits** the document's own top-level `status:`, so the chip cannot\n * disagree with it — which is the entire point of §5.3 (\"makes `status: draft`\n * visible rather than only buried in a metadata card\"; the row stays, the chip\n * promotes the value rather than moving it). A literal token is kept\n * because the design doc's first draft of that example used one, and the\n * disagreement it makes possible is turned into a checker finding rather than\n * banned.\n *\n * Discrimination is on `typeof`, never truthiness: `true` is a YAML boolean and\n * `2026-08-31` is a `Date`, and both would sail through a truthy test.\n */\nfunction readStatusChip(\n frontmatter: Record<string, unknown>,\n raw: unknown,\n issues: VantageFrontmatterIssue[],\n): DocStatus | undefined {\n const status = frontmatter[\"status\"];\n\n // Explicitly off. Not an issue: saying so is the point of a token vocabulary\n // that can be canceled (the same reason `collapsed` has a `false`).\n if (raw === false) return undefined;\n\n if (raw === true) {\n if (isDocStatus(status)) return status;\n issues.push({ kind: \"status-chip-orphan\", status });\n return undefined;\n }\n\n // Exact match, no case folding and no trimming — the same all-or-nothing\n // posture as the directive grammar and the sanitizer. `status-chip: Draft`\n // is dropped, and the checker is what says so.\n if (isDocStatus(raw)) {\n if (isDocStatus(status) && status !== raw) {\n issues.push({ kind: \"status-chip-disagrees\", chip: raw, status });\n }\n return raw;\n }\n\n issues.push({\n kind: \"bad-value\",\n key: \"status-chip\",\n value: raw,\n legal: STATUS_CHIP_VALUES,\n });\n return undefined;\n}\n","/**\n * Which mermaid theme the page is asking for, and what to hand mermaid so a\n * diagram sits on a Vantage surface rather than on the one mermaid assumes.\n *\n * `.dark` on `<html>` is the app's dark selector — the same switch\n * `styles/directives.css` reads for the tone palette — so a package consumer\n * gets dark diagrams by toggling the class it already toggles.\n *\n * ## Why the variables exist at all\n *\n * Mermaid's stock `dark` theme is built for a near-black page: `mainBkg` is\n * `#1f2020`, which against Vantage's `slate-900` content column (`#0f172b`) is\n * 1.28:1. Every node box vanished, and a flowchart read as floating labels\n * joined by lines. Its `edgeLabelBackground` is a mid-gray `#585858` that\n * matched no surface in either theme, so every edge label wore a gray chip.\n *\n * So the two palettes below are the app's own slate steps, chosen the way the\n * rest of the dark audit was: a node box one step off its surface so the box is\n * visible, ink and borders at 4.5:1 or better against that box. They are stated\n * here rather than read out of the page because mermaid wants hex strings at\n * `initialize()` time, before any diagram exists to measure.\n *\n * The set is deliberately **structural only** — the surface, the node box, its\n * border and ink, the lines, the edge-label chip. `primaryColor` and its two\n * siblings are left alone on purpose: mermaid derives the categorical series\n * from them, so overriding them with slate turned a pie chart into three\n * near-black wedges. What is wrong in the stock dark theme is where a diagram\n * sits, not which colors it tells things apart with.\n */\nexport type MermaidThemeName = \"dark\" | \"default\";\n\n/** Whether the document is asking for the dark palette right now. */\nexport function currentMermaidTheme(): MermaidThemeName {\n return typeof document !== \"undefined\" &&\n document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"default\";\n}\n\n/**\n * The attribute on `<html>` naming the active color theme. Absent means the\n * built-in look. The app sets it only once the theme's stylesheet has loaded,\n * so a reader of this attribute can trust the theme's variables are in effect.\n */\nexport const COLOR_THEME_ATTRIBUTE = \"data-vantage-theme\";\n\n/**\n * The attribute on `<html>` saying where the active theme came from: `\"user\"`\n * for a stylesheet in the reader's themes directory, `\"built-in\"` for one the\n * app ships. Set with {@link COLOR_THEME_ATTRIBUTE}, and absent with it.\n *\n * It exists because an id alone is not a palette. A user theme may share a\n * built-in's id — that is how a reader tweaks one — and the app applies the\n * stored built-in synchronously, then swaps in the same-id user file once\n * /api/themes answers. Keyed on the id, the diagrams drawn in between kept the\n * built-in's colors: the key did not change, so neither the cache nor\n * `useSyncExternalStore` saw a reason to redraw.\n */\nexport const COLOR_THEME_SOURCE_ATTRIBUTE = \"data-vantage-theme-source\";\n\n/** The active color theme's id, or `\"\"` for the built-in look. */\nexport function currentColorTheme(): string {\n if (typeof document === \"undefined\") return \"\";\n return document.documentElement.getAttribute(COLOR_THEME_ATTRIBUTE) ?? \"\";\n}\n\n/**\n * Everything a rendered diagram's colors depend on, as one string: the\n * light/dark mode, plus the color theme when one is active.\n *\n * A diagram is baked at render time, so anything that changes its colors has\n * to change this key — it is what the SVG cache and the loader's \"configured\n * for\" check compare. Under the built-in look it is exactly the mode name,\n * which is what both keyed on before color themes existed.\n */\nexport function currentMermaidPalette(): string {\n const theme = currentColorTheme();\n if (!theme) return currentMermaidTheme();\n // Only a user theme is marked, so a built-in's key stays \"mode id\".\n const user =\n document.documentElement.getAttribute(COLOR_THEME_SOURCE_ATTRIBUTE) ===\n \"user\";\n return `${currentMermaidTheme()} ${theme}${user ? \" user\" : \"\"}`;\n}\n\n/** The mermaid theme name a palette key was built from. */\nexport function mermaidThemeOf(palette: string): MermaidThemeName {\n return palette.startsWith(\"dark\") ? \"dark\" : \"default\";\n}\n\n/**\n * Theme variables per theme. Mermaid derives most of its palette from these, so\n * the set is deliberately small: the surfaces, the ink, and the lines.\n */\nconst THEME_VARIABLES: Record<MermaidThemeName, Record<string, string>> = {\n dark: {\n // slate-800: the fence the app leaves around a rendered diagram.\n background: \"#1d293d\",\n // slate-700 boxes on that fence, outlined in slate-400 — the outline is\n // what makes a node read as a box, and slate-100 ink sits at 11:1 inside it.\n mainBkg: \"#314158\",\n nodeBorder: \"#90a1b9\",\n nodeTextColor: \"#f1f5f9\",\n // slate-400: 5.6:1 on the fence, so an edge is a line and not a smudge.\n lineColor: \"#90a1b9\",\n textColor: \"#e2e8f0\",\n // The chip under a label on an edge. Matching the surface behind the\n // diagram is what makes it read as a gap in the line rather than as a tag.\n edgeLabelBackground: \"#1d293d\",\n },\n default: {\n background: \"#f8fafc\",\n mainBkg: \"#f1f5f9\",\n nodeBorder: \"#62748e\",\n nodeTextColor: \"#0f172b\",\n lineColor: \"#62748e\",\n textColor: \"#1d293d\",\n edgeLabelBackground: \"#f8fafc\",\n },\n};\n\n/**\n * Where each variable comes from under a color theme: the palette step the\n * built-in value was chosen from. The hex above IS that step in Tailwind's own\n * palette, so reading the step back out of the page gives the same diagram\n * under the built-in look and the theme's colors under any other.\n */\nconst THEME_SOURCES: Record<MermaidThemeName, Record<string, string>> = {\n dark: {\n background: \"--color-slate-800\",\n mainBkg: \"--color-slate-700\",\n nodeBorder: \"--color-slate-400\",\n nodeTextColor: \"--color-slate-100\",\n lineColor: \"--color-slate-400\",\n textColor: \"--color-slate-200\",\n edgeLabelBackground: \"--color-slate-800\",\n },\n default: {\n background: \"--color-slate-50\",\n mainBkg: \"--color-slate-100\",\n nodeBorder: \"--color-slate-500\",\n nodeTextColor: \"--color-slate-900\",\n lineColor: \"--color-slate-500\",\n textColor: \"--color-slate-800\",\n edgeLabelBackground: \"--color-slate-50\",\n },\n};\n\nexport function mermaidThemeVariables(\n theme: MermaidThemeName,\n): Record<string, string> {\n const fixed = THEME_VARIABLES[theme];\n // The built-in look keeps its measured constants verbatim: no DOM reads, and\n // nothing a color conversion could round.\n if (!currentColorTheme()) return fixed;\n const out: Record<string, string> = {};\n for (const [key, hex] of Object.entries(fixed)) {\n out[key] = resolveCssColor(THEME_SOURCES[theme][key]) ?? hex;\n }\n return out;\n}\n\n/**\n * A custom property's color as `#rrggbb`, or `null` when it cannot be read.\n *\n * Mermaid wants hex at `initialize()` time, and a theme's value can be any CSS\n * color — `oklch()`, `color-mix()`, a `var()` of another variable. So the\n * browser does the work: a probe element resolves the property to a computed\n * color, and a 1×1 canvas turns that into sRGB bytes whatever syntax it came\n * back in. No canvas (jsdom, a locked-down embed) is `null`, and the caller\n * falls back to the built-in value.\n *\n * A property nobody declared is `null` too, and has to be checked for up\n * front: `color: var(--unset)` is invalid at computed-value time, so the probe\n * *inherits* its color instead — the page's text color, which the canvas\n * would dutifully turn into a plausible hex and mermaid would paint every node\n * box with.\n */\nexport function resolveCssColor(property: string): string | null {\n if (typeof document === \"undefined\") return null;\n const declared = getComputedStyle(document.documentElement)\n .getPropertyValue(property)\n .trim();\n if (!declared) return null;\n const probe = document.createElement(\"span\");\n probe.style.display = \"none\";\n probe.style.color = `var(${property})`;\n document.documentElement.appendChild(probe);\n const computed = getComputedStyle(probe).color;\n probe.remove();\n if (!computed) return null;\n\n const ctx = pixelContext();\n if (!ctx) return null;\n ctx.fillStyle = computed;\n ctx.fillRect(0, 0, 1, 1);\n const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;\n return `#${[r, g, b].map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** A 1×1 2D context to read a color back from, or `null` where there is none. */\nfunction pixelContext(): CanvasRenderingContext2D | null {\n try {\n const canvas = document.createElement(\"canvas\");\n canvas.width = canvas.height = 1;\n return canvas.getContext(\"2d\", { willReadFrequently: true });\n } catch {\n return null;\n }\n}\n","// Rendered SVGs, kept so a re-render of the same document does not re-run\n// mermaid. Keyed by theme as well as code: the same fence renders to a\n// different SVG in each palette, and a cache keyed by code alone served the\n// light diagram — white boxes, black ink — onto the dark page after a theme\n// switch, and the dark one onto the light page.\n//\n// \"Theme\" here is the palette key (`currentMermaidPalette`): the mode, plus the\n// color theme when one is active, since switching color themes changes a\n// diagram's colors exactly as a light/dark flip does.\nimport { currentMermaidPalette } from \"./mermaidTheme.js\";\n\nconst svgCache = new Map<string, string>();\n\n// The separator is written as an escape on purpose: a literal NUL byte in the\n// source makes git call this file binary, so every change to it arrives as\n// \"Binary files differ\" with no diff to review. It is still a NUL at runtime —\n// the one byte neither a palette key nor a fence's code can contain.\nconst cacheKey = (code: string, theme: string) => `${theme}\\u0000${code}`;\n\n/** The SVG for this fence in the theme the page is currently asking for. */\nexport function getCachedSvg(\n code: string,\n theme: string = currentMermaidPalette(),\n): string | undefined {\n return svgCache.get(cacheKey(code, theme));\n}\n\nexport function hasCachedSvg(\n code: string,\n theme: string = currentMermaidPalette(),\n): boolean {\n return svgCache.has(cacheKey(code, theme));\n}\n\nexport function setCachedSvg(\n code: string,\n svg: string,\n theme: string = currentMermaidPalette(),\n): void {\n svgCache.set(cacheKey(code, theme), svg);\n}\n\nexport function clearMermaidCache() {\n svgCache.clear();\n}\n","import type mermaidAPI from \"mermaid\";\nimport {\n currentMermaidPalette,\n mermaidThemeOf,\n mermaidThemeVariables,\n} from \"./mermaidTheme.js\";\n\nlet mermaidInstance: typeof mermaidAPI | null = null;\nlet mermaidLoading: Promise<typeof mermaidAPI> | null = null;\n/**\n * The palette key (`currentMermaidPalette`) the loaded instance was last\n * configured for, `null` until loaded.\n */\nlet configuredTheme: string | null = null;\n\n/**\n * The configuration keys a diagram's own source may not set, from an `init`\n * directive or from frontmatter: Mermaid's default list, then the three\n * that write a diagram's stylesheet.\n *\n * **A diagram's stylesheet reaches past the diagram.** Mermaid puts every\n * selector of it under the diagram's id, but leaves the names of its\n * `@keyframes` global, so a diagram that defined `flash-update` redefined the\n * animation the viewer plays on a block that just changed. Given\n * `position: fixed` at the size of the window, the next flash laid that block,\n * and the link it held, over the header and the sidebar. The same stylesheet\n * fetched images from any host, which is why `SAFE_STYLE` refuses parentheses.\n *\n * - `themeCSS` is a stylesheet, and Mermaid checks only that its braces pair.\n * - `fontFamily` is copied into `themeVariables` *after* Mermaid's check on\n * theme variables has run, and lands in the stylesheet as a declaration's\n * value, where a nested block becomes a rule of its own.\n * - `altFontFamily` lands in a rule the browser parses first, which drops\n * what does not belong. It is refused anyway: nothing needs it, and that\n * parse is all that stands in the way.\n *\n * Mermaid applies the list at every depth, so `themeVariables.fontFamily` in\n * a diagram is refused too. The list is spelled whole rather than relying on\n * Mermaid merging it with its default. `frontend/e2e/mermaid.spec.ts` measures\n * all four routes; see \"Mermaid\" in `docs/reference/inline-markup.md`.\n */\nconst MERMAID_SECURE_KEYS = [\n \"secure\",\n \"securityLevel\",\n \"startOnLoad\",\n \"maxTextSize\",\n \"suppressErrorRendering\",\n \"maxEdges\",\n \"themeCSS\",\n \"fontFamily\",\n \"altFontFamily\",\n];\n\nfunction configure(m: typeof mermaidAPI, palette: string) {\n const theme = mermaidThemeOf(palette);\n m.initialize({\n startOnLoad: false,\n theme,\n themeVariables: mermaidThemeVariables(theme),\n securityLevel: \"strict\",\n suppressErrorRendering: true,\n secure: MERMAID_SECURE_KEYS,\n });\n configuredTheme = palette;\n}\n\n/**\n * The mermaid module, configured for the theme the page is asking for *now*.\n *\n * Re-configuring on a theme change is the point. `initialize` used to run once,\n * on first import, so every diagram rendered after a light/dark switch still\n * came out in the palette the session started in — a white slab of a flowchart\n * on the dark page, or a black one on the light page. `initialize` merges into\n * mermaid's global config, so calling it again is how the next `render` picks\n * the new palette up; the cache is keyed by theme so the old SVGs are not\n * served instead (`mermaidCache.ts`).\n */\nexport async function getMermaid(): Promise<typeof mermaidAPI> {\n const theme = currentMermaidPalette();\n if (mermaidInstance) {\n if (configuredTheme !== theme) configure(mermaidInstance, theme);\n return mermaidInstance;\n }\n if (!mermaidLoading) {\n mermaidLoading = import(\"mermaid\").then((mod) => {\n const m = mod.default;\n configure(m, currentMermaidPalette());\n mermaidInstance = m;\n return m;\n });\n }\n const loaded = await mermaidLoading;\n const wanted = currentMermaidPalette();\n if (configuredTheme !== wanted) configure(loaded, wanted);\n return loaded;\n}\n\nexport function resetMermaidLoader() {\n mermaidInstance = null;\n mermaidLoading = null;\n configuredTheme = null;\n}\n","/**\n * Client-side utility to find and render mermaid code blocks in a container.\n *\n * After calling `renderMarkdown()`, mermaid blocks come through as\n * `<pre><code class=\"language-mermaid\">...</code></pre>`. This function\n * finds those blocks and replaces them with rendered SVG diagrams.\n *\n * It also draws diagrams ahead of a render: `mermaidFences` reads them out of\n * Markdown and `prerenderMermaid` puts each in the SVG cache, where the React\n * `MermaidDiagram` finds it on mount.\n *\n * Framework-agnostic — works in any browser environment.\n */\n\nimport type { Code } from \"mdast\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\nimport { visit } from \"unist-util-visit\";\nimport { getCachedSvg, hasCachedSvg, setCachedSvg } from \"./mermaidCache.js\";\nimport { getMermaid } from \"./mermaidLoader.js\";\nimport { currentMermaidPalette } from \"./mermaidTheme.js\";\nimport { buildRemarkPlugins } from \"./remarkPlugins.js\";\n\nexport interface RenderMermaidOptions {\n /** CSS class to add to the SVG wrapper div (default: \"mermaid\") */\n className?: string;\n /** Called when a diagram fails to render */\n onError?: (code: string, error: Error) => void;\n}\n\n/**\n * Find all `<pre><code class=\"language-mermaid\">` blocks in a container\n * and replace them with rendered SVG diagrams.\n *\n * @param container - DOM element containing rendered markdown HTML\n * @param options - Optional configuration\n * @returns Promise that resolves when all diagrams are rendered\n *\n * @example\n * ```ts\n * import { renderMarkdown, renderMermaidBlocks } from \"vantage-md\";\n *\n * const { html } = await renderMarkdown(content);\n * container.innerHTML = html;\n * await renderMermaidBlocks(container);\n * ```\n */\nexport async function renderMermaidBlocks(\n container: HTMLElement,\n options: RenderMermaidOptions = {},\n): Promise<void> {\n const { className = \"mermaid\", onError } = options;\n\n const codeBlocks = container.querySelectorAll(\n 'pre > code.language-mermaid, pre > code[class*=\"language-mermaid\"]',\n );\n if (codeBlocks.length === 0) return;\n\n // Loaded on the first cache miss, not up front: mermaid is the heaviest\n // dependency in the package, and a container whose every diagram is already\n // in `svgCache` — the common case on a re-render — has no use for it.\n let loading: Promise<Awaited<ReturnType<typeof getMermaid>>> | undefined;\n const mermaidOnce = () => (loading ??= getMermaid());\n\n const renderPromises = Array.from(codeBlocks).map(async (codeEl) => {\n const preEl = codeEl.parentElement;\n if (!preEl) return;\n\n const code = codeEl.textContent || \"\";\n if (!code.trim()) return;\n\n // Check cache first. Keyed by theme as well as code — the same fence\n // renders to a different SVG in each palette.\n const cached = getCachedSvg(code);\n if (cached) {\n replaceWithSvg(preEl, cached, className);\n return;\n }\n\n try {\n const mermaid = await mermaidOnce();\n // Generate a stable ID from code hash\n let hash = 0;\n for (let i = 0; i < code.length; i++) {\n hash = (hash << 5) - hash + code.charCodeAt(i);\n hash = hash & hash;\n }\n const id = `mermaid-${Math.abs(hash).toString(36)}-${Date.now()}`;\n\n const { svg } = await mermaid.render(id, code);\n setCachedSvg(code, svg);\n replaceWithSvg(preEl, svg, className);\n } catch (err) {\n if (onError) {\n onError(code, err instanceof Error ? err : new Error(String(err)));\n }\n }\n });\n\n await Promise.all(renderPromises);\n}\n\n/**\n * Attributes the wrapper inherits from the `<pre>` it replaces.\n *\n * The splice is the same shape of problem `rehypeVantageMathStamps` solves for\n * KaTeX: the pipeline stamped the fence, and swapping the element out throws\n * the stamps away. A mermaid diagram inside a toned section then drew no slice\n * of the section's vertical rule, leaving a hole as tall as the diagram; a\n * collapsed section left the diagram visible under a closed heading; and a\n * `#L` anchor pointing at the fence resolved to nothing.\n *\n * Named individually rather than copied wholesale: `class` is the caller's\n * (`className`), and `id` would be duplicated onto a second element.\n */\nconst CARRIED_ATTRIBUTES = [\n \"data-source-line\",\n \"data-vantage-tone\",\n \"data-vantage-emphasis\",\n \"data-vantage-run\",\n \"data-vantage-collapsed\",\n \"data-vantage-collapse-group\",\n];\n\nfunction replaceWithSvg(\n preEl: HTMLElement,\n svg: string,\n className: string,\n): void {\n const wrapper = document.createElement(\"div\");\n wrapper.className = className;\n for (const name of CARRIED_ATTRIBUTES) {\n const value = preEl.getAttribute(name);\n if (value !== null) wrapper.setAttribute(name, value);\n }\n wrapper.innerHTML = svg;\n preEl.replaceWith(wrapper);\n}\n\n/* ------------------------------------------------------------------ *\n * Drawing ahead of a render\n * ------------------------------------------------------------------ */\n\n/** The viewer's parser: the remark half of `buildPipeline`. */\nconst fenceParser = unified().use(remarkParse).use(buildRemarkPlugins());\n\n/**\n * The code of every Mermaid fence in `markdown`, in source order, exactly as\n * a viewer's `MermaidDiagram` receives it: the fence's content, without its\n * container's indentation or its closing newline.\n *\n * A fence is a diagram when its info string's first word begins with the word\n * `mermaid`, which is the test the viewer applies to the `language-…` class\n * the fence renders with (`/language-(\\w+)/`). Text that never says\n * `mermaid` is not parsed at all.\n */\nexport function mermaidFences(markdown: string): string[] {\n if (!markdown.includes(\"mermaid\")) return [];\n const codes: string[] = [];\n visit(fenceParser.parse(markdown), \"code\", (node: Code) => {\n if (/^\\w+/.exec(node.lang ?? \"\")?.[0] === \"mermaid\") codes.push(node.value);\n });\n return codes;\n}\n\n/** Draws under way, by palette and code, so asking twice draws once. */\nconst drawing = new Map<string, Promise<void>>();\nlet drawn = 0;\n\n/**\n * Draw one diagram into the SVG cache that `MermaidDiagram` reads when it\n * mounts, so a diagram rendered after this has settled is drawn at its full\n * size on its first paint instead of growing into place once Mermaid answers.\n * The planning page draws every diagram of the cards it is about to show this\n * way, before it commits them.\n *\n * Mermaid is loaded through `getMermaid`, so the drawing is configured, and\n * made safe, exactly as every other diagram of the page is. `theme` is the\n * palette key (`currentMermaidPalette`) to draw for; when the page has moved\n * to another palette by the time Mermaid is ready, nothing is cached, since\n * Mermaid now draws for the page's palette and not for `theme`.\n *\n * Resolves once the diagram is in the cache, at once when it already was, and\n * rejects when Mermaid cannot draw it.\n */\nexport function prerenderMermaid(\n code: string,\n theme: string = currentMermaidPalette(),\n): Promise<void> {\n if (!code.trim() || hasCachedSvg(code, theme)) return Promise.resolve();\n const key = `${theme}\\u0000${code}`;\n let pending = drawing.get(key);\n if (pending === undefined) {\n pending = (async () => {\n const mermaid = await getMermaid();\n if (currentMermaidPalette() !== theme) return;\n const { svg } = await mermaid.render(\n `mermaid-ahead-${(++drawn).toString(36)}`,\n code,\n );\n setCachedSvg(code, svg, theme);\n })().finally(() => drawing.delete(key));\n drawing.set(key, pending);\n }\n return pending;\n}\n","/**\n * Rewrite relative links in rendered markdown HTML.\n *\n * After `renderMarkdown()` produces HTML, relative `href` values need to\n * be mapped to the consumer's routing structure. This utility handles that\n * without requiring a DOM — it operates on the HTML string directly.\n */\n\nexport interface ResolveLinkOptions {\n /** Base path to prepend to relative links (default: \"/\") */\n basePath?: string;\n /**\n * Custom rewriter function. Called for every relative href.\n * Return the rewritten href, or null to leave it unchanged.\n * If provided, basePath is ignored.\n */\n rewriter?: (href: string, currentPath: string) => string | null;\n /** Current file path — used to resolve relative references like `./other.md` */\n currentPath?: string;\n}\n\n/**\n * Rewrite relative links in rendered HTML.\n *\n * Processes all `href=\"...\"` attributes, skipping:\n * - Absolute URLs (http://, https://, mailto:, etc.)\n * - Anchor-only links (#section)\n * - Already-absolute paths (/path/to/file)\n *\n * @example\n * ```ts\n * import { renderMarkdown, resolveLinks } from \"vantage-md\";\n *\n * const { html } = await renderMarkdown(content);\n *\n * // Simple: prepend a base path\n * const resolved = resolveLinks(html, { basePath: \"/docs/\", currentPath: \"guides/setup.md\" });\n *\n * // Custom: full control over link rewriting\n * const resolved = resolveLinks(html, {\n * currentPath: \"guides/setup.md\",\n * rewriter: (href, currentPath) => `/kb/${currentPath}/../${href}`,\n * });\n * ```\n */\nexport function resolveLinks(\n html: string,\n options: ResolveLinkOptions = {},\n): string {\n const { basePath = \"/\", rewriter, currentPath = \"\" } = options;\n\n // Resolve the directory of the current file\n const parts = currentPath.split(\"/\");\n parts.pop(); // remove filename\n const currentDir = parts.join(\"/\");\n\n return html.replace(\n /href=\"([^\"]*?)\"/g,\n (_match: string, href: string): string => {\n // Skip absolute URLs, anchors, and already-absolute paths\n if (\n href.startsWith(\"http://\") ||\n href.startsWith(\"https://\") ||\n href.startsWith(\"mailto:\") ||\n href.startsWith(\"data:\") ||\n href.startsWith(\"#\") ||\n href.startsWith(\"/\")\n ) {\n return `href=\"${href}\"`;\n }\n\n if (rewriter) {\n const result = rewriter(href, currentPath);\n if (result !== null) {\n return `href=\"${result}\"`;\n }\n return `href=\"${href}\"`;\n }\n\n // Default: resolve relative to currentPath, prepend basePath\n const [pathPart, hashPart] = href.split(\"#\");\n const cleanHref = pathPart.replace(/^\\.\\//, \"\");\n const resolvedPath = currentDir\n ? `${currentDir}/${cleanHref}`\n : cleanHref;\n const base = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n const finalHref = `${base}${resolvedPath}${hashPart ? `#${hashPart}` : \"\"}`;\n\n return `href=\"${finalHref}\"`;\n },\n );\n}\n","/**\n * The canonical Vantage Markdown style guide.\n *\n * This string is the single source of truth for the conventions Vantage's\n * renderer expects. It reaches readers three ways:\n *\n * - the in-app \"Style Guide for Agents\" modal, which shows it with a copy\n * button,\n * - the `vantage-check style-guide` command, which prints it so an agent can\n * fetch it without a human in the loop, and\n * - this package's own export, for anyone who renders with `vantage-md`.\n *\n * How the CLI serves it, and why there is one copy: docs/reference/agent-cli.md\n *\n * Every rule stated here should be one a checker can enforce or a renderer\n * actually cares about — if a line is neither, it does not belong.\n */\n\nexport const STYLE_GUIDE = `## Markdown style guide (for Vantage viewer)\n\nWhen writing or updating markdown documents that will be viewed in Vantage, follow these conventions:\n\n### Structure\n- Use headings (## and ###) to organize content — they become navigable outline anchors.\n- Keep paragraphs focused and concise. Break up dense text with subheadings, lists, or tables.\n\n### Links and cross-references\n- **Relative paths only**: Always link relative to the *current file's directory*:\n - Sibling in same folder: \\`[Other Doc](./other-doc.md)\\` or \\`[Other Doc](other-doc.md)\\`\n - Subdirectory: \\`[Design Doc](./design/auth.md)\\`\n - Parent / sibling folder: \\`[Overview](../overview.md)\\` or \\`[Spec](../specs/api.md)\\`\n- **Never use leading slashes**:\n - ❌ \\`[Doc](/docs/guide.md)\\` (breaks web routing and multi-repo scoping)\n - ✅ \\`[Doc](../docs/guide.md)\\` or \\`[Doc](./guide.md)\\`\n- **Never use absolute filesystem paths or URI schemes**:\n - ❌ \\`file:///workspace/docs/guide.md\\`, \\`/workspace/docs/guide.md\\`, \\`C:\\\\...\\`\n - ✅ \\`[Doc](./guide.md)\\` or \\`[Doc](../guide.md)\\`\n- **Always include the file extension**: Use \\`.md\\`, \\`.ts\\`, \\`.go\\`, etc. (e.g. \\`[Model](model.go)\\`).\n- **Line anchors and ranges**:\n - Link to specific lines: \\`[Handler](../server/api.go#L42)\\` or \\`[Range](../server/api.go#L42-L58)\\`\n - Same-file line anchor: \\`[See lines](#L10-L25)\\`\n - Vantage scrolls to and highlights the target lines.\n- **Section anchors**:\n - Same doc: \\`[Usage](#usage)\\`\n - Cross-doc: \\`[Architecture](../overview.md#system-architecture)\\`\n - Anchor slugs are lowercase, hyphenated, and punctuation-stripped.\n- **Backticks in links**: Place backticks inside the link label, not around the markdown link syntax:\n - ✅ \\`[\\`config.json\\`](./config.json)\\` or \\`[config.json](./config.json)\\`\n - ❌ \\`\\`[config.json](./config.json)\\`\\`\n\n### Frontmatter (Metadata)\n- Include structured metadata at the very top of docs delimited by \\`---\\` (YAML) or \\`+++\\` (TOML). Vantage renders this as a metadata card:\n\\`\\`\\`yaml\n---\ntitle: \"Feature Specification\"\nauthor: \"Agent\"\ndate: 2026-08-15\nstatus: in-review # draft | in-review | accepted | deprecated\ntags: [architecture, backend, api]\nsummary: \"Brief description of the document purpose.\"\nvantage:\n status-chip: true # show \\`status\\` as a chip above the metadata card\n---\n\\`\\`\\`\n- **Nothing may sit above the opening delimiter** — not a blank line, not an editorial comment, not a \\`<!-- vantage: … -->\\` directive. Frontmatter is recognized only at the very first byte of the file (in Vantage, on GitHub, and in every other reader), so one line above it turns the whole block into body text: a horizontal rule followed by a heading made of the raw keys, with every field lost. \\`vantage-check\\` reports it as \\`frontmatter/not-at-top\\`.\n- **\\`vantage:\\` is Vantage's own reserved key.** It holds chrome that belongs to the file rather than to a section, it never shows up in the metadata card, and every other renderer ignores it. One key today: \\`status-chip\\`.\n- **Prefer \\`status-chip: true\\`**, which shows the document's own \\`status:\\` and therefore cannot disagree with it. A literal \\`status-chip: accepted\\` is accepted too, but it is a second value that goes stale on its own — \\`vantage-check\\` reports the disagreement.\n- The chip's vocabulary is \\`status\\`'s, exactly: \\`draft | in-review | accepted | deprecated\\`, lowercase. \\`Draft\\` renders no chip at all, silently.\n\n### Planning documents: \\`stage\\`, \\`next\\`, \\`depends-on\\`\n- **Vantage reads a repository's plans as a set.** A document whose frontmatter has \\`status\\` or \\`stage\\`, or that carries a \\`question\\` directive (or an \\`oq\\`, the name it replaces), is a *planning document*, and Vantage shows its state (status, stage, open questions) in a badge beside every link to it. So write each fact once, in the document it belongs to, and link to it everywhere else: a copied status or count is the one that goes stale.\n- Three top-level keys beside \\`status\\` hold the rest of a document's planning state. They are facts about the file, so they sit at the top level, never under \\`vantage:\\`:\n\\`\\`\\`yaml\n---\ntitle: \"Payload bootstrap\"\nstatus: in-review\nstage: DESIGN\nnext: \"Rule OQ-B2 \\u2014 the install step waits on it\"\ndepends-on:\n - pypi-distribution.md\n - ../plans/rollout.md#OQ-R1\n---\n\\`\\`\\`\n- **\\`stage\\` is one word from the repository's own vocabulary**, the words \\`[planning.stages]\\` declares (below), spelled exactly as declared: matching is case-sensitive, so \\`Decided\\` is not \\`DECIDED\\`. \\`vantage-check\\` reports a word outside them as \\`planning/stage-vocabulary\\`.\n- **Frontmatter is the stage's one home.** Do not repeat the word in a prose \\`**Status:**\\` line: that is a second copy, and no tool can read or check it. Keep the prose line, where a document has one, for the date and the why.\n- **\\`next\\` is the next step, on one line of plain text.** A bare \\`OQ-\\` id in it links to this document's question of that id, so name the question rather than paraphrasing it.\n- **\\`depends-on\\` lists what the document waits on**: relative paths, resolved like links, each optionally ending in \\`#OQ-\\u2026\\` to name one question. A single path may stand on its own. A target that does not exist, lies outside the repository, or does not contain the id is an error (\\`planning/depends-on-missing\\`).\n- **Never a \\`priority\\` key.** A priority only means something relative to the others, so it lives in an ordered list, a roadmap, and not in each document.\n- **A roadmap is ordered links.** Every \\`roadmap.md\\` Vantage reads is a roadmap, in any directory, unless \\`[planning]\\` lists the roadmaps instead; one in a hidden directory, matched by \\`.vantageignore\\`, or ruled out by \\`include\\` or \\`exclude\\` is not read, and so is not a roadmap. Each entry is a link to a document or a question, then a one-clause reason for its place; the badge beside the link carries the rest, so never copy a status, a stage or a count into it. Prose beneath an entry holds only what has no other home, such as what would unblock it. A bare link to a document *routes* every question in it into the roadmap's order, and a link to one question's \\`#OQ-\\u2026\\` anchor routes that question; a link to any other heading routes nothing. A question is routed when any roadmap routes it, and with \\`planning/unrouted\\` turned on, \\`vantage-check\\` reports an open question no roadmap routes.\n- **\\`[planning]\\` in \\`.vantage.toml\\`** says which files are read, which of them are roadmaps, and what each stage word means. Every key is optional:\n\\`\\`\\`toml\n[planning]\n# roadmap = [\"roadmap.md\", \"docs/plans/roadmap.md\"] # absent: every roadmap.md\nexclude = [\"docs/gallery/**\"] # gitignore syntax; every .md is included by default\n\n[planning.stages] # each word maps to open | ready | built | done\nDESIGN = \"open\"\nDECIDED = \"ready\"\nBUILT = \"built\"\nSUPERSEDED = \"done\"\n\\`\\`\\`\n- **Each stage word maps to one of four roles**, which is what the word means to Vantage: \\`open\\` is still being decided, \\`ready\\` is decided and not built, \\`built\\` is built, and \\`done\\` is no longer a live proposal, so its questions leave every list of what needs a ruling. \\`vantage-check index\\` prints those lists.\n- **Never change \\`target\\`.** A \\`target = \"X.Y\"\\` line at the top of \\`.vantage.toml\\` names the oldest Vantage release the repository's readers use, and only a human edits it. A \\`vantage-check\\` older than the target refuses to run and names the release it needs: run that release, and leave the target as it is.\n\n### Mermaid diagrams\n- Use \\`\\`\\`mermaid code blocks for flowcharts, sequence diagrams, and architecture diagrams. Vantage provides interactive zoom, pan, dark/light theme adaptation, and SVG export.\n- **Quote labels with special characters**: Always quote node labels containing parentheses, brackets, or colons to prevent syntax errors:\n\\`\\`\\`mermaid\nflowchart TD\n client[\"Client (React SPA)\"] -->|WebSocket| srv[\"Vantage Server (Go)\"]\n srv --> git[\"Git CLI (git diff)\"]\n\\`\\`\\`\n\n### Inline SVG (needs Vantage 0.8 or later)\n- **Inline \\`<svg>\\` is a capability, not notation: it needs Vantage 0.8 or later.** GitHub, every other renderer and Vantage before 0.8 drop the drawing and leave the words of its \\`<text>\\` elements behind as loose text, so keep \\`<text>\\` to short labels a reader can bear to see as loose words.\n- **Name the drawing with \\`aria-label\\` on the \\`<svg>\\`, never with \\`<title>\\` or \\`<desc>\\`.** Vantage 0.8 removes both, so they name nothing there, and every other reader prints them: Vantage before 0.8 as loose words, GitHub with the tags.\n- **Pair every inline drawing with a fallback block**: a \\`<!-- vantage: fallback -->\\` directive above the block that stands in for the drawing. Vantage 0.8 and later never show that block; every other reader drops the comment and shows it. Say what the drawing shows and that it needs Vantage 0.8 or later, and link a copy of it committed as a file where there is one (\\`[the drawing](diagram.svg)\\`). The block is a paragraph, a list, a quote, a code block, a table or a \\`<div>\\`; Vantage shows a raw \\`<img>\\`, \\`<figure>\\` or \\`<details>\\` there, so wrap one in a \\`<div>\\`.\n- **Wrap the drawing in a \\`<div>\\` on a line of its own, with no blank line anywhere inside it**, and give it a \\`width\\`, a \\`height\\` and a \\`viewBox\\`. Paint with \\`fill\\`/\\`stroke\\` set to \\`currentColor\\` or a hex color, so it reads in light and dark mode.\n\\`\\`\\`markdown\n<div>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"120\" height=\"40\" viewBox=\"0 0 120 40\" role=\"img\" aria-label=\"Two boxes joined by an arrow\">\n <rect x=\"1\" y=\"1\" width=\"40\" height=\"38\" fill=\"none\" stroke=\"currentColor\"/>\n <path d=\"M45 20 H75 M68 14 L75 20 L68 26\" fill=\"none\" stroke=\"currentColor\"/>\n <rect x=\"79\" y=\"1\" width=\"40\" height=\"38\" fill=\"none\" stroke=\"currentColor\"/>\n</svg>\n</div>\n\n<!-- vantage: fallback -->\n\nTwo boxes joined by an arrow. This drawing needs Vantage 0.8 or later.\n\\`\\`\\`\n- **For a document read mostly on GitHub**, commit the drawing as a file and embed it with \\`\\` instead, which every renderer shows.\n\n### Code blocks and diffs\n- Always tag fenced code blocks with language identifiers (\\`ts\\`, \\`go\\`, \\`python\\`, \\`bash\\`, \\`json\\`, \\`yaml\\`, \\`diff\\`, \\`sql\\`, etc.) for syntax highlighting.\n- For proposed code modifications, use \\`\\`\\`diff blocks with \\`+\\` and \\`-\\` prefixes:\n\\`\\`\\`diff\n-const oldUrl = \"/api/v1\";\n+const newUrl = \"/api/v2\";\n\\`\\`\\`\n\n### Callouts and alerts\n- Use GitHub-style blockquote callouts for notes, tips, and warnings:\n> [!NOTE]\n> Background context or helpful explanation.\n\n> [!TIP]\n> Best practice advice or optimization suggestions.\n\n> [!IMPORTANT]\n> Key requirements or crucial information.\n\n> [!WARNING]\n> Urgent caution, breaking changes, or potential pitfalls.\n\n> [!CAUTION]\n> High-risk actions that could cause data loss or security issues.\n\n### Vantage directives (optional, and Vantage-only)\n\nVantage reads a few styling hints from ordinary HTML comments. Every other renderer — GitHub included — drops them, so a document has to read exactly the same without them: directives decorate, they never carry meaning. One goes on a line of its own, with a blank line after it, and applies to the block that follows:\n\n\\`\\`\\`markdown\n<!-- vantage: section tone=warning badge=stale -->\n\n## Migration path\n\nThe steps below predate the rewrite.\n\\`\\`\\`\n\n- **Four names to write**: \\`section\\` (the heading and everything under it), \\`block\\` (the one block after it), \\`question\\` (one Open Question, in any state; see Open questions below), \\`fallback\\` (the one block after it, which Vantage 0.8 and later never show; see Inline SVG above). \\`fallback\\` takes no keys, and withholds only a paragraph, a list, a quote, a code block, a table, a rule or a \\`<div>\\`, never a heading. A fifth, \\`oq\\`, is the name \\`question\\` replaces (below).\n- **The keys and values are a closed set**: \\`tone\\` = \\`note | tip | important | warning | caution | muted\\`; \\`emphasis\\` = \\`strong | normal | quiet\\`; \\`badge\\` = \\`draft | stale | blocked | done | wip\\`; \\`collapsed\\` = \\`true | false\\`. Name a *tone*, never a color — the theme decides what a warning looks like, in light mode, in dark mode, and in print.\n- **Use them sparingly.** One or two per document, on the sections that genuinely differ. A document where everything is toned says nothing, and a rainbow one is harder to read than a plain one.\n- **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.\n- **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.\n- **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.\n- **A 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.\n- **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.\n\n### Open questions\nA question a document leaves for a human to rule on is an *Open Question*, written in a numbered list under a heading of its own.\n\n- **Write each question in its parts, never as one run-together paragraph.** Vantage lays a question out by these parts: the contents column and the planning page's card show its title as the question, and the card shows the leaning as the leaning and the Answer as the answer, so a part run into another one is read as neither. Each part is a block of its own, with a blank line before it:\n 1. **The title line**: the status marker, the id and the question itself in bold, saying what is being decided: \\`\\u{1F4AC} **OQ-9: Where does a job go when it re-enters the queue?**\\`.\n 2. **The context**, in short paragraphs: only what a ruling needs. Background, history and cross-references belong in the document's own sections, linked from the question.\n 3. **The options as a list**, one item each, when there are options to choose between.\n 4. **\\`_Leaning:_\\` as a paragraph of its own**: which option, and why, in a sentence or two.\n 5. **\\`**Answer:**\\` as a paragraph of its own**, with the ruling below it once there is one.\n\n\\`\\`\\`markdown\n## Open Questions\n\n1. \\u{1F4AC} **OQ-9: Where does a job go when it re-enters the queue?**\n\n A job that fails its checks leaves the queue and comes back once it is fixed. What merged while it was out may change what it does.\n\n - **A — The back of the queue.** It waits its turn again.\n - **B — Its old place.** It skips ahead of what joined since.\n\n <!-- vantage: question id=OQ-9 leaning=\"A — the back of the queue: the fix might interact with what merged while it was out.\" -->\n\n _Leaning:_ A. The fix might interact with what merged while it was out.\n\n **Answer:**\n\n > _(empty — fill in when decided)_\n\n2. \\u{1F512} **OQ-10: How large is the retry budget?**\n\n <!-- vantage: question id=OQ-10 -->\n\n Waits on the load test, so there is no leaning to state yet.\n\\`\\`\\`\n\n- **The marker is the question's state**: \\u{1F4AC} open (\\u{1F4AC} \\u{1F937} when it is a matter of preference), \\u{1F512} blocked on something upstream that has to happen first, \\u2705 answered. Review mode offers a one-click **Take this leaning** on an open question, and on nothing else.\n- **Every question gets a \\`question\\` directive**, \\`<!-- vantage: question id=OQ-9 leaning=\"…\" -->\\`, in every state, indented into its list item on a line of its own directly above the \\`_Leaning:_\\` paragraph, or above another of its paragraphs when it states no leaning. The directive is what makes it a question to Vantage: it gives the question its \\`#OQ-9\\` anchor, and Vantage's planning index (its model of a repository's plans) reads a question from nothing else, so without one nothing counts it, badges it or lists it as waiting. It goes when the question is compacted into the Decision Ledger, and not before.\n- **Changing a question's state changes its marker and nothing else.** Mark it \\u{1F512} when it cannot be answered yet, and \\u2705 once it is decided; the directive stays as it is, its \\`leaning\\` included, which nothing offers to take once the question is not open.\n- **Every open question with a stated leaning restates it as the directive's \\`leaning\\`.** The convention's prose — the emoji, the \\`OQ-N\\` id, the \\`_Leaning:_\\` line, the fill-in \\`**Answer:**\\` — produces no button on its own, and writing it and stopping there is the most common way the button goes missing: the questions look complete, review mode is on, and there is nothing to click. \\`vantage-check\\` reports an open question with a \\`_Leaning:_\\` and no directive as an error (\\`vantage/oq-missing\\`). An open question with no leaning yet takes the directive without \\`leaning\\`, so the planning index still counts it; until it states one, its button files the literal text \"Take the stated leaning.\"\n- **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.\n- **Keep the text below the title short.** The planning page shows each question as a card that leads with the title and shows only the first few lines of the rest, so a question buried in a paragraph of background is one the reviewer has to dig for. \\`vantage-check\\` warns when a question's text, not counting its \\`_Leaning:_\\` paragraph and its \\`**Answer:**\\`, runs past 120 words (\\`planning/question-length\\`), and when a leaning shares a paragraph with other text (\\`vantage/question-layout\\`); a repository can raise the limit with \\`max-words\\`, or turn either rule off, under \\`[check.rules]\\`.\n- **Enumerate with a list, never inline.** A question's options, or any three steps or parts, go in a Markdown list, one item per line, rather than run together in one sentence as \\`(a) \\u2026 (b) \\u2026 (c)\\`, where a reader has to find where each one starts. \\`vantage-check\\` warns on a paragraph that runs three or more together (\\`prose/inline-list\\`).\n- **Write a question as a list item.** Outside a list, a question runs from the block its directive lands on through the blocks after it, up to the next heading (for a question written as a heading, the next one of its level or higher), the next rule or the next question, so whatever prose follows it before then is read as part of it. There the directive goes above the question's title, since every viewer reads the question's marker from the block it lands on: a directive below the bold title lands on the block after it, which carries no marker, so every viewer reads the question as open. \\`vantage-check\\` reports that as \\`vantage/question-name\\`.\n- **\\`oq\\` is the name \\`question\\` replaces.** Vantage keeps reading \\`<!-- vantage: oq id=OQ-9 leaning=\"…\" -->\\` as it always has, and \\`vantage-check\\` warns on it (\\`vantage/oq-deprecated\\`) with the \\`question\\` to write instead, keys unchanged. Vantage before 0.8 drops \\`question\\` whole, so a reader still on 0.7 gets no one-click answer and no anchor on a question written with it, and misreads nothing. So keep an \\`oq\\` on an open question unless you know every reader of the repository is on 0.8 or later; a repository whose readers are still on 0.7 keeps its \\`oq\\`s and turns the warning off in \\`.vantage.toml\\`, with \\`\"vantage/oq-deprecated\" = \"off\"\\` under \\`[check.rules]\\`, which a \\`vantage-check\\` before 0.8 rejects as a rule it does not know. **Never put \\`oq\\` on a \\u{1F512} or \\u2705 question**: every Vantage before 0.8 offers the one-click button on every \\`oq\\`, whatever its marker says, and on a question with no leaning it files the literal text \"Take the stated leaning.\" \\`vantage-check\\` reports that as an error (\\`vantage/question-name\\`).\n\n### Tables, task lists, and math\n- **Tables**: Use standard markdown tables for structured comparisons and schemas.\n- **Task lists**: Use \\`- [ ]\\` and \\`- [x]\\` for actionable checklists and status tracking.\n- **LaTeX Math**: Use \\`$$...$$\\` for *all* KaTeX math — display blocks (\\`$$\\` alone on its own lines) and inline alike (\\`$$E = mc^2$$\\` mid-sentence).\n - Single dollars are **not** math delimiters: \\`$HOME\\` and \\`$100\\` stay literal, so prose and shell snippets are safe to write as-is.\n`;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["OQ_ID_PROPERTY","visit","defaultSchema","remarkGfm","remarkMath","rehypeRaw","rehypeSanitize","rehypeSlug","rehypeHighlight","rehypeKatex","parseTOML","YAML","remarkParse","remarkRehype","rehypeStringify","remarkParse"],"sources":["../src/rehypeVantageAnchors.ts","../src/rehypeSourceLines.ts","../src/rehypeVantageAlerts.ts","../src/vantageDirectives.ts","../src/rehypeVantageDirectives.ts","../src/rehypeVantageMathStamps.ts","../src/sanitize.ts","../src/remarkPlugins.ts","../src/pipeline.ts","../src/frontmatter.ts","../src/renderMarkdown.ts","../src/lineAnchor.ts","../src/scrollToLineAnchor.ts","../src/vantageFrontmatter.ts","../src/mermaidTheme.ts","../src/mermaidCache.ts","../src/mermaidLoader.ts","../src/renderMermaidBlocks.ts","../src/resolveLinks.ts","../src/styleGuide.ts"],"sourcesContent":["/**\n * Promote a carried Open Question id to a real `id`, on the safe side of the\n * sanitizer.\n *\n * `rehypeVantageDirectives` cannot write `id` itself. It runs before\n * `rehypeSanitize` — it must, because it reads HTML comments and the sanitizer\n * deletes them — and the sanitizer's default schema clobbers `id` with the\n * prefix `user-content-`. So the directive plugin stamps `data-vantage-oq-id`,\n * the sanitizer validates that value against the id grammar, and this plugin\n * moves it onto `id` afterwards.\n *\n * That is the same reason `rehypeSlug` is registered after the sanitizer\n * (`pipeline.ts`), and this plugin has to run **before** `rehypeSlug`:\n * `rehype-slug` leaves an element that already has an `id` alone, so promoting\n * first is what lets a question written as a heading keep `OQ-4` instead of\n * acquiring a heading slug. Where both could apply, the Open Question id wins.\n *\n * The data attribute is removed on the way through. Nothing downstream reads\n * it — every reader finds a question by `[data-vantage-question]`\n * (`VANTAGE_QUESTION_SELECTOR`) — and leaving a second copy of the id in the\n * markup invites a future reader to use the wrong one.\n *\n * Architecture and invariants: docs/reference/linked-references.md\n */\n\nimport type { Root, Element } from \"hast\";\nimport { visit } from \"unist-util-visit\";\n\n/** What `rehypeVantageDirectives` stamps, in hast property form. */\nconst OQ_ID_PROPERTY = \"dataVantageOqId\";\n\nexport default function rehypeVantageAnchors() {\n return (tree: Root) => {\n visit(tree, \"element\", (node: Element) => {\n const carried = node.properties?.[OQ_ID_PROPERTY];\n if (typeof carried !== \"string\" || carried === \"\") return;\n\n delete node.properties[OQ_ID_PROPERTY];\n\n // An element that already carries an `id` keeps it. Nothing in the\n // pipeline produces one this early, but a document can hand-write raw\n // HTML, and silently overwriting an author's own id would be a surprise\n // with no error attached.\n if (typeof node.properties.id === \"string\" && node.properties.id !== \"\") {\n return;\n }\n node.properties.id = carried;\n });\n };\n}\n","/**\n * Rehype plugin that adds `data-source-line` attributes to block-level\n * elements based on their position in the original markdown source.\n *\n * This enables GitHub-style line anchors (#L42, #L42-L50) by giving\n * each rendered block a traceable line number from the source.\n */\n\nimport type { Root, Element } from \"hast\";\nimport type { Plugin } from \"unified\";\n\n/**\n * Tags that get a `data-source-line`.\n *\n * `td`/`th` are here for review mode: a comment anchors to the cell it was\n * written on, so the cell needs a line of its own to be found again. Every cell\n * in a row reports the *row's* start line — a GFM row is one source line — so a\n * line no longer names at most one anchorable element, and whatever resolves an\n * anchor has to break the tie by block hash (`useReviewHighlights`).\n */\nconst BLOCK_TAGS = new Set([\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"li\",\n \"blockquote\",\n \"pre\",\n \"table\",\n \"td\",\n \"th\",\n \"tr\",\n \"ul\",\n \"ol\",\n \"hr\",\n \"div\",\n]);\n\nexport interface RehypeSourceLinesOptions {\n /**\n * Lines stripped off the front of the file before parsing — frontmatter,\n * essentially. Added to every emitted line number so `data-source-line`\n * names a line in the *file* rather than in the parsed body, which is what\n * a `#L42` link written against the file means. Defaults to 0.\n */\n offset?: number;\n}\n\nfunction visit(node: Root | Element, offset: number) {\n if (\"children\" in node) {\n for (const child of node.children) {\n if (child.type === \"element\") {\n if (BLOCK_TAGS.has(child.tagName) && child.position?.start?.line) {\n child.properties = child.properties || {};\n child.properties[\"dataSourceLine\"] =\n child.position.start.line + offset;\n }\n visit(child, offset);\n }\n }\n }\n}\n\nconst rehypeSourceLines: Plugin<[RehypeSourceLinesOptions?], Root> = (\n options,\n) => {\n const offset = options?.offset ?? 0;\n return (tree: Root) => {\n visit(tree, offset);\n };\n};\n\nexport default rehypeSourceLines;\n","/**\n * GFM alerts — `> [!WARNING]` — compiled into `data-vantage-alert`.\n *\n * `remark-gfm` does not implement alerts, so until this plugin existed a\n * `> [!WARNING]` rendered as an ordinary blockquote with the literal marker\n * visible as its first words. Worse than merely unstyled: `@tailwindcss/typography`\n * italicizes blockquotes and draws `open-quote`/`close-quote` around the first\n * paragraph, so a callout came out as an italic *quotation* whose opening words\n * were `\"[!WARNING]`. That was the \"Known gaps\" entry in\n * `docs/reference/inline-markup.md` and OQ-10, filed rather than fixed, while\n * `styleGuide.ts` went on telling every agent to write them.\n *\n * The tokens are deliberately the ones the `tone` vocabulary already resolves —\n * an alert *is* the six-color light/dark treatment `tone` shipped, which is\n * exactly what the gap entry said whoever fixed this should do rather than\n * building a second palette. `[!WARNING]` and `<!-- vantage: block tone=warning -->`\n * therefore agree by construction, and adding a theme still touches one\n * custom-property block.\n *\n * **This runs in the shared pipeline, so all four renderers get it** — the live\n * viewer, the package's exported viewer, the static export and the CLI checker's\n * `renderMarkdown`. That is what makes an injected title element acceptable here\n * where the collapse caret's glyph had to be drawn in CSS: the caret is injected\n * by app JS that may never run, and this is not (D5).\n *\n * ## What it does not do\n *\n * It does not touch a blockquote that carries no marker, and an unrecognized\n * marker (`[!HINT]`) is left exactly as it was — visible literal text, which is\n * the honest rendering of something GitHub also would not style. Silently\n * swallowing it would hide a typo that reads as a callout on neither renderer.\n */\n\nimport { visit } from \"unist-util-visit\";\nimport type { Element, Root, Text } from \"hast\";\n\n/**\n * The five GFM alert kinds, lowercased.\n *\n * Deliberately *not* re-derived from `VANTAGE_TONES`: that list carries a sixth\n * token, `muted`, which is ours and is not an alert word. The overlap is the\n * point — the five that coincide share a palette — but the two vocabularies are\n * closed by different authorities and a change to one must not silently move the\n * other. A test asserts the five are a subset of the tones.\n */\nexport const VANTAGE_ALERTS = [\n \"note\",\n \"tip\",\n \"important\",\n \"warning\",\n \"caution\",\n] as const;\n\nexport type VantageAlert = (typeof VANTAGE_ALERTS)[number];\n\n/**\n * The class on the title element this plugin injects, which\n * `styles/directives.css` styles. Exported because the sanitizer runs after\n * this plugin and admits a `div`'s class only when it is this one.\n */\nexport const ALERT_TITLE_CLASS = \"vantage-alert-title\";\n\n/** The visible label per kind. Title case, as GitHub renders it. */\nexport const ALERT_TITLES: Readonly<Record<VantageAlert, string>> = {\n note: \"Note\",\n tip: \"Tip\",\n important: \"Important\",\n warning: \"Warning\",\n caution: \"Caution\",\n};\n\n/**\n * The marker, anchored and requiring the rest of its line to be empty.\n *\n * GFM puts the marker alone on the blockquote's first line, and holding to that\n * is what keeps a paragraph that merely *begins* with bracketed text from being\n * eaten. The trailing newline is optional only for the degenerate blockquote\n * whose entire content is the marker.\n *\n * Measured against the real chain rather than assumed: `remark-parse` reads\n * `[!TIP]` as a shortcut link reference, and because no definition matches,\n * `mdast-util-to-hast` puts it back as **one** leading text node —\n * `\"[!TIP]\\nThe generalization: \"` — not as a `[`/label/`]` triple. So a single\n * anchored test on the first text node is enough, and the plugin does not have\n * to reassemble the marker across siblings.\n */\nexport const ALERT_MARKER =\n /^\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\][ \\t]*(?:\\r?\\n|$)/;\n\n/** The first child, if it is an element. */\nfunction firstElement(node: Element): Element | undefined {\n const child = node.children.find(\n (c) => c.type === \"element\" || (c.type === \"text\" && c.value.trim() !== \"\"),\n );\n return child?.type === \"element\" ? child : undefined;\n}\n\n/**\n * Compile `> [!KIND]` blockquotes into `data-vantage-alert=\"kind\"`.\n *\n * Order in the chain matters twice, and both are stated in `pipeline.ts`:\n *\n * - **after `rehypeSourceLines`**, so the injected title carries no\n * `data-source-line`. That is what keeps it out of `anchorBlockWithin`, which\n * filters candidates to those with a finite line — otherwise a review comment\n * on an alert would anchor to the word \"Warning\" instead of to the prose.\n * - **before `rehypeSanitize`**, so nothing reaches the DOM the schema has not\n * passed. `dataVantageAlert` is allowlisted there by name *and* value, like\n * every other `data-vantage-*` attribute.\n */\nexport function rehypeVantageAlerts() {\n return (tree: Root): void => {\n visit(tree, \"element\", (node: Element) => {\n if (node.tagName !== \"blockquote\") return;\n\n const paragraph = firstElement(node);\n if (paragraph === undefined || paragraph.tagName !== \"p\") return;\n\n const lead = paragraph.children[0];\n if (lead === undefined || lead.type !== \"text\") return;\n\n const match = ALERT_MARKER.exec(lead.value);\n if (match === null) return;\n\n const kind = match[1].toLowerCase() as VantageAlert;\n lead.value = lead.value.slice(match[0].length);\n\n // A paragraph holding nothing but the marker leaves an empty <p> that\n // typography still gives a margin to, so the callout opens with a blank\n // line. Drop it — but only when it is genuinely empty, since\n // `> [!NOTE]\\n> text` puts the text in this same node.\n if (lead.value === \"\" && paragraph.children.length === 1) {\n node.children = node.children.filter((c) => c !== paragraph);\n }\n\n node.properties = { ...node.properties, dataVantageAlert: kind };\n node.children.unshift({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [ALERT_TITLE_CLASS] },\n children: [{ type: \"text\", value: ALERT_TITLES[kind] } as Text],\n } as Element);\n });\n };\n}\n","/**\n * The directive grammar and the closed vocabulary — one parser, no renderer.\n *\n * A Vantage directive is an ordinary HTML comment carrying a `vantage:`\n * sentinel: `<!-- vantage: section tone=warning -->`. GitHub drops it, every\n * other Markdown renderer drops it, and Vantage compiles it into\n * `data-vantage-*` attributes on the block that follows\n * (`rehypeVantageDirectives`). See `docs/reference/inline-markup.md`, \"The carrier and the grammar\".\n *\n * This module is deliberately **zero-dependency — not even a type import**, and\n * it knows nothing about hast. Two callers need it and only one of them has a\n * tree: the rehype plugin stamps attributes, and the `vantage-check` CLI\n * validates directives with no rendering at all, importing this file by\n * relative path. A checker with its own copy of the grammar is a checker that\n * disagrees with the renderer, which is the failure D5 names.\n *\n * Everything here is a pure function of a string. Nothing throws, nothing logs\n * (P3): a comment that is not a directive is `null`, and a comment that carries\n * the sentinel but does not parse is `malformed` with a reason only the checker\n * reads.\n */\n\n/**\n * The mandatory sentinel — the full word, never a terser `v:`.\n *\n * It is what keeps an ordinary `<!-- TODO: rewrite this -->` from being parsed\n * as markup, and it makes the common case a prefix test rather than a grammar\n * attempt (Ledger OQ-1).\n */\nexport const VANTAGE_SENTINEL = \"vantage:\";\n\n/**\n * The closed name set. An unknown name drops the **whole** directive: there is\n * no target semantics without a name. An unknown key or value drops only that\n * pair (D2 is per-key).\n *\n * Position picks the target; the name picks the extent. `section` before a\n * heading reaches the heading's whole section, `block` reaches one block, and\n * `question` declares one question in any state, as `oq`, the name it\n * replaces, still does (`VANTAGE_QUESTION_NAMES`). The name cannot disagree\n * with position — it only says how far the stamp reaches — so §4.2's refusal\n * of a `scope=` key stands.\n *\n * `fallback` is the one name that stamps nothing: it withholds the block after\n * it, which is written for the renderers that cannot show something Vantage\n * shows — an inline `<svg>`, today. Every renderer that does not know the name\n * drops the comment and shows the block, a 0.7.x Vantage included, which is\n * the whole mechanism: the name is new, so no older reader gives it a meaning\n * (`docs/reference/inline-markup.md`, \"Fallback blocks\").\n */\nexport const DIRECTIVE_NAMES = [\n \"section\",\n \"block\",\n \"oq\",\n \"question\",\n \"fallback\",\n] as const;\n\n/**\n * The two names that declare a question: `question`, the one to write, and\n * `oq`, which it replaces and which every Vantage keeps reading.\n *\n * - **`question`** declares a question in any state: open (💬, 💬 🤷 or no\n * marker at all), 🔒 blocked or ✅ answered. The state comes from the marker\n * and never from the name, so a question that changes state changes its\n * marker and nothing else. A viewer from 0.8 on offers Take this leaning on\n * an open one, and never on a 🔒 or ✅ one.\n * - **`oq`** is deprecated, never removed. It keeps the meaning it shipped\n * with in 0.7, an open question to answer in one click, because documents\n * outlive releases and a release never gives existing notation a new\n * meaning (`docs/design/checker-version-skew.md`, P0). Every viewer before\n * 0.8 offers the button on every `oq` it meets, whatever its marker says,\n * so `vantage-check` reports one on a 🔒 or ✅ question as an error and any\n * other as a warning that names the `question` to write.\n *\n * The two take the same keys, `id` and `leaning`, with one grammar. A viewer\n * that predates `question` drops it whole, as it drops every name it does not\n * know, so on a question written with it that viewer offers no button and no\n * anchor: it does less, and misreads nothing. A run holding both names is one\n * question, read as such a viewer reads it: where both set a key, the `oq`'s\n * value wins.\n */\nexport const VANTAGE_QUESTION_NAMES = [\"oq\", \"question\"] as const;\n\n/** `oq` or `question`: a directive that declares a question. */\nexport type VantageQuestionName = (typeof VANTAGE_QUESTION_NAMES)[number];\n\n/** Whether `name` declares a question, `question` or the deprecated `oq`. */\nexport function isQuestionDirective(name: string): name is VantageQuestionName {\n return name === \"oq\" || name === \"question\";\n}\n\n/**\n * The attribute every question carries in the rendered page, whichever name\n * declared it, with the value `\"true\"`. It is how a reader of this package's\n * markup finds every question: `[data-vantage-question]`\n * (`VANTAGE_QUESTION_SELECTOR`).\n */\nexport const VANTAGE_QUESTION_ATTRIBUTE = \"data-vantage-question\";\n\n/** `[data-vantage-question]`: every question in a rendered page. */\nexport const VANTAGE_QUESTION_SELECTOR = `[${VANTAGE_QUESTION_ATTRIBUTE}]`;\n\n/**\n * The attribute a question declared with `oq` carries besides\n * `VANTAGE_QUESTION_ATTRIBUTE`, with the value `\"true\"`. It means what it has\n * meant since 0.7, \"declared with `oq`\", and nothing more: whether a question\n * offers Take this leaning is read from its state (`questionOffersTake`),\n * never from this attribute.\n */\nexport const VANTAGE_OQ_ATTRIBUTE = \"data-vantage-oq\";\n\n/**\n * The attribute that carries a question's `leaning=`, normalized\n * (`normalizeLeaning`), on the element `VANTAGE_QUESTION_ATTRIBUTE` is on,\n * whichever name declared it. Absent when the directive states none.\n */\nexport const VANTAGE_LEANING_ATTRIBUTE = \"data-vantage-leaning\";\n\n/**\n * The `tone` vocabulary: GitHub's alert words plus `muted`.\n *\n * Semantic, never chromatic (P2, Ledger OQ-3). A document says what a section\n * *is*; the theme decides what that looks like, which is what lets one document\n * render correctly in light, in dark, and in themes that do not exist yet.\n */\nexport const VANTAGE_TONES = [\n \"note\",\n \"tip\",\n \"important\",\n \"warning\",\n \"caution\",\n \"muted\",\n] as const;\n\n/** How much the block should pull the eye — separate from `tone` on purpose. */\nexport const VANTAGE_EMPHASIS = [\"strong\", \"normal\", \"quiet\"] as const;\n\n/** A small chip beside the heading. */\nexport const VANTAGE_BADGES = [\n \"draft\",\n \"stale\",\n \"blocked\",\n \"done\",\n \"wip\",\n] as const;\n\n/**\n * `collapsed` is a token, not a flag: `false` is the default written down.\n *\n * It stamps nothing on its own. Its one real effect is overriding a\n * `collapsed=true` earlier in the same merged directive run — last key wins — so\n * it is in the vocabulary rather than being an unknown value that drops. It\n * cannot cancel an *enclosing* collapsed section: a nested heading is a hidden\n * member of the outer group by design (A3), and the outer run is stamped before\n * any inner directive has been resolved.\n */\nexport const VANTAGE_COLLAPSED = [\"true\", \"false\"] as const;\n\n/**\n * Where a block sits in a stamped run, so section-wide CSS can join its members\n * without an adjacent-sibling combinator.\n *\n * Not cosmetic. Review mode inserts comment cards as siblings *inside* a\n * stamped run (`useReviewHighlights`), so `[tone] + [tone]` severs at every\n * commented paragraph and bleeds across the boundary between two adjacent runs\n * of different tone. An attribute survives both.\n */\nexport const VANTAGE_RUNS = [\"start\", \"middle\", \"end\", \"only\"] as const;\n\n/**\n * The tags a `section`/`block` directive may **target**.\n *\n * Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a directive's target should\n * also be a block with a `data-source-line`, so the styling surface and the\n * anchor surface coincide. It also keeps an inline directive from stamping the\n * `<em>` that happens to follow it inside a paragraph.\n *\n * It does **not** bound a `section`'s range. Every element in the span is\n * stamped, on the tag list or not, because a member only has to be a box in the\n * flow for the section's vertical rule to cross it — see `styleRange` in\n * `rehypeVantageDirectives.ts` for the hole that restricting the range left.\n *\n * It lives here rather than in the plugin because the CLI checker has to answer\n * \"will this directive stamp anything?\" from an mdast tree with no hast in\n * sight. A checker with its own copy of this list is a checker that calls a\n * working directive an orphan, or stays silent about a dead one (D5).\n */\nexport const VANTAGE_STYLE_TARGETS = [\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"li\",\n \"blockquote\",\n \"pre\",\n \"table\",\n \"tr\",\n \"ul\",\n \"ol\",\n \"hr\",\n \"div\",\n] as const;\n\n/**\n * The tags a `fallback` directive may **withhold**: `VANTAGE_STYLE_TARGETS`\n * minus the six headings, written as a subtraction so the narrowing stays\n * visible.\n *\n * A heading is refused because it is the document's structure rather than a\n * stand-in for anything: the contents column lists it, other documents link to\n * its slug, and the planning index reads sections under it. A fallback heading\n * would be an anchor that exists on GitHub and nowhere in Vantage. What a\n * fallback holds — a sentence naming the release a drawing needs, a link to\n * the drawing as a file, an ASCII sketch in a fence — is a paragraph, a list,\n * a quote, a code block, a table or a `<div>`, and those are all here.\n *\n * Three callers read it, and they must agree: `rehypeVantageDirectives`, which\n * withholds the block; the planning index's scan, which must not read a\n * question or a link from a block the page never shows; and the checker's\n * `vantage/orphan`, which reports a fallback that withholds nothing.\n */\nexport const VANTAGE_FALLBACK_TARGETS = VANTAGE_STYLE_TARGETS.filter(\n (tag) => !/^h[1-6]$/.test(tag),\n);\n\n/**\n * The tags a question directive, `oq` or `question`, may stamp — strictly the\n * tags the review system can resolve an anchor on (`ANCHOR_TAGS` in the app's\n * `MarkdownViewer`, and the block map in `useReviewHighlights`). `ul`, `ol`,\n * `tr`, `hr` and `div` are in neither, so a button on one of them would build\n * an anchor no review pass can find — the \"mis-wired button\" D6 forbids.\n *\n * The gap between this list and `VANTAGE_STYLE_TARGETS` is why a question\n * directive at column 0 above a list silently does nothing: the target is the\n * `<ul>`, not the `<li>`. The checker says so.\n */\nexport const VANTAGE_ANCHOR_TARGETS = [\n \"p\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"h5\",\n \"h6\",\n \"li\",\n \"blockquote\",\n \"pre\",\n \"table\",\n] as const;\n\n/**\n * The tags a question directive declares a question on, and so the only ones\n * an open question's *button* can sit on — `VANTAGE_ANCHOR_TARGETS` minus `pre`\n * and `table`, written as an explicit subtraction so the narrowing stays\n * visible. One list for both names and every state, so a question that changes\n * state is declared in exactly the same place.\n *\n * Anchorable and button-hosting are different questions, and this is the second\n * one. A comment *can* be anchored on a `<pre>` or a `<table>` — both are in\n * `ANCHOR_TAGS` — but neither can hold the affordance: inside a `<pre>` the\n * button renders as part of the code, and a `<button>` child of `<table>` is not\n * valid HTML at all, so the parser hoists it out.\n *\n * Every consumer reads it from here: `OQ_HOST_TAGS` in the app's\n * `useOpenQuestionButtons`, the planning index's scan, and the question branch\n * of the checker's `vantage/orphan`. They were two hand-written lists that\n * disagreed — the checker called an `oq` above a fence fine while the app\n * rendered no button and said nothing, which is the D5 break this module\n * exists to prevent.\n */\nexport const VANTAGE_OQ_HOST_TARGETS = VANTAGE_ANCHOR_TARGETS.filter(\n (tag) => tag !== \"pre\" && tag !== \"table\",\n);\n\n/**\n * The shape of a question directive's `id`, on `oq` and `question` alike:\n * `OQ-` then an optional short uppercase prefix then digits. `OQ-9`, `OQ-TP6`\n * and `OQ-A03` are ids; `OQ-foo`, `OQ-tp6` and a bare `OQ6` are not.\n *\n * The prefix is what keeps ids distinct once one document references another's\n * questions — `trust-paths.md`'s `OQ-4` and a design sketch's `OQ-4` are\n * different questions, and a bare number cannot say which one a cross-document\n * reference means. It is optional because most documents never leave their own\n * file, and requiring it everywhere would fire on every single-doc sketch.\n *\n * Read from here, never re-spelled, by the sanitizer that allowlists the value,\n * by the checker's `collectOqIds` (which feeds `vantage/oq-id-format` and the\n * anchors the checker accepts) and by the planning index. The directive plugin\n * that stamps the anchor deliberately does not test it: it stamps any non-empty\n * id and leaves refusing a malformed one to the sanitizer. A re-spelled copy is\n * how the checker starts calling a working anchor malformed — and the patterns\n * that look for an id in text (`OQ_REFERENCE` and `OQ_EXACT` in the checker's\n * `rules/references.ts`, `OQ_TOKEN` in `planning/scan.ts` and in\n * `FrontmatterDisplay.tsx`) restate this shape, so they change with it.\n */\nexport const VANTAGE_OQ_ID = /^OQ-(?:[A-Z][A-Z0-9]{0,5})?[0-9]+$/;\n\n/**\n * The status emoji the documentation convention marks an Open Question with, and\n * what each one means to a reader deciding whether the question wants them.\n *\n * These are *prose* — ordinary characters in the question's title, not part of\n * any directive — which is exactly why they need a home that both readers of\n * them can import. Two consumers ask what a marker means and they must not\n * answer differently: the checker's `vantage/oq-missing`, which demands a\n * directive on an open question and must never demand one on a blocked one, and\n * the viewer's contents column, which shows the marker and needs a word for it\n * that a screen reader can say. They were private constants in the checker until\n * the second consumer arrived.\n *\n * `open` is the only state that wants a one-click answer, so it is the only one\n * a viewer offers Take this leaning on (`questionOffersTake`). A `settled` or\n * `blocked` question carries no control — a control offering to answer a\n * question that is already decided, or that cannot be answered yet, is a lie.\n * The checker keys on the distinction rather than on the word \"Leaning:\"\n * alone. Either non-open marker wins when both appear on one item.\n */\nexport const VANTAGE_OQ_STATUS = {\n /** 💬 — an active decision awaiting a ruling. */\n open: \"\\u{1F4AC}\",\n /** ✅ — decided, awaiting compaction into a Decision Ledger. */\n settled: \"\\u2705\",\n /** 🔒 — blocked on an upstream decision or experiment. */\n blocked: \"\\u{1F512}\",\n} as const;\n\n/** One of the convention's three states, as [VANTAGE_OQ_STATUS] names them. */\nexport type VantageOqStatus = keyof typeof VANTAGE_OQ_STATUS;\n\n/**\n * What a marker says, in words, for somewhere an emoji cannot go.\n *\n * An accessible name is the reason this exists: a contents entry whose whole\n * status is one glyph says nothing to a screen reader, and \"speech bubble\" —\n * which is what it would otherwise read out — is worse than nothing.\n */\nexport const VANTAGE_OQ_STATUS_LABEL: Readonly<\n Record<VantageOqStatus, string>\n> = {\n open: \"Open question\",\n settled: \"Answered question\",\n blocked: \"Blocked question\",\n};\n\n/**\n * The state `text` is marked with, or `null` when it carries no marker.\n *\n * Non-open wins over open, the resolution `vantage/oq-missing` has always made:\n * a question marked both 💬 and ✅ has been answered and the stale marker simply\n * has not been cleared yet, so treating it as open would re-open a ruling.\n */\nexport function vantageOqStatus(text: string): VantageOqStatus | null {\n if (text.includes(VANTAGE_OQ_STATUS.settled)) return \"settled\";\n if (text.includes(VANTAGE_OQ_STATUS.blocked)) return \"blocked\";\n if (text.includes(VANTAGE_OQ_STATUS.open)) return \"open\";\n return null;\n}\n\n/**\n * Whether a question in this state offers Take this leaning in review mode: an\n * open one, marked 💬 or 💬 🤷, or one carrying no marker, which counts as\n * open. A 🔒 question cannot be answered yet and a ✅ one has been ruled, so\n * neither has a leaning left to take, whatever its directive's name.\n *\n * One home for the rule, because every surface that answers it must agree:\n * the page's button and the planning card, the counts of questions waiting on\n * a human, and `vantage-check`, which reports an `oq` on a question this says\n * no to, since every viewer before 0.8 offers the button there anyway.\n */\nexport function questionOffersTake(status: VantageOqStatus | null): boolean {\n return status === null || status === \"open\";\n}\n\n/** `null` for a key the grammar accepts but no closed set covers. */\nexport type KeyVocabulary = readonly string[] | null;\n\n/** The keys one directive name accepts. `undefined` for an unknown key. */\nexport type KeyTable = Readonly<Record<string, KeyVocabulary | undefined>>;\n\n/** The whole vocabulary. `undefined` for an unknown directive name. */\nexport type DirectiveVocabulary = Readonly<\n Record<string, KeyTable | undefined>\n>;\n\nconst STYLE_KEYS: KeyTable = {\n tone: VANTAGE_TONES,\n emphasis: VANTAGE_EMPHASIS,\n badge: VANTAGE_BADGES,\n collapsed: VANTAGE_COLLAPSED,\n};\n\n/**\n * Name → key → the closed value set for that key.\n *\n * `section` and `block` share their keys: they differ in *extent*, not in what\n * they can say. The two question names share theirs too, and they are the\n * design's only values with no closed set — `id` is a token an author chose\n * and `leaning` is a sentence (§8.3) — so neither can be value-allowlisted,\n * which is recorded here as `null` rather than left to a caller to guess.\n *\n * `question` takes exactly `oq`'s keys and nothing more. Starting narrow is\n * the forward-compatible choice: a key added later is one older viewers drop\n * pair by pair, while a key accepted now could never be given a meaning (P0).\n * A `leaning` on a 🔒 or ✅ question is allowed and simply not offered\n * (`questionOffersTake`), so changing a question's state changes only its\n * marker.\n *\n * `fallback` takes no keys: whether a block is withheld is the whole of what\n * it says, and a key added later is dropped pair by pair by this release, as\n * D2 drops any other.\n */\nexport const DIRECTIVE_VOCABULARY: DirectiveVocabulary = {\n section: STYLE_KEYS,\n block: STYLE_KEYS,\n oq: { id: null, leaning: null },\n question: { id: null, leaning: null },\n fallback: {},\n};\n\n/**\n * The one question a run of directives declares, merged: the name that won,\n * and its keys.\n */\nexport interface QuestionRun {\n /** `oq` when the run holds one, otherwise `question`. */\n name: VantageQuestionName;\n /** Every key the winning name's directives set that it accepts, merged. */\n keys: Map<string, string>;\n /** Which directive of the run, by index, each merged key's value came from. */\n sources: Map<string, number>;\n}\n\n/**\n * Merge the question directives of one run — the comments up to the block they\n * land on — into the one question they declare, or `undefined` when the run\n * holds none. Directives of any other name are skipped.\n *\n * A run holding an `oq` is read exactly as a viewer that predates `question`\n * reads it: that viewer drops every `question` and reads the `oq` alone, so\n * the `oq` wins wherever it appears in the run, and only its keys apply — a\n * key only a `question` in the same run sets applies nowhere, in no release.\n * That is the one reading every viewer agrees on (`VANTAGE_QUESTION_NAMES`),\n * so the same bytes file the same Take in 0.7 and in 0.8. Within one name, the\n * last value of a key wins, as `section` and `block` resolve theirs. A pair\n * its name does not accept, or a value outside a closed set, is dropped (D2).\n *\n * One function for every reader of a run: the plugin that stamps the question\n * and the planning index that reads it, which the checker reads a question\n * through, so none can disagree about which leaning or which id a run carries\n * (D5).\n */\nexport function mergeQuestionRun(\n run: readonly {\n name: string;\n pairs: readonly { key: string; value: string }[];\n }[],\n): QuestionRun | undefined {\n let name: VantageQuestionName | undefined;\n for (const directive of run) {\n if (!isQuestionDirective(directive.name)) continue;\n if (name !== \"oq\") name = directive.name;\n }\n if (name === undefined) return undefined;\n const accepted = DIRECTIVE_VOCABULARY[name] ?? {};\n const keys = new Map<string, string>();\n const sources = new Map<string, number>();\n run.forEach((directive, index) => {\n if (directive.name !== name) return;\n for (const pair of directive.pairs) {\n const values = accepted[pair.key];\n if (values === undefined) continue;\n if (values !== null && !values.includes(pair.value)) continue;\n keys.set(pair.key, pair.value);\n sources.set(pair.key, index);\n }\n });\n return { name, keys, sources };\n}\n\nexport interface DirectivePair {\n key: string;\n /** The value with quotes stripped, if it was quoted. */\n value: string;\n /** Offset of `key` within the comment's inner text. */\n keyOffset: number;\n /** Offset of the value token — opening quote included — within it. */\n valueOffset: number;\n quoted: boolean;\n}\n\nexport interface ParsedDirective {\n kind: \"directive\";\n name: string;\n /** Offset of `name` within the comment's inner text. */\n nameOffset: number;\n /** In written order, duplicates included: a checker reports them, the\n * renderer resolves them last-one-wins. */\n pairs: DirectivePair[];\n}\n\n/** Sentinel present, grammar not satisfied. The renderer ignores `reason`. */\nexport interface MalformedDirective {\n kind: \"malformed\";\n /** One clause a checker can quote verbatim, lowercase and unpunctuated. */\n reason: string;\n /** Offset of the first character the parse could not use. */\n offset: number;\n}\n\nexport type DirectiveParse = ParsedDirective | MalformedDirective | null;\n\n/**\n * `ws` is `[ \\t\\r\\n]` — the design's grammar leaves it undefined, and `\\n` has\n * to be in the set because a directive may legally wrap: a multi-line comment\n * is one node whose value contains the newlines.\n */\nconst WS = /[ \\t\\r\\n]*/y;\nconst SENTINEL_PREFIX = /^[ \\t\\r\\n]*vantage:/;\nconst NAME = /[a-z][a-z0-9-]*/y;\nconst UNQUOTED = /[A-Za-z0-9_.:#-]+/y;\n/**\n * A quoted value holds anything but a `\"`, `--` included: measured through the\n * real chain, `leaning=\"a--b\"` reaches the tree intact, because HTML5 closes a\n * comment on `-->` or `--!>` and on nothing else. There is deliberately **no**\n * `--` restriction here. What a quoted value cannot hold is a terminator: a\n * `-->` inside one ends the comment early and spills the tail into the document\n * as literal text, which is a finding for the checker rather than a rule here —\n * by the time this function runs, the truncation has already happened.\n */\nconst QUOTED = /\"[^\"]*\"/y;\n\n/**\n * The cheap prefix test. Runs first on every comment in every document, so an\n * ordinary editorial comment never reaches the tokenizer.\n *\n * Note `<!--- vantage: x -->` is *not* a directive: its inner text begins with\n * the extra `-`, and the sentinel must be the first thing in the comment.\n */\nexport function hasVantageSentinel(comment: string): boolean {\n return SENTINEL_PREFIX.test(comment);\n}\n\n/** The whole non-whitespace run at `offset`, capped, for a quotable message. */\nfunction token(comment: string, offset: number): string {\n const rest = comment.slice(offset);\n const end = rest.search(/[ \\t\\r\\n]/);\n const word = end === -1 ? rest : rest.slice(0, end);\n return word.length > 24 ? `${word.slice(0, 24)}…` : word;\n}\n\n/** The sticky match at `offset`, or `null` if the pattern does not apply. */\nfunction matchAt(\n pattern: RegExp,\n comment: string,\n offset: number,\n): string | null {\n pattern.lastIndex = offset;\n const match = pattern.exec(comment);\n return match === null ? null : match[0];\n}\n\n/** How much whitespace sits at `offset`. `WS` matches everywhere, empty. */\nfunction skipWhitespace(comment: string, offset: number): number {\n return matchAt(WS, comment, offset)?.length ?? 0;\n}\n\nfunction malformed(reason: string, offset: number): MalformedDirective {\n return { kind: \"malformed\", reason, offset };\n}\n\n/**\n * Parse one comment's **inner** text — the value of a hast `comment` node, with\n * `<!--` and `-->` already stripped. `null` means \"no sentinel, not ours\".\n *\n * Hand-rolled rather than one regular expression, because a repeated capture\n * group keeps only its last match and the checker needs an offset per token to\n * point at the character that broke.\n */\nexport function parseVantageDirective(comment: string): DirectiveParse {\n const sentinel = SENTINEL_PREFIX.exec(comment);\n if (sentinel === null) return null;\n\n let at = sentinel[0].length;\n at += skipWhitespace(comment, at);\n\n const nameOffset = at;\n const name = matchAt(NAME, comment, at);\n if (name === null) {\n return malformed(\"no directive name after `vantage:`\", at);\n }\n at += name.length;\n\n const pairs: DirectivePair[] = [];\n while (at < comment.length) {\n const gap = skipWhitespace(comment, at);\n at += gap;\n if (at >= comment.length) break;\n if (gap === 0) {\n return malformed(`\\`${token(comment, at)}\\` needs a space before it`, at);\n }\n\n const keyOffset = at;\n const key = matchAt(NAME, comment, at);\n if (key === null) {\n return malformed(\n `\\`${token(comment, at)}\\` is not a \\`key=value\\` pair`,\n at,\n );\n }\n at += key.length;\n\n if (comment[at] !== \"=\") {\n return malformed(`\\`${key}\\` is not followed by \\`=value\\``, at);\n }\n at += 1;\n\n const valueOffset = at;\n const quoted = matchAt(QUOTED, comment, at);\n if (quoted !== null) {\n at += quoted.length;\n pairs.push({\n key,\n value: quoted.slice(1, -1),\n keyOffset,\n valueOffset,\n quoted: true,\n });\n continue;\n }\n\n const unquoted = matchAt(UNQUOTED, comment, at);\n if (unquoted === null) {\n const found = token(comment, at);\n return malformed(\n found === \"\"\n ? `\\`${key}=\\` has no value`\n : `\\`${found}\\` is not a valid value for \\`${key}\\``,\n at,\n );\n }\n at += unquoted.length;\n pairs.push({ key, value: unquoted, keyOffset, valueOffset, quoted: false });\n }\n\n return { kind: \"directive\", name, nameOffset, pairs };\n}\n\n/**\n * 🤷 — the second glyph of `💬 🤷`, which marks an open question the author\n * flags as a matter of preference rather than a decision with stakes.\n *\n * Not a state: a question marked `💬 🤷` is open, and `vantageOqStatus` says so.\n * The planning index records the flag beside the state, so this lives with the\n * three state glyphs rather than in the index, where a second spelling of the\n * convention's markers would start.\n */\nexport const VANTAGE_OQ_PREFERENCE = \"\\u{1F937}\";\n\n/**\n * The longest leaning a question directive carries into the page, in\n * characters.\n *\n * A leaning becomes the body of a review comment, not prose, so it is bounded;\n * 500 is generous for the one sentence the convention asks for.\n */\nexport const MAX_LEANING = 500;\n\n/**\n * A question directive's `leaning=` value as the page carries it: whitespace\n * runs collapsed to one space, trimmed, and capped at `MAX_LEANING`.\n *\n * A wrapped directive puts newlines and indentation into the value. Two readers\n * need the result and must agree on it: `rehypeVantageDirectives`, which stamps\n * it as `data-vantage-leaning` for the one-click answer, and the planning index,\n * which reports the leaning a question carries. An empty result means the\n * question states no leaning.\n */\nexport function normalizeLeaning(raw: string): string {\n return raw.replace(/\\s+/g, \" \").trim().slice(0, MAX_LEANING);\n}\n","/**\n * Rehype plugin that compiles `<!-- vantage: … -->` directives into\n * `data-vantage-*` attributes on the block that follows them.\n *\n * It has to run between `rehype-raw` — which turns the comment into a hast node\n * — and `rehype-sanitize`, which deletes every comment node. That is the only\n * window in which the information exists (`docs/reference/inline-markup.md`, \"Where the plugin runs\"),\n * and `pipeline.ts` is where the slot is spelled out.\n *\n * The grammar and the vocabulary live in `./vantageDirectives.js`, which the\n * CLI checker imports too: one parser, two callers, so a directive cannot mean\n * one thing in the viewer and another in the tool that validates it (D5).\n *\n * Nothing here throws and nothing logs. An unknown name drops the whole\n * directive, an unknown key or value drops that pair only, and a directive with\n * no block after it does nothing at all (P3/D2/D6). The comment node is left\n * where it is: the sanitizer removes it, which is why no Vantage-specific\n * markup other than these attributes ever reaches the DOM.\n *\n * A `fallback` run is the one exception, and it stamps nothing: the run and\n * the block it lands on are removed from the tree, in a pass of their own that\n * runs before any stamping (`withholdFallbacks`). Removing the run's comments\n * with the block is what keeps a `block tone=…` written in the same run from\n * re-targeting the block after it, and nothing is lost, because the sanitizer\n * would have deleted those comments a few plugins later.\n */\n\nimport type { Element, Parents, Properties, RootContent, Root } from \"hast\";\nimport type { Plugin } from \"unified\";\nimport {\n DIRECTIVE_VOCABULARY,\n mergeQuestionRun,\n normalizeLeaning,\n parseVantageDirective,\n VANTAGE_ANCHOR_TARGETS,\n VANTAGE_FALLBACK_TARGETS,\n VANTAGE_STYLE_TARGETS,\n} from \"./vantageDirectives.js\";\nimport type {\n KeyVocabulary,\n ParsedDirective,\n VantageQuestionName,\n} from \"./vantageDirectives.js\";\n\n/**\n * What a `section`/`block` and a question directive (`oq`, `question`) may\n * **target**.\n *\n * Both lists live in `vantageDirectives.ts`, with the reasoning for each tag,\n * because the CLI checker resolves the same question over mdast and must reach\n * the same answer (D5).\n *\n * Neither list bounds a `section`'s range: see `styleRange`.\n */\nconst STYLE_TARGET_TAGS = new Set<string>(VANTAGE_STYLE_TARGETS);\nconst ANCHOR_TARGET_TAGS = new Set<string>(VANTAGE_ANCHOR_TARGETS);\n/** What a `fallback` may withhold: every style target but a heading. */\nconst FALLBACK_TARGET_TAGS = new Set<string>(VANTAGE_FALLBACK_TARGETS);\n\nconst HEADING_DEPTHS = new Map([\n [\"h1\", 1],\n [\"h2\", 2],\n [\"h3\", 3],\n [\"h4\", 4],\n [\"h5\", 5],\n [\"h6\", 6],\n]);\n\n/**\n * Key → hast property, for the keys that treat a whole run.\n *\n * A camelCase hast property serializes to the kebab-case attribute, so\n * `dataVantageTone` is `data-vantage-tone` in every renderer.\n *\n * `tone` and `emphasis` describe what a section *is* and how loud it is, so\n * every block in the range wears them: the tone rule is a slice of one\n * continuous line down the section, and the weight applies to all of its prose.\n *\n * `collapsed` is not here because it is not one property on one block: it puts a\n * toggle on the heading and a collapsed flag plus a group id on every block the\n * heading hides, which `stampStyle` does with the three properties below.\n */\nconst RANGE_PROPERTIES = new Map([\n [\"tone\", \"dataVantageTone\"],\n [\"emphasis\", \"dataVantageEmphasis\"],\n]);\n\n/**\n * Key → hast property, for the keys that mark one block: the directive's target.\n *\n * `badge` is the asymmetry in the vocabulary and the reason this second map\n * exists. It is not a treatment of a run but a single chip — \"a small chip after\n * the heading text\" (§4.3), drawn as `[data-vantage-badge]::after` — so a\n * section-wide stamp paints the word once per paragraph, list, table and fence\n * under the heading instead of once beside it.\n *\n * The chip is fixed here rather than in the stylesheet, because narrowing the\n * CSS to `:is(h1, …, h6)` would silently draw nothing for the two placements\n * that legitimately badge a non-heading — `block badge=…` on a paragraph, and a\n * `section` that degraded onto one (A1) — and would leave an attribute stamped\n * on every block that says something untrue about it.\n */\nconst POINT_PROPERTIES = new Map([[\"badge\", \"dataVantageBadge\"]]);\n\nconst RUN_PROPERTY = \"dataVantageRun\";\n/**\n * What a question stamps, whichever name declared it: `data-vantage-question`\n * on every question, and `data-vantage-oq` besides on one an `oq` declared\n * (`VANTAGE_QUESTION_ATTRIBUTE`, `VANTAGE_OQ_ATTRIBUTE`).\n *\n * Two attributes, and neither one ever means more than it says. Every release\n * since 0.7 stamps `data-vantage-oq` on an `oq` and on nothing else, so a\n * reader of this package's markup that has ever read it keeps reading exactly\n * what it read; `data-vantage-question` is the one to read for \"a question\",\n * and the question's state, from its marker, is what decides whether anything\n * offers to answer it (`questionOffersTake`).\n */\nconst QUESTION_PROPERTY = \"dataVantageQuestion\";\nconst OQ_PROPERTY = \"dataVantageOq\";\nconst LEANING_PROPERTY = \"dataVantageLeaning\";\n/**\n * The id, carried as a `data-` attribute rather than written straight to `id`.\n *\n * This plugin runs *before* `rehypeSanitize` — it has to, it reads comments and\n * the sanitizer deletes them — and the sanitizer's default schema clobbers `id`\n * with the prefix `user-content-`. A bare `id` set here would reach the page as\n * `user-content-OQ-4`, every `#OQ-4` link in every document would land nowhere,\n * and nothing would error. `rehypeVantageAnchors` promotes this to a real `id`\n * on the other side of the sanitizer, which is the same reason `rehypeSlug` is\n * registered there (`pipeline.ts`).\n */\nconst OQ_ID_PROPERTY = \"dataVantageOqId\";\n\n/**\n * The three properties `collapsed=true` stamps across a section.\n *\n * The heading takes a *different* attribute from the blocks it hides, and that\n * asymmetry is the whole design (A3): a nested `###` inside a collapsed `##` is\n * both a hidden member of the outer group and the toggle for its own, so one\n * shared attribute would make it permanently invisible and unreachable by\n * either toggle. There is no `<details>` and no wrapper — the run stays a flat\n * list of siblings, which is what keeps review comment cards, the typography\n * plugin's `h2 + *` margin resets and the anchor surface working.\n *\n * Hiding is CSS, and that CSS is gated on two markers the toggle JS sets — the\n * prose container's readiness, and an armed marker on each block whose group it\n * gave a caret (`docs/reference/inline-markup.md`, \"Collapse without a wrapper\"). A renderer without the JS\n * — the CLI checker's HTML, an external consumer of this package — shows every\n * block, and so does any block that ended up with no control.\n */\nconst COLLAPSED_PROPERTY = \"dataVantageCollapsed\";\nconst COLLAPSE_GROUP_PROPERTY = \"dataVantageCollapseGroup\";\nconst COLLAPSE_TOGGLE_PROPERTY = \"dataVantageCollapseToggle\";\n\n/**\n * Per-tree state. Group ids are `1`, `2`, `3`… in the document order of the\n * headings that own them, so the same document always numbers the same way and\n * an inner section always draws a higher number than the section enclosing it.\n *\n * It lives in the transformer's closure rather than at module scope: a counter\n * shared between trees would renumber a document because another one rendered\n * first, and `renderMarkdown` running twice in one process has to produce\n * byte-identical HTML.\n */\ninterface CollapseState {\n nextGroup: number;\n}\n\n/**\n * Nodes that may sit between a directive and its target.\n *\n * A whitespace-only `text` node always does — measured, with or without a blank\n * line in the source. Comments do too, and an unrelated `<!-- TODO -->` must not\n * break the chain: it is invisible in every renderer and deleted by the\n * sanitizer, so letting it change a directive's meaning would make behavior\n * depend on something no reader can see.\n */\nfunction isSkippable(node: RootContent): boolean {\n if (node.type === \"comment\" || node.type === \"doctype\") return true;\n if (node.type === \"text\") return node.value.trim() === \"\";\n return false;\n}\n\nfunction headingDepth(node: RootContent): number | undefined {\n if (node.type !== \"element\") return undefined;\n return HEADING_DEPTHS.get(node.tagName);\n}\n\nfunction setProperty(element: Element, property: string, value: string) {\n element.properties = element.properties ?? ({} as Properties);\n element.properties[property] = value;\n}\n\n/**\n * Where a member sits in a stamped run: `only` for a lone block, otherwise\n * `start`, `middle`, `end`. See `VANTAGE_RUNS`.\n */\nfunction runValue(index: number, length: number): string {\n if (length === 1) return \"only\";\n if (index === 0) return \"start\";\n return index === length - 1 ? \"end\" : \"middle\";\n}\n\n/** The closed value set for one key, or `undefined` when the key is unknown. */\nfunction vocabularyOf(name: string, key: string): KeyVocabulary | undefined {\n return DIRECTIVE_VOCABULARY[name]?.[key];\n}\n\nfunction accepts(name: string, key: string, value: string): boolean {\n const values = vocabularyOf(name, key);\n if (values === undefined) return false;\n return values === null || values.includes(value);\n}\n\n/**\n * The nodes one style directive reaches, as indexes into its own parent's\n * children — never outside that array, so a directive inside a blockquote or a\n * list item cannot stamp past it.\n *\n * Position picks the target (the next sibling element); the name picks how far\n * the stamp goes. `section` before a heading takes the heading and every\n * following sibling until the first heading of the same or shallower depth;\n * `section` before anything else degrades to that one block, and `block` is\n * always that one block. A heading nested inside a stamped `blockquote` or\n * `li` does not end the section: the walk never descends.\n *\n * **Every element in the span, not only a `VANTAGE_STYLE_TARGETS` one.** That\n * list gates the *target* and nothing else. Restricting the range to it as well\n * used to leave a raw-HTML `<figure>`, `<dl>` or `<details>` unstamped between\n * two stamped paragraphs — and the section's one continuous vertical rule is\n * drawn per member, so an unstamped member is a hole the height of the block\n * plus its margins. Measured over the real stylesheet: 44px for a one-line\n * `<figure>`, against the 40px a neighbor can bleed upward, and arbitrarily\n * large for anything taller. `collapsed=true` had the same shape of bug the\n * other way round — it hid the paragraphs and left the figure on the page.\n *\n * The two lists answering different questions is the point, not an oversight:\n * a *target* must be a block a review anchor can name, because a directive\n * pointing at something unanchorable is a directive with no addressable effect.\n * A *member* only has to be a box in the flow, because all it does is carry the\n * run's tone across itself.\n */\nfunction styleRange(\n children: RootContent[],\n targetIndex: number,\n name: string,\n): number[] {\n const range = [targetIndex];\n const depth =\n name === \"section\" ? headingDepth(children[targetIndex]) : undefined;\n if (depth === undefined) return range;\n\n for (let i = targetIndex + 1; i < children.length; i++) {\n const node = children[i];\n const nodeDepth = headingDepth(node);\n if (nodeDepth !== undefined && nodeDepth <= depth) break;\n if (node.type === \"element\") range.push(i);\n }\n return range;\n}\n\n/**\n * Whether this directive collapses its section — three ways to say no.\n *\n * `collapsed=false` stamps nothing: it is the default written down, and \"not\n * collapsed\" is not a thing an attribute can usefully say. Its only effect is\n * upstream of here — `stampRun` merges a run of comments last-key-wins, so a\n * `false` cancels a `true` in the *same* run. It does not cancel an *enclosing*\n * section: `styleRange` walks the outer heading's whole sibling span before any\n * inner directive is resolved, and a nested heading being a hidden member of the\n * outer group is the design (A3), not an oversight.\n *\n * A `block` scope is **dropped**, and so is a `section` that degraded onto\n * a non-heading, because both would hide a lone paragraph with nothing left\n * behind to reveal it — content that is simply gone, which is the P1/D8 failure\n * the readiness gate exists to prevent. Only a heading can be a summary.\n */\nfunction collapsesSection(\n name: string,\n pairs: Map<string, string>,\n target: RootContent,\n): boolean {\n if (name !== \"section\") return false;\n if (pairs.get(\"collapsed\") !== \"true\") return false;\n return headingDepth(target) !== undefined;\n}\n\nfunction stampStyle(\n children: RootContent[],\n targetIndex: number,\n name: string,\n pairs: Map<string, string>,\n state: CollapseState,\n) {\n const target = children[targetIndex] as Element;\n if (!STYLE_TARGET_TAGS.has(target.tagName)) return;\n\n // Resolve before stamping, and resolve the two reaches apart: a directive\n // whose every key was dropped stamps nothing at all, not even a run marker, so\n // `<!-- vantage: section -->` and `<!-- vantage: section tone=chartreuse -->`\n // are both plain documents.\n const rangeStamps: [string, string][] = [];\n const targetStamps: [string, string][] = [];\n for (const [key, value] of pairs) {\n if (!accepts(name, key, value)) continue;\n const rangeProperty = RANGE_PROPERTIES.get(key);\n if (rangeProperty !== undefined) {\n rangeStamps.push([rangeProperty, value]);\n continue;\n }\n const pointProperty = POINT_PROPERTIES.get(key);\n if (pointProperty !== undefined) targetStamps.push([pointProperty, value]);\n }\n const collapses = collapsesSection(name, pairs, target);\n if (rangeStamps.length === 0 && targetStamps.length === 0 && !collapses) {\n return;\n }\n\n const range = styleRange(children, targetIndex, name);\n // A heading with no body blocks gets no toggle: a caret that hides nothing is\n // an affordance that lies. The counter only advances for a group that exists,\n // so the ids stay dense.\n const group =\n collapses && range.length > 1 ? String(state.nextGroup++) : undefined;\n\n for (let i = 0; i < range.length; i++) {\n const element = children[range[i]] as Element;\n for (const [property, value] of rangeStamps) {\n setProperty(element, property, value);\n }\n // `range[0]` is the target, always: `styleRange` starts there and only ever\n // walks forward. A point marker stops here, and it is stamped before the run\n // marker so the attribute order of a badged heading is the order written.\n if (i === 0) {\n for (const [property, value] of targetStamps) {\n setProperty(element, property, value);\n }\n }\n // Only where a run treatment was stamped to join up: `run` describes the\n // extent of a tone's rule, and a collapse-only or badge-only section has no\n // rule to draw.\n if (rangeStamps.length > 0) {\n setProperty(element, RUN_PROPERTY, runValue(i, range.length));\n }\n if (group === undefined) continue;\n if (i === 0) {\n setProperty(element, COLLAPSE_TOGGLE_PROPERTY, group);\n } else {\n setProperty(element, COLLAPSED_PROPERTY, \"true\");\n setProperty(element, COLLAPSE_GROUP_PROPERTY, group);\n }\n }\n}\n\nfunction stampQuestion(\n target: Element,\n name: VantageQuestionName,\n pairs: Map<string, string>,\n) {\n // The string, never the boolean: `rehype-stringify` emits a bare\n // `data-vantage-oq` for `true` while react-markdown emits `=\"true\"`, and D5\n // requires every renderer to emit the same markup.\n setProperty(target, QUESTION_PROPERTY, \"true\");\n if (name === \"oq\") setProperty(target, OQ_PROPERTY, \"true\");\n\n // `id` becomes the block's anchor, so `[OQ-4](#OQ-4)` scrolls to the question.\n // Verbatim, case intact: heading slugs are lowercased by `github-slugger` and\n // these are author-chosen tokens that already appear verbatim in prose, so\n // the two id namespaces cannot collide by accident. A malformed id is stamped\n // anyway — the grammar is the checker's to enforce (`vantage/oq-id-format`),\n // and a renderer that silently dropped one would hide the mistake it is the\n // checker's job to name. The sanitizer's value allowlist is the backstop.\n const id = pairs.get(\"id\");\n if (id !== undefined && id !== \"\") setProperty(target, OQ_ID_PROPERTY, id);\n\n // On either name, in every state: whether the leaning is offered is the\n // viewer's reading of the question's state, never the renderer's.\n const leaning = pairs.get(\"leaning\");\n if (leaning === undefined) return;\n // A wrapped directive puts newlines and indentation in the value, and this is\n // about to become the body of a review comment, so collapse and cap it — the\n // same normalization the planning index reports a question's leaning with.\n const text = normalizeLeaning(leaning);\n if (text !== \"\") setProperty(target, LEANING_PROPERTY, text);\n}\n\n/**\n * Merge one run of directives onto one target, then stamp.\n *\n * Merging is defined on the tree, not on the source: every directive comment up\n * to the target merges, last-key-wins, whether or not blank lines separate\n * them. Measured — adjacent comments and comments separated by a blank line\n * produce byte-identical trees, so a rule that told them apart would have to\n * re-read line numbers to do it.\n */\nfunction stampRun(\n children: RootContent[],\n targetIndex: number,\n run: ParsedDirective[],\n state: CollapseState,\n) {\n const target = children[targetIndex] as Element;\n const style = new Map<string, string>();\n // The last style directive in the run decides the extent, on the same\n // last-one-wins principle that resolves a repeated key.\n let styleName: string | undefined;\n\n for (const directive of run) {\n if (directive.name === \"section\" || directive.name === \"block\") {\n styleName = directive.name;\n for (const pair of directive.pairs) style.set(pair.key, pair.value);\n }\n // A question directive is merged below, and any other name drops the\n // whole directive: there is no target semantics without a name. A\n // `fallback` reaching this point is one whose target is not a block it may\n // withhold — a heading — so it is inert here too, and the rest of its run\n // stamps as if it were not there.\n }\n\n if (styleName !== undefined) {\n stampStyle(children, targetIndex, styleName, style, state);\n }\n // One question per run, whichever names declared it, merged as every reader\n // of a run merges it (`mergeQuestionRun`).\n const question = mergeQuestionRun(run);\n if (question !== undefined && ANCHOR_TARGET_TAGS.has(target.tagName)) {\n stampQuestion(target, question.name, question.keys);\n }\n}\n\n/** A directive comment, or `undefined` for anything else — malformed included. */\nfunction directiveOf(node: RootContent): ParsedDirective | undefined {\n if (node.type !== \"comment\") return undefined;\n const parsed = parseVantageDirective(node.value);\n return parsed !== null && parsed.kind === \"directive\" ? parsed : undefined;\n}\n\n/**\n * One left-to-right pass over a parent's children, recursing into elements.\n *\n * The whole tree, not just the root: `rehype-raw` leaves comment nodes inside\n * `blockquote`, inside `li`, inside `td` and inline inside `p`, and the real\n * Open Questions layout puts the question directive inside a list item — so a\n * root-only walk finds none of them.\n *\n * Pass order is also what resolves a nested section: an inner heading's\n * directive necessarily sits at a higher child index than the outer directive\n * that ranged over it, so each property is simply last-write-wins.\n */\nfunction processChildren(parent: Parents, state: CollapseState) {\n const children = parent.children;\n let i = 0;\n while (i < children.length) {\n const node = children[i];\n if (node.type === \"element\") {\n processChildren(node, state);\n i++;\n continue;\n }\n\n const first = directiveOf(node);\n if (first === undefined) {\n i++;\n continue;\n }\n\n // Consume the run forward to the first element (the target) or the first\n // non-whitespace text (no target — the directive is inert). One pass, so a\n // run is never processed twice and document order is preserved.\n const run = [first];\n let j = i + 1;\n let targetIndex = -1;\n for (; j < children.length; j++) {\n const next = children[j];\n if (next.type === \"element\") {\n targetIndex = j;\n break;\n }\n if (!isSkippable(next)) break;\n const directive = directiveOf(next);\n if (directive !== undefined) run.push(directive);\n }\n\n if (targetIndex >= 0) stampRun(children, targetIndex, run, state);\n i = j; // resume at the target, or at the blocker — never inside the run\n }\n}\n\n/**\n * Remove every block a `fallback` run lands on, with the run itself.\n *\n * A fallback block is written for the renderers that cannot show something\n * Vantage shows — \"This drawing needs Vantage 0.8 or later\", beside an inline\n * `<svg>`. Every renderer that does not know the name drops the comment and\n * shows the block; this one never renders it, in any mode, in print, or\n * through any of the renderers that share this plugin (D5).\n *\n * The run is found exactly as `processChildren` finds one — the same skip\n * rules, the same first-element target, the same whole-tree walk — and it\n * withholds when any directive in it is a `fallback` and the target is on\n * `VANTAGE_FALLBACK_TARGETS`. Anything else is left for the stamping pass,\n * where a `fallback` is inert.\n *\n * A pass of its own, and first, because a `section` reaches *forward* over its\n * siblings: stamped in one pass, a toned section would count a fallback block\n * after it among its members, give it the run's last `end` marker, and leave\n * the last block anyone sees marked `middle`. Withheld first, the block was\n * never there for any later decision — the section's extent, its run markers,\n * a collapse group's members, a review anchor or an outline entry.\n */\nfunction withholdFallbacks(parent: Parents) {\n const children = parent.children;\n let i = 0;\n while (i < children.length) {\n const node = children[i];\n if (node.type === \"element\") {\n withholdFallbacks(node);\n i++;\n continue;\n }\n\n const first = directiveOf(node);\n if (first === undefined) {\n i++;\n continue;\n }\n\n let withholds = first.name === \"fallback\";\n let j = i + 1;\n let targetIndex = -1;\n for (; j < children.length; j++) {\n const next = children[j];\n if (next.type === \"element\") {\n targetIndex = j;\n break;\n }\n if (!isSkippable(next)) break;\n if (directiveOf(next)?.name === \"fallback\") withholds = true;\n }\n\n const target = targetIndex >= 0 ? children[targetIndex] : undefined;\n if (\n withholds &&\n target?.type === \"element\" &&\n FALLBACK_TARGET_TAGS.has(target.tagName)\n ) {\n // `i` now indexes whatever followed the block, which may start a run of\n // its own.\n children.splice(i, targetIndex - i + 1);\n continue;\n }\n i = j; // resume at the target, or at the blocker — never inside the run\n }\n}\n\nconst rehypeVantageDirectives: Plugin<[], Root> = () => {\n return (tree: Root) => {\n withholdFallbacks(tree);\n processChildren(tree, { nextGroup: 1 });\n };\n};\n\nexport default rehypeVantageDirectives;\n","/**\n * The two halves of one repair: carry a display-math block's own attributes\n * across the element swap `rehype-katex` performs.\n *\n * `$$…$$` (and a ` ```math ` fence) reaches rehype as `<pre><code\n * class=\"language-math\">`. `pre` is in `VANTAGE_STYLE_TARGETS`, so\n * `rehypeVantageDirectives` stamps it like any other block — it becomes a real\n * member of a toned section's run, and `rehypeSourceLines` has already given it\n * a `data-source-line`. Then `rehype-katex` reaches the same node, and for a\n * `code.language-math` inside a `pre` it takes the **`pre`** as its scope and\n * does `parent.children.splice(index, 1, …result)`: the stamped element is\n * *replaced* by a fresh `<span class=\"katex-display\">`, and every attribute on\n * it dies with it.\n *\n * Measured consequences, all three of them silent:\n *\n * - the section's vertical rule breaks across the formula. The rule is drawn\n * per member and bled upward by a fixed 40px, so the void is about the\n * formula's own height — 58px for a one-line fraction over the real\n * Tailwind build, more for a matrix — and the section reads as two;\n * - `#L` line anchors and review highlights stop resolving to the formula,\n * because `data-source-line` went with it;\n * - `collapsed=true` over a heading whose body includes a formula hides the\n * prose and leaves the formula on screen, since `data-vantage-collapsed`\n * never reached the span the toggle JS can see.\n *\n * The fix is to snapshot before and re-apply after, which is why this is a pair\n * and why the pair must bracket `rehype-katex` in `pipeline.ts`. Registering\n * only one half is inert, not wrong: capture alone writes to `file.data` and\n * nothing reads it, restore alone finds nothing to restore.\n *\n * Both halves run *after* `rehype-sanitize` — which is not a detail, twice\n * over. `rehype-sanitize` rebuilds the tree, so node identities taken before it\n * would all be stale; and every attribute carried here is one the sanitizer\n * already passed on the node it came from, so nothing here can reintroduce\n * markup the schema rejects.\n */\n\nimport type { Element, Parents, Properties, Root, RootContent } from \"hast\";\nimport type { Plugin } from \"unified\";\n\n/**\n * Where the snapshot lives between the two halves.\n *\n * `file.data` rather than a closure or a module-level map: `buildPipeline` is\n * allowed to be built once and run over many documents, and per-file state is\n * the only kind that cannot leak from one of those to the next.\n */\nconst CARRIED_KEY = \"vantageDisplayMathStamps\";\n\n/** The narrowest shape of the VFile these two need. */\ninterface StampFile {\n data: Record<string, unknown>;\n}\n\ninterface CarriedStamp {\n /** The stamped `<pre>`'s parent, which `rehype-katex` never replaces. */\n parent: Parents;\n /**\n * The sibling immediately before the `<pre>`, or `undefined` when it was the\n * first child. This is how the replacement is found again: surviving nodes\n * keep their identity across the splice, so the node one past the anchor is\n * whatever took the `<pre>`'s place, however many other blocks were rewritten\n * elsewhere in the tree.\n */\n anchor: RootContent | undefined;\n properties: Properties;\n}\n\nfunction classNames(node: Element): string[] {\n const value = node.properties?.className;\n return Array.isArray(value) ? value.map(String) : [];\n}\n\n/**\n * A `<pre>` `rehype-katex` will replace — its own condition, restated.\n *\n * `language-math` is the only class to test: `rehype-katex` keys the\n * pre-as-scope branch on it, and the sanitizer strips the `math-display` that\n * `remark-math` also emits (measured — a stamped fence arrives here with\n * `className: [\"language-math\"]` alone).\n */\nfunction isDisplayMath(node: RootContent): node is Element {\n if (node.type !== \"element\" || node.tagName !== \"pre\") return false;\n return node.children.some(\n (child) =>\n child.type === \"element\" &&\n child.tagName === \"code\" &&\n classNames(child).includes(\"language-math\"),\n );\n}\n\n/**\n * What is worth carrying: everything this pipeline stamped itself.\n *\n * Deliberately not the whole property bag. `className`, `style` and `id` belong\n * to the element KaTeX is about to build, and copying a `<pre>`'s onto a\n * `<span class=\"katex-display\">` would fight it.\n */\nfunction carriedProperties(properties: Properties | undefined): Properties {\n const carried: Properties = {};\n for (const [key, value] of Object.entries(properties ?? {})) {\n if (key === \"dataSourceLine\" || key.startsWith(\"dataVantage\")) {\n carried[key] = value;\n }\n }\n return carried;\n}\n\nfunction collect(parent: Parents, out: CarriedStamp[]) {\n const children: RootContent[] = parent.children;\n for (let i = 0; i < children.length; i++) {\n const node = children[i];\n if (node.type !== \"element\") continue;\n if (isDisplayMath(node)) {\n const properties = carriedProperties(node.properties);\n // An unstamped formula needs nothing carried, and recording it would only\n // give the restore pass a node to touch for no reason.\n if (Object.keys(properties).length > 0) {\n out.push({\n parent,\n anchor: i === 0 ? undefined : children[i - 1],\n properties,\n });\n }\n continue;\n }\n collect(node, out);\n }\n}\n\nfunction reapply(carried: CarriedStamp[]) {\n for (const { parent, anchor, properties } of carried) {\n // One annotation, because `Parents[\"children\"]` is a union of two array\n // types and `indexOf` on a union has no callable signature.\n const siblings: RootContent[] = parent.children;\n let index = 0;\n if (anchor !== undefined) {\n const at = siblings.indexOf(anchor);\n // The anchor was itself rewritten — two formulae with no node between\n // them, which mdast-to-hast does not produce (it separates siblings with\n // newline text nodes) but raw HTML could. Give up on this one rather than\n // guess: the result is the unrepaired gap, never a stamp on the wrong\n // block.\n if (at === -1) continue;\n index = at + 1;\n }\n const replacement = siblings[index];\n if (replacement === undefined || replacement.type !== \"element\") continue;\n // KaTeX emits `katex-display` normally and `katex-error` when the formula\n // does not parse; both are the block that took the `<pre>`'s place, and\n // anything else means the tree is not the shape this assumed.\n if (!classNames(replacement).some((name) => name.startsWith(\"katex\"))) {\n continue;\n }\n replacement.properties ??= {};\n for (const [key, value] of Object.entries(properties)) {\n replacement.properties[key] ??= value;\n }\n }\n}\n\n/** Snapshot every stamped display-math block. Register before `rehypeKatex`. */\nexport const rehypeCaptureMathStamps: Plugin<[], Root> = () => {\n return (tree: Root, file: StampFile) => {\n const carried: CarriedStamp[] = [];\n collect(tree, carried);\n file.data[CARRIED_KEY] = carried;\n };\n};\n\n/** Re-apply the snapshot. Register immediately after `rehypeKatex`. */\nexport const rehypeRestoreMathStamps: Plugin<[], Root> = () => {\n return (_tree: Root, file: StampFile) => {\n const carried = file.data[CARRIED_KEY];\n delete file.data[CARRIED_KEY];\n if (Array.isArray(carried)) reapply(carried as CarriedStamp[]);\n };\n};\n","/**\n * Sanitization schema for the rendering pipeline.\n * Allows GFM, KaTeX MathML, `data-source-line` attributes, a filtered inline\n * `style` on every element but `input` (see `UNSTYLED_TAGS`), a `class` that\n * carries only the names the pipeline itself emits (see `PIPELINE_CLASSES`),\n * and inline SVG as static drawing (see `SVG_CHILD_TAGS`), while blocking XSS\n * vectors and anything that lays a document over the app.\n */\n\nimport type { Element, Root } from \"hast\";\nimport { defaultSchema } from \"rehype-sanitize\";\nimport {\n VANTAGE_BADGES,\n VANTAGE_COLLAPSED,\n VANTAGE_EMPHASIS,\n VANTAGE_RUNS,\n VANTAGE_TONES,\n VANTAGE_OQ_ID,\n} from \"./vantageDirectives.js\";\nimport { ALERT_TITLE_CLASS, VANTAGE_ALERTS } from \"./rehypeVantageAlerts.js\";\n\ntype Schema = typeof defaultSchema;\ntype AttributeList = NonNullable<Schema[\"attributes\"]>[string];\ntype ClassValue = string | RegExp;\n\n/** Whether an attribute definition is for `className`. */\nconst isClassName = (definition: AttributeList[number]) =>\n (typeof definition === \"string\" ? definition : definition[0]) === \"className\";\n\n/**\n * The value list the default schema gives `className` on `tag`, or an empty\n * list if it gives none.\n */\nfunction defaultClasses(tag: string): ClassValue[] {\n const entry = defaultSchema.attributes?.[tag]?.find(isClassName);\n return Array.isArray(entry) ? (entry.slice(1) as ClassValue[]) : [];\n}\n\n/**\n * The only classes an element keeps: the ones the pipeline itself emits on it\n * before `rehypeSanitize` runs. Every other element keeps none.\n *\n * **A class is a style by another name.** The app ships Tailwind's utility CSS\n * for its own markup, so `<div class=\"fixed inset-0 z-50 bg-white\">` in a\n * document laid a white sheet over the whole Vantage window, header and\n * sidebar included. That is the overlay `SAFE_STYLE` bans `position` to\n * prevent, spelled as a class instead of a declaration, and `className` used\n * to be admitted with any value on every element. A document could also\n * borrow a class the app's own code looks for: `.overflow-y-auto` is how an\n * anchor link finds the element to scroll when no `data-content-scroll` is in\n * reach, and `.heading-anchor` is what the outline strips from a heading's\n * text. The comment-body sanitizer (`frontend/src/lib/commentMarkdown.ts`)\n * already refused `class` for the same reason.\n *\n * What each entry is for, and who emits it:\n *\n * - `code`: `language-*`, from a fence's info string (`mdast-util-to-hast`).\n * `rehype-highlight` reads it, `rehype-katex` keys on `language-math`, and\n * the viewers render `language-mermaid` as a diagram.\n * - `ul`, `ol` and `li`: `contains-task-list` and `task-list-item`, from GFM\n * task lists. `styles/task-list.css` draws the checkbox from them.\n * - `section`, `h2` and `a`: GFM footnotes (`mdast-util-to-hast`), on the\n * section, its label and each back-reference.\n * - `div`: `ALERT_TITLE_CLASS`, on the title `rehypeVantageAlerts` injects.\n *\n * `remark-math` also emits `math-display` and `math-inline` on its `code`.\n * They were never kept, because the default schema's `code` entry takes only\n * `language-*`, and nothing needs them: `rehype-katex` keys on `language-math`\n * and tells display from inline by whether the `code` sits in a `pre`.\n *\n * **A class added after the sanitizer needs no entry**, and that is most of\n * the classes the page styles: `rehype-highlight`'s `hljs-*`, KaTeX's output,\n * Mermaid's diagrams, and everything the app's components and hooks add. A\n * document that writes one of those on an element keeps nothing.\n *\n * **Mermaid source is the route this list cannot close.** Mermaid renders\n * after the sanitizer and copies the names a diagram's source gives a node\n * (`:::name`, `class A name`, `classDef`) onto that node, so a diagram can\n * carry any of the app's utilities on its own nodes. The diagram bounds it:\n * `position` does nothing on an SVG group, and the diagram's `svg` clips what\n * is inside it. `frontend/e2e/mermaid.spec.ts` measures that bound. The\n * stylesheet a diagram's source could write did reach past the diagram, and\n * `MERMAID_SECURE_KEYS` in `mermaidLoader.ts` refuses the settings that write\n * it.\n *\n * A document may still write these names on these elements. By the time the\n * sanitizer runs, a hand-written `<li class=\"task-list-item\">` and the one GFM\n * emitted are the same node, and none of the names can lay an element over\n * anything outside the document's scroll container. `styles/task-list.css`\n * floats the checkbox of a `task-list-item` instead of positioning it\n * absolutely, so it stays in the document's flow whatever the item's own\n * `style` does, and the checkbox takes no `style` itself (see\n * `UNSTYLED_TAGS`). The footnote label's class, where a stylesheet defines it,\n * clips its element to a single pixel.\n *\n * **The footnote label's class is read from the default schema, never\n * spelled here, and the tests spell it in halves.** It is a Tailwind utility,\n * and Tailwind generates a utility for every class name it finds in the files\n * it scans, comments included — this directory and all of `frontend/`. The app\n * renders the label as a visible heading only because nothing it scans spells\n * that name. Spelled whole in a test, it generated the rule, and the label\n * would have been hidden from sight as it is on GitHub: a change of rendering,\n * not a sanitizer fix.\n *\n * The list is measured, not remembered. The test \"takes no class from\n * anything the pipeline emits\" in `frontend/src/lib/sanitize.test.ts` records\n * every class on the tree on both sides of `rehypeSanitize`, over a document\n * that uses every feature that emits one, so a plugin that starts emitting a\n * new class fails there instead of losing it in every renderer.\n */\nconst PIPELINE_CLASSES: Readonly<Record<string, readonly ClassValue[]>> = {\n code: [/^language-./],\n ul: [\"contains-task-list\"],\n ol: [\"contains-task-list\"],\n li: [\"task-list-item\"],\n section: [\"footnotes\"],\n // The footnote label. Read, not spelled: see above.\n h2: defaultClasses(\"h2\"),\n a: [\"data-footnote-backref\"],\n div: [ALERT_TITLE_CLASS],\n};\n\n/**\n * `tag`'s attributes: the default schema's, less any `className`, then its\n * `PIPELINE_CLASSES` entry, then `extra`.\n *\n * **Always a value list, never a bare `\"className\"`.** `hast-util-sanitize`\n * reads a bare name, or a tuple holding no values, as \"any value\", and an\n * element's own entry is consulted before `*`'s — the first match wins, and\n * `*` is not consulted when that element's own entry filters a class list down\n * to nothing. An element with no entry has none added.\n */\nfunction withClasses(tag: string, ...extra: AttributeList): AttributeList {\n const classes = PIPELINE_CLASSES[tag] ?? [];\n const own: AttributeList = (defaultSchema.attributes?.[tag] ?? []).filter(\n (definition) => !isClassName(definition),\n );\n if (classes.length > 0) own.push([\"className\", ...classes]);\n return [...own, ...extra];\n}\n\n/**\n * CSS properties an inline `style` may set.\n *\n * **The only `style` this list ever filters is one a document wrote by hand.**\n * Nothing the pipeline generates reaches it: `rehypeKatex` and `rehypeHighlight`\n * both run *after* `rehypeSanitize` (`pipeline.ts`), so their output is trusted\n * rather than filtered, and `remark-gfm` emits table alignment as an `align`\n * attribute rather than as CSS. So this is a filter on untrusted author HTML and\n * nothing else — which is exactly the hole that made it necessary: `<div\n * style=\"position:fixed;inset:0\">` covered the viewport and\n * `style=\"background:url(https://…)\"` called home on render, both verbatim,\n * because `rehype-sanitize` does not parse CSS. Scripts were never the risk\n * here; layout and network were.\n *\n * The list is therefore deliberately typographic: the styling a prose document\n * has any business asking for. It is *not* sized to KaTeX, and a KaTeX release\n * that starts using a new property is a non-event here — `\\pmb` already emits\n * `text-shadow`, which is not on this list and renders anyway.\n *\n * **The design doc used to argue the opposite — that `style` had to be allowed\n * and `position` enumerated because KaTeX needs them — and it was wrong.** The\n * measurement behind it was real (KaTeX does emit `position:relative` on every\n * integral) but the inference was not, because the sanitizer has finished before\n * the first KaTeX span exists. Rebuilding the shipped rehype order with a filter\n * that rejects *every* value leaves all ten of the integral's style attributes\n * untouched. The \"Security\" section of `docs/reference/inline-markup.md`\n * records the correction; the test that would catch a reordering is in\n * `frontend/src/lib/sanitize.test.ts`.\n */\nconst SAFE_STYLE_PROPERTIES = [\n // Box metrics. `top`/`right`/`bottom`/`left` do nothing on an element no\n // stylesheet positions, and a document cannot position one, since `position`\n // is banned. The app positions headings and toned blocks relatively, and on\n // those they move the element no further than a negative `margin` would,\n // inside the scroll container that clips it. The one other element a\n // stylesheet positions, the task-list checkbox, takes no `style` at all (see\n // `UNSTYLED_TAGS`). They stay only because dropping them would fail the\n // whole attribute for a document that writes one — the all-or-nothing rule\n // below makes every removal a behavior change.\n \"height\",\n \"min-height\",\n \"max-height\",\n \"width\",\n \"min-width\",\n \"max-width\",\n \"top\",\n \"bottom\",\n \"left\",\n \"right\",\n \"margin\",\n \"margin-top\",\n \"margin-right\",\n \"margin-bottom\",\n \"margin-left\",\n \"padding\",\n \"padding-top\",\n \"padding-right\",\n \"padding-bottom\",\n \"padding-left\",\n // Rules and boxes.\n \"border\",\n \"border-style\",\n \"border-color\",\n \"border-width\",\n \"border-top-width\",\n \"border-right-width\",\n \"border-bottom-width\",\n \"border-left-width\",\n \"border-top-style\",\n \"border-right-style\",\n \"border-bottom-style\",\n \"border-left-style\",\n \"border-top-color\",\n \"border-right-color\",\n \"border-bottom-color\",\n \"border-left-color\",\n \"border-radius\",\n // Typography.\n \"color\",\n \"background-color\",\n \"font\",\n \"font-size\",\n \"font-style\",\n \"font-weight\",\n \"font-family\",\n \"font-variant\",\n \"line-height\",\n \"letter-spacing\",\n \"word-spacing\",\n \"text-align\",\n \"text-decoration\",\n \"text-indent\",\n \"white-space\",\n \"vertical-align\",\n \"list-style-type\",\n // Flow. `display` is not here: it takes only the values in `DISPLAY_VALUES`.\n \"float\",\n \"clear\",\n \"opacity\",\n \"overflow\",\n];\n\n/**\n * A `style` value we will keep, as a whole.\n *\n * One rule beyond the property list does the work: **no parentheses anywhere**,\n * which closes `url(…)` and `expression(…)` in one stroke — the network and\n * legacy-script vectors. Its cost is borne entirely by authors, who lose\n * `calc()`, `rgb()` and `var()` along with them; that is the trade, and it is\n * worth it for a filter this small.\n *\n * **`position` is not on the property list at all**, so every value of it is\n * refused — `static` and `relative` along with `fixed` and `sticky`. It used to\n * be enumerated, on the belief that KaTeX needed `relative`; KaTeX renders after\n * the sanitizer and never meets this regex, so the enumeration was buying\n * nothing but the residual it conceded. Banning the property closes that\n * residual, and it is bigger than \"overlaps its neighbors\" made it sound:\n * measured in Chrome against the viewer's real ancestor chain, an author's\n * `position:absolute;top:0;left:0;width:100%;height:100%` is sized to the whole\n * content pane (the nearest positioned ancestor is outside the scroll\n * container), is not clipped by the scroller, and survives scrolling to the end\n * of the document. It was `position:fixed` in all but the keyword.\n *\n * Matching is all-or-nothing: one unrecognized declaration drops the whole\n * attribute, and the element renders unstyled rather than partly styled. That\n * is the safe direction to fail, and it degrades to plain text rather than to a\n * broken page.\n *\n * **The grammar must stay unambiguous, and `;` is what keeps it so.** The value\n * class is \"anything but the delimiters\", which includes whitespace — a value\n * legitimately contains it (`margin: 0 auto`). So if whitespace could *also*\n * end a declaration, both constructs would compete for the same characters and\n * the match would fork at every declaration; on a value that ultimately fails,\n * the engine explores every fork. An earlier form of this regex separated\n * declarations with `\\s*;?\\s*`, and 200 document-controlled characters took the\n * renderer — and the CLI checker, and therefore CI — 94 seconds. Requiring `;`\n * pins each declaration's extent to the delimiter positions, so there is exactly\n * one way to parse any input and rejection is linear. `VALUE` absorbs the\n * padding on both sides for the same reason: a separate `\\\\s*` next to it would\n * put the ambiguity straight back. Pinned by the flat-time test in\n * `frontend/src/lib/sanitize.test.ts` — do not loosen the separator.\n *\n * Residual, stated plainly and now genuinely small: negative `margin` still lets\n * an element overlap its neighbors *inside the flow*. That one scrolls with the\n * content and is clipped by the scroll container, and closing it means giving up\n * margins, which prose actually uses. Containment in the stylesheet, not another\n * rule here, is what would close it. `overflow: visible` on an inline `<svg>` is\n * the same residual by another route — see `SVG_ROOT_ATTRIBUTES`.\n *\n * **The app's table of contents is inside that scroll container too**, beside\n * the document column, so a block carried over it took its clicks whenever the\n * block painted in the positioned layer: one with `opacity`, a toned block, a\n * heading, or a link to anywhere. The contents sit at `z-10` for that\n * (`frontend/src/components/TableOfContents.tsx`), and a document cannot climb\n * back over them only because `z-index` is not on the property list.\n */\nconst VALUE = `[^;:()\"'\\\\\\\\]*`;\n// Wrapped in its own group, and the trailing `?` below applies to that group.\n// Interpolating the declaration bare would attach the `?` to `VALUE`'s `*`,\n// making the last declaration's value *lazy* instead of the whole declaration\n// optional — which rejects a trailing `;` (`color:red;`). The semicolon test in\n// `frontend/src/lib/sanitize.test.ts` is what catches that.\n/**\n * The values `display` may take: every keyword that leaves the element a box of\n * its own, and not `contents`, the one that takes it away.\n *\n * **An element with `display: contents` generates no box, so it is no\n * containing block** for anything a stylesheet positions inside it, and the\n * app positions two things inside a document's elements. A toned block's rule\n * is a `::before` placed absolutely inside the block (`styles/directives.css`),\n * and a heading's link anchor is placed absolutely inside the heading\n * (`frontend/src/index.css`). Written on such an element, `display: contents`\n * placed them against the nearest positioned ancestor instead, which is outside\n * the scroll container, so neither was clipped by it nor scrolled with the\n * document. Measured in Chromium at 1280x800: a hand-written\n * `data-vantage-tone` on a paragraph drew its rule the full height of the\n * content pane, and on a heading the heading's `font-size` moved that rule, and\n * the anchor, which takes clicks, anywhere across the pane. The same held for\n * the task-list checkbox until `styles/task-list.css` floated it.\n *\n * So the values are listed rather than `contents` refused, for two reasons. A\n * CSS comment is legal between the colon and the keyword, and `VALUE` admits\n * the `/` and `*` that spell one, so a pattern that refused the word would keep\n * it behind an empty comment. And a CSS-wide keyword (`inherit`, `unset`,\n * `revert`) names no value of its own. Measured in Chromium, every keyword\n * below gives a toned block a rule as tall as the block, or, for `none` and\n * the two column values, generates none. The multi-keyword forms (`block\n * flow`) are refused along with them: nothing a prose document writes needs\n * them, and each has a single-keyword spelling here.\n *\n * Ordered longest first within each prefix, which the match does not need —\n * the `;`, `!` or end that must follow decides — but a reader checking the\n * list does.\n */\nconst DISPLAY_VALUES = [\n \"none\",\n \"block\",\n \"inline-block\",\n \"inline-flex\",\n \"inline-grid\",\n \"inline-table\",\n \"inline\",\n \"flow-root\",\n \"flex\",\n \"grid\",\n \"table-row-group\",\n \"table-row\",\n \"table-header-group\",\n \"table-footer-group\",\n \"table-cell\",\n \"table-column-group\",\n \"table-column\",\n \"table-caption\",\n \"table\",\n \"list-item\",\n];\n// Its own alternative, never folded into `VALUE`: a keyword, then an optional\n// `!important`, each padded by whitespace that no neighbor can also claim, so\n// the grammar keeps its one parse of any input. No other property's name\n// begins with `display`, so the two alternatives never compete either.\nconst DISPLAY_DECLARATION = `display\\\\s*:\\\\s*(?:${DISPLAY_VALUES.join(\"|\")})\\\\s*(?:!\\\\s*important\\\\s*)?`;\nconst DECLARATION = `(?:(?:${SAFE_STYLE_PROPERTIES.join(\"|\")})\\\\s*:${VALUE}|${DISPLAY_DECLARATION})`;\n\nexport const SAFE_STYLE = new RegExp(\n `^\\\\s*(?:${DECLARATION};\\\\s*)*${DECLARATION}?$`,\n \"i\",\n);\n\n/**\n * A collapse group id: one or more digits, anchored.\n *\n * The plugin mints these as a per-document counter, so there is no vocabulary to\n * list. Keeping the shape narrow matters anyway — the toggle JS builds a\n * `[data-vantage-collapse-group=\"…\"]` selector out of the value, and a document\n * that hand-wrote raw HTML is the only way a non-numeric one could ever appear.\n */\nconst COLLAPSE_GROUP_ID = /^[0-9]+$/;\n\n/**\n * Inline SVG, admitted as static drawing and nothing else.\n *\n * The allowlist is shapes, text and grouping. Everything in SVG that can run\n * code, fetch, or reach outside the element is refused: `script`,\n * `foreignObject`, `use` and `image` (both take an `href`), `xlink:href` on\n * `a`, the `animate`/`set` family (which can rewrite an `href` after the\n * sanitizer has looked), `style` elements, and every attribute that takes a\n * `url(…)` reference — `filter`, `mask`, `clip-path`, `marker-*`, `cursor`.\n *\n * Gradients, patterns and `<defs>` are out for a second reason: they are only\n * reachable through `url(#id)`, and the sanitizer prefixes every `id` with\n * `user-content-`, so the reference would dangle even if it were allowed.\n *\n * **Refusing an element is not the same as removing it.** `hast-util-sanitize`\n * *unwraps* an element it does not allow — drops the tag and keeps its\n * children — unless the tag is on `strip`. See `SVG_STRIPPED` for why that\n * matters here, and for the pass that removes those containers instead.\n *\n * `switch` is admitted, as a plain container, because draw.io wraps every label\n * in one: `<switch><foreignObject>…</foreignObject><text>…</text></switch>`. A\n * `switch` renders only its first child whose conditions hold, and with the\n * `foreignObject` removed and the conditional attributes (`requiredFeatures`,\n * `systemLanguage`) refused, that is the `<text>` fallback. Unwrapped instead,\n * it would render *every* branch — including the \"Text is not SVG - cannot\n * display\" notice draw.io appends for viewers without `foreignObject`.\n *\n * Every child requires an `svg` ancestor, so no shape or text element is\n * admitted into HTML flow on its own.\n *\n * `title` and `desc` are not children at all. They are admitted nowhere, inside\n * a drawing they are removed with their contents (see `SVG_STRIPPED`), and\n * `aria-label` on the root is the drawing's accessible name. Both are HTML\n * integration points — the parser reads what is inside them as HTML — and an\n * `svg` ancestor turned out not to be enough to keep a `<title>` in SVG\n * context. `<math><svg><title>` keeps MathML context, and React hoists any\n * `title` outside SVG context into the page's `<head>`, so the document set the\n * tab title. In `renderMarkdown`'s string output, `<svg><desc><title>`,\n * `<svg><title><title>` and a `title` inside a `foreignObject` all re-parse into\n * an HTML `title`, which is what `document.title` reads on a page that has none\n * of its own.\n */\nconst SVG_CHILD_TAGS = [\n \"g\",\n \"path\",\n \"rect\",\n \"circle\",\n \"ellipse\",\n \"line\",\n \"polyline\",\n \"polygon\",\n \"text\",\n \"tspan\",\n \"switch\",\n];\n\n/**\n * Refused SVG elements removed *with their contents* when they sit inside an\n * `<svg>`, rather than unwrapped.\n *\n * Each of these holds children that are never meant to be painted where they\n * stand: a clip region, a mask, a gradient's stops, a marker's arrowhead, a\n * symbol's body, a filter's primitives, an exporter's RDF, the text of a\n * `title` or `desc` (see `SVG_CHILD_TAGS`), or — for `foreignObject` — HTML.\n * Unwrapped, those children land in the drawing as ordinary shapes. A default\n * Figma export ends in `<defs><clipPath><rect fill=\"white\"/>`, and unwrapping\n * it painted that white rect over the whole drawing; a `<marker>` arrowhead\n * became a stray triangle at the origin. The HTML inside a `foreignObject`\n * survived as descendants of `svg`, which React then created as invisible\n * SVG-namespace `div`s and `p`s — the anchor an Open Question button looks for\n * — and which a browser re-parsing `renderMarkdown`'s string output breaks out\n * of the `svg` at, spilling the rest of the drawing into the page.\n *\n * **Only inside an `<svg>`, which is why this is a pass and not the schema's\n * `strip`.** `strip` removes a tag wherever it stands, and most of these names\n * are lowercase in prose too. There a bare `<pattern>` on a line of its own is\n * an HTML element nothing closes, so every Markdown block after it was parsed\n * into it, and stripping it removed the rest of the document — in every\n * renderer, with `vantage-check` silent. Inside a drawing the same container is\n * bounded by the `svg`, because a Markdown paragraph or heading breaks out of\n * SVG. Outside one the sanitizer unwraps these like any tag it does not know:\n * `title` and `desc` are not admitted anywhere, so no `title` element survives\n * to be hoisted, and what they held is text or ordinary sanitized HTML.\n *\n * Names compare case-insensitively. The camel-cased ones are how the HTML\n * parser spells these elements in SVG, but under `<math><svg>` the parser stays\n * in MathML and leaves them lowercase.\n */\nconst SVG_STRIPPED = [\n \"foreignObject\",\n \"defs\",\n \"clipPath\",\n \"mask\",\n \"pattern\",\n \"marker\",\n \"symbol\",\n \"linearGradient\",\n \"radialGradient\",\n \"filter\",\n \"metadata\",\n \"title\",\n \"desc\",\n];\n\nconst SVG_STRIPPED_NAMES = new Set(\n SVG_STRIPPED.map((name) => name.toLowerCase()),\n);\n\n/**\n * The camel-cased `SVG_STRIPPED` names, which the schema strips on its own.\n *\n * Only an SVG-namespace element carries one of these spellings, so stripping\n * them everywhere costs prose nothing. It keeps the worst of the list — the\n * HTML inside a `foreignObject` — out of a drawing for a consumer who hands\n * `sanitizeSchema` to `rehype-sanitize` without the rest of `buildPipeline`.\n */\nconst SVG_STRIPPED_EVERYWHERE = SVG_STRIPPED.filter(\n (name) => name !== name.toLowerCase(),\n);\n\nfunction stripSvgContainers(parent: Root | Element, inSvg: boolean): void {\n if (inSvg) {\n parent.children = parent.children.filter(\n (child) =>\n child.type !== \"element\" ||\n !SVG_STRIPPED_NAMES.has(child.tagName.toLowerCase()),\n );\n }\n for (const child of parent.children) {\n if (child.type === \"element\") {\n stripSvgContainers(child, inSvg || child.tagName.toLowerCase() === \"svg\");\n }\n }\n}\n\n/**\n * Remove every `SVG_STRIPPED` element below an `svg`, with its contents.\n *\n * Runs immediately before `rehypeSanitize` (`pipeline.ts`), and only when the\n * pipeline sanitizes: with the sanitizer off these elements are left to work\n * as SVG defines them.\n */\nexport function rehypeStripSvgContainers() {\n return (tree: Root) => {\n stripSvgContainers(tree, false);\n };\n}\n\n/**\n * A paint value: a keyword, a named color, or a hex color. No parentheses, for\n * the same reason `SAFE_STYLE` refuses them — `fill=\"url(https://…)\"` is a\n * render-time fetch, and `rgb()` is the price of closing it.\n */\nconst SVG_PAINT = /^(?:#[0-9a-f]{3,8}|[a-z]+)$/i;\n\n/**\n * Attributes any SVG element may carry, drawing and typography alike.\n *\n * **Each entry is a hast property name, which is not always the camel case of\n * the attribute.** `property-information` decides the spelling, and it has\n * `strokeLineCap` for `stroke-linecap` — a naive `strokeLinecap` matches\n * nothing, and the attribute is dropped in every renderer without a word. The\n * table-driven test in `frontend/src/lib/sanitize.test.ts` renders every entry\n * in its written spelling, so a misspelled one fails there.\n *\n * `stroke-dasharray` (and with it `stroke-dashoffset`, which does nothing\n * alone) is refused, because it lets a few bytes buy an unbounded paint. The\n * number of dashes is the path's length over the dash period, both in user\n * units the document chooses. Measured in headless Chromium, fifty 90-byte\n * paths with a 0.0011 dash took 21 s to paint and a hundred took 37 s. A value\n * grammar cannot bound it: refusing sub-unit dashes is undone by scaling the\n * user units, and the same fifty paths with `stroke-dasharray=\"1.1\"` under a\n * `viewBox` a thousand times larger still took 15 s.\n */\nconst SVG_ATTRIBUTES: NonNullable<Schema[\"attributes\"]>[string] = [\n [\"fill\", SVG_PAINT],\n [\"stroke\", SVG_PAINT],\n \"fillOpacity\",\n \"fillRule\",\n \"strokeOpacity\",\n \"strokeWidth\",\n \"strokeLineCap\",\n \"strokeLineJoin\",\n \"opacity\",\n \"transform\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"fontStyle\",\n \"letterSpacing\",\n \"textAnchor\",\n \"dominantBaseline\",\n // Geometry. Names are shared across shapes, so one list serves them all.\n \"x\",\n \"y\",\n \"x1\",\n \"y1\",\n \"x2\",\n \"y2\",\n \"cx\",\n \"cy\",\n \"r\",\n \"rx\",\n \"ry\",\n \"dx\",\n \"dy\",\n \"d\",\n \"points\",\n];\n\n/**\n * The root `<svg>`'s attributes: `SVG_ATTRIBUTES` less `transform`, plus the\n * viewport, a `role` from a closed list, and the accessible name.\n *\n * `transform` on the root is not an SVG transform at all. The root is an\n * in-flow CSS box, and the attribute becomes a CSS transform of that box, so\n * `<svg width=\"10\" height=\"10\" transform=\"translate(-300 -300) scale(80)\">`\n * laid out as a 10px box and painted as an 800px one over the paragraphs\n * around it. On a child it moves shapes inside the viewport, which clips them.\n *\n * Residual: that clip is the root's `overflow`, which `SAFE_STYLE` lets a\n * document set. `style=\"overflow:visible\"` on the root lets a child with a\n * large `transform` or large coordinates paint over its neighbors — measured in\n * Chromium, the same overlap. It is the negative-`margin` residual described at\n * `SAFE_STYLE` by another route: it stays in the flow, scrolls with the content,\n * and the scroll container clips it.\n */\nconst SVG_ROOT_ATTRIBUTES: NonNullable<Schema[\"attributes\"]>[string] = [\n ...SVG_ATTRIBUTES.filter((attribute) => attribute !== \"transform\"),\n \"xmlns\",\n \"viewBox\",\n \"preserveAspectRatio\",\n // The roles that describe a drawing: a picture, or a decoration. Any other\n // value let a document present its drawing to assistive technology as an\n // alert, a button or a dialog, with a label it wrote itself. The default\n // schema admits `role` nowhere, so this is the only place it gets through.\n [\"role\", \"img\", \"presentation\", \"none\"],\n \"ariaLabel\",\n];\n\n/** Every element the sanitizer admits. */\nconst TAG_NAMES = [\n ...(defaultSchema.tagNames || []),\n // KaTeX MathML elements\n \"math\",\n \"semantics\",\n \"mrow\",\n \"mi\",\n \"mo\",\n \"mn\",\n \"msup\",\n \"msub\",\n \"mfrac\",\n \"mover\",\n \"munder\",\n \"msqrt\",\n \"mroot\",\n \"mtable\",\n \"mtr\",\n \"mtd\",\n \"mtext\",\n \"mspace\",\n \"annotation\",\n // Other\n \"figure\",\n \"figcaption\",\n \"summary\",\n \"details\",\n \"svg\",\n ...SVG_CHILD_TAGS,\n];\n\n/**\n * The admitted elements that take no `style`: only `input`, which GFM emits as\n * a task list's checkbox, and never with a style.\n *\n * `styles/task-list.css` draws that checkbox and positions it, so on it `top`\n * and `left` are not the no-ops they are on an element nothing positions, and\n * with `width`, `height` and `background-color` beside them a document laid a\n * box of its own over the page. Measured in Chromium at 1280x800 while the\n * checkbox was positioned absolutely,\n * `top:-3000px;left:-3000px;width:9000px;height:9000px;background-color:white`\n * on it covered the whole content pane. It needed no class written:\n * GFM puts `task-list-item` on the item, and a checkbox typed into the item's\n * text is a child of that item too.\n *\n * **This is why `style` is on each element's own entry, never on `*`.**\n * `hast-util-sanitize` consults `*` whenever an element's own entry yields\n * nothing, so no entry on `input` could refuse a value that `*` admits. An\n * element takes `style` only by being in `TAG_NAMES` and not in this set.\n */\nconst UNSTYLED_TAGS: ReadonlySet<string> = new Set([\"input\"]);\n\ntype Attributes = NonNullable<Schema[\"attributes\"]>;\n\n/**\n * `attributes`, with `[\"style\", SAFE_STYLE]` added to the entry of every\n * element in `TAG_NAMES` but the `UNSTYLED_TAGS`.\n */\nfunction withStyle(attributes: Attributes): Attributes {\n const styled: Attributes = { ...attributes };\n for (const tag of new Set(TAG_NAMES)) {\n if (!UNSTYLED_TAGS.has(tag)) {\n styled[tag] = [...(styled[tag] ?? []), [\"style\", SAFE_STYLE]];\n }\n }\n return styled;\n}\n\n/**\n * Never set `allowComments` here.\n *\n * `hast-util-sanitize` drops comment nodes because that boolean defaults to\n * `false` — comments are not elements, so `tagNames` has nothing to do with it.\n * `rehypeVantageDirectives` relies on that deletion: it consumes a\n * `<!-- vantage: … -->` comment into attributes and deliberately leaves the node\n * for the sanitizer. Turning the switch on readmits every directive comment —\n * valid and malformed alike — into the rendered HTML, which breaks the carrier's\n * whole premise. `vantageDirectives.test.ts` (\"leaves no comment in the rendered\n * markup\") is the guard.\n */\nexport const sanitizeSchema: Schema = {\n ...defaultSchema,\n strip: [...(defaultSchema.strip || []), ...SVG_STRIPPED_EVERYWHERE],\n tagNames: TAG_NAMES,\n ancestors: {\n ...defaultSchema.ancestors,\n ...Object.fromEntries(SVG_CHILD_TAGS.map((tag) => [tag, [\"svg\"]])),\n },\n attributes: withStyle({\n ...defaultSchema.attributes,\n // No `className` here: see `PIPELINE_CLASSES`. No `style` either: see\n // `UNSTYLED_TAGS`.\n \"*\": [\n ...(defaultSchema.attributes?.[\"*\"] || []),\n \"dataSourceLine\",\n // What `rehypeVantageDirectives` compiles a `<!-- vantage: … -->` comment\n // into, named individually — never by a `data-vantage-*` wildcard, which\n // would readmit whatever a future bug emits and whatever a document\n // hand-writes as raw HTML.\n //\n // The value lists are the belt to the plugin's braces: the vocabulary is\n // closed in the plugin *and* here, imported from the one module that\n // defines it, so even if a refactor let an unvalidated value reach the\n // tree the sanitizer still refuses it.\n [\"dataVantageTone\", ...VANTAGE_TONES],\n [\"dataVantageEmphasis\", ...VANTAGE_EMPHASIS],\n [\"dataVantageBadge\", ...VANTAGE_BADGES],\n [\"dataVantageCollapsed\", ...VANTAGE_COLLAPSED],\n // The other half of `collapsed`: which group a hidden block belongs to,\n // and which group a heading toggles. Both are plugin-minted counters with\n // no vocabulary to allowlist, so they take a pattern instead —\n // `hast-util-sanitize` accepts a `RegExp` in place of a literal value.\n // A pattern rather than a bare name because the JS interpolates the value\n // into a selector: anything but digits has no business reaching it.\n [\"dataVantageCollapseGroup\", COLLAPSE_GROUP_ID],\n [\"dataVantageCollapseToggle\", COLLAPSE_GROUP_ID],\n [\"dataVantageRun\", ...VANTAGE_RUNS],\n // A question declared with `oq`, the name every release since 0.7 has\n // stamped this for, and a question under either name: the second is the\n // one to read for \"a question\" (`VANTAGE_QUESTION_ATTRIBUTE`).\n [\"dataVantageOq\", \"true\"],\n [\"dataVantageQuestion\", \"true\"],\n // The Open Question's id on its way to becoming a real `id`, pattern-\n // allowlisted like the collapse-group counters above rather than left\n // name-only: `rehypeVantageAnchors` writes this value straight into the\n // document's id namespace on the other side of this schema, so anything\n // a document could smuggle through here it would be smuggling into `id`.\n // The grammar is imported, never restated — see `VANTAGE_OQ_ID`.\n [\"dataVantageOqId\", VANTAGE_OQ_ID],\n // GFM alerts, compiled by `rehypeVantageAlerts`. Value-allowlisted like\n // the tone tokens it shares a palette with, so a document cannot forge a\n // sixth kind through raw HTML.\n [\"dataVantageAlert\", ...VANTAGE_ALERTS],\n // The design's one genuinely free-text value: the body of a review\n // comment, so it cannot be value-allowlisted and this entry is name-only.\n // Two defenses remain rather than three — `hast` escapes the value on\n // serialization and React sets it through the DOM property path, so it\n // cannot break out of the attribute — and the honest record of that is in\n // the design doc rather than a third layer implied here.\n \"dataVantageLeaning\",\n ],\n // Every element that keeps a class. `a` is built again below, by the same\n // helper, with the attribute it takes besides.\n ...Object.fromEntries(\n Object.keys(PIPELINE_CLASSES).map((tag) => [tag, withClasses(tag)]),\n ),\n a: withClasses(\"a\", \"id\"),\n math: [\"xmlns\"],\n annotation: [\"encoding\"],\n img: [...(defaultSchema.attributes?.img || []), \"loading\"],\n svg: SVG_ROOT_ATTRIBUTES,\n ...Object.fromEntries(SVG_CHILD_TAGS.map((tag) => [tag, SVG_ATTRIBUTES])),\n }),\n};\n","/**\n * The mdast half of the Vantage chain (`pipeline.ts`), in a module of its own\n * so that what imports it imports no rehype plugin.\n *\n * The planning scan parses with it, and the scan runs in a worker\n * (`docs/reference/planning-index.md` §10): `pipeline.ts` also imports\n * `rehype-katex`, whose browser build builds a `DOMParser` the moment it\n * loads, and a worker has none. A production bundle shakes those imports out,\n * but the dev server loads every module a module imports, so the worker would\n * die on its first import. `pipeline.ts` re-exports it, so every other caller\n * is unchanged.\n */\n\nimport type { PluggableList } from \"unified\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkMath from \"remark-math\";\nimport type { PipelineOptions } from \"./pipeline.js\";\n\n/**\n * The mdast half of the chain. Exported on its own because there is a real\n * mdast-only consumer: the CLI checker parses documents without ever running\n * rehype (`packages/vantage-check/src/core/document.ts`), and it has to parse\n * them exactly the way the viewer does.\n */\nexport function buildRemarkPlugins(\n options: PipelineOptions = {},\n): PluggableList {\n const { gfm = true, math = true } = options;\n const plugins: PluggableList = [];\n // `singleTilde: false` — `~x~` is not strikethrough, so a lone tilde in\n // prose survives. `singleDollarTextMath: false` — `$` is not a math\n // delimiter, so `$HOME` and `$100` stay literal. Both are contracts the\n // style guide and the user guide state, not preferences.\n if (gfm) plugins.push([remarkGfm, { singleTilde: false }]);\n if (math) plugins.push([remarkMath, { singleDollarTextMath: false }]);\n return plugins;\n}\n","/**\n * The one definition of the Vantage remark/rehype chain.\n *\n * Three call sites render Markdown — `renderMarkdown` (string in, HTML out,\n * which is what the CLI checker runs), the app's `<MarkdownViewer>`, and this\n * package's exported `<MarkdownViewer>` — and each one used to hand-write the\n * same plugin list in the same order. Three copies kept in sync by hand is how\n * a plugin lands in the viewer and not in the checker: a document that styles\n * in the app and renders bare through the tool that is supposed to validate it,\n * with no error anywhere.\n *\n * The order is load-bearing, not incidental:\n *\n * - `rehypeRaw` first: `remark-rehype` runs with `allowDangerousHtml: true`,\n * so raw HTML is still a string until this plugin parses it.\n * - `rehypeSourceLines` before `rehypeSanitize`: `data-source-line` has to be\n * an allowlisted attribute on an element the sanitizer keeps.\n * - `rehypeVantageAnchors`, `rehypeSlug`, `rehypeHighlight` and `rehypeKatex`\n * after `rehypeSanitize`. For the first two this is not a preference: the\n * sanitizer's default schema clobbers `id` with the prefix `user-content-`,\n * so writing one before it turns every `#heading` and every `#OQ-4` link in\n * every document into a dead anchor. `rehypeVantageAnchors` precedes\n * `rehypeSlug` because `rehype-slug` leaves an element that already has an\n * `id` alone. For the other two, being here means their output is trusted\n * rather than filtered — KaTeX emits inline `style` on nearly every glyph.\n *\n * Anything that reads HTML comments must sit between `rehypeRaw` and\n * `rehypeSanitize`: before `rehypeRaw` there are no comment nodes, and\n * `rehypeSanitize` deletes them. `rehypeVantageDirectives` is what occupies\n * that slot, and it is registered unconditionally — a renderer that skipped it\n * would disagree with the others about what a document means.\n */\n\nimport type { PluggableList } from \"unified\";\nimport rehypeRaw from \"rehype-raw\";\nimport rehypeSanitize from \"rehype-sanitize\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeKatex from \"rehype-katex\";\nimport rehypeSlug from \"rehype-slug\";\nimport rehypeVantageAnchors from \"./rehypeVantageAnchors.js\";\nimport rehypeSourceLines from \"./rehypeSourceLines.js\";\nimport { rehypeVantageAlerts } from \"./rehypeVantageAlerts.js\";\nimport rehypeVantageDirectives from \"./rehypeVantageDirectives.js\";\nimport {\n rehypeCaptureMathStamps,\n rehypeRestoreMathStamps,\n} from \"./rehypeVantageMathStamps.js\";\nimport { rehypeStripSvgContainers, sanitizeSchema } from \"./sanitize.js\";\nimport { buildRemarkPlugins } from \"./remarkPlugins.js\";\n\nexport { buildRemarkPlugins };\n\nexport interface PipelineOptions {\n /** GFM tables, strikethrough, task lists (default: true) */\n gfm?: boolean;\n /** KaTeX math, `$$…$$` only (default: true) */\n math?: boolean;\n /** Syntax highlighting via highlight.js (default: true) */\n highlight?: boolean;\n /** `data-source-line` attributes for line anchors (default: true) */\n sourceLines?: boolean;\n /** XSS sanitization (default: true) */\n sanitize?: boolean;\n /**\n * Lines the frontmatter consumed, added to every emitted line number so\n * `data-source-line` names a line in the *file* rather than in the parsed\n * body — which is what a `#L42` link written against the file means.\n * Defaults to 0. Ignored when `sourceLines` is false.\n */\n bodyLineOffset?: number;\n}\n\nexport interface Pipeline {\n remarkPlugins: PluggableList;\n rehypePlugins: PluggableList;\n}\n\n/** The hast half. Deliberately not exported: see `buildPipeline`. */\nfunction buildRehypePlugins(options: PipelineOptions = {}): PluggableList {\n const {\n math = true,\n highlight = true,\n sourceLines = true,\n sanitize = true,\n bodyLineOffset = 0,\n } = options;\n\n const plugins: PluggableList = [rehypeRaw];\n if (sourceLines) {\n plugins.push([rehypeSourceLines, { offset: bodyLineOffset }]);\n }\n // ── The comment slot ──────────────────────────────────────────────────\n // `rehypeVantageDirectives` compiles `<!-- vantage: … -->` comments into\n // `data-vantage-*` attributes, and it can only do that here: before\n // `rehypeRaw` there are no comment nodes, and `rehypeSanitize` deletes them.\n // It gets no option of its own: every renderer has to agree about what a\n // document means, and a flag is a way for them to disagree.\n // GFM alerts. After `rehypeSourceLines` so the title it injects carries no\n // `data-source-line` and therefore cannot become a review anchor, and before\n // the sanitizer so its one attribute is allowlisted like every other\n // `data-vantage-*`. No option of its own, for the same reason the directives\n // plugin has none: a flag is a way for two renderers to disagree about what a\n // document means.\n plugins.push(rehypeVantageAlerts);\n plugins.push(rehypeVantageDirectives);\n // The SVG containers that must go with their contents go only inside a\n // drawing; the schema's `strip` would take them out of prose as well, and a\n // bare `<pattern>` there took the rest of the document with it. See\n // `SVG_STRIPPED`.\n if (sanitize) {\n plugins.push(rehypeStripSvgContainers, [rehypeSanitize, sanitizeSchema]);\n }\n // ── The id slot ───────────────────────────────────────────────────────\n // Both of these write `id`, and both are here rather than earlier for the\n // same reason: the sanitizer clobbers `id` with `user-content-`, so anything\n // that sets one upstream of it produces dead anchors in every document with\n // no error anywhere. `rehypeVantageAnchors` goes first — `rehypeSlug` skips\n // an element that already has an `id`, so promoting an Open Question's id\n // before slugging is what lets a question written as a heading keep it.\n plugins.push(rehypeVantageAnchors);\n plugins.push(rehypeSlug);\n if (highlight) plugins.push(rehypeHighlight);\n // ── The KaTeX bracket ─────────────────────────────────────────────────\n // `rehype-katex` does not decorate a display-math block, it *replaces* it:\n // `$$…$$` arrives as a `<pre>`, which `rehypeVantageDirectives` has already\n // stamped as a member of its section's run and `rehypeSourceLines` has already\n // given a `data-source-line`, and the splice throws all of that away. The two\n // plugins around it snapshot those attributes and put them back on the\n // `<span class=\"katex-display\">` that took the block's place — which is what\n // keeps a toned section's rule continuous across a formula, a `#L` anchor\n // pointing at one resolvable, and `collapsed=true` able to hide it. They are a\n // pair and they must bracket `rehypeKatex`; see `rehypeVantageMathStamps.ts`.\n if (math) {\n plugins.push(rehypeCaptureMathStamps, rehypeKatex, rehypeRestoreMathStamps);\n }\n return plugins;\n}\n\n/**\n * Both halves from one options object.\n *\n * This is what every renderer calls. It takes one object rather than exposing\n * the two builders because `math` spans both halves — `remark-math` parses the\n * delimiters, `rehype-katex` renders the result — and two calls are two places\n * to forget the second one.\n *\n * Returns fresh arrays on every call and reads no module-level state; keep it\n * that way, so a plugin in the chain cannot become a function of how many times\n * the chain has been built.\n */\nexport function buildPipeline(options: PipelineOptions = {}): Pipeline {\n return {\n remarkPlugins: buildRemarkPlugins(options),\n rehypePlugins: buildRehypePlugins(options),\n };\n}\n","/**\n * Frontmatter parser for YAML (---) and TOML (+++) delimited content.\n * Works in both browser and server environments.\n */\n\nimport YAML from \"yaml\";\nimport { parse as parseTOML } from \"smol-toml\";\n\nexport type FrontmatterFormat = \"yaml\" | \"toml\" | \"none\";\n\n/**\n * Why a document that *looks* like it has frontmatter ended up without any.\n *\n * The parser deliberately never throws: a document whose frontmatter is broken\n * still renders, with the block treated as body text. That is the right\n * behavior for a viewer and the wrong one for an author, who gets no signal\n * at all — so the reason is recorded here for anything that wants to report it\n * (`vantage-check` does; see its frontmatter rules).\n *\n * - `unterminated` — an opening delimiter with no closing one.\n * - `invalid` — the block did not parse; `message` is the parser's own words,\n * and `line`/`column` are 1-based *within the block* when it said.\n * - `not-a-mapping` — it parsed, but to a string or a list rather than a table\n * of fields, which is not something a metadata card can render.\n */\nexport interface FrontmatterProblem {\n kind: \"unterminated\" | \"invalid\" | \"not-a-mapping\";\n /** The delimiter the document opened with. */\n delimiter: string;\n message?: string;\n line?: number;\n column?: number;\n}\n\nexport interface ParsedFrontmatter {\n frontmatter: Record<string, unknown>;\n body: string;\n format: FrontmatterFormat;\n /**\n * How many source lines the frontmatter block consumed — the shift between a\n * line number in `body` and the same line in the original file:\n * `fileLine = bodyLine + bodyLineOffset`.\n *\n * Anything that renders `body` and reports line numbers (line anchors, review\n * comment anchors) has to add this back, or every number it produces points\n * `bodyLineOffset` lines short of the text it names.\n */\n bodyLineOffset: number;\n /**\n * Set when the document opens with a frontmatter delimiter that did not\n * yield a metadata table. Everything else in this result is unchanged —\n * this records *why*, it does not change what rendering does.\n */\n problem?: FrontmatterProblem;\n}\n\n/**\n * Parse frontmatter from markdown content.\n * Supports YAML (delimited by ---) and TOML (delimited by +++).\n */\nexport function parseFrontmatter(content: string): ParsedFrontmatter {\n if (content.startsWith(\"+++\")) {\n return parseFrontmatterWithDelimiter(content, \"+++\", \"toml\");\n }\n if (content.startsWith(\"---\")) {\n return parseFrontmatterWithDelimiter(content, \"---\", \"yaml\");\n }\n return withOffset(content, {\n frontmatter: {},\n body: content,\n format: \"none\",\n });\n}\n\n/**\n * Fill in `bodyLineOffset`. `body` is always a suffix of `content`, so the\n * newlines in the prefix that was stripped are exactly the shift — which also\n * accounts for the blank line consumed after the closing delimiter.\n */\nfunction withOffset(\n content: string,\n parsed: Omit<ParsedFrontmatter, \"bodyLineOffset\">,\n): ParsedFrontmatter {\n const stripped = content.slice(0, content.length - parsed.body.length);\n let bodyLineOffset = 0;\n for (const ch of stripped) {\n if (ch === \"\\n\") bodyLineOffset++;\n }\n return { ...parsed, bodyLineOffset };\n}\n\nfunction parseFrontmatterWithDelimiter(\n content: string,\n delimiter: string,\n format: \"yaml\" | \"toml\",\n): ParsedFrontmatter {\n const searchStart = delimiter.length;\n const endIndex = content.indexOf(`\\n${delimiter}`, searchStart);\n if (endIndex === -1) {\n return withOffset(content, {\n frontmatter: {},\n body: content,\n format: \"none\",\n problem: { kind: \"unterminated\", delimiter },\n });\n }\n\n const raw = content.slice(searchStart + 1, endIndex).trim();\n const bodyStart = endIndex + 1 + delimiter.length;\n const body = content.slice(bodyStart).replace(/^\\n/, \"\");\n\n try {\n const parsed: unknown =\n format === \"toml\" ? parseTOML(raw) : YAML.parse(raw);\n return withOffset(content, {\n frontmatter: (parsed as Record<string, unknown>) || {},\n body,\n format,\n ...(isMapping(parsed)\n ? {}\n : { problem: { kind: \"not-a-mapping\" as const, delimiter } }),\n });\n } catch (error) {\n return withOffset(content, {\n frontmatter: {},\n body: content,\n format: \"none\",\n problem: { kind: \"invalid\", delimiter, ...errorPosition(error) },\n });\n }\n}\n\n/** Empty frontmatter is fine; a scalar or a list where a table belongs is not. */\nfunction isMapping(value: unknown): boolean {\n return (\n value === null ||\n value === undefined ||\n (typeof value === \"object\" && !Array.isArray(value))\n );\n}\n\n/**\n * Pull the parser's message and, where it gave one, the position inside the\n * frontmatter block. `yaml` reports `linePos`; `smol-toml` reports `line` and\n * `column`. Both are optional and both are read defensively — a missing\n * position costs a less precise report, a wrong assumption costs a crash.\n */\nfunction errorPosition(error: unknown): {\n message: string;\n line?: number;\n column?: number;\n} {\n const message = error instanceof Error ? error.message : String(error);\n const source = error as {\n linePos?: Array<{ line?: number; col?: number }>;\n line?: number;\n column?: number;\n };\n\n const yamlPosition = source?.linePos?.[0];\n if (typeof yamlPosition?.line === \"number\") {\n return {\n message,\n line: yamlPosition.line,\n ...(typeof yamlPosition.col === \"number\"\n ? { column: yamlPosition.col }\n : {}),\n };\n }\n if (typeof source?.line === \"number\") {\n return {\n message,\n line: source.line,\n ...(typeof source.column === \"number\" ? { column: source.column } : {}),\n };\n }\n return { message };\n}\n","/**\n * Framework-agnostic markdown -> HTML rendering pipeline.\n * Uses the same remark/rehype chain as the Vantage viewer.\n */\n\nimport type { Root } from \"mdast\";\nimport { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkRehype from \"remark-rehype\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { buildPipeline } from \"./pipeline.js\";\nimport { parseFrontmatter } from \"./frontmatter.js\";\nimport type { ParsedFrontmatter } from \"./frontmatter.js\";\n\nexport interface RenderOptions {\n /** Enable GFM tables, strikethrough, task lists (default: true) */\n gfm?: boolean;\n /** Enable KaTeX math rendering (default: true) */\n math?: boolean;\n /** Enable syntax highlighting (default: true) */\n highlight?: boolean;\n /** Add data-source-line attributes for line anchors (default: true) */\n sourceLines?: boolean;\n /** Enable XSS sanitization (default: true) */\n sanitize?: boolean;\n /** Parse and strip frontmatter (default: true) */\n frontmatter?: boolean;\n /**\n * The body's mdast, already parsed — an optimization, not a second input.\n *\n * Parsing is the most expensive step in this function: measured over\n * `docs/design/`, `remark-parse` with GFM costs about twice what the whole\n * rehype half costs. A caller that already holds the tree — the CLI checker\n * does, because every `mdast` rule ran on it before the render rule got its\n * turn — was paying for the same parse twice.\n *\n * The tree must have been parsed by **this package's remark half with these\n * same options** (`buildRemarkPlugins`, which is exported for exactly that\n * reason). Anything parsed some other way renders through a chain the viewer\n * does not use, which is the one thing the shared pipeline exists to prevent,\n * and nothing here can detect it: a tree is a tree.\n *\n * `content` is still read, for its frontmatter. The two have to describe the\n * same document.\n */\n tree?: Root;\n}\n\nexport interface RenderResult {\n /** The rendered HTML string */\n html: string;\n /** Parsed frontmatter (empty object if none or disabled) */\n frontmatter: Record<string, unknown>;\n /** The markdown body with frontmatter stripped */\n body: string;\n}\n\n/**\n * Render a markdown string to HTML using the full Vantage pipeline.\n *\n * Features (all enabled by default):\n * - GitHub Flavored Markdown (tables, strikethrough, task lists)\n * - KaTeX math rendering, inline and block ($$...$$ only; single $ is not a delimiter)\n * - Syntax highlighting via highlight.js\n * - `data-source-line` attributes for line anchors\n * - XSS sanitization\n * - Heading slugs/anchors\n * - YAML/TOML frontmatter parsing\n *\n * Mermaid diagrams are NOT rendered server-side (they require a browser).\n * Mermaid code blocks are preserved as `<pre><code class=\"language-mermaid\">`.\n * Use the React `<MarkdownViewer>` component for client-side mermaid rendering.\n */\nexport async function renderMarkdown(\n content: string,\n options: RenderOptions = {},\n): Promise<RenderResult> {\n const {\n gfm = true,\n math = true,\n highlight = true,\n sourceLines = true,\n sanitize = true,\n frontmatter: parseFm = true,\n tree,\n } = options;\n\n // Parse frontmatter\n let parsed: ParsedFrontmatter;\n if (parseFm) {\n parsed = parseFrontmatter(content);\n } else {\n parsed = {\n frontmatter: {},\n body: content,\n format: \"none\",\n bodyLineOffset: 0,\n };\n }\n\n // One chain, defined in ./pipeline.ts and shared with both React viewers —\n // the checker must not render through a different pipeline than the app.\n const { remarkPlugins, rehypePlugins } = buildPipeline({\n gfm,\n math,\n highlight,\n sourceLines,\n sanitize,\n bodyLineOffset: parsed.bodyLineOffset,\n });\n\n // `allowDangerousHtml` is why raw HTML reaches `rehypeRaw` at all.\n const processor = unified()\n .use(remarkParse)\n .use(remarkPlugins)\n .use(remarkRehype, { allowDangerousHtml: true })\n .use(rehypePlugins)\n .use(rehypeStringify);\n\n // `process` is `parse` then `run` then `stringify`. With the tree in hand the\n // first of those is already done, so the other two are called directly —\n // same processor, same plugins, same order, one parse less. `parsed.body` is\n // handed to both as the file, which is what `process` does with it too.\n const result =\n tree === undefined\n ? String(await processor.process(parsed.body))\n : processor.stringify(\n await processor.run(tree, parsed.body),\n parsed.body,\n );\n\n return {\n html: String(result),\n frontmatter: parsed.frontmatter,\n body: parsed.body,\n };\n}\n","/**\n * Parsing for GitHub-style line anchors, with no DOM in sight.\n *\n * Split out from scrollToLineAnchor.ts so that non-browser consumers — the\n * `vantage-check` CLI, which validates `#L42` links against the file on disk —\n * can share the *same* syntax the viewer honors instead of reimplementing it\n * and drifting.\n */\n\n/**\n * Parse a GitHub-style line anchor hash.\n * Supports: #L42, #L42-L50, #L42-50\n * Returns null if the hash is not a line anchor.\n */\nexport function parseLineAnchor(\n hash: string,\n): { start: number; end: number } | null {\n if (!hash) return null;\n const frag = hash.startsWith(\"#\") ? hash.slice(1) : hash;\n const match = frag.match(/^L(\\d+)(?:-L?(\\d+))?$/);\n if (!match) return null;\n\n const start = parseInt(match[1], 10);\n const end = match[2] ? parseInt(match[2], 10) : start;\n return { start: Math.min(start, end), end: Math.max(start, end) };\n}\n","/**\n * Framework-agnostic line anchor utilities.\n * Scroll to and highlight the elements a GitHub-style line anchor\n * (#L42, #L42-L50) names. The parsing half lives in lineAnchor.ts, which has\n * no DOM dependency.\n */\n\nimport { parseLineAnchor } from \"./lineAnchor.js\";\n\nconst HIGHLIGHT_CLASS = \"line-anchor-highlight\";\n\n/**\n * Clear all line anchor highlights from a container.\n */\nexport function clearLineAnchorHighlights(container: HTMLElement): void {\n container.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((node) => {\n (node as HTMLElement).classList.remove(HIGHLIGHT_CLASS);\n });\n}\n\n/**\n * Scroll to and highlight line-anchored elements in a container.\n *\n * @param container - The DOM element containing rendered markdown\n * @param hash - The URL hash (e.g. \"#L42\" or \"#L42-L50\")\n * @returns A cleanup function that removes the highlights\n */\nexport function scrollToLineAnchor(\n container: HTMLElement,\n hash: string,\n): (() => void) | null {\n clearLineAnchorHighlights(container);\n\n const range = parseLineAnchor(hash);\n if (!range) return null;\n\n const blocks = container.querySelectorAll(\"[data-source-line]\");\n let firstMatch: HTMLElement | null = null;\n\n for (const block of blocks) {\n const line = parseInt((block as HTMLElement).dataset.sourceLine || \"0\", 10);\n if (line >= range.start && line <= range.end) {\n (block as HTMLElement).classList.add(HIGHLIGHT_CLASS);\n if (!firstMatch) firstMatch = block as HTMLElement;\n }\n }\n\n // If exact line not found, find the nearest block before the target line\n if (!firstMatch) {\n let closest: HTMLElement | null = null;\n let closestLine = 0;\n for (const block of blocks) {\n const line = parseInt(\n (block as HTMLElement).dataset.sourceLine || \"0\",\n 10,\n );\n if (line <= range.start && line > closestLine) {\n closestLine = line;\n closest = block as HTMLElement;\n }\n }\n if (closest) {\n closest.classList.add(HIGHLIGHT_CLASS);\n firstMatch = closest;\n }\n }\n\n // Scroll to the first highlighted element\n if (firstMatch) {\n requestAnimationFrame(() => {\n // Find the nearest scrollable ancestor\n const scrollParent = findScrollParent(container);\n if (scrollParent) {\n const offset =\n firstMatch!.getBoundingClientRect().top -\n scrollParent.getBoundingClientRect().top +\n scrollParent.scrollTop;\n scrollParent.scrollTo({ top: offset - 32, behavior: \"smooth\" });\n } else {\n firstMatch!.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n }\n });\n }\n\n return () => clearLineAnchorHighlights(container);\n}\n\nfunction findScrollParent(el: HTMLElement): HTMLElement | null {\n let node: HTMLElement | null = el;\n while (node) {\n const overflow = getComputedStyle(node).overflowY;\n if (overflow === \"auto\" || overflow === \"scroll\") return node;\n node = node.parentElement;\n }\n return null;\n}\n","/**\n * The `vantage:` frontmatter key — file-scoped chrome (`docs/reference/inline-markup.md`, \"File-scoped chrome\").\n *\n * One reserved key at the top level of a document's frontmatter, holding the\n * chrome that belongs to the *file* rather than to a section. Today that is one\n * thing: whether the document's lifecycle `status:` is shown as a chip above the\n * metadata card, instead of being buried as one row inside it.\n *\n * Read only at the top level, and **inert on every failure** (P3): an unknown\n * key, a value outside the closed set, or a `vantage:` that is not a table\n * produces no chrome, no throw and no console output. The reasons are returned\n * as data in `issues`, for anything that wants to report them — `vantage-check`\n * does, and it is the only signal an author gets. That split is exactly the one\n * `FrontmatterProblem` already uses in `frontmatter.ts`: the viewer reads the\n * value, the checker reads the reasons.\n *\n * Like `vantageDirectives.ts`, this module is imported by the CLI checker **by\n * relative path**, so it must stay a pure function of already-parsed data: no\n * hast, no React, no filesystem.\n */\n\nimport { VANTAGE_TONES } from \"./vantageDirectives.js\";\n\n/**\n * The document lifecycle vocabulary. Closed; extending it is a code change.\n *\n * This is the repo's own existing set, not a new one — `styleGuide.ts` tells\n * every agent to write `status: in-review # draft | in-review | accepted |\n * deprecated`, and every document under `docs/` follows it. It is deliberately\n * *not* the `badge` set (`draft stale blocked done wip`): `badge` is\n * section-scoped workflow state, `status` is document lifecycle state, and\n * `in-review` — the value the design doc's own only example renders — is not a\n * badge word at all. Only `draft` is a member of both, and a token set is per key.\n */\nexport const DOC_STATUSES = [\n \"draft\",\n \"in-review\",\n \"accepted\",\n \"deprecated\",\n] as const;\n\nexport type DocStatus = (typeof DOC_STATUSES)[number];\n\n/** Every key this build knows under `vantage:`. Closed. */\nexport const VANTAGE_FRONTMATTER_KEYS = [\"status-chip\"] as const;\n\n/**\n * Which tone each status borrows its colors from.\n *\n * The chip has no palette of its own: it reuses the tone chips\n * (`.vantage-chip--<tone>` in `styles/directives.css`), which is also what makes\n * a `draft` chip and a `badge=draft` chip the same visual object. A map rather\n * than a computed class name, so the whole status→tone relation is one readable\n * table and a test can assert it covers the vocabulary.\n */\nexport const DOC_STATUS_TONES: Readonly<\n Record<DocStatus, (typeof VANTAGE_TONES)[number]>\n> = {\n draft: \"muted\",\n \"in-review\": \"warning\",\n accepted: \"tip\",\n deprecated: \"caution\",\n};\n\n/**\n * Why something under `vantage:` produced no chrome.\n *\n * `status-chip-orphan` and `status-chip-disagrees` are not vocabulary errors —\n * both values are legal — but both are the markup rot R3 is about: a chip that\n * says something the document's own `status:` does not.\n */\nexport type VantageFrontmatterIssue =\n | { kind: \"not-a-table\"; value: unknown }\n | { kind: \"unknown-key\"; key: string }\n | { kind: \"bad-value\"; key: string; value: unknown; legal: readonly string[] }\n | { kind: \"status-chip-orphan\"; status: unknown }\n | { kind: \"status-chip-disagrees\"; chip: DocStatus; status: unknown };\n\nexport interface VantageFrontmatter {\n /** The chip's text, or `undefined` for no chip. */\n statusChip?: DocStatus;\n /** Why something was dropped. A viewer must never read this (P3). */\n issues: VantageFrontmatterIssue[];\n}\n\n/** The legal `status-chip` values, in the order a message should list them. */\nconst STATUS_CHIP_VALUES: readonly string[] = [\n ...DOC_STATUSES,\n \"true\",\n \"false\",\n];\n\n/** Narrowing helper the chip and the checker both use. */\nexport function isDocStatus(value: unknown): value is DocStatus {\n return (\n typeof value === \"string\" &&\n (DOC_STATUSES as readonly string[]).includes(value)\n );\n}\n\nfunction isTable(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\n/**\n * Read the `vantage:` key out of parsed frontmatter.\n *\n * Pure: no module state, no mutation of the input, no logging. The same object\n * in twice gives equal results out.\n */\nexport function readVantageFrontmatter(\n frontmatter: Record<string, unknown>,\n): VantageFrontmatter {\n const issues: VantageFrontmatterIssue[] = [];\n if (!Object.hasOwn(frontmatter, \"vantage\")) return { issues };\n\n const value = frontmatter[\"vantage\"];\n // A `Date` is an object and would otherwise pass for a table: `yaml` parses\n // `vantage: 2026-08-31` into one, which `Object.keys` reports as empty.\n if (!isTable(value)) {\n issues.push({ kind: \"not-a-table\", value });\n return { issues };\n }\n\n let statusChip: DocStatus | undefined;\n\n for (const key of Object.keys(value)) {\n // D2 is per key: an unknown key drops that key and nothing else, so a newer\n // document keeps working in an older build.\n if (!(VANTAGE_FRONTMATTER_KEYS as readonly string[]).includes(key)) {\n issues.push({ kind: \"unknown-key\", key });\n continue;\n }\n if (key === \"status-chip\") {\n statusChip = readStatusChip(frontmatter, value[key], issues);\n }\n }\n\n return { ...(statusChip === undefined ? {} : { statusChip }), issues };\n}\n\n/**\n * `status-chip` takes two shapes, and the boolean one is the recommended shape.\n *\n * `true` **inherits** the document's own top-level `status:`, so the chip cannot\n * disagree with it — which is the entire point of §5.3 (\"makes `status: draft`\n * visible rather than only buried in a metadata card\"; the row stays, the chip\n * promotes the value rather than moving it). A literal token is kept\n * because the design doc's first draft of that example used one, and the\n * disagreement it makes possible is turned into a checker finding rather than\n * banned.\n *\n * Discrimination is on `typeof`, never truthiness: `true` is a YAML boolean and\n * `2026-08-31` is a `Date`, and both would sail through a truthy test.\n */\nfunction readStatusChip(\n frontmatter: Record<string, unknown>,\n raw: unknown,\n issues: VantageFrontmatterIssue[],\n): DocStatus | undefined {\n const status = frontmatter[\"status\"];\n\n // Explicitly off. Not an issue: saying so is the point of a token vocabulary\n // that can be canceled (the same reason `collapsed` has a `false`).\n if (raw === false) return undefined;\n\n if (raw === true) {\n if (isDocStatus(status)) return status;\n issues.push({ kind: \"status-chip-orphan\", status });\n return undefined;\n }\n\n // Exact match, no case folding and no trimming — the same all-or-nothing\n // posture as the directive grammar and the sanitizer. `status-chip: Draft`\n // is dropped, and the checker is what says so.\n if (isDocStatus(raw)) {\n if (isDocStatus(status) && status !== raw) {\n issues.push({ kind: \"status-chip-disagrees\", chip: raw, status });\n }\n return raw;\n }\n\n issues.push({\n kind: \"bad-value\",\n key: \"status-chip\",\n value: raw,\n legal: STATUS_CHIP_VALUES,\n });\n return undefined;\n}\n","/**\n * Which mermaid theme the page is asking for, and what to hand mermaid so a\n * diagram sits on a Vantage surface rather than on the one mermaid assumes.\n *\n * `.dark` on `<html>` is the app's dark selector — the same switch\n * `styles/directives.css` reads for the tone palette — so a package consumer\n * gets dark diagrams by toggling the class it already toggles.\n *\n * ## Why the variables exist at all\n *\n * Mermaid's stock `dark` theme is built for a near-black page: `mainBkg` is\n * `#1f2020`, which against Vantage's `slate-900` content column (`#0f172b`) is\n * 1.28:1. Every node box vanished, and a flowchart read as floating labels\n * joined by lines. Its `edgeLabelBackground` is a mid-gray `#585858` that\n * matched no surface in either theme, so every edge label wore a gray chip.\n *\n * So the two palettes below are the app's own slate steps, chosen the way the\n * rest of the dark audit was: a node box one step off its surface so the box is\n * visible, ink and borders at 4.5:1 or better against that box. They are stated\n * here rather than read out of the page because mermaid wants hex strings at\n * `initialize()` time, before any diagram exists to measure.\n *\n * The set is deliberately **structural only** — the surface, the node box, its\n * border and ink, the lines, the edge-label chip. `primaryColor` and its two\n * siblings are left alone on purpose: mermaid derives the categorical series\n * from them, so overriding them with slate turned a pie chart into three\n * near-black wedges. What is wrong in the stock dark theme is where a diagram\n * sits, not which colors it tells things apart with.\n */\nexport type MermaidThemeName = \"dark\" | \"default\";\n\n/** Whether the document is asking for the dark palette right now. */\nexport function currentMermaidTheme(): MermaidThemeName {\n return typeof document !== \"undefined\" &&\n document.documentElement.classList.contains(\"dark\")\n ? \"dark\"\n : \"default\";\n}\n\n/**\n * The attribute on `<html>` naming the active color theme. Absent means the\n * built-in look. The app sets it only once the theme's stylesheet has loaded,\n * so a reader of this attribute can trust the theme's variables are in effect.\n */\nexport const COLOR_THEME_ATTRIBUTE = \"data-vantage-theme\";\n\n/**\n * The attribute on `<html>` saying where the active theme came from: `\"user\"`\n * for a stylesheet in the reader's themes directory, `\"built-in\"` for one the\n * app ships. Set with {@link COLOR_THEME_ATTRIBUTE}, and absent with it.\n *\n * It exists because an id alone is not a palette. A user theme may share a\n * built-in's id — that is how a reader tweaks one — and the app applies the\n * stored built-in synchronously, then swaps in the same-id user file once\n * /api/themes answers. Keyed on the id, the diagrams drawn in between kept the\n * built-in's colors: the key did not change, so neither the cache nor\n * `useSyncExternalStore` saw a reason to redraw.\n */\nexport const COLOR_THEME_SOURCE_ATTRIBUTE = \"data-vantage-theme-source\";\n\n/** The active color theme's id, or `\"\"` for the built-in look. */\nexport function currentColorTheme(): string {\n if (typeof document === \"undefined\") return \"\";\n return document.documentElement.getAttribute(COLOR_THEME_ATTRIBUTE) ?? \"\";\n}\n\n/**\n * Everything a rendered diagram's colors depend on, as one string: the\n * light/dark mode, plus the color theme when one is active.\n *\n * A diagram is baked at render time, so anything that changes its colors has\n * to change this key — it is what the SVG cache and the loader's \"configured\n * for\" check compare. Under the built-in look it is exactly the mode name,\n * which is what both keyed on before color themes existed.\n */\nexport function currentMermaidPalette(): string {\n const theme = currentColorTheme();\n if (!theme) return currentMermaidTheme();\n // Only a user theme is marked, so a built-in's key stays \"mode id\".\n const user =\n document.documentElement.getAttribute(COLOR_THEME_SOURCE_ATTRIBUTE) ===\n \"user\";\n return `${currentMermaidTheme()} ${theme}${user ? \" user\" : \"\"}`;\n}\n\n/** The mermaid theme name a palette key was built from. */\nexport function mermaidThemeOf(palette: string): MermaidThemeName {\n return palette.startsWith(\"dark\") ? \"dark\" : \"default\";\n}\n\n/**\n * Theme variables per theme. Mermaid derives most of its palette from these, so\n * the set is deliberately small: the surfaces, the ink, and the lines.\n */\nconst THEME_VARIABLES: Record<MermaidThemeName, Record<string, string>> = {\n dark: {\n // slate-800: the fence the app leaves around a rendered diagram.\n background: \"#1d293d\",\n // slate-700 boxes on that fence, outlined in slate-400 — the outline is\n // what makes a node read as a box, and slate-100 ink sits at 11:1 inside it.\n mainBkg: \"#314158\",\n nodeBorder: \"#90a1b9\",\n nodeTextColor: \"#f1f5f9\",\n // slate-400: 5.6:1 on the fence, so an edge is a line and not a smudge.\n lineColor: \"#90a1b9\",\n textColor: \"#e2e8f0\",\n // The chip under a label on an edge. Matching the surface behind the\n // diagram is what makes it read as a gap in the line rather than as a tag.\n edgeLabelBackground: \"#1d293d\",\n },\n default: {\n background: \"#f8fafc\",\n mainBkg: \"#f1f5f9\",\n nodeBorder: \"#62748e\",\n nodeTextColor: \"#0f172b\",\n lineColor: \"#62748e\",\n textColor: \"#1d293d\",\n edgeLabelBackground: \"#f8fafc\",\n },\n};\n\n/**\n * Where each variable comes from under a color theme: the palette step the\n * built-in value was chosen from. The hex above IS that step in Tailwind's own\n * palette, so reading the step back out of the page gives the same diagram\n * under the built-in look and the theme's colors under any other.\n */\nconst THEME_SOURCES: Record<MermaidThemeName, Record<string, string>> = {\n dark: {\n background: \"--color-slate-800\",\n mainBkg: \"--color-slate-700\",\n nodeBorder: \"--color-slate-400\",\n nodeTextColor: \"--color-slate-100\",\n lineColor: \"--color-slate-400\",\n textColor: \"--color-slate-200\",\n edgeLabelBackground: \"--color-slate-800\",\n },\n default: {\n background: \"--color-slate-50\",\n mainBkg: \"--color-slate-100\",\n nodeBorder: \"--color-slate-500\",\n nodeTextColor: \"--color-slate-900\",\n lineColor: \"--color-slate-500\",\n textColor: \"--color-slate-800\",\n edgeLabelBackground: \"--color-slate-50\",\n },\n};\n\nexport function mermaidThemeVariables(\n theme: MermaidThemeName,\n): Record<string, string> {\n const fixed = THEME_VARIABLES[theme];\n // The built-in look keeps its measured constants verbatim: no DOM reads, and\n // nothing a color conversion could round.\n if (!currentColorTheme()) return fixed;\n const out: Record<string, string> = {};\n for (const [key, hex] of Object.entries(fixed)) {\n out[key] = resolveCssColor(THEME_SOURCES[theme][key]) ?? hex;\n }\n return out;\n}\n\n/**\n * A custom property's color as `#rrggbb`, or `null` when it cannot be read.\n *\n * Mermaid wants hex at `initialize()` time, and a theme's value can be any CSS\n * color — `oklch()`, `color-mix()`, a `var()` of another variable. So the\n * browser does the work: a probe element resolves the property to a computed\n * color, and a 1×1 canvas turns that into sRGB bytes whatever syntax it came\n * back in. No canvas (jsdom, a locked-down embed) is `null`, and the caller\n * falls back to the built-in value.\n *\n * A property nobody declared is `null` too, and has to be checked for up\n * front: `color: var(--unset)` is invalid at computed-value time, so the probe\n * *inherits* its color instead — the page's text color, which the canvas\n * would dutifully turn into a plausible hex and mermaid would paint every node\n * box with.\n */\nexport function resolveCssColor(property: string): string | null {\n if (typeof document === \"undefined\") return null;\n const declared = getComputedStyle(document.documentElement)\n .getPropertyValue(property)\n .trim();\n if (!declared) return null;\n const probe = document.createElement(\"span\");\n probe.style.display = \"none\";\n probe.style.color = `var(${property})`;\n document.documentElement.appendChild(probe);\n const computed = getComputedStyle(probe).color;\n probe.remove();\n if (!computed) return null;\n\n const ctx = pixelContext();\n if (!ctx) return null;\n ctx.fillStyle = computed;\n ctx.fillRect(0, 0, 1, 1);\n const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;\n return `#${[r, g, b].map((v) => v.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\n/** A 1×1 2D context to read a color back from, or `null` where there is none. */\nfunction pixelContext(): CanvasRenderingContext2D | null {\n try {\n const canvas = document.createElement(\"canvas\");\n canvas.width = canvas.height = 1;\n return canvas.getContext(\"2d\", { willReadFrequently: true });\n } catch {\n return null;\n }\n}\n","// Rendered SVGs, kept so a re-render of the same document does not re-run\n// mermaid. Keyed by theme as well as code: the same fence renders to a\n// different SVG in each palette, and a cache keyed by code alone served the\n// light diagram — white boxes, black ink — onto the dark page after a theme\n// switch, and the dark one onto the light page.\n//\n// \"Theme\" here is the palette key (`currentMermaidPalette`): the mode, plus the\n// color theme when one is active, since switching color themes changes a\n// diagram's colors exactly as a light/dark flip does.\nimport { currentMermaidPalette } from \"./mermaidTheme.js\";\n\nconst svgCache = new Map<string, string>();\n\n// The separator is written as an escape on purpose: a literal NUL byte in the\n// source makes git call this file binary, so every change to it arrives as\n// \"Binary files differ\" with no diff to review. It is still a NUL at runtime —\n// the one byte neither a palette key nor a fence's code can contain.\nconst cacheKey = (code: string, theme: string) => `${theme}\\u0000${code}`;\n\n/** The SVG for this fence in the theme the page is currently asking for. */\nexport function getCachedSvg(\n code: string,\n theme: string = currentMermaidPalette(),\n): string | undefined {\n return svgCache.get(cacheKey(code, theme));\n}\n\nexport function hasCachedSvg(\n code: string,\n theme: string = currentMermaidPalette(),\n): boolean {\n return svgCache.has(cacheKey(code, theme));\n}\n\nexport function setCachedSvg(\n code: string,\n svg: string,\n theme: string = currentMermaidPalette(),\n): void {\n svgCache.set(cacheKey(code, theme), svg);\n}\n\nexport function clearMermaidCache() {\n svgCache.clear();\n}\n","import type mermaidAPI from \"mermaid\";\nimport {\n currentMermaidPalette,\n mermaidThemeOf,\n mermaidThemeVariables,\n} from \"./mermaidTheme.js\";\n\nlet mermaidInstance: typeof mermaidAPI | null = null;\nlet mermaidLoading: Promise<typeof mermaidAPI> | null = null;\n/**\n * The palette key (`currentMermaidPalette`) the loaded instance was last\n * configured for, `null` until loaded.\n */\nlet configuredTheme: string | null = null;\n\n/**\n * The configuration keys a diagram's own source may not set, from an `init`\n * directive or from frontmatter: Mermaid's default list, then the three\n * that write a diagram's stylesheet.\n *\n * **A diagram's stylesheet reaches past the diagram.** Mermaid puts every\n * selector of it under the diagram's id, but leaves the names of its\n * `@keyframes` global, so a diagram that defined `flash-update` redefined the\n * animation the viewer plays on a block that just changed. Given\n * `position: fixed` at the size of the window, the next flash laid that block,\n * and the link it held, over the header and the sidebar. The same stylesheet\n * fetched images from any host, which is why `SAFE_STYLE` refuses parentheses.\n *\n * - `themeCSS` is a stylesheet, and Mermaid checks only that its braces pair.\n * - `fontFamily` is copied into `themeVariables` *after* Mermaid's check on\n * theme variables has run, and lands in the stylesheet as a declaration's\n * value, where a nested block becomes a rule of its own.\n * - `altFontFamily` lands in a rule the browser parses first, which drops\n * what does not belong. It is refused anyway: nothing needs it, and that\n * parse is all that stands in the way.\n *\n * Mermaid applies the list at every depth, so `themeVariables.fontFamily` in\n * a diagram is refused too. The list is spelled whole rather than relying on\n * Mermaid merging it with its default. `frontend/e2e/mermaid.spec.ts` measures\n * all four routes; see \"Mermaid\" in `docs/reference/inline-markup.md`.\n */\nconst MERMAID_SECURE_KEYS = [\n \"secure\",\n \"securityLevel\",\n \"startOnLoad\",\n \"maxTextSize\",\n \"suppressErrorRendering\",\n \"maxEdges\",\n \"themeCSS\",\n \"fontFamily\",\n \"altFontFamily\",\n];\n\nfunction configure(m: typeof mermaidAPI, palette: string) {\n const theme = mermaidThemeOf(palette);\n m.initialize({\n startOnLoad: false,\n theme,\n themeVariables: mermaidThemeVariables(theme),\n securityLevel: \"strict\",\n suppressErrorRendering: true,\n secure: MERMAID_SECURE_KEYS,\n });\n configuredTheme = palette;\n}\n\n/**\n * The mermaid module, configured for the theme the page is asking for *now*.\n *\n * Re-configuring on a theme change is the point. `initialize` used to run once,\n * on first import, so every diagram rendered after a light/dark switch still\n * came out in the palette the session started in — a white slab of a flowchart\n * on the dark page, or a black one on the light page. `initialize` merges into\n * mermaid's global config, so calling it again is how the next `render` picks\n * the new palette up; the cache is keyed by theme so the old SVGs are not\n * served instead (`mermaidCache.ts`).\n */\nexport async function getMermaid(): Promise<typeof mermaidAPI> {\n const theme = currentMermaidPalette();\n if (mermaidInstance) {\n if (configuredTheme !== theme) configure(mermaidInstance, theme);\n return mermaidInstance;\n }\n if (!mermaidLoading) {\n mermaidLoading = import(\"mermaid\").then((mod) => {\n const m = mod.default;\n configure(m, currentMermaidPalette());\n mermaidInstance = m;\n return m;\n });\n }\n const loaded = await mermaidLoading;\n const wanted = currentMermaidPalette();\n if (configuredTheme !== wanted) configure(loaded, wanted);\n return loaded;\n}\n\nexport function resetMermaidLoader() {\n mermaidInstance = null;\n mermaidLoading = null;\n configuredTheme = null;\n}\n","/**\n * Client-side utility to find and render mermaid code blocks in a container.\n *\n * After calling `renderMarkdown()`, mermaid blocks come through as\n * `<pre><code class=\"language-mermaid\">...</code></pre>`. This function\n * finds those blocks and replaces them with rendered SVG diagrams.\n *\n * It also draws diagrams ahead of a render: `mermaidFences` reads them out of\n * Markdown and `prerenderMermaid` puts each in the SVG cache, where the React\n * `MermaidDiagram` finds it on mount.\n *\n * Framework-agnostic — works in any browser environment.\n */\n\nimport type { Code } from \"mdast\";\nimport remarkParse from \"remark-parse\";\nimport { unified } from \"unified\";\nimport { visit } from \"unist-util-visit\";\nimport { getCachedSvg, hasCachedSvg, setCachedSvg } from \"./mermaidCache.js\";\nimport { getMermaid } from \"./mermaidLoader.js\";\nimport { currentMermaidPalette } from \"./mermaidTheme.js\";\nimport { buildRemarkPlugins } from \"./remarkPlugins.js\";\n\nexport interface RenderMermaidOptions {\n /** CSS class to add to the SVG wrapper div (default: \"mermaid\") */\n className?: string;\n /** Called when a diagram fails to render */\n onError?: (code: string, error: Error) => void;\n}\n\n/**\n * Find all `<pre><code class=\"language-mermaid\">` blocks in a container\n * and replace them with rendered SVG diagrams.\n *\n * @param container - DOM element containing rendered markdown HTML\n * @param options - Optional configuration\n * @returns Promise that resolves when all diagrams are rendered\n *\n * @example\n * ```ts\n * import { renderMarkdown, renderMermaidBlocks } from \"vantage-md\";\n *\n * const { html } = await renderMarkdown(content);\n * container.innerHTML = html;\n * await renderMermaidBlocks(container);\n * ```\n */\nexport async function renderMermaidBlocks(\n container: HTMLElement,\n options: RenderMermaidOptions = {},\n): Promise<void> {\n const { className = \"mermaid\", onError } = options;\n\n const codeBlocks = container.querySelectorAll(\n 'pre > code.language-mermaid, pre > code[class*=\"language-mermaid\"]',\n );\n if (codeBlocks.length === 0) return;\n\n // Loaded on the first cache miss, not up front: mermaid is the heaviest\n // dependency in the package, and a container whose every diagram is already\n // in `svgCache` — the common case on a re-render — has no use for it.\n let loading: Promise<Awaited<ReturnType<typeof getMermaid>>> | undefined;\n const mermaidOnce = () => (loading ??= getMermaid());\n\n const renderPromises = Array.from(codeBlocks).map(async (codeEl) => {\n const preEl = codeEl.parentElement;\n if (!preEl) return;\n\n const code = codeEl.textContent || \"\";\n if (!code.trim()) return;\n\n // Check cache first. Keyed by theme as well as code — the same fence\n // renders to a different SVG in each palette.\n const cached = getCachedSvg(code);\n if (cached) {\n replaceWithSvg(preEl, cached, className);\n return;\n }\n\n try {\n const mermaid = await mermaidOnce();\n // Generate a stable ID from code hash\n let hash = 0;\n for (let i = 0; i < code.length; i++) {\n hash = (hash << 5) - hash + code.charCodeAt(i);\n hash = hash & hash;\n }\n const id = `mermaid-${Math.abs(hash).toString(36)}-${Date.now()}`;\n\n const { svg } = await mermaid.render(id, code);\n setCachedSvg(code, svg);\n replaceWithSvg(preEl, svg, className);\n } catch (err) {\n if (onError) {\n onError(code, err instanceof Error ? err : new Error(String(err)));\n }\n }\n });\n\n await Promise.all(renderPromises);\n}\n\n/**\n * Attributes the wrapper inherits from the `<pre>` it replaces.\n *\n * The splice is the same shape of problem `rehypeVantageMathStamps` solves for\n * KaTeX: the pipeline stamped the fence, and swapping the element out throws\n * the stamps away. A mermaid diagram inside a toned section then drew no slice\n * of the section's vertical rule, leaving a hole as tall as the diagram; a\n * collapsed section left the diagram visible under a closed heading; and a\n * `#L` anchor pointing at the fence resolved to nothing.\n *\n * Named individually rather than copied wholesale: `class` is the caller's\n * (`className`), and `id` would be duplicated onto a second element.\n */\nconst CARRIED_ATTRIBUTES = [\n \"data-source-line\",\n \"data-vantage-tone\",\n \"data-vantage-emphasis\",\n \"data-vantage-run\",\n \"data-vantage-collapsed\",\n \"data-vantage-collapse-group\",\n];\n\nfunction replaceWithSvg(\n preEl: HTMLElement,\n svg: string,\n className: string,\n): void {\n const wrapper = document.createElement(\"div\");\n wrapper.className = className;\n for (const name of CARRIED_ATTRIBUTES) {\n const value = preEl.getAttribute(name);\n if (value !== null) wrapper.setAttribute(name, value);\n }\n wrapper.innerHTML = svg;\n preEl.replaceWith(wrapper);\n}\n\n/* ------------------------------------------------------------------ *\n * Drawing ahead of a render\n * ------------------------------------------------------------------ */\n\n/** The viewer's parser: the remark half of `buildPipeline`. */\nconst fenceParser = unified().use(remarkParse).use(buildRemarkPlugins());\n\n/**\n * The code of every Mermaid fence in `markdown`, in source order, exactly as\n * a viewer's `MermaidDiagram` receives it: the fence's content, without its\n * container's indentation or its closing newline.\n *\n * A fence is a diagram when its info string's first word begins with the word\n * `mermaid`, which is the test the viewer applies to the `language-…` class\n * the fence renders with (`/language-(\\w+)/`). Text that never says\n * `mermaid` is not parsed at all.\n */\nexport function mermaidFences(markdown: string): string[] {\n if (!markdown.includes(\"mermaid\")) return [];\n const codes: string[] = [];\n visit(fenceParser.parse(markdown), \"code\", (node: Code) => {\n if (/^\\w+/.exec(node.lang ?? \"\")?.[0] === \"mermaid\") codes.push(node.value);\n });\n return codes;\n}\n\n/** Draws under way, by palette and code, so asking twice draws once. */\nconst drawing = new Map<string, Promise<void>>();\nlet drawn = 0;\n\n/**\n * Draw one diagram into the SVG cache that `MermaidDiagram` reads when it\n * mounts, so a diagram rendered after this has settled is drawn at its full\n * size on its first paint instead of growing into place once Mermaid answers.\n * The planning page draws every diagram of the cards it is about to show this\n * way, before it commits them.\n *\n * Mermaid is loaded through `getMermaid`, so the drawing is configured, and\n * made safe, exactly as every other diagram of the page is. `theme` is the\n * palette key (`currentMermaidPalette`) to draw for; when the page has moved\n * to another palette by the time Mermaid is ready, nothing is cached, since\n * Mermaid now draws for the page's palette and not for `theme`.\n *\n * Resolves once the diagram is in the cache, at once when it already was, and\n * rejects when Mermaid cannot draw it.\n */\nexport function prerenderMermaid(\n code: string,\n theme: string = currentMermaidPalette(),\n): Promise<void> {\n if (!code.trim() || hasCachedSvg(code, theme)) return Promise.resolve();\n const key = `${theme}\\u0000${code}`;\n let pending = drawing.get(key);\n if (pending === undefined) {\n pending = (async () => {\n const mermaid = await getMermaid();\n if (currentMermaidPalette() !== theme) return;\n const { svg } = await mermaid.render(\n `mermaid-ahead-${(++drawn).toString(36)}`,\n code,\n );\n setCachedSvg(code, svg, theme);\n })().finally(() => drawing.delete(key));\n drawing.set(key, pending);\n }\n return pending;\n}\n","/**\n * Rewrite relative links in rendered markdown HTML.\n *\n * After `renderMarkdown()` produces HTML, relative `href` values need to\n * be mapped to the consumer's routing structure. This utility handles that\n * without requiring a DOM — it operates on the HTML string directly.\n */\n\nexport interface ResolveLinkOptions {\n /** Base path to prepend to relative links (default: \"/\") */\n basePath?: string;\n /**\n * Custom rewriter function. Called for every relative href.\n * Return the rewritten href, or null to leave it unchanged.\n * If provided, basePath is ignored.\n */\n rewriter?: (href: string, currentPath: string) => string | null;\n /** Current file path — used to resolve relative references like `./other.md` */\n currentPath?: string;\n}\n\n/**\n * Rewrite relative links in rendered HTML.\n *\n * Processes all `href=\"...\"` attributes, skipping:\n * - Absolute URLs (http://, https://, mailto:, etc.)\n * - Anchor-only links (#section)\n * - Already-absolute paths (/path/to/file)\n *\n * @example\n * ```ts\n * import { renderMarkdown, resolveLinks } from \"vantage-md\";\n *\n * const { html } = await renderMarkdown(content);\n *\n * // Simple: prepend a base path\n * const resolved = resolveLinks(html, { basePath: \"/docs/\", currentPath: \"guides/setup.md\" });\n *\n * // Custom: full control over link rewriting\n * const resolved = resolveLinks(html, {\n * currentPath: \"guides/setup.md\",\n * rewriter: (href, currentPath) => `/kb/${currentPath}/../${href}`,\n * });\n * ```\n */\nexport function resolveLinks(\n html: string,\n options: ResolveLinkOptions = {},\n): string {\n const { basePath = \"/\", rewriter, currentPath = \"\" } = options;\n\n // Resolve the directory of the current file\n const parts = currentPath.split(\"/\");\n parts.pop(); // remove filename\n const currentDir = parts.join(\"/\");\n\n return html.replace(\n /href=\"([^\"]*?)\"/g,\n (_match: string, href: string): string => {\n // Skip absolute URLs, anchors, and already-absolute paths\n if (\n href.startsWith(\"http://\") ||\n href.startsWith(\"https://\") ||\n href.startsWith(\"mailto:\") ||\n href.startsWith(\"data:\") ||\n href.startsWith(\"#\") ||\n href.startsWith(\"/\")\n ) {\n return `href=\"${href}\"`;\n }\n\n if (rewriter) {\n const result = rewriter(href, currentPath);\n if (result !== null) {\n return `href=\"${result}\"`;\n }\n return `href=\"${href}\"`;\n }\n\n // Default: resolve relative to currentPath, prepend basePath\n const [pathPart, hashPart] = href.split(\"#\");\n const cleanHref = pathPart.replace(/^\\.\\//, \"\");\n const resolvedPath = currentDir\n ? `${currentDir}/${cleanHref}`\n : cleanHref;\n const base = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n const finalHref = `${base}${resolvedPath}${hashPart ? `#${hashPart}` : \"\"}`;\n\n return `href=\"${finalHref}\"`;\n },\n );\n}\n","/**\n * The canonical Vantage Markdown style guide.\n *\n * This string is the single source of truth for the conventions Vantage's\n * renderer expects. It reaches readers three ways:\n *\n * - the in-app \"Style Guide for Agents\" modal, which shows it with a copy\n * button,\n * - the `vantage-check style-guide` command, which prints it so an agent can\n * fetch it without a human in the loop, and\n * - this package's own export, for anyone who renders with `vantage-md`.\n *\n * How the CLI serves it, and why there is one copy: docs/reference/agent-cli.md\n *\n * Every rule stated here should be one a checker can enforce or a renderer\n * actually cares about — if a line is neither, it does not belong.\n */\n\nexport const STYLE_GUIDE = `## Markdown style guide (for Vantage viewer)\n\nWhen writing or updating markdown documents that will be viewed in Vantage, follow these conventions:\n\n### Structure\n- Use headings (## and ###) to organize content — they become navigable outline anchors.\n- Keep paragraphs focused and concise. Break up dense text with subheadings, lists, or tables.\n\n### Links and cross-references\n- **Relative paths only**: Always link relative to the *current file's directory*:\n - Sibling in same folder: \\`[Other Doc](./other-doc.md)\\` or \\`[Other Doc](other-doc.md)\\`\n - Subdirectory: \\`[Design Doc](./design/auth.md)\\`\n - Parent / sibling folder: \\`[Overview](../overview.md)\\` or \\`[Spec](../specs/api.md)\\`\n- **Never use leading slashes**:\n - ❌ \\`[Doc](/docs/guide.md)\\` (breaks web routing and multi-repo scoping)\n - ✅ \\`[Doc](../docs/guide.md)\\` or \\`[Doc](./guide.md)\\`\n- **Never use absolute filesystem paths or URI schemes**:\n - ❌ \\`file:///workspace/docs/guide.md\\`, \\`/workspace/docs/guide.md\\`, \\`C:\\\\...\\`\n - ✅ \\`[Doc](./guide.md)\\` or \\`[Doc](../guide.md)\\`\n- **Always include the file extension**: Use \\`.md\\`, \\`.ts\\`, \\`.go\\`, etc. (e.g. \\`[Model](model.go)\\`).\n- **Line anchors and ranges**:\n - Link to specific lines: \\`[Handler](../server/api.go#L42)\\` or \\`[Range](../server/api.go#L42-L58)\\`\n - Same-file line anchor: \\`[See lines](#L10-L25)\\`\n - Vantage scrolls to and highlights the target lines.\n- **Section anchors**:\n - Same doc: \\`[Usage](#usage)\\`\n - Cross-doc: \\`[Architecture](../overview.md#system-architecture)\\`\n - Anchor slugs are lowercase, hyphenated, and punctuation-stripped.\n- **Backticks in links**: Place backticks inside the link label, not around the markdown link syntax:\n - ✅ \\`[\\`config.json\\`](./config.json)\\` or \\`[config.json](./config.json)\\`\n - ❌ \\`\\`[config.json](./config.json)\\`\\`\n\n### Frontmatter (Metadata)\n- Include structured metadata at the very top of docs delimited by \\`---\\` (YAML) or \\`+++\\` (TOML). Vantage renders this as a metadata card:\n\\`\\`\\`yaml\n---\ntitle: \"Feature Specification\"\nauthor: \"Agent\"\ndate: 2026-08-15\nstatus: in-review # draft | in-review | accepted | deprecated\ntags: [architecture, backend, api]\nsummary: \"Brief description of the document purpose.\"\nvantage:\n status-chip: true # show \\`status\\` as a chip above the metadata card\n---\n\\`\\`\\`\n- **Nothing may sit above the opening delimiter** — not a blank line, not an editorial comment, not a \\`<!-- vantage: … -->\\` directive. Frontmatter is recognized only at the very first byte of the file (in Vantage, on GitHub, and in every other reader), so one line above it turns the whole block into body text: a horizontal rule followed by a heading made of the raw keys, with every field lost. \\`vantage-check\\` reports it as \\`frontmatter/not-at-top\\`.\n- **\\`vantage:\\` is Vantage's own reserved key.** It holds chrome that belongs to the file rather than to a section, it never shows up in the metadata card, and every other renderer ignores it. One key today: \\`status-chip\\`.\n- **Prefer \\`status-chip: true\\`**, which shows the document's own \\`status:\\` and therefore cannot disagree with it. A literal \\`status-chip: accepted\\` is accepted too, but it is a second value that goes stale on its own — \\`vantage-check\\` reports the disagreement.\n- The chip's vocabulary is \\`status\\`'s, exactly: \\`draft | in-review | accepted | deprecated\\`, lowercase. \\`Draft\\` renders no chip at all, silently.\n\n### Planning documents: \\`stage\\`, \\`next\\`, \\`depends-on\\`\n- **Vantage reads a repository's plans as a set.** A document whose frontmatter has \\`status\\` or \\`stage\\`, or that carries a \\`question\\` directive (or an \\`oq\\`, the name it replaces), is a *planning document*, and Vantage shows its state (status, stage, open questions) in a badge beside every link to it. So write each fact once, in the document it belongs to, and link to it everywhere else: a copied status or count is the one that goes stale.\n- Three top-level keys beside \\`status\\` hold the rest of a document's planning state. They are facts about the file, so they sit at the top level, never under \\`vantage:\\`:\n\\`\\`\\`yaml\n---\ntitle: \"Payload bootstrap\"\nstatus: in-review\nstage: DESIGN\nnext: \"Rule OQ-B2 \\u2014 the install step waits on it\"\ndepends-on:\n - pypi-distribution.md\n - ../plans/rollout.md#OQ-R1\n---\n\\`\\`\\`\n- **\\`stage\\` is one word from the repository's own vocabulary**, the words \\`[planning.stages]\\` declares (below), spelled exactly as declared: matching is case-sensitive, so \\`Decided\\` is not \\`DECIDED\\`. \\`vantage-check\\` reports a word outside them as \\`planning/stage-vocabulary\\`.\n- **Frontmatter is the stage's one home.** Do not repeat the word in a prose \\`**Status:**\\` line: that is a second copy, and no tool can read or check it. Keep the prose line, where a document has one, for the date and the why.\n- **\\`next\\` is the next step, on one line of plain text.** A bare \\`OQ-\\` id in it links to this document's question of that id, so name the question rather than paraphrasing it.\n- **\\`depends-on\\` lists what the document waits on**: relative paths, resolved like links, each optionally ending in \\`#OQ-\\u2026\\` to name one question. A single path may stand on its own. A target that does not exist, lies outside the repository, or does not contain the id is an error (\\`planning/depends-on-missing\\`).\n- **Never a \\`priority\\` key.** A priority only means something relative to the others, so it lives in an ordered list, a roadmap, and not in each document.\n- **A roadmap is ordered links.** Every \\`roadmap.md\\` Vantage reads is a roadmap, in any directory, unless \\`[planning]\\` lists the roadmaps instead; one in a hidden directory, matched by \\`.vantageignore\\`, or ruled out by \\`include\\` or \\`exclude\\` is not read, and so is not a roadmap. Each entry is a link to a document or a question, then a one-clause reason for its place; the badge beside the link carries the rest, so never copy a status, a stage or a count into it. Prose beneath an entry holds only what has no other home, such as what would unblock it. A bare link to a document *routes* every question in it into the roadmap's order, and a link to one question's \\`#OQ-\\u2026\\` anchor routes that question; a link to any other heading routes nothing. A question is routed when any roadmap routes it, and with \\`planning/unrouted\\` turned on, \\`vantage-check\\` reports an open question no roadmap routes.\n- **\\`[planning]\\` in \\`.vantage.toml\\`** says which files are read, which of them are roadmaps, and what each stage word means. Every key is optional:\n\\`\\`\\`toml\n[planning]\n# roadmap = [\"roadmap.md\", \"docs/plans/roadmap.md\"] # absent: every roadmap.md\nexclude = [\"docs/gallery/**\"] # gitignore syntax; every .md is included by default\n\n[planning.stages] # each word maps to open | ready | built | done\nDESIGN = \"open\"\nDECIDED = \"ready\"\nBUILT = \"built\"\nSUPERSEDED = \"done\"\n\\`\\`\\`\n- **Each stage word maps to one of four roles**, which is what the word means to Vantage: \\`open\\` is still being decided, \\`ready\\` is decided and not built, \\`built\\` is built, and \\`done\\` is no longer a live proposal, so its questions leave every list of what needs a ruling. \\`vantage-check index\\` prints those lists.\n- **Never change \\`target\\`.** A \\`target = \"X.Y\"\\` line at the top of \\`.vantage.toml\\` names the oldest Vantage release the repository's readers use, and only a human edits it. A \\`vantage-check\\` older than the target refuses to run and names the release it needs: run that release, and leave the target as it is.\n\n### Mermaid diagrams\n- Use \\`\\`\\`mermaid code blocks for flowcharts, sequence diagrams, and architecture diagrams. Vantage provides interactive zoom, pan, dark/light theme adaptation, and SVG export.\n- **Quote labels with special characters**: Always quote node labels containing parentheses, brackets, or colons to prevent syntax errors:\n\\`\\`\\`mermaid\nflowchart TD\n client[\"Client (React SPA)\"] -->|WebSocket| srv[\"Vantage Server (Go)\"]\n srv --> git[\"Git CLI (git diff)\"]\n\\`\\`\\`\n\n### Inline SVG (needs Vantage 0.8 or later)\n- **Inline \\`<svg>\\` is a capability, not notation: it needs Vantage 0.8 or later.** GitHub, every other renderer and Vantage before 0.8 drop the drawing and leave the words of its \\`<text>\\` elements behind as loose text, so keep \\`<text>\\` to short labels a reader can bear to see as loose words.\n- **Name the drawing with \\`aria-label\\` on the \\`<svg>\\`, never with \\`<title>\\` or \\`<desc>\\`.** Vantage 0.8 removes both, so they name nothing there, and every other reader prints them: Vantage before 0.8 as loose words, GitHub with the tags.\n- **Pair every inline drawing with a fallback block**: a \\`<!-- vantage: fallback -->\\` directive above the block that stands in for the drawing. Vantage 0.8 and later never show that block; every other reader drops the comment and shows it. Say what the drawing shows and that it needs Vantage 0.8 or later, and link a copy of it committed as a file where there is one (\\`[the drawing](diagram.svg)\\`). The block is a paragraph, a list, a quote, a code block, a table or a \\`<div>\\`; Vantage shows a raw \\`<img>\\`, \\`<figure>\\` or \\`<details>\\` there, so wrap one in a \\`<div>\\`.\n- **Wrap the drawing in a \\`<div>\\` on a line of its own, with no blank line anywhere inside it**, and give it a \\`width\\`, a \\`height\\` and a \\`viewBox\\`. Paint with \\`fill\\`/\\`stroke\\` set to \\`currentColor\\` or a hex color, so it reads in light and dark mode.\n\\`\\`\\`markdown\n<div>\n<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"120\" height=\"40\" viewBox=\"0 0 120 40\" role=\"img\" aria-label=\"Two boxes joined by an arrow\">\n <rect x=\"1\" y=\"1\" width=\"40\" height=\"38\" fill=\"none\" stroke=\"currentColor\"/>\n <path d=\"M45 20 H75 M68 14 L75 20 L68 26\" fill=\"none\" stroke=\"currentColor\"/>\n <rect x=\"79\" y=\"1\" width=\"40\" height=\"38\" fill=\"none\" stroke=\"currentColor\"/>\n</svg>\n</div>\n\n<!-- vantage: fallback -->\n\nTwo boxes joined by an arrow. This drawing needs Vantage 0.8 or later.\n\\`\\`\\`\n- **For a document read mostly on GitHub**, commit the drawing as a file and embed it with \\`\\` instead, which every renderer shows.\n\n### Code blocks and diffs\n- Always tag fenced code blocks with language identifiers (\\`ts\\`, \\`go\\`, \\`python\\`, \\`bash\\`, \\`json\\`, \\`yaml\\`, \\`diff\\`, \\`sql\\`, etc.) for syntax highlighting.\n- For proposed code modifications, use \\`\\`\\`diff blocks with \\`+\\` and \\`-\\` prefixes:\n\\`\\`\\`diff\n-const oldUrl = \"/api/v1\";\n+const newUrl = \"/api/v2\";\n\\`\\`\\`\n\n### Callouts and alerts\n- Use GitHub-style blockquote callouts for notes, tips, and warnings:\n> [!NOTE]\n> Background context or helpful explanation.\n\n> [!TIP]\n> Best practice advice or optimization suggestions.\n\n> [!IMPORTANT]\n> Key requirements or crucial information.\n\n> [!WARNING]\n> Urgent caution, breaking changes, or potential pitfalls.\n\n> [!CAUTION]\n> High-risk actions that could cause data loss or security issues.\n\n### Vantage directives (optional, and Vantage-only)\n\nVantage reads a few styling hints from ordinary HTML comments. Every other renderer — GitHub included — drops them, so a document has to read exactly the same without them: directives decorate, they never carry meaning. One goes on a line of its own, with a blank line after it, and applies to the block that follows:\n\n\\`\\`\\`markdown\n<!-- vantage: section tone=warning badge=stale -->\n\n## Migration path\n\nThe steps below predate the rewrite.\n\\`\\`\\`\n\n- **Four names to write**: \\`section\\` (the heading and everything under it), \\`block\\` (the one block after it), \\`question\\` (one Open Question, in any state; see Open questions below), \\`fallback\\` (the one block after it, which Vantage 0.8 and later never show; see Inline SVG above). \\`fallback\\` takes no keys, and withholds only a paragraph, a list, a quote, a code block, a table, a rule or a \\`<div>\\`, never a heading. A fifth, \\`oq\\`, is the name \\`question\\` replaces (below).\n- **The keys and values are a closed set**: \\`tone\\` = \\`note | tip | important | warning | caution | muted\\`; \\`emphasis\\` = \\`strong | normal | quiet\\`; \\`badge\\` = \\`draft | stale | blocked | done | wip\\`; \\`collapsed\\` = \\`true | false\\`. Name a *tone*, never a color — the theme decides what a warning looks like, in light mode, in dark mode, and in print.\n- **Use them sparingly.** One or two per document, on the sections that genuinely differ. A document where everything is toned says nothing, and a rainbow one is harder to read than a plain one.\n- **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.\n- **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.\n- **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.\n- **A 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.\n- **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.\n\n### Open questions\nA question a document leaves for a human to rule on is an *Open Question*, written in a numbered list under a heading of its own.\n\n- **Write each question in its parts, never as one run-together paragraph.** Vantage lays a question out by these parts: the contents column and the planning page's card show its title as the question, and the card shows the leaning as the leaning and the Answer as the answer, so a part run into another one is read as neither. Each part is a block of its own, with a blank line before it:\n 1. **The title line**: the status marker, the id and the question itself in bold, saying what is being decided: \\`\\u{1F4AC} **OQ-9: Where does a job go when it re-enters the queue?**\\`.\n 2. **The context**, in short paragraphs: only what a ruling needs. Background, history and cross-references belong in the document's own sections, linked from the question.\n 3. **The options as a list**, one item each, when there are options to choose between.\n 4. **\\`_Leaning:_\\` as a paragraph of its own**: which option, and why, in a sentence or two.\n 5. **\\`**Answer:**\\` as a paragraph of its own**, with the ruling below it once there is one.\n\n\\`\\`\\`markdown\n## Open Questions\n\n1. \\u{1F4AC} **OQ-9: Where does a job go when it re-enters the queue?**\n\n A job that fails its checks leaves the queue and comes back once it is fixed. What merged while it was out may change what it does.\n\n - **A — The back of the queue.** It waits its turn again.\n - **B — Its old place.** It skips ahead of what joined since.\n\n <!-- vantage: question id=OQ-9 leaning=\"A — the back of the queue: the fix might interact with what merged while it was out.\" -->\n\n _Leaning:_ A. The fix might interact with what merged while it was out.\n\n **Answer:**\n\n > _(empty — fill in when decided)_\n\n2. \\u{1F512} **OQ-10: How large is the retry budget?**\n\n <!-- vantage: question id=OQ-10 -->\n\n Waits on the load test, so there is no leaning to state yet.\n\\`\\`\\`\n\n- **The marker is the question's state**: \\u{1F4AC} open (\\u{1F4AC} \\u{1F937} when it is a matter of preference), \\u{1F512} blocked on something upstream that has to happen first, \\u2705 answered. Review mode offers a one-click **Take this leaning** on an open question, and on nothing else.\n- **Every question gets a \\`question\\` directive**, \\`<!-- vantage: question id=OQ-9 leaning=\"…\" -->\\`, in every state, indented into its list item on a line of its own directly above the \\`_Leaning:_\\` paragraph, or above another of its paragraphs when it states no leaning. The directive is what makes it a question to Vantage: it gives the question its \\`#OQ-9\\` anchor, and Vantage's planning index (its model of a repository's plans) reads a question from nothing else, so without one nothing counts it, badges it or lists it as waiting. It goes when the question is compacted into the Decision Ledger, and not before.\n- **Changing a question's state changes its marker and nothing else.** Mark it \\u{1F512} when it cannot be answered yet, and \\u2705 once it is decided; the directive stays as it is, its \\`leaning\\` included, which nothing offers to take once the question is not open.\n- **Every open question with a stated leaning restates it as the directive's \\`leaning\\`.** The convention's prose — the emoji, the \\`OQ-N\\` id, the \\`_Leaning:_\\` line, the fill-in \\`**Answer:**\\` — produces no button on its own, and writing it and stopping there is the most common way the button goes missing: the questions look complete, review mode is on, and there is nothing to click. \\`vantage-check\\` reports an open question with a \\`_Leaning:_\\` and no directive as an error (\\`vantage/oq-missing\\`). An open question with no leaning yet takes the directive without \\`leaning\\`, so the planning index still counts it; until it states one, its button files the literal text \"Take the stated leaning.\"\n- **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.\n- **Keep the text below the title short.** The planning page shows each question as a card that leads with the title and shows only the first few lines of the rest, so a question buried in a paragraph of background is one the reviewer has to dig for. \\`vantage-check\\` warns when a question's text, not counting its \\`_Leaning:_\\` paragraph and its \\`**Answer:**\\`, runs past 120 words (\\`planning/question-length\\`), and when a leaning shares a paragraph with other text (\\`vantage/question-layout\\`); a repository can raise the limit with \\`max-words\\`, or turn either rule off, under \\`[check.rules]\\`.\n- **Enumerate with a list, never inline.** A question's options, or any three steps or parts, go in a Markdown list, one item per line, rather than run together in one sentence as \\`(a) \\u2026 (b) \\u2026 (c)\\`, where a reader has to find where each one starts. \\`vantage-check\\` warns on a paragraph that runs three or more together (\\`prose/inline-list\\`).\n- **Write a question as a list item.** Outside a list, a question runs from the block its directive lands on through the blocks after it, up to the next heading (for a question written as a heading, the next one of its level or higher), the next rule or the next question, so whatever prose follows it before then is read as part of it. There the directive goes above the question's title, since every viewer reads the question's marker from the block it lands on: a directive below the bold title lands on the block after it, which carries no marker, so every viewer reads the question as open. \\`vantage-check\\` reports that as \\`vantage/question-name\\`.\n- **\\`oq\\` is the name \\`question\\` replaces.** Vantage keeps reading \\`<!-- vantage: oq id=OQ-9 leaning=\"…\" -->\\` as it always has, and \\`vantage-check\\` warns on it (\\`vantage/oq-deprecated\\`) with the \\`question\\` to write instead, keys unchanged. Vantage before 0.8 drops \\`question\\` whole, so a reader still on 0.7 gets no one-click answer and no anchor on a question written with it, and misreads nothing. So keep an \\`oq\\` on an open question unless you know every reader of the repository is on 0.8 or later. A repository whose readers are still on 0.7 says so with a \\`target\\` before 0.8, such as \\`target = \"0.7\"\\`: there an open question takes \\`oq\\`, new ones included, and \\`vantage-check\\` asks for \\`oq\\` rather than \\`question\\` on one. **Never put \\`oq\\` on a \\u{1F512} or \\u2705 question**: every Vantage before 0.8 offers the one-click button on every \\`oq\\`, whatever its marker says, and on a question with no leaning it files the literal text \"Take the stated leaning.\" \\`vantage-check\\` reports that as an error (\\`vantage/question-name\\`).\n\n### Tables, task lists, and math\n- **Tables**: Use standard markdown tables for structured comparisons and schemas.\n- **Task lists**: Use \\`- [ ]\\` and \\`- [x]\\` for actionable checklists and status tracking.\n- **LaTeX Math**: Use \\`$$...$$\\` for *all* KaTeX math — display blocks (\\`$$\\` alone on its own lines) and inline alike (\\`$$E = mc^2$$\\` mid-sentence).\n - Single dollars are **not** math delimiters: \\`$HOME\\` and \\`$100\\` stay literal, so prose and shell snippets are safe to write as-is.\n`;\n"],"mappings":"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"}
|