vantage-md 0.5.7 → 0.5.8

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["visit","defaultSchema","remarkGfm","remarkMath","rehypeRaw","rehypeSanitize","rehypeSlug","rehypeHighlight","rehypeKatex","parseTOML","YAML","remarkParse","remarkRehype","rehypeStringify"],"sources":["../src/rehypeSourceLines.ts","../src/rehypeVantageAlerts.ts","../src/vantageDirectives.ts","../src/rehypeVantageDirectives.ts","../src/rehypeVantageMathStamps.ts","../src/sanitize.ts","../src/pipeline.ts","../src/frontmatter.ts","../src/renderMarkdown.ts","../src/lineAnchor.ts","../src/scrollToLineAnchor.ts","../src/vantageFrontmatter.ts","../src/mermaidCache.ts","../src/mermaidLoader.ts","../src/renderMermaidBlocks.ts","../src/resolveLinks.ts","../src/styleGuide.ts"],"sourcesContent":["/**\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\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 \"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 * italicises 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-colour 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 unrecognised\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/** 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 */\nconst MARKER = /^\\[!(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 = 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: [\"vantage-alert-title\"] },\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 * `oq` marks one answerable question. The name cannot disagree with position —\n * it only says how far the stamp reaches — so §4.2's refusal of a `scope=` key\n * stands.\n */\nexport const DIRECTIVE_NAMES = [\"section\", \"block\", \"oq\"] as const;\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 stamp.\n *\n * Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a stamped block should also\n * be a block with a `data-source-line`, so the styling surface and the anchor\n * surface coincide. It also keeps an inline directive from stamping the `<em>`\n * that happens to follow it inside a paragraph.\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 an `oq` directive may stamp — strictly the tags the review system\n * can resolve an anchor on (`ANCHOR_TAGS` in the app's `MarkdownViewer`, and the\n * block map in `useReviewHighlights`). `ul`, `ol`, `tr`, `hr` and `div` are in\n * neither, so a button on one of them would build an anchor no review pass can\n * find — the \"mis-wired button\" D6 forbids.\n *\n * The gap between this list and `VANTAGE_STYLE_TARGETS` is why an `oq`\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 `<!-- vantage: oq … -->` directive actually yields a *button* on —\n * `VANTAGE_ANCHOR_TARGETS` minus `pre` and `table`, written as an explicit\n * subtraction so the narrowing stays visible.\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 * Both consumers read it from here: `OQ_HOST_TAGS` in the app's\n * `useOpenQuestionButtons`, and the `oq` branch of the checker's\n * `vantage/orphan`. They were two hand-written lists that disagreed — the\n * checker called an `oq` above a fence fine while the app rendered no button\n * and said nothing, which is the D5 break this module exists to prevent.\n */\nexport const VANTAGE_OQ_HOST_TARGETS = VANTAGE_ANCHOR_TARGETS.filter(\n (tag) => tag !== \"pre\" && tag !== \"table\",\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. `oq`'s two keys are the design's only values with no closed set\n * — `id` is a token an author chose and `leaning` is a sentence (§8.3) — so\n * neither can be value-allowlisted, which is recorded here as `null` rather\n * than left to a caller to guess.\n */\nexport const DIRECTIVE_VOCABULARY: DirectiveVocabulary = {\n section: STYLE_KEYS,\n block: STYLE_KEYS,\n oq: { id: null, leaning: null },\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 * 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 sanitiser removes it, which is why no Vantage-specific\n * markup other than these attributes ever reaches the DOM.\n */\n\nimport type { Element, Parents, Properties, RootContent, Root } from \"hast\";\nimport type { Plugin } from \"unified\";\nimport {\n DIRECTIVE_VOCABULARY,\n parseVantageDirective,\n VANTAGE_ANCHOR_TARGETS,\n VANTAGE_STYLE_TARGETS,\n} from \"./vantageDirectives.js\";\nimport type { KeyVocabulary, ParsedDirective } from \"./vantageDirectives.js\";\n\n/**\n * What a `section`/`block` and an `oq` directive may stamp.\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 */\nconst STYLE_TARGET_TAGS = new Set<string>(VANTAGE_STYLE_TARGETS);\nconst ANCHOR_TARGET_TAGS = new Set<string>(VANTAGE_ANCHOR_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 serialises 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\";\nconst OQ_PROPERTY = \"dataVantageOq\";\nconst LEANING_PROPERTY = \"dataVantageLeaning\";\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/** A review-comment body, not prose. Bounds the attribute; 500 is generous. */\nconst MAX_LEANING = 500;\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 * sanitiser, so letting it change a directive's meaning would make behaviour\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 */\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\" && STYLE_TARGET_TAGS.has(node.tagName)) {\n range.push(i);\n }\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 stampOq(target: Element, pairs: Map<string, string>) {\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, OQ_PROPERTY, \"true\");\n\n // `id` resolves and is deliberately not stamped: nothing in the DOM reads it\n // — the button finds its block by `[data-vantage-oq]` and its text by\n // `data-vantage-leaning` — and an attribute nobody reads is a sanitiser entry\n // bought for nothing. It stays in the source for the checker and for `rg`.\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.\n const text = leaning.replace(/\\s+/g, \" \").trim().slice(0, MAX_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 const oq = 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 let hasOq = false;\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 } else if (directive.name === \"oq\") {\n hasOq = true;\n for (const pair of directive.pairs) oq.set(pair.key, pair.value);\n }\n // Any other name drops the whole directive: there is no target semantics\n // without a name.\n }\n\n if (styleName !== undefined) {\n stampStyle(children, targetIndex, styleName, style, state);\n }\n if (hasOq && ANCHOR_TARGET_TAGS.has(target.tagName)) {\n stampOq(target, oq);\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 `oq` 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\nconst rehypeVantageDirectives: Plugin<[], Root> = () => {\n return (tree: Root) => {\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 sanitiser\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 sanitiser 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, syntax highlighting classes, and\n * data-source-line attributes while blocking XSS vectors.\n */\n\nimport { defaultSchema } from \"rehype-sanitize\";\nimport {\n VANTAGE_BADGES,\n VANTAGE_COLLAPSED,\n VANTAGE_EMPHASIS,\n VANTAGE_RUNS,\n VANTAGE_TONES,\n} from \"./vantageDirectives.js\";\nimport { VANTAGE_ALERTS } from \"./rehypeVantageAlerts.js\";\n\ntype Schema = typeof defaultSchema;\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 sanitiser 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` are inert now that `position` is\n // banned, and they stay only because dropping them would fail the whole\n // attribute for a document that writes one — the all-or-nothing rule below\n // makes every removal a behaviour change. They buy an attacker nothing that\n // negative `margin` does not already buy.\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.\n \"display\",\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 sanitiser 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 neighbours\" 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 unrecognised 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 neighbours *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.\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.\nconst DECLARATION = `(?:(?:${SAFE_STYLE_PROPERTIES.join(\"|\")})\\\\s*:${VALUE})`;\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 * 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 sanitiser. 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 tagNames: [\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 ],\n attributes: {\n ...defaultSchema.attributes,\n \"*\": [\n ...(defaultSchema.attributes?.[\"*\"] || []),\n \"className\",\n [\"style\", SAFE_STYLE],\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 sanitiser 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 [\"dataVantageOq\", \"true\"],\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 defences remain rather than three — `hast` escapes the value on\n // serialisation 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 code: [...(defaultSchema.attributes?.code || []), \"className\"],\n span: [\n ...(defaultSchema.attributes?.span || []),\n \"className\",\n [\"style\", SAFE_STYLE],\n ],\n div: [\n ...(defaultSchema.attributes?.div || []),\n \"className\",\n [\"style\", SAFE_STYLE],\n ],\n a: [...(defaultSchema.attributes?.a || []), \"id\", \"className\"],\n math: [\"xmlns\"],\n annotation: [\"encoding\"],\n img: [...(defaultSchema.attributes?.img || []), \"loading\"],\n td: [...(defaultSchema.attributes?.td || []), [\"style\", SAFE_STYLE]],\n th: [...(defaultSchema.attributes?.th || []), [\"style\", SAFE_STYLE]],\n },\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 sanitiser keeps.\n * - `rehypeSlug`, `rehypeHighlight` and `rehypeKatex` after `rehypeSanitize`.\n * For `rehypeSlug` this is not a preference: the sanitiser's default schema\n * clobbers `id` with the prefix `user-content-`, so slugging before it turns\n * every `#heading` link in every document into a dead anchor. For the other\n * two it means their output is trusted rather than filtered — KaTeX emits\n * 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 remarkGfm from \"remark-gfm\";\nimport remarkMath from \"remark-math\";\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 rehypeSourceLines from \"./rehypeSourceLines.js\";\nimport { rehypeVantageAlerts } from \"./rehypeVantageAlerts.js\";\nimport rehypeVantageDirectives from \"./rehypeVantageDirectives.js\";\nimport {\n rehypeCaptureMathStamps,\n rehypeRestoreMathStamps,\n} from \"./rehypeVantageMathStamps.js\";\nimport { sanitizeSchema } from \"./sanitize.js\";\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 sanitisation (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/**\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 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 sanitiser 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 if (sanitize) plugins.push([rehypeSanitize, sanitizeSchema]);\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 * behaviour 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 { 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\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 } = 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 const result = await processor.process(parsed.body);\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 honours 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 colours 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 cancelled (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 sanitiser. `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","// Global cache for rendered SVGs to prevent re-renders\nexport const svgCache = new Map<string, string>();\n\nexport function clearMermaidCache() {\n svgCache.clear();\n}\n","import type mermaidAPI from \"mermaid\";\n\nlet mermaidInstance: typeof mermaidAPI | null = null;\nlet mermaidLoading: Promise<typeof mermaidAPI> | null = null;\n\nconst isDark = () =>\n typeof document !== \"undefined\" &&\n document.documentElement.classList.contains(\"dark\");\n\nexport async function getMermaid(): Promise<typeof mermaidAPI> {\n if (mermaidInstance) return mermaidInstance;\n if (!mermaidLoading) {\n mermaidLoading = import(\"mermaid\").then((mod) => {\n const m = mod.default;\n m.initialize({\n startOnLoad: false,\n theme: isDark() ? \"dark\" : \"default\",\n securityLevel: \"strict\",\n suppressErrorRendering: true,\n });\n mermaidInstance = m;\n return m;\n });\n }\n return mermaidLoading;\n}\n\nexport function resetMermaidLoader() {\n mermaidInstance = null;\n mermaidLoading = 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 * Framework-agnostic — works in any browser environment.\n */\n\nimport { svgCache } from \"./mermaidCache.js\";\nimport { getMermaid } from \"./mermaidLoader.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 const mermaid = await 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\n const cached = svgCache.get(code);\n if (cached) {\n replaceWithSvg(preEl, cached, className);\n return;\n }\n\n try {\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 svgCache.set(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\nfunction replaceWithSvg(\n preEl: HTMLElement,\n svg: string,\n className: string,\n): void {\n const wrapper = document.createElement(\"div\");\n wrapper.className = className;\n wrapper.innerHTML = svg;\n preEl.replaceWith(wrapper);\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. Two consumers read it:\n *\n * - the in-app \"Style Guide for Agents\" modal, which shows it with a copy\n * button, and\n * - the `vantage-check style-guide` command, which prints it so an agent can\n * fetch it without a human in the loop.\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 recognised 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### 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### 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- **Three names**: \\`section\\` (the heading and everything under it), \\`block\\` (the one block after it), \\`oq\\` (one answerable Open Question).\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 colour — 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- **Every open question (\\u{1F4AC}) with a stated leaning gets an \\`oq\\` directive.** The convention's prose — the emoji, the \\`OQ-N\\` id, the \\`_Leaning:_\\` line, the fill-in \\`**Answer:**\\` — produces no button on its own. Writing the convention and stopping there is the most common way this feature goes missing: the questions look complete, review mode is on, and there is nothing to click. **\\`vantage-check\\` reports it as an error** (\\`vantage/oq-missing\\`), because a question awaiting a ruling that the reviewer cannot file is not a style preference. Mark it \\u{1F512} if it is blocked on something upstream and cannot be answered yet, or \\u2705 once it is decided; either state needs no directive.\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\n\\`\\`\\`markdown\n1. **OQ-9: Queue position on re-entry.**\n\n <!-- vantage: oq id=OQ-9 leaning=\"Back of the queue — the fix might interact with what merged while it was out.\" -->\n\n _Leaning:_ Back of the queue.\n\\`\\`\\`\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":["visit","defaultSchema","remarkGfm","remarkMath","rehypeRaw","rehypeSanitize","rehypeSlug","rehypeHighlight","rehypeKatex","parseTOML","YAML","remarkParse","remarkRehype","rehypeStringify"],"sources":["../src/rehypeSourceLines.ts","../src/rehypeVantageAlerts.ts","../src/vantageDirectives.ts","../src/rehypeVantageDirectives.ts","../src/rehypeVantageMathStamps.ts","../src/sanitize.ts","../src/pipeline.ts","../src/frontmatter.ts","../src/renderMarkdown.ts","../src/lineAnchor.ts","../src/scrollToLineAnchor.ts","../src/vantageFrontmatter.ts","../src/mermaidCache.ts","../src/mermaidLoader.ts","../src/renderMermaidBlocks.ts","../src/resolveLinks.ts","../src/styleGuide.ts"],"sourcesContent":["/**\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 * italicises 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-colour 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 unrecognised\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/** 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 */\nconst MARKER = /^\\[!(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 = 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: [\"vantage-alert-title\"] },\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 * `oq` marks one answerable question. The name cannot disagree with position —\n * it only says how far the stamp reaches — so §4.2's refusal of a `scope=` key\n * stands.\n */\nexport const DIRECTIVE_NAMES = [\"section\", \"block\", \"oq\"] as const;\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 stamp.\n *\n * Deliberately `rehypeSourceLines`'s `BLOCK_TAGS`: a stamped block should also\n * be a block with a `data-source-line`, so the styling surface and the anchor\n * surface coincide. It also keeps an inline directive from stamping the `<em>`\n * that happens to follow it inside a paragraph.\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 an `oq` directive may stamp — strictly the tags the review system\n * can resolve an anchor on (`ANCHOR_TAGS` in the app's `MarkdownViewer`, and the\n * block map in `useReviewHighlights`). `ul`, `ol`, `tr`, `hr` and `div` are in\n * neither, so a button on one of them would build an anchor no review pass can\n * find — the \"mis-wired button\" D6 forbids.\n *\n * The gap between this list and `VANTAGE_STYLE_TARGETS` is why an `oq`\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 `<!-- vantage: oq … -->` directive actually yields a *button* on —\n * `VANTAGE_ANCHOR_TARGETS` minus `pre` and `table`, written as an explicit\n * subtraction so the narrowing stays visible.\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 * Both consumers read it from here: `OQ_HOST_TAGS` in the app's\n * `useOpenQuestionButtons`, and the `oq` branch of the checker's\n * `vantage/orphan`. They were two hand-written lists that disagreed — the\n * checker called an `oq` above a fence fine while the app rendered no button\n * and said nothing, which is the D5 break this module exists to prevent.\n */\nexport const VANTAGE_OQ_HOST_TARGETS = VANTAGE_ANCHOR_TARGETS.filter(\n (tag) => tag !== \"pre\" && tag !== \"table\",\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. `oq`'s two keys are the design's only values with no closed set\n * — `id` is a token an author chose and `leaning` is a sentence (§8.3) — so\n * neither can be value-allowlisted, which is recorded here as `null` rather\n * than left to a caller to guess.\n */\nexport const DIRECTIVE_VOCABULARY: DirectiveVocabulary = {\n section: STYLE_KEYS,\n block: STYLE_KEYS,\n oq: { id: null, leaning: null },\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 * 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 sanitiser removes it, which is why no Vantage-specific\n * markup other than these attributes ever reaches the DOM.\n */\n\nimport type { Element, Parents, Properties, RootContent, Root } from \"hast\";\nimport type { Plugin } from \"unified\";\nimport {\n DIRECTIVE_VOCABULARY,\n parseVantageDirective,\n VANTAGE_ANCHOR_TARGETS,\n VANTAGE_STYLE_TARGETS,\n} from \"./vantageDirectives.js\";\nimport type { KeyVocabulary, ParsedDirective } from \"./vantageDirectives.js\";\n\n/**\n * What a `section`/`block` and an `oq` directive may stamp.\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 */\nconst STYLE_TARGET_TAGS = new Set<string>(VANTAGE_STYLE_TARGETS);\nconst ANCHOR_TARGET_TAGS = new Set<string>(VANTAGE_ANCHOR_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 serialises 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\";\nconst OQ_PROPERTY = \"dataVantageOq\";\nconst LEANING_PROPERTY = \"dataVantageLeaning\";\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/** A review-comment body, not prose. Bounds the attribute; 500 is generous. */\nconst MAX_LEANING = 500;\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 * sanitiser, so letting it change a directive's meaning would make behaviour\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 */\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\" && STYLE_TARGET_TAGS.has(node.tagName)) {\n range.push(i);\n }\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 stampOq(target: Element, pairs: Map<string, string>) {\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, OQ_PROPERTY, \"true\");\n\n // `id` resolves and is deliberately not stamped: nothing in the DOM reads it\n // — the button finds its block by `[data-vantage-oq]` and its text by\n // `data-vantage-leaning` — and an attribute nobody reads is a sanitiser entry\n // bought for nothing. It stays in the source for the checker and for `rg`.\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.\n const text = leaning.replace(/\\s+/g, \" \").trim().slice(0, MAX_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 const oq = 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 let hasOq = false;\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 } else if (directive.name === \"oq\") {\n hasOq = true;\n for (const pair of directive.pairs) oq.set(pair.key, pair.value);\n }\n // Any other name drops the whole directive: there is no target semantics\n // without a name.\n }\n\n if (styleName !== undefined) {\n stampStyle(children, targetIndex, styleName, style, state);\n }\n if (hasOq && ANCHOR_TARGET_TAGS.has(target.tagName)) {\n stampOq(target, oq);\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 `oq` 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\nconst rehypeVantageDirectives: Plugin<[], Root> = () => {\n return (tree: Root) => {\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 sanitiser\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 sanitiser 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, syntax highlighting classes, and\n * data-source-line attributes while blocking XSS vectors.\n */\n\nimport { defaultSchema } from \"rehype-sanitize\";\nimport {\n VANTAGE_BADGES,\n VANTAGE_COLLAPSED,\n VANTAGE_EMPHASIS,\n VANTAGE_RUNS,\n VANTAGE_TONES,\n} from \"./vantageDirectives.js\";\nimport { VANTAGE_ALERTS } from \"./rehypeVantageAlerts.js\";\n\ntype Schema = typeof defaultSchema;\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 sanitiser 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` are inert now that `position` is\n // banned, and they stay only because dropping them would fail the whole\n // attribute for a document that writes one — the all-or-nothing rule below\n // makes every removal a behaviour change. They buy an attacker nothing that\n // negative `margin` does not already buy.\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.\n \"display\",\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 sanitiser 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 neighbours\" 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 unrecognised 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 neighbours *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.\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.\nconst DECLARATION = `(?:(?:${SAFE_STYLE_PROPERTIES.join(\"|\")})\\\\s*:${VALUE})`;\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 * 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 sanitiser. 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 tagNames: [\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 ],\n attributes: {\n ...defaultSchema.attributes,\n \"*\": [\n ...(defaultSchema.attributes?.[\"*\"] || []),\n \"className\",\n [\"style\", SAFE_STYLE],\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 sanitiser 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 [\"dataVantageOq\", \"true\"],\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 defences remain rather than three — `hast` escapes the value on\n // serialisation 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 code: [...(defaultSchema.attributes?.code || []), \"className\"],\n span: [\n ...(defaultSchema.attributes?.span || []),\n \"className\",\n [\"style\", SAFE_STYLE],\n ],\n div: [\n ...(defaultSchema.attributes?.div || []),\n \"className\",\n [\"style\", SAFE_STYLE],\n ],\n a: [...(defaultSchema.attributes?.a || []), \"id\", \"className\"],\n math: [\"xmlns\"],\n annotation: [\"encoding\"],\n img: [...(defaultSchema.attributes?.img || []), \"loading\"],\n td: [...(defaultSchema.attributes?.td || []), [\"style\", SAFE_STYLE]],\n th: [...(defaultSchema.attributes?.th || []), [\"style\", SAFE_STYLE]],\n },\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 sanitiser keeps.\n * - `rehypeSlug`, `rehypeHighlight` and `rehypeKatex` after `rehypeSanitize`.\n * For `rehypeSlug` this is not a preference: the sanitiser's default schema\n * clobbers `id` with the prefix `user-content-`, so slugging before it turns\n * every `#heading` link in every document into a dead anchor. For the other\n * two it means their output is trusted rather than filtered — KaTeX emits\n * 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 remarkGfm from \"remark-gfm\";\nimport remarkMath from \"remark-math\";\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 rehypeSourceLines from \"./rehypeSourceLines.js\";\nimport { rehypeVantageAlerts } from \"./rehypeVantageAlerts.js\";\nimport rehypeVantageDirectives from \"./rehypeVantageDirectives.js\";\nimport {\n rehypeCaptureMathStamps,\n rehypeRestoreMathStamps,\n} from \"./rehypeVantageMathStamps.js\";\nimport { sanitizeSchema } from \"./sanitize.js\";\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 sanitisation (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/**\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 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 sanitiser 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 if (sanitize) plugins.push([rehypeSanitize, sanitizeSchema]);\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 * behaviour 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 { 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\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 } = 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 const result = await processor.process(parsed.body);\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 honours 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 colours 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 cancelled (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 sanitiser. `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","// Global cache for rendered SVGs to prevent re-renders\nexport const svgCache = new Map<string, string>();\n\nexport function clearMermaidCache() {\n svgCache.clear();\n}\n","import type mermaidAPI from \"mermaid\";\n\nlet mermaidInstance: typeof mermaidAPI | null = null;\nlet mermaidLoading: Promise<typeof mermaidAPI> | null = null;\n\nconst isDark = () =>\n typeof document !== \"undefined\" &&\n document.documentElement.classList.contains(\"dark\");\n\nexport async function getMermaid(): Promise<typeof mermaidAPI> {\n if (mermaidInstance) return mermaidInstance;\n if (!mermaidLoading) {\n mermaidLoading = import(\"mermaid\").then((mod) => {\n const m = mod.default;\n m.initialize({\n startOnLoad: false,\n theme: isDark() ? \"dark\" : \"default\",\n securityLevel: \"strict\",\n suppressErrorRendering: true,\n });\n mermaidInstance = m;\n return m;\n });\n }\n return mermaidLoading;\n}\n\nexport function resetMermaidLoader() {\n mermaidInstance = null;\n mermaidLoading = 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 * Framework-agnostic — works in any browser environment.\n */\n\nimport { svgCache } from \"./mermaidCache.js\";\nimport { getMermaid } from \"./mermaidLoader.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 const mermaid = await 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\n const cached = svgCache.get(code);\n if (cached) {\n replaceWithSvg(preEl, cached, className);\n return;\n }\n\n try {\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 svgCache.set(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\nfunction replaceWithSvg(\n preEl: HTMLElement,\n svg: string,\n className: string,\n): void {\n const wrapper = document.createElement(\"div\");\n wrapper.className = className;\n wrapper.innerHTML = svg;\n preEl.replaceWith(wrapper);\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. Two consumers read it:\n *\n * - the in-app \"Style Guide for Agents\" modal, which shows it with a copy\n * button, and\n * - the `vantage-check style-guide` command, which prints it so an agent can\n * fetch it without a human in the loop.\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 recognised 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### 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### 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- **Three names**: \\`section\\` (the heading and everything under it), \\`block\\` (the one block after it), \\`oq\\` (one answerable Open Question).\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 colour — 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- **Every open question (\\u{1F4AC}) with a stated leaning gets an \\`oq\\` directive.** The convention's prose — the emoji, the \\`OQ-N\\` id, the \\`_Leaning:_\\` line, the fill-in \\`**Answer:**\\` — produces no button on its own. Writing the convention and stopping there is the most common way this feature goes missing: the questions look complete, review mode is on, and there is nothing to click. **\\`vantage-check\\` reports it as an error** (\\`vantage/oq-missing\\`), because a question awaiting a ruling that the reviewer cannot file is not a style preference. Mark it \\u{1F512} if it is blocked on something upstream and cannot be answered yet, or \\u2705 once it is decided; either state needs no directive.\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\n\\`\\`\\`markdown\n1. **OQ-9: Queue position on re-entry.**\n\n <!-- vantage: oq id=OQ-9 leaning=\"Back of the queue — the fix might interact with what merged while it was out.\" -->\n\n _Leaning:_ Back of the queue.\n\\`\\`\\`\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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"}