vantage-md 0.1.0

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/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # vantage-md
2
+
3
+ Markdown rendering pipeline with GitHub-style line anchors (`#L42`, `#L42-L50`), mermaid diagrams, KaTeX math, and syntax highlighting.
4
+
5
+ Extracted from [Vantage](https://github.com/mschulkind-oss/vantage), a markdown documentation viewer.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install vantage-md
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Framework-agnostic: markdown string to HTML
16
+
17
+ ```typescript
18
+ import { renderMarkdown } from "vantage-md";
19
+
20
+ const { html, frontmatter } = await renderMarkdown("# Hello\n\nSome **bold** text");
21
+ // html: '<h1 data-source-line="1">Hello</h1>\n<p data-source-line="3">Some <strong>bold</strong> text</p>'
22
+ ```
23
+
24
+ Every rendered block element gets a `data-source-line` attribute, enabling GitHub-style line anchors.
25
+
26
+ ### Options
27
+
28
+ All features are enabled by default. Disable what you don't need:
29
+
30
+ ```typescript
31
+ const { html } = await renderMarkdown(content, {
32
+ gfm: true, // GFM tables, strikethrough, task lists
33
+ math: true, // KaTeX rendering
34
+ highlight: true, // Syntax highlighting
35
+ sourceLines: true, // data-source-line attributes
36
+ sanitize: true, // XSS sanitization
37
+ frontmatter: true, // Parse and strip YAML/TOML frontmatter
38
+ });
39
+ ```
40
+
41
+ ### Line anchors
42
+
43
+ Scroll to and highlight lines in rendered markdown:
44
+
45
+ ```typescript
46
+ import { scrollToLineAnchor } from "vantage-md";
47
+ import "vantage-md/styles";
48
+
49
+ // Highlight lines 42-50 and scroll to them
50
+ const cleanup = scrollToLineAnchor(container, "#L42-L50");
51
+
52
+ // Remove highlights
53
+ cleanup?.();
54
+ ```
55
+
56
+ ### React component
57
+
58
+ ```tsx
59
+ import { MarkdownViewer } from "vantage-md/react";
60
+ import "vantage-md/styles";
61
+
62
+ function Docs({ content, path }) {
63
+ return (
64
+ <MarkdownViewer
65
+ content={content}
66
+ currentPath={path}
67
+ hash={window.location.hash}
68
+ onNavigate={(path) => navigate(path)}
69
+ />
70
+ );
71
+ }
72
+ ```
73
+
74
+ The React component includes mermaid diagram rendering (lazy-loaded), frontmatter display, and syntax highlighting out of the box.
75
+
76
+ ### Rehype plugin (bring your own pipeline)
77
+
78
+ ```typescript
79
+ import { rehypeSourceLines } from "vantage-md";
80
+
81
+ const processor = unified()
82
+ .use(remarkParse)
83
+ .use(remarkRehype)
84
+ .use(rehypeSourceLines) // adds data-source-line to block elements
85
+ .use(rehypeStringify);
86
+ ```
87
+
88
+ ### Svelte / Vue / plain HTML
89
+
90
+ ```svelte
91
+ <script>
92
+ import { renderMarkdown } from "vantage-md";
93
+ import "vantage-md/styles";
94
+
95
+ let html = "";
96
+ renderMarkdown(content).then((result) => (html = result.html));
97
+ </script>
98
+
99
+ {@html html}
100
+ ```
101
+
102
+ ## Exports
103
+
104
+ | Entry point | Description |
105
+ |-------------|-------------|
106
+ | `vantage-md` | `renderMarkdown`, `rehypeSourceLines`, `scrollToLineAnchor`, `parseLineAnchor`, `parseFrontmatter`, `sanitizeSchema` |
107
+ | `vantage-md/react` | `MarkdownViewer`, `useLineAnchor`, `MermaidDiagram`, `FrontmatterDisplay` + all core exports |
108
+ | `vantage-md/styles` | Line-anchor highlight CSS (light + dark mode) |
109
+
110
+ ## Features
111
+
112
+ - **Line anchors** — `data-source-line` attributes on every block element, with scroll/highlight utilities
113
+ - **GFM** — tables, strikethrough, task lists, autolinks
114
+ - **KaTeX** — `$$...$$` math blocks
115
+ - **Mermaid** — diagram rendering (client-side, lazy-loaded)
116
+ - **Syntax highlighting** — via highlight.js
117
+ - **Frontmatter** — YAML (`---`) and TOML (`+++`) parsing
118
+ - **Sanitization** — XSS-safe with allowlisted KaTeX/MathML elements
119
+ - **Dark mode** — all styles support `.dark` class
120
+
121
+ ## License
122
+
123
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,271 @@
1
+ 'use strict';
2
+
3
+ var unified = require('unified');
4
+ var remarkParse = require('remark-parse');
5
+ var remarkGfm = require('remark-gfm');
6
+ var remarkMath = require('remark-math');
7
+ var remarkRehype = require('remark-rehype');
8
+ var rehypeRaw = require('rehype-raw');
9
+ var rehypeSanitize = require('rehype-sanitize');
10
+ var rehypeHighlight = require('rehype-highlight');
11
+ var rehypeKatex = require('rehype-katex');
12
+ var rehypeSlug = require('rehype-slug');
13
+ var rehypeStringify = require('rehype-stringify');
14
+ var YAML = require('yaml');
15
+ var smolToml = require('smol-toml');
16
+
17
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
+
19
+ var remarkParse__default = /*#__PURE__*/_interopDefault(remarkParse);
20
+ var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
21
+ var remarkMath__default = /*#__PURE__*/_interopDefault(remarkMath);
22
+ var remarkRehype__default = /*#__PURE__*/_interopDefault(remarkRehype);
23
+ var rehypeRaw__default = /*#__PURE__*/_interopDefault(rehypeRaw);
24
+ var rehypeSanitize__default = /*#__PURE__*/_interopDefault(rehypeSanitize);
25
+ var rehypeHighlight__default = /*#__PURE__*/_interopDefault(rehypeHighlight);
26
+ var rehypeKatex__default = /*#__PURE__*/_interopDefault(rehypeKatex);
27
+ var rehypeSlug__default = /*#__PURE__*/_interopDefault(rehypeSlug);
28
+ var rehypeStringify__default = /*#__PURE__*/_interopDefault(rehypeStringify);
29
+ var YAML__default = /*#__PURE__*/_interopDefault(YAML);
30
+
31
+ // src/renderMarkdown.ts
32
+
33
+ // src/rehypeSourceLines.ts
34
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
35
+ "p",
36
+ "h1",
37
+ "h2",
38
+ "h3",
39
+ "h4",
40
+ "h5",
41
+ "h6",
42
+ "li",
43
+ "blockquote",
44
+ "pre",
45
+ "table",
46
+ "tr",
47
+ "ul",
48
+ "ol",
49
+ "hr",
50
+ "div"
51
+ ]);
52
+ function visit(node) {
53
+ if ("children" in node) {
54
+ for (const child of node.children) {
55
+ if (child.type === "element") {
56
+ if (BLOCK_TAGS.has(child.tagName) && child.position?.start?.line) {
57
+ child.properties = child.properties || {};
58
+ child.properties["dataSourceLine"] = child.position.start.line;
59
+ }
60
+ visit(child);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ var rehypeSourceLines = () => {
66
+ return (tree) => {
67
+ visit(tree);
68
+ };
69
+ };
70
+ var rehypeSourceLines_default = rehypeSourceLines;
71
+ var sanitizeSchema = {
72
+ ...rehypeSanitize.defaultSchema,
73
+ tagNames: [
74
+ ...rehypeSanitize.defaultSchema.tagNames || [],
75
+ // KaTeX MathML elements
76
+ "math",
77
+ "semantics",
78
+ "mrow",
79
+ "mi",
80
+ "mo",
81
+ "mn",
82
+ "msup",
83
+ "msub",
84
+ "mfrac",
85
+ "mover",
86
+ "munder",
87
+ "msqrt",
88
+ "mroot",
89
+ "mtable",
90
+ "mtr",
91
+ "mtd",
92
+ "mtext",
93
+ "mspace",
94
+ "annotation",
95
+ // Other
96
+ "figure",
97
+ "figcaption",
98
+ "summary",
99
+ "details"
100
+ ],
101
+ attributes: {
102
+ ...rehypeSanitize.defaultSchema.attributes,
103
+ "*": [
104
+ ...rehypeSanitize.defaultSchema.attributes?.["*"] || [],
105
+ "className",
106
+ "style",
107
+ "dataSourceLine"
108
+ ],
109
+ code: [...rehypeSanitize.defaultSchema.attributes?.code || [], "className"],
110
+ span: [...rehypeSanitize.defaultSchema.attributes?.span || [], "className", "style"],
111
+ div: [...rehypeSanitize.defaultSchema.attributes?.div || [], "className", "style"],
112
+ a: [...rehypeSanitize.defaultSchema.attributes?.a || [], "id", "className"],
113
+ math: ["xmlns"],
114
+ annotation: ["encoding"],
115
+ img: [...rehypeSanitize.defaultSchema.attributes?.img || [], "loading"],
116
+ td: [...rehypeSanitize.defaultSchema.attributes?.td || [], "style"],
117
+ th: [...rehypeSanitize.defaultSchema.attributes?.th || [], "style"]
118
+ }
119
+ };
120
+ function parseFrontmatter(content) {
121
+ if (content.startsWith("+++")) {
122
+ return parseFrontmatterWithDelimiter(content, "+++", "toml");
123
+ }
124
+ if (content.startsWith("---")) {
125
+ return parseFrontmatterWithDelimiter(content, "---", "yaml");
126
+ }
127
+ return { frontmatter: {}, body: content, format: "none" };
128
+ }
129
+ function parseFrontmatterWithDelimiter(content, delimiter, format) {
130
+ const searchStart = delimiter.length;
131
+ const endIndex = content.indexOf(`
132
+ ${delimiter}`, searchStart);
133
+ if (endIndex === -1) {
134
+ return { frontmatter: {}, body: content, format: "none" };
135
+ }
136
+ const raw = content.slice(searchStart + 1, endIndex).trim();
137
+ const bodyStart = endIndex + 1 + delimiter.length;
138
+ const body = content.slice(bodyStart).replace(/^\n/, "");
139
+ try {
140
+ const frontmatter = format === "toml" ? smolToml.parse(raw) : YAML__default.default.parse(raw);
141
+ return { frontmatter: frontmatter || {}, body, format };
142
+ } catch {
143
+ return { frontmatter: {}, body: content, format: "none" };
144
+ }
145
+ }
146
+
147
+ // src/renderMarkdown.ts
148
+ async function renderMarkdown(content, options = {}) {
149
+ const {
150
+ gfm = true,
151
+ math = true,
152
+ highlight = true,
153
+ sourceLines = true,
154
+ sanitize = true,
155
+ frontmatter: parseFm = true
156
+ } = options;
157
+ let parsed;
158
+ if (parseFm) {
159
+ parsed = parseFrontmatter(content);
160
+ } else {
161
+ parsed = { frontmatter: {}, body: content, format: "none" };
162
+ }
163
+ const remarkPlugins = [];
164
+ const rehypePlugins = [];
165
+ if (gfm) remarkPlugins.push([remarkGfm__default.default, { singleTilde: false }]);
166
+ if (math) remarkPlugins.push([remarkMath__default.default, { singleDollarTextMath: false }]);
167
+ rehypePlugins.push([rehypeRaw__default.default]);
168
+ if (sourceLines) rehypePlugins.push([rehypeSourceLines_default]);
169
+ if (sanitize) rehypePlugins.push([rehypeSanitize__default.default, sanitizeSchema]);
170
+ rehypePlugins.push([rehypeSlug__default.default]);
171
+ if (highlight) rehypePlugins.push([rehypeHighlight__default.default]);
172
+ if (math) rehypePlugins.push([rehypeKatex__default.default]);
173
+ let processor = unified.unified().use(remarkParse__default.default);
174
+ for (const [plugin, ...args] of remarkPlugins) {
175
+ processor = processor.use(plugin, ...args);
176
+ }
177
+ processor = processor.use(remarkRehype__default.default, { allowDangerousHtml: true });
178
+ for (const [plugin, ...args] of rehypePlugins) {
179
+ processor = processor.use(plugin, ...args);
180
+ }
181
+ processor = processor.use(rehypeStringify__default.default);
182
+ const result = await processor.process(parsed.body);
183
+ return {
184
+ html: String(result),
185
+ frontmatter: parsed.frontmatter,
186
+ body: parsed.body
187
+ };
188
+ }
189
+
190
+ // src/scrollToLineAnchor.ts
191
+ var HIGHLIGHT_CLASS = "line-anchor-highlight";
192
+ function parseLineAnchor(hash) {
193
+ if (!hash) return null;
194
+ const frag = hash.startsWith("#") ? hash.slice(1) : hash;
195
+ const match = frag.match(/^L(\d+)(?:-L?(\d+))?$/);
196
+ if (!match) return null;
197
+ const start = parseInt(match[1], 10);
198
+ const end = match[2] ? parseInt(match[2], 10) : start;
199
+ return { start: Math.min(start, end), end: Math.max(start, end) };
200
+ }
201
+ function clearLineAnchorHighlights(container) {
202
+ container.querySelectorAll(`.${HIGHLIGHT_CLASS}`).forEach((node) => {
203
+ node.classList.remove(HIGHLIGHT_CLASS);
204
+ });
205
+ }
206
+ function scrollToLineAnchor(container, hash) {
207
+ clearLineAnchorHighlights(container);
208
+ const range = parseLineAnchor(hash);
209
+ if (!range) return null;
210
+ const blocks = container.querySelectorAll("[data-source-line]");
211
+ let firstMatch = null;
212
+ for (const block of blocks) {
213
+ const line = parseInt(
214
+ block.dataset.sourceLine || "0",
215
+ 10
216
+ );
217
+ if (line >= range.start && line <= range.end) {
218
+ block.classList.add(HIGHLIGHT_CLASS);
219
+ if (!firstMatch) firstMatch = block;
220
+ }
221
+ }
222
+ if (!firstMatch) {
223
+ let closest = null;
224
+ let closestLine = 0;
225
+ for (const block of blocks) {
226
+ const line = parseInt(
227
+ block.dataset.sourceLine || "0",
228
+ 10
229
+ );
230
+ if (line <= range.start && line > closestLine) {
231
+ closestLine = line;
232
+ closest = block;
233
+ }
234
+ }
235
+ if (closest) {
236
+ closest.classList.add(HIGHLIGHT_CLASS);
237
+ firstMatch = closest;
238
+ }
239
+ }
240
+ if (firstMatch) {
241
+ requestAnimationFrame(() => {
242
+ const scrollParent = findScrollParent(container);
243
+ if (scrollParent) {
244
+ const offset = firstMatch.getBoundingClientRect().top - scrollParent.getBoundingClientRect().top + scrollParent.scrollTop;
245
+ scrollParent.scrollTo({ top: offset - 32, behavior: "smooth" });
246
+ } else {
247
+ firstMatch.scrollIntoView({ behavior: "smooth", block: "start" });
248
+ }
249
+ });
250
+ }
251
+ return () => clearLineAnchorHighlights(container);
252
+ }
253
+ function findScrollParent(el) {
254
+ let node = el;
255
+ while (node) {
256
+ const overflow = getComputedStyle(node).overflowY;
257
+ if (overflow === "auto" || overflow === "scroll") return node;
258
+ node = node.parentElement;
259
+ }
260
+ return null;
261
+ }
262
+
263
+ exports.clearLineAnchorHighlights = clearLineAnchorHighlights;
264
+ exports.parseFrontmatter = parseFrontmatter;
265
+ exports.parseLineAnchor = parseLineAnchor;
266
+ exports.rehypeSourceLines = rehypeSourceLines_default;
267
+ exports.renderMarkdown = renderMarkdown;
268
+ exports.sanitizeSchema = sanitizeSchema;
269
+ exports.scrollToLineAnchor = scrollToLineAnchor;
270
+ //# sourceMappingURL=index.cjs.map
271
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rehypeSourceLines.ts","../src/sanitize.ts","../src/frontmatter.ts","../src/renderMarkdown.ts","../src/scrollToLineAnchor.ts"],"names":["defaultSchema","parseTOML","YAML","remarkGfm","remarkMath","rehypeRaw","rehypeSanitize","rehypeSlug","rehypeHighlight","rehypeKatex","unified","remarkParse","remarkRehype","rehypeStringify"],"mappings":"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","file":"index.cjs","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\nfunction visit(node: Root | Element) {\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\"] = child.position.start.line;\n }\n visit(child);\n }\n }\n }\n}\n\nconst rehypeSourceLines: Plugin<[], Root> = () => {\n return (tree: Root) => {\n visit(tree);\n };\n};\n\nexport default rehypeSourceLines;\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\";\n\ntype Schema = typeof defaultSchema;\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\",\n \"dataSourceLine\",\n ],\n code: [...(defaultSchema.attributes?.code || []), \"className\"],\n span: [...(defaultSchema.attributes?.span || []), \"className\", \"style\"],\n div: [...(defaultSchema.attributes?.div || []), \"className\", \"style\"],\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\"],\n th: [...(defaultSchema.attributes?.th || []), \"style\"],\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\nexport interface ParsedFrontmatter {\n frontmatter: Record<string, unknown>;\n body: string;\n format: FrontmatterFormat;\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 { frontmatter: {}, body: content, format: \"none\" };\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 { frontmatter: {}, body: content, format: \"none\" };\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 frontmatter =\n format === \"toml\"\n ? (parseTOML(raw) as Record<string, unknown>)\n : (YAML.parse(raw) as Record<string, unknown>);\n return { frontmatter: frontmatter || {}, body, format };\n } catch {\n return { frontmatter: {}, body: content, format: \"none\" };\n }\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 remarkGfm from \"remark-gfm\";\nimport remarkMath from \"remark-math\";\nimport remarkRehype from \"remark-rehype\";\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 rehypeStringify from \"rehype-stringify\";\nimport rehypeSourceLines from \"./rehypeSourceLines.js\";\nimport { sanitizeSchema } from \"./sanitize.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 ($$...$$ blocks)\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 = { frontmatter: {}, body: content, format: \"none\" };\n }\n\n // Build the unified pipeline using a single chain.\n // We use `any` for the processor to avoid unified's strict generic\n // type constraints that make conditional plugin registration painful.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const remarkPlugins: [any, ...any[]][] = [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const rehypePlugins: [any, ...any[]][] = [];\n\n if (gfm) remarkPlugins.push([remarkGfm, { singleTilde: false }]);\n if (math) remarkPlugins.push([remarkMath, { singleDollarTextMath: false }]);\n\n rehypePlugins.push([rehypeRaw]);\n if (sourceLines) rehypePlugins.push([rehypeSourceLines]);\n if (sanitize) rehypePlugins.push([rehypeSanitize, sanitizeSchema]);\n rehypePlugins.push([rehypeSlug]);\n if (highlight) rehypePlugins.push([rehypeHighlight]);\n if (math) rehypePlugins.push([rehypeKatex]);\n\n // Build the processor. We type as `any` because unified's generic\n // Processor type changes shape with every .use() call, making\n // conditional plugin registration impractical with strict types.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let processor: any = unified().use(remarkParse);\n for (const [plugin, ...args] of remarkPlugins) {\n processor = processor.use(plugin, ...args);\n }\n processor = processor.use(remarkRehype, { allowDangerousHtml: true });\n for (const [plugin, ...args] of rehypePlugins) {\n processor = processor.use(plugin, ...args);\n }\n processor = processor.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 * Framework-agnostic line anchor utilities.\n * Parse GitHub-style line anchors (#L42, #L42-L50) and scroll/highlight\n * matching elements in a container.\n */\n\nconst HIGHLIGHT_CLASS = \"line-anchor-highlight\";\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/**\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(\n (block as HTMLElement).dataset.sourceLine || \"0\",\n 10,\n );\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"]}
@@ -0,0 +1,111 @@
1
+ import { Root } from 'hast';
2
+ import { Plugin } from 'unified';
3
+ import { defaultSchema } from 'rehype-sanitize';
4
+
5
+ /**
6
+ * Framework-agnostic markdown -> HTML rendering pipeline.
7
+ * Uses the same remark/rehype chain as the Vantage viewer.
8
+ */
9
+ interface RenderOptions {
10
+ /** Enable GFM tables, strikethrough, task lists (default: true) */
11
+ gfm?: boolean;
12
+ /** Enable KaTeX math rendering (default: true) */
13
+ math?: boolean;
14
+ /** Enable syntax highlighting (default: true) */
15
+ highlight?: boolean;
16
+ /** Add data-source-line attributes for line anchors (default: true) */
17
+ sourceLines?: boolean;
18
+ /** Enable XSS sanitization (default: true) */
19
+ sanitize?: boolean;
20
+ /** Parse and strip frontmatter (default: true) */
21
+ frontmatter?: boolean;
22
+ }
23
+ interface RenderResult {
24
+ /** The rendered HTML string */
25
+ html: string;
26
+ /** Parsed frontmatter (empty object if none or disabled) */
27
+ frontmatter: Record<string, unknown>;
28
+ /** The markdown body with frontmatter stripped */
29
+ body: string;
30
+ }
31
+ /**
32
+ * Render a markdown string to HTML using the full Vantage pipeline.
33
+ *
34
+ * Features (all enabled by default):
35
+ * - GitHub Flavored Markdown (tables, strikethrough, task lists)
36
+ * - KaTeX math rendering ($$...$$ blocks)
37
+ * - Syntax highlighting via highlight.js
38
+ * - `data-source-line` attributes for line anchors
39
+ * - XSS sanitization
40
+ * - Heading slugs/anchors
41
+ * - YAML/TOML frontmatter parsing
42
+ *
43
+ * Mermaid diagrams are NOT rendered server-side (they require a browser).
44
+ * Mermaid code blocks are preserved as `<pre><code class="language-mermaid">`.
45
+ * Use the React `<MarkdownViewer>` component for client-side mermaid rendering.
46
+ */
47
+ declare function renderMarkdown(content: string, options?: RenderOptions): Promise<RenderResult>;
48
+
49
+ /**
50
+ * Rehype plugin that adds `data-source-line` attributes to block-level
51
+ * elements based on their position in the original markdown source.
52
+ *
53
+ * This enables GitHub-style line anchors (#L42, #L42-L50) by giving
54
+ * each rendered block a traceable line number from the source.
55
+ */
56
+
57
+ declare const rehypeSourceLines: Plugin<[], Root>;
58
+
59
+ /**
60
+ * Framework-agnostic line anchor utilities.
61
+ * Parse GitHub-style line anchors (#L42, #L42-L50) and scroll/highlight
62
+ * matching elements in a container.
63
+ */
64
+ /**
65
+ * Parse a GitHub-style line anchor hash.
66
+ * Supports: #L42, #L42-L50, #L42-50
67
+ * Returns null if the hash is not a line anchor.
68
+ */
69
+ declare function parseLineAnchor(hash: string): {
70
+ start: number;
71
+ end: number;
72
+ } | null;
73
+ /**
74
+ * Clear all line anchor highlights from a container.
75
+ */
76
+ declare function clearLineAnchorHighlights(container: HTMLElement): void;
77
+ /**
78
+ * Scroll to and highlight line-anchored elements in a container.
79
+ *
80
+ * @param container - The DOM element containing rendered markdown
81
+ * @param hash - The URL hash (e.g. "#L42" or "#L42-L50")
82
+ * @returns A cleanup function that removes the highlights
83
+ */
84
+ declare function scrollToLineAnchor(container: HTMLElement, hash: string): (() => void) | null;
85
+
86
+ /**
87
+ * Frontmatter parser for YAML (---) and TOML (+++) delimited content.
88
+ * Works in both browser and server environments.
89
+ */
90
+ type FrontmatterFormat = "yaml" | "toml" | "none";
91
+ interface ParsedFrontmatter {
92
+ frontmatter: Record<string, unknown>;
93
+ body: string;
94
+ format: FrontmatterFormat;
95
+ }
96
+ /**
97
+ * Parse frontmatter from markdown content.
98
+ * Supports YAML (delimited by ---) and TOML (delimited by +++).
99
+ */
100
+ declare function parseFrontmatter(content: string): ParsedFrontmatter;
101
+
102
+ /**
103
+ * Sanitization schema for the rendering pipeline.
104
+ * Allows GFM, KaTeX MathML, syntax highlighting classes, and
105
+ * data-source-line attributes while blocking XSS vectors.
106
+ */
107
+
108
+ type Schema = typeof defaultSchema;
109
+ declare const sanitizeSchema: Schema;
110
+
111
+ export { type FrontmatterFormat, type ParsedFrontmatter, type RenderOptions, type RenderResult, clearLineAnchorHighlights, parseFrontmatter, parseLineAnchor, rehypeSourceLines, renderMarkdown, sanitizeSchema, scrollToLineAnchor };
@@ -0,0 +1,111 @@
1
+ import { Root } from 'hast';
2
+ import { Plugin } from 'unified';
3
+ import { defaultSchema } from 'rehype-sanitize';
4
+
5
+ /**
6
+ * Framework-agnostic markdown -> HTML rendering pipeline.
7
+ * Uses the same remark/rehype chain as the Vantage viewer.
8
+ */
9
+ interface RenderOptions {
10
+ /** Enable GFM tables, strikethrough, task lists (default: true) */
11
+ gfm?: boolean;
12
+ /** Enable KaTeX math rendering (default: true) */
13
+ math?: boolean;
14
+ /** Enable syntax highlighting (default: true) */
15
+ highlight?: boolean;
16
+ /** Add data-source-line attributes for line anchors (default: true) */
17
+ sourceLines?: boolean;
18
+ /** Enable XSS sanitization (default: true) */
19
+ sanitize?: boolean;
20
+ /** Parse and strip frontmatter (default: true) */
21
+ frontmatter?: boolean;
22
+ }
23
+ interface RenderResult {
24
+ /** The rendered HTML string */
25
+ html: string;
26
+ /** Parsed frontmatter (empty object if none or disabled) */
27
+ frontmatter: Record<string, unknown>;
28
+ /** The markdown body with frontmatter stripped */
29
+ body: string;
30
+ }
31
+ /**
32
+ * Render a markdown string to HTML using the full Vantage pipeline.
33
+ *
34
+ * Features (all enabled by default):
35
+ * - GitHub Flavored Markdown (tables, strikethrough, task lists)
36
+ * - KaTeX math rendering ($$...$$ blocks)
37
+ * - Syntax highlighting via highlight.js
38
+ * - `data-source-line` attributes for line anchors
39
+ * - XSS sanitization
40
+ * - Heading slugs/anchors
41
+ * - YAML/TOML frontmatter parsing
42
+ *
43
+ * Mermaid diagrams are NOT rendered server-side (they require a browser).
44
+ * Mermaid code blocks are preserved as `<pre><code class="language-mermaid">`.
45
+ * Use the React `<MarkdownViewer>` component for client-side mermaid rendering.
46
+ */
47
+ declare function renderMarkdown(content: string, options?: RenderOptions): Promise<RenderResult>;
48
+
49
+ /**
50
+ * Rehype plugin that adds `data-source-line` attributes to block-level
51
+ * elements based on their position in the original markdown source.
52
+ *
53
+ * This enables GitHub-style line anchors (#L42, #L42-L50) by giving
54
+ * each rendered block a traceable line number from the source.
55
+ */
56
+
57
+ declare const rehypeSourceLines: Plugin<[], Root>;
58
+
59
+ /**
60
+ * Framework-agnostic line anchor utilities.
61
+ * Parse GitHub-style line anchors (#L42, #L42-L50) and scroll/highlight
62
+ * matching elements in a container.
63
+ */
64
+ /**
65
+ * Parse a GitHub-style line anchor hash.
66
+ * Supports: #L42, #L42-L50, #L42-50
67
+ * Returns null if the hash is not a line anchor.
68
+ */
69
+ declare function parseLineAnchor(hash: string): {
70
+ start: number;
71
+ end: number;
72
+ } | null;
73
+ /**
74
+ * Clear all line anchor highlights from a container.
75
+ */
76
+ declare function clearLineAnchorHighlights(container: HTMLElement): void;
77
+ /**
78
+ * Scroll to and highlight line-anchored elements in a container.
79
+ *
80
+ * @param container - The DOM element containing rendered markdown
81
+ * @param hash - The URL hash (e.g. "#L42" or "#L42-L50")
82
+ * @returns A cleanup function that removes the highlights
83
+ */
84
+ declare function scrollToLineAnchor(container: HTMLElement, hash: string): (() => void) | null;
85
+
86
+ /**
87
+ * Frontmatter parser for YAML (---) and TOML (+++) delimited content.
88
+ * Works in both browser and server environments.
89
+ */
90
+ type FrontmatterFormat = "yaml" | "toml" | "none";
91
+ interface ParsedFrontmatter {
92
+ frontmatter: Record<string, unknown>;
93
+ body: string;
94
+ format: FrontmatterFormat;
95
+ }
96
+ /**
97
+ * Parse frontmatter from markdown content.
98
+ * Supports YAML (delimited by ---) and TOML (delimited by +++).
99
+ */
100
+ declare function parseFrontmatter(content: string): ParsedFrontmatter;
101
+
102
+ /**
103
+ * Sanitization schema for the rendering pipeline.
104
+ * Allows GFM, KaTeX MathML, syntax highlighting classes, and
105
+ * data-source-line attributes while blocking XSS vectors.
106
+ */
107
+
108
+ type Schema = typeof defaultSchema;
109
+ declare const sanitizeSchema: Schema;
110
+
111
+ export { type FrontmatterFormat, type ParsedFrontmatter, type RenderOptions, type RenderResult, clearLineAnchorHighlights, parseFrontmatter, parseLineAnchor, rehypeSourceLines, renderMarkdown, sanitizeSchema, scrollToLineAnchor };