vantage-md 0.1.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { RefObject } from 'react';
2
2
  import { Root } from 'hast';
3
- import { Plugin } from 'unified';
3
+ import { Plugin, PluggableList } from 'unified';
4
4
  import { defaultSchema } from 'rehype-sanitize';
5
5
 
6
6
  /**
@@ -56,6 +56,107 @@ interface FrontmatterDisplayProps {
56
56
  }
57
57
  declare const FrontmatterDisplay: React.NamedExoticComponent<FrontmatterDisplayProps>;
58
58
 
59
+ /**
60
+ * The `tone` vocabulary: GitHub's alert words plus `muted`.
61
+ *
62
+ * Semantic, never chromatic (P2, Ledger OQ-3). A document says what a section
63
+ * *is*; the theme decides what that looks like, which is what lets one document
64
+ * render correctly in light, in dark, and in themes that do not exist yet.
65
+ */
66
+ declare const VANTAGE_TONES: readonly ["note", "tip", "important", "warning", "caution", "muted"];
67
+
68
+ /**
69
+ * The `vantage:` frontmatter key — file-scoped chrome (`docs/reference/inline-markup.md`, "File-scoped chrome").
70
+ *
71
+ * One reserved key at the top level of a document's frontmatter, holding the
72
+ * chrome that belongs to the *file* rather than to a section. Today that is one
73
+ * thing: whether the document's lifecycle `status:` is shown as a chip above the
74
+ * metadata card, instead of being buried as one row inside it.
75
+ *
76
+ * Read only at the top level, and **inert on every failure** (P3): an unknown
77
+ * key, a value outside the closed set, or a `vantage:` that is not a table
78
+ * produces no chrome, no throw and no console output. The reasons are returned
79
+ * as data in `issues`, for anything that wants to report them — `vantage-check`
80
+ * does, and it is the only signal an author gets. That split is exactly the one
81
+ * `FrontmatterProblem` already uses in `frontmatter.ts`: the viewer reads the
82
+ * value, the checker reads the reasons.
83
+ *
84
+ * Like `vantageDirectives.ts`, this module is imported by the CLI checker **by
85
+ * relative path**, so it must stay a pure function of already-parsed data: no
86
+ * hast, no React, no filesystem.
87
+ */
88
+
89
+ /**
90
+ * The document lifecycle vocabulary. Closed; extending it is a code change.
91
+ *
92
+ * This is the repo's own existing set, not a new one — `styleGuide.ts` tells
93
+ * every agent to write `status: in-review # draft | in-review | accepted |
94
+ * deprecated`, and every document under `docs/` follows it. It is deliberately
95
+ * *not* the `badge` set (`draft stale blocked done wip`): `badge` is
96
+ * section-scoped workflow state, `status` is document lifecycle state, and
97
+ * `in-review` — the value the design doc's own only example renders — is not a
98
+ * badge word at all. Only `draft` is a member of both, and a token set is per key.
99
+ */
100
+ declare const DOC_STATUSES: readonly ["draft", "in-review", "accepted", "deprecated"];
101
+ type DocStatus = (typeof DOC_STATUSES)[number];
102
+ /** Every key this build knows under `vantage:`. Closed. */
103
+ declare const VANTAGE_FRONTMATTER_KEYS: readonly ["status-chip"];
104
+ /**
105
+ * Which tone each status borrows its colours from.
106
+ *
107
+ * The chip has no palette of its own: it reuses the tone chips
108
+ * (`.vantage-chip--<tone>` in `styles/directives.css`), which is also what makes
109
+ * a `draft` chip and a `badge=draft` chip the same visual object. A map rather
110
+ * than a computed class name, so the whole status→tone relation is one readable
111
+ * table and a test can assert it covers the vocabulary.
112
+ */
113
+ declare const DOC_STATUS_TONES: Readonly<Record<DocStatus, (typeof VANTAGE_TONES)[number]>>;
114
+ /**
115
+ * Why something under `vantage:` produced no chrome.
116
+ *
117
+ * `status-chip-orphan` and `status-chip-disagrees` are not vocabulary errors —
118
+ * both values are legal — but both are the markup rot R3 is about: a chip that
119
+ * says something the document's own `status:` does not.
120
+ */
121
+ type VantageFrontmatterIssue = {
122
+ kind: "not-a-table";
123
+ value: unknown;
124
+ } | {
125
+ kind: "unknown-key";
126
+ key: string;
127
+ } | {
128
+ kind: "bad-value";
129
+ key: string;
130
+ value: unknown;
131
+ legal: readonly string[];
132
+ } | {
133
+ kind: "status-chip-orphan";
134
+ status: unknown;
135
+ } | {
136
+ kind: "status-chip-disagrees";
137
+ chip: DocStatus;
138
+ status: unknown;
139
+ };
140
+ interface VantageFrontmatter {
141
+ /** The chip's text, or `undefined` for no chip. */
142
+ statusChip?: DocStatus;
143
+ /** Why something was dropped. A viewer must never read this (P3). */
144
+ issues: VantageFrontmatterIssue[];
145
+ }
146
+ /** Narrowing helper the chip and the checker both use. */
147
+ declare function isDocStatus(value: unknown): value is DocStatus;
148
+ /**
149
+ * Read the `vantage:` key out of parsed frontmatter.
150
+ *
151
+ * Pure: no module state, no mutation of the input, no logging. The same object
152
+ * in twice gives equal results out.
153
+ */
154
+ declare function readVantageFrontmatter(frontmatter: Record<string, unknown>): VantageFrontmatter;
155
+
156
+ declare const DocumentStatusChip: React.NamedExoticComponent<{
157
+ status: DocStatus;
158
+ }>;
159
+
59
160
  /**
60
161
  * Framework-agnostic markdown -> HTML rendering pipeline.
61
162
  * Uses the same remark/rehype chain as the Vantage viewer.
@@ -87,7 +188,7 @@ interface RenderResult {
87
188
  *
88
189
  * Features (all enabled by default):
89
190
  * - GitHub Flavored Markdown (tables, strikethrough, task lists)
90
- * - KaTeX math rendering ($$...$$ blocks)
191
+ * - KaTeX math rendering, inline and block ($$...$$ only; single $ is not a delimiter)
91
192
  * - Syntax highlighting via highlight.js
92
193
  * - `data-source-line` attributes for line anchors
93
194
  * - XSS sanitization
@@ -108,22 +209,98 @@ declare function renderMarkdown(content: string, options?: RenderOptions): Promi
108
209
  * each rendered block a traceable line number from the source.
109
210
  */
110
211
 
111
- declare const rehypeSourceLines: Plugin<[], Root>;
212
+ interface RehypeSourceLinesOptions {
213
+ /**
214
+ * Lines stripped off the front of the file before parsing — frontmatter,
215
+ * essentially. Added to every emitted line number so `data-source-line`
216
+ * names a line in the *file* rather than in the parsed body, which is what
217
+ * a `#L42` link written against the file means. Defaults to 0.
218
+ */
219
+ offset?: number;
220
+ }
221
+ declare const rehypeSourceLines: Plugin<[RehypeSourceLinesOptions?], Root>;
112
222
 
113
223
  /**
114
- * Framework-agnostic line anchor utilities.
115
- * Parse GitHub-style line anchors (#L42, #L42-L50) and scroll/highlight
116
- * matching elements in a container.
224
+ * The one definition of the Vantage remark/rehype chain.
225
+ *
226
+ * Three call sites render Markdown — `renderMarkdown` (string in, HTML out,
227
+ * which is what the CLI checker runs), the app's `<MarkdownViewer>`, and this
228
+ * package's exported `<MarkdownViewer>` — and each one used to hand-write the
229
+ * same plugin list in the same order. Three copies kept in sync by hand is how
230
+ * a plugin lands in the viewer and not in the checker: a document that styles
231
+ * in the app and renders bare through the tool that is supposed to validate it,
232
+ * with no error anywhere.
233
+ *
234
+ * The order is load-bearing, not incidental:
235
+ *
236
+ * - `rehypeRaw` first: `remark-rehype` runs with `allowDangerousHtml: true`,
237
+ * so raw HTML is still a string until this plugin parses it.
238
+ * - `rehypeSourceLines` before `rehypeSanitize`: `data-source-line` has to be
239
+ * an allowlisted attribute on an element the sanitiser keeps.
240
+ * - `rehypeSlug`, `rehypeHighlight` and `rehypeKatex` after `rehypeSanitize`.
241
+ * For `rehypeSlug` this is not a preference: the sanitiser's default schema
242
+ * clobbers `id` with the prefix `user-content-`, so slugging before it turns
243
+ * every `#heading` link in every document into a dead anchor. For the other
244
+ * two it means their output is trusted rather than filtered — KaTeX emits
245
+ * inline `style` on nearly every glyph.
246
+ *
247
+ * Anything that reads HTML comments must sit between `rehypeRaw` and
248
+ * `rehypeSanitize`: before `rehypeRaw` there are no comment nodes, and
249
+ * `rehypeSanitize` deletes them. `rehypeVantageDirectives` is what occupies
250
+ * that slot, and it is registered unconditionally — a renderer that skipped it
251
+ * would disagree with the others about what a document means.
252
+ */
253
+
254
+ interface PipelineOptions {
255
+ /** GFM tables, strikethrough, task lists (default: true) */
256
+ gfm?: boolean;
257
+ /** KaTeX math, `$$…$$` only (default: true) */
258
+ math?: boolean;
259
+ /** Syntax highlighting via highlight.js (default: true) */
260
+ highlight?: boolean;
261
+ /** `data-source-line` attributes for line anchors (default: true) */
262
+ sourceLines?: boolean;
263
+ /** XSS sanitisation (default: true) */
264
+ sanitize?: boolean;
265
+ /**
266
+ * Lines the frontmatter consumed, added to every emitted line number so
267
+ * `data-source-line` names a line in the *file* rather than in the parsed
268
+ * body — which is what a `#L42` link written against the file means.
269
+ * Defaults to 0. Ignored when `sourceLines` is false.
270
+ */
271
+ bodyLineOffset?: number;
272
+ }
273
+ interface Pipeline {
274
+ remarkPlugins: PluggableList;
275
+ rehypePlugins: PluggableList;
276
+ }
277
+ /**
278
+ * The mdast half of the chain. Exported on its own because there is a real
279
+ * mdast-only consumer: the CLI checker parses documents without ever running
280
+ * rehype (`packages/vantage-check/src/core/document.ts`), and it has to parse
281
+ * them exactly the way the viewer does.
117
282
  */
283
+ declare function buildRemarkPlugins(options?: PipelineOptions): PluggableList;
118
284
  /**
119
- * Parse a GitHub-style line anchor hash.
120
- * Supports: #L42, #L42-L50, #L42-50
121
- * Returns null if the hash is not a line anchor.
285
+ * Both halves from one options object.
286
+ *
287
+ * This is what every renderer calls. It takes one object rather than exposing
288
+ * the two builders because `math` spans both halves — `remark-math` parses the
289
+ * delimiters, `rehype-katex` renders the result — and two calls are two places
290
+ * to forget the second one.
291
+ *
292
+ * Returns fresh arrays on every call and reads no module-level state; keep it
293
+ * that way, so a plugin in the chain cannot become a function of how many times
294
+ * the chain has been built.
295
+ */
296
+ declare function buildPipeline(options?: PipelineOptions): Pipeline;
297
+
298
+ /**
299
+ * Framework-agnostic line anchor utilities.
300
+ * Scroll to and highlight the elements a GitHub-style line anchor
301
+ * (#L42, #L42-L50) names. The parsing half lives in lineAnchor.ts, which has
302
+ * no DOM dependency.
122
303
  */
123
- declare function parseLineAnchor(hash: string): {
124
- start: number;
125
- end: number;
126
- } | null;
127
304
  /**
128
305
  * Clear all line anchor highlights from a container.
129
306
  */
@@ -137,15 +314,72 @@ declare function clearLineAnchorHighlights(container: HTMLElement): void;
137
314
  */
138
315
  declare function scrollToLineAnchor(container: HTMLElement, hash: string): (() => void) | null;
139
316
 
317
+ /**
318
+ * Parsing for GitHub-style line anchors, with no DOM in sight.
319
+ *
320
+ * Split out from scrollToLineAnchor.ts so that non-browser consumers — the
321
+ * `vantage-check` CLI, which validates `#L42` links against the file on disk —
322
+ * can share the *same* syntax the viewer honours instead of reimplementing it
323
+ * and drifting.
324
+ */
325
+ /**
326
+ * Parse a GitHub-style line anchor hash.
327
+ * Supports: #L42, #L42-L50, #L42-50
328
+ * Returns null if the hash is not a line anchor.
329
+ */
330
+ declare function parseLineAnchor(hash: string): {
331
+ start: number;
332
+ end: number;
333
+ } | null;
334
+
140
335
  /**
141
336
  * Frontmatter parser for YAML (---) and TOML (+++) delimited content.
142
337
  * Works in both browser and server environments.
143
338
  */
144
339
  type FrontmatterFormat = "yaml" | "toml" | "none";
340
+ /**
341
+ * Why a document that *looks* like it has frontmatter ended up without any.
342
+ *
343
+ * The parser deliberately never throws: a document whose frontmatter is broken
344
+ * still renders, with the block treated as body text. That is the right
345
+ * behaviour for a viewer and the wrong one for an author, who gets no signal
346
+ * at all — so the reason is recorded here for anything that wants to report it
347
+ * (`vantage-check` does; see its frontmatter rules).
348
+ *
349
+ * - `unterminated` — an opening delimiter with no closing one.
350
+ * - `invalid` — the block did not parse; `message` is the parser's own words,
351
+ * and `line`/`column` are 1-based *within the block* when it said.
352
+ * - `not-a-mapping` — it parsed, but to a string or a list rather than a table
353
+ * of fields, which is not something a metadata card can render.
354
+ */
355
+ interface FrontmatterProblem {
356
+ kind: "unterminated" | "invalid" | "not-a-mapping";
357
+ /** The delimiter the document opened with. */
358
+ delimiter: string;
359
+ message?: string;
360
+ line?: number;
361
+ column?: number;
362
+ }
145
363
  interface ParsedFrontmatter {
146
364
  frontmatter: Record<string, unknown>;
147
365
  body: string;
148
366
  format: FrontmatterFormat;
367
+ /**
368
+ * How many source lines the frontmatter block consumed — the shift between a
369
+ * line number in `body` and the same line in the original file:
370
+ * `fileLine = bodyLine + bodyLineOffset`.
371
+ *
372
+ * Anything that renders `body` and reports line numbers (line anchors, review
373
+ * comment anchors) has to add this back, or every number it produces points
374
+ * `bodyLineOffset` lines short of the text it names.
375
+ */
376
+ bodyLineOffset: number;
377
+ /**
378
+ * Set when the document opens with a frontmatter delimiter that did not
379
+ * yield a metadata table. Everything else in this result is unchanged —
380
+ * this records *why*, it does not change what rendering does.
381
+ */
382
+ problem?: FrontmatterProblem;
149
383
  }
150
384
  /**
151
385
  * Parse frontmatter from markdown content.
@@ -160,6 +394,18 @@ declare function parseFrontmatter(content: string): ParsedFrontmatter;
160
394
  */
161
395
 
162
396
  type Schema = typeof defaultSchema;
397
+ /**
398
+ * Never set `allowComments` here.
399
+ *
400
+ * `hast-util-sanitize` drops comment nodes because that boolean defaults to
401
+ * `false` — comments are not elements, so `tagNames` has nothing to do with it.
402
+ * `rehypeVantageDirectives` relies on that deletion: it consumes a
403
+ * `<!-- vantage: … -->` comment into attributes and deliberately leaves the node
404
+ * for the sanitiser. Turning the switch on readmits every directive comment —
405
+ * valid and malformed alike — into the rendered HTML, which breaks the carrier's
406
+ * whole premise. `vantageDirectives.test.ts` ("leaves no comment in the rendered
407
+ * markup") is the guard.
408
+ */
163
409
  declare const sanitizeSchema: Schema;
164
410
 
165
411
  /**
@@ -241,4 +487,4 @@ interface ResolveLinkOptions {
241
487
  */
242
488
  declare function resolveLinks(html: string, options?: ResolveLinkOptions): string;
243
489
 
244
- export { FrontmatterDisplay, type FrontmatterFormat, MarkdownViewer, type MarkdownViewerProps, MermaidDiagram, type ParsedFrontmatter, type RenderMermaidOptions, type RenderOptions, type RenderResult, type ResolveLinkOptions, clearLineAnchorHighlights, parseFrontmatter, parseLineAnchor, rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor, useLineAnchor };
490
+ export { DOC_STATUSES, DOC_STATUS_TONES, type DocStatus, DocumentStatusChip, FrontmatterDisplay, type FrontmatterFormat, MarkdownViewer, type MarkdownViewerProps, MermaidDiagram, type ParsedFrontmatter, type Pipeline, type PipelineOptions, type RenderMermaidOptions, type RenderOptions, type RenderResult, type ResolveLinkOptions, VANTAGE_FRONTMATTER_KEYS, type VantageFrontmatter, type VantageFrontmatterIssue, buildPipeline, buildRemarkPlugins, clearLineAnchorHighlights, isDocStatus, parseFrontmatter, parseLineAnchor, readVantageFrontmatter, rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor, useLineAnchor };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { RefObject } from 'react';
2
2
  import { Root } from 'hast';
3
- import { Plugin } from 'unified';
3
+ import { Plugin, PluggableList } from 'unified';
4
4
  import { defaultSchema } from 'rehype-sanitize';
5
5
 
6
6
  /**
@@ -56,6 +56,107 @@ interface FrontmatterDisplayProps {
56
56
  }
57
57
  declare const FrontmatterDisplay: React.NamedExoticComponent<FrontmatterDisplayProps>;
58
58
 
59
+ /**
60
+ * The `tone` vocabulary: GitHub's alert words plus `muted`.
61
+ *
62
+ * Semantic, never chromatic (P2, Ledger OQ-3). A document says what a section
63
+ * *is*; the theme decides what that looks like, which is what lets one document
64
+ * render correctly in light, in dark, and in themes that do not exist yet.
65
+ */
66
+ declare const VANTAGE_TONES: readonly ["note", "tip", "important", "warning", "caution", "muted"];
67
+
68
+ /**
69
+ * The `vantage:` frontmatter key — file-scoped chrome (`docs/reference/inline-markup.md`, "File-scoped chrome").
70
+ *
71
+ * One reserved key at the top level of a document's frontmatter, holding the
72
+ * chrome that belongs to the *file* rather than to a section. Today that is one
73
+ * thing: whether the document's lifecycle `status:` is shown as a chip above the
74
+ * metadata card, instead of being buried as one row inside it.
75
+ *
76
+ * Read only at the top level, and **inert on every failure** (P3): an unknown
77
+ * key, a value outside the closed set, or a `vantage:` that is not a table
78
+ * produces no chrome, no throw and no console output. The reasons are returned
79
+ * as data in `issues`, for anything that wants to report them — `vantage-check`
80
+ * does, and it is the only signal an author gets. That split is exactly the one
81
+ * `FrontmatterProblem` already uses in `frontmatter.ts`: the viewer reads the
82
+ * value, the checker reads the reasons.
83
+ *
84
+ * Like `vantageDirectives.ts`, this module is imported by the CLI checker **by
85
+ * relative path**, so it must stay a pure function of already-parsed data: no
86
+ * hast, no React, no filesystem.
87
+ */
88
+
89
+ /**
90
+ * The document lifecycle vocabulary. Closed; extending it is a code change.
91
+ *
92
+ * This is the repo's own existing set, not a new one — `styleGuide.ts` tells
93
+ * every agent to write `status: in-review # draft | in-review | accepted |
94
+ * deprecated`, and every document under `docs/` follows it. It is deliberately
95
+ * *not* the `badge` set (`draft stale blocked done wip`): `badge` is
96
+ * section-scoped workflow state, `status` is document lifecycle state, and
97
+ * `in-review` — the value the design doc's own only example renders — is not a
98
+ * badge word at all. Only `draft` is a member of both, and a token set is per key.
99
+ */
100
+ declare const DOC_STATUSES: readonly ["draft", "in-review", "accepted", "deprecated"];
101
+ type DocStatus = (typeof DOC_STATUSES)[number];
102
+ /** Every key this build knows under `vantage:`. Closed. */
103
+ declare const VANTAGE_FRONTMATTER_KEYS: readonly ["status-chip"];
104
+ /**
105
+ * Which tone each status borrows its colours from.
106
+ *
107
+ * The chip has no palette of its own: it reuses the tone chips
108
+ * (`.vantage-chip--<tone>` in `styles/directives.css`), which is also what makes
109
+ * a `draft` chip and a `badge=draft` chip the same visual object. A map rather
110
+ * than a computed class name, so the whole status→tone relation is one readable
111
+ * table and a test can assert it covers the vocabulary.
112
+ */
113
+ declare const DOC_STATUS_TONES: Readonly<Record<DocStatus, (typeof VANTAGE_TONES)[number]>>;
114
+ /**
115
+ * Why something under `vantage:` produced no chrome.
116
+ *
117
+ * `status-chip-orphan` and `status-chip-disagrees` are not vocabulary errors —
118
+ * both values are legal — but both are the markup rot R3 is about: a chip that
119
+ * says something the document's own `status:` does not.
120
+ */
121
+ type VantageFrontmatterIssue = {
122
+ kind: "not-a-table";
123
+ value: unknown;
124
+ } | {
125
+ kind: "unknown-key";
126
+ key: string;
127
+ } | {
128
+ kind: "bad-value";
129
+ key: string;
130
+ value: unknown;
131
+ legal: readonly string[];
132
+ } | {
133
+ kind: "status-chip-orphan";
134
+ status: unknown;
135
+ } | {
136
+ kind: "status-chip-disagrees";
137
+ chip: DocStatus;
138
+ status: unknown;
139
+ };
140
+ interface VantageFrontmatter {
141
+ /** The chip's text, or `undefined` for no chip. */
142
+ statusChip?: DocStatus;
143
+ /** Why something was dropped. A viewer must never read this (P3). */
144
+ issues: VantageFrontmatterIssue[];
145
+ }
146
+ /** Narrowing helper the chip and the checker both use. */
147
+ declare function isDocStatus(value: unknown): value is DocStatus;
148
+ /**
149
+ * Read the `vantage:` key out of parsed frontmatter.
150
+ *
151
+ * Pure: no module state, no mutation of the input, no logging. The same object
152
+ * in twice gives equal results out.
153
+ */
154
+ declare function readVantageFrontmatter(frontmatter: Record<string, unknown>): VantageFrontmatter;
155
+
156
+ declare const DocumentStatusChip: React.NamedExoticComponent<{
157
+ status: DocStatus;
158
+ }>;
159
+
59
160
  /**
60
161
  * Framework-agnostic markdown -> HTML rendering pipeline.
61
162
  * Uses the same remark/rehype chain as the Vantage viewer.
@@ -87,7 +188,7 @@ interface RenderResult {
87
188
  *
88
189
  * Features (all enabled by default):
89
190
  * - GitHub Flavored Markdown (tables, strikethrough, task lists)
90
- * - KaTeX math rendering ($$...$$ blocks)
191
+ * - KaTeX math rendering, inline and block ($$...$$ only; single $ is not a delimiter)
91
192
  * - Syntax highlighting via highlight.js
92
193
  * - `data-source-line` attributes for line anchors
93
194
  * - XSS sanitization
@@ -108,22 +209,98 @@ declare function renderMarkdown(content: string, options?: RenderOptions): Promi
108
209
  * each rendered block a traceable line number from the source.
109
210
  */
110
211
 
111
- declare const rehypeSourceLines: Plugin<[], Root>;
212
+ interface RehypeSourceLinesOptions {
213
+ /**
214
+ * Lines stripped off the front of the file before parsing — frontmatter,
215
+ * essentially. Added to every emitted line number so `data-source-line`
216
+ * names a line in the *file* rather than in the parsed body, which is what
217
+ * a `#L42` link written against the file means. Defaults to 0.
218
+ */
219
+ offset?: number;
220
+ }
221
+ declare const rehypeSourceLines: Plugin<[RehypeSourceLinesOptions?], Root>;
112
222
 
113
223
  /**
114
- * Framework-agnostic line anchor utilities.
115
- * Parse GitHub-style line anchors (#L42, #L42-L50) and scroll/highlight
116
- * matching elements in a container.
224
+ * The one definition of the Vantage remark/rehype chain.
225
+ *
226
+ * Three call sites render Markdown — `renderMarkdown` (string in, HTML out,
227
+ * which is what the CLI checker runs), the app's `<MarkdownViewer>`, and this
228
+ * package's exported `<MarkdownViewer>` — and each one used to hand-write the
229
+ * same plugin list in the same order. Three copies kept in sync by hand is how
230
+ * a plugin lands in the viewer and not in the checker: a document that styles
231
+ * in the app and renders bare through the tool that is supposed to validate it,
232
+ * with no error anywhere.
233
+ *
234
+ * The order is load-bearing, not incidental:
235
+ *
236
+ * - `rehypeRaw` first: `remark-rehype` runs with `allowDangerousHtml: true`,
237
+ * so raw HTML is still a string until this plugin parses it.
238
+ * - `rehypeSourceLines` before `rehypeSanitize`: `data-source-line` has to be
239
+ * an allowlisted attribute on an element the sanitiser keeps.
240
+ * - `rehypeSlug`, `rehypeHighlight` and `rehypeKatex` after `rehypeSanitize`.
241
+ * For `rehypeSlug` this is not a preference: the sanitiser's default schema
242
+ * clobbers `id` with the prefix `user-content-`, so slugging before it turns
243
+ * every `#heading` link in every document into a dead anchor. For the other
244
+ * two it means their output is trusted rather than filtered — KaTeX emits
245
+ * inline `style` on nearly every glyph.
246
+ *
247
+ * Anything that reads HTML comments must sit between `rehypeRaw` and
248
+ * `rehypeSanitize`: before `rehypeRaw` there are no comment nodes, and
249
+ * `rehypeSanitize` deletes them. `rehypeVantageDirectives` is what occupies
250
+ * that slot, and it is registered unconditionally — a renderer that skipped it
251
+ * would disagree with the others about what a document means.
252
+ */
253
+
254
+ interface PipelineOptions {
255
+ /** GFM tables, strikethrough, task lists (default: true) */
256
+ gfm?: boolean;
257
+ /** KaTeX math, `$$…$$` only (default: true) */
258
+ math?: boolean;
259
+ /** Syntax highlighting via highlight.js (default: true) */
260
+ highlight?: boolean;
261
+ /** `data-source-line` attributes for line anchors (default: true) */
262
+ sourceLines?: boolean;
263
+ /** XSS sanitisation (default: true) */
264
+ sanitize?: boolean;
265
+ /**
266
+ * Lines the frontmatter consumed, added to every emitted line number so
267
+ * `data-source-line` names a line in the *file* rather than in the parsed
268
+ * body — which is what a `#L42` link written against the file means.
269
+ * Defaults to 0. Ignored when `sourceLines` is false.
270
+ */
271
+ bodyLineOffset?: number;
272
+ }
273
+ interface Pipeline {
274
+ remarkPlugins: PluggableList;
275
+ rehypePlugins: PluggableList;
276
+ }
277
+ /**
278
+ * The mdast half of the chain. Exported on its own because there is a real
279
+ * mdast-only consumer: the CLI checker parses documents without ever running
280
+ * rehype (`packages/vantage-check/src/core/document.ts`), and it has to parse
281
+ * them exactly the way the viewer does.
117
282
  */
283
+ declare function buildRemarkPlugins(options?: PipelineOptions): PluggableList;
118
284
  /**
119
- * Parse a GitHub-style line anchor hash.
120
- * Supports: #L42, #L42-L50, #L42-50
121
- * Returns null if the hash is not a line anchor.
285
+ * Both halves from one options object.
286
+ *
287
+ * This is what every renderer calls. It takes one object rather than exposing
288
+ * the two builders because `math` spans both halves — `remark-math` parses the
289
+ * delimiters, `rehype-katex` renders the result — and two calls are two places
290
+ * to forget the second one.
291
+ *
292
+ * Returns fresh arrays on every call and reads no module-level state; keep it
293
+ * that way, so a plugin in the chain cannot become a function of how many times
294
+ * the chain has been built.
295
+ */
296
+ declare function buildPipeline(options?: PipelineOptions): Pipeline;
297
+
298
+ /**
299
+ * Framework-agnostic line anchor utilities.
300
+ * Scroll to and highlight the elements a GitHub-style line anchor
301
+ * (#L42, #L42-L50) names. The parsing half lives in lineAnchor.ts, which has
302
+ * no DOM dependency.
122
303
  */
123
- declare function parseLineAnchor(hash: string): {
124
- start: number;
125
- end: number;
126
- } | null;
127
304
  /**
128
305
  * Clear all line anchor highlights from a container.
129
306
  */
@@ -137,15 +314,72 @@ declare function clearLineAnchorHighlights(container: HTMLElement): void;
137
314
  */
138
315
  declare function scrollToLineAnchor(container: HTMLElement, hash: string): (() => void) | null;
139
316
 
317
+ /**
318
+ * Parsing for GitHub-style line anchors, with no DOM in sight.
319
+ *
320
+ * Split out from scrollToLineAnchor.ts so that non-browser consumers — the
321
+ * `vantage-check` CLI, which validates `#L42` links against the file on disk —
322
+ * can share the *same* syntax the viewer honours instead of reimplementing it
323
+ * and drifting.
324
+ */
325
+ /**
326
+ * Parse a GitHub-style line anchor hash.
327
+ * Supports: #L42, #L42-L50, #L42-50
328
+ * Returns null if the hash is not a line anchor.
329
+ */
330
+ declare function parseLineAnchor(hash: string): {
331
+ start: number;
332
+ end: number;
333
+ } | null;
334
+
140
335
  /**
141
336
  * Frontmatter parser for YAML (---) and TOML (+++) delimited content.
142
337
  * Works in both browser and server environments.
143
338
  */
144
339
  type FrontmatterFormat = "yaml" | "toml" | "none";
340
+ /**
341
+ * Why a document that *looks* like it has frontmatter ended up without any.
342
+ *
343
+ * The parser deliberately never throws: a document whose frontmatter is broken
344
+ * still renders, with the block treated as body text. That is the right
345
+ * behaviour for a viewer and the wrong one for an author, who gets no signal
346
+ * at all — so the reason is recorded here for anything that wants to report it
347
+ * (`vantage-check` does; see its frontmatter rules).
348
+ *
349
+ * - `unterminated` — an opening delimiter with no closing one.
350
+ * - `invalid` — the block did not parse; `message` is the parser's own words,
351
+ * and `line`/`column` are 1-based *within the block* when it said.
352
+ * - `not-a-mapping` — it parsed, but to a string or a list rather than a table
353
+ * of fields, which is not something a metadata card can render.
354
+ */
355
+ interface FrontmatterProblem {
356
+ kind: "unterminated" | "invalid" | "not-a-mapping";
357
+ /** The delimiter the document opened with. */
358
+ delimiter: string;
359
+ message?: string;
360
+ line?: number;
361
+ column?: number;
362
+ }
145
363
  interface ParsedFrontmatter {
146
364
  frontmatter: Record<string, unknown>;
147
365
  body: string;
148
366
  format: FrontmatterFormat;
367
+ /**
368
+ * How many source lines the frontmatter block consumed — the shift between a
369
+ * line number in `body` and the same line in the original file:
370
+ * `fileLine = bodyLine + bodyLineOffset`.
371
+ *
372
+ * Anything that renders `body` and reports line numbers (line anchors, review
373
+ * comment anchors) has to add this back, or every number it produces points
374
+ * `bodyLineOffset` lines short of the text it names.
375
+ */
376
+ bodyLineOffset: number;
377
+ /**
378
+ * Set when the document opens with a frontmatter delimiter that did not
379
+ * yield a metadata table. Everything else in this result is unchanged —
380
+ * this records *why*, it does not change what rendering does.
381
+ */
382
+ problem?: FrontmatterProblem;
149
383
  }
150
384
  /**
151
385
  * Parse frontmatter from markdown content.
@@ -160,6 +394,18 @@ declare function parseFrontmatter(content: string): ParsedFrontmatter;
160
394
  */
161
395
 
162
396
  type Schema = typeof defaultSchema;
397
+ /**
398
+ * Never set `allowComments` here.
399
+ *
400
+ * `hast-util-sanitize` drops comment nodes because that boolean defaults to
401
+ * `false` — comments are not elements, so `tagNames` has nothing to do with it.
402
+ * `rehypeVantageDirectives` relies on that deletion: it consumes a
403
+ * `<!-- vantage: … -->` comment into attributes and deliberately leaves the node
404
+ * for the sanitiser. Turning the switch on readmits every directive comment —
405
+ * valid and malformed alike — into the rendered HTML, which breaks the carrier's
406
+ * whole premise. `vantageDirectives.test.ts` ("leaves no comment in the rendered
407
+ * markup") is the guard.
408
+ */
163
409
  declare const sanitizeSchema: Schema;
164
410
 
165
411
  /**
@@ -241,4 +487,4 @@ interface ResolveLinkOptions {
241
487
  */
242
488
  declare function resolveLinks(html: string, options?: ResolveLinkOptions): string;
243
489
 
244
- export { FrontmatterDisplay, type FrontmatterFormat, MarkdownViewer, type MarkdownViewerProps, MermaidDiagram, type ParsedFrontmatter, type RenderMermaidOptions, type RenderOptions, type RenderResult, type ResolveLinkOptions, clearLineAnchorHighlights, parseFrontmatter, parseLineAnchor, rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor, useLineAnchor };
490
+ export { DOC_STATUSES, DOC_STATUS_TONES, type DocStatus, DocumentStatusChip, FrontmatterDisplay, type FrontmatterFormat, MarkdownViewer, type MarkdownViewerProps, MermaidDiagram, type ParsedFrontmatter, type Pipeline, type PipelineOptions, type RenderMermaidOptions, type RenderOptions, type RenderResult, type ResolveLinkOptions, VANTAGE_FRONTMATTER_KEYS, type VantageFrontmatter, type VantageFrontmatterIssue, buildPipeline, buildRemarkPlugins, clearLineAnchorHighlights, isDocStatus, parseFrontmatter, parseLineAnchor, readVantageFrontmatter, rehypeSourceLines, renderMarkdown, renderMermaidBlocks, resolveLinks, sanitizeSchema, scrollToLineAnchor, useLineAnchor };