@classic-homes/theme-docs 0.0.2

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.
Files changed (34) hide show
  1. package/dist/lib/components/CodeBlock.svelte +73 -0
  2. package/dist/lib/components/CodeBlock.svelte.d.ts +16 -0
  3. package/dist/lib/components/DocsCard.svelte +99 -0
  4. package/dist/lib/components/DocsCard.svelte.d.ts +11 -0
  5. package/dist/lib/components/DocsHub.svelte +83 -0
  6. package/dist/lib/components/DocsHub.svelte.d.ts +28 -0
  7. package/dist/lib/components/MarkdownPage.svelte +100 -0
  8. package/dist/lib/components/MarkdownPage.svelte.d.ts +33 -0
  9. package/dist/lib/components/MermaidDiagram.svelte +87 -0
  10. package/dist/lib/components/MermaidDiagram.svelte.d.ts +14 -0
  11. package/dist/lib/components/MermaidInit.svelte +100 -0
  12. package/dist/lib/components/MermaidInit.svelte.d.ts +9 -0
  13. package/dist/lib/components/TableOfContents.svelte +179 -0
  14. package/dist/lib/components/TableOfContents.svelte.d.ts +16 -0
  15. package/dist/lib/components/TocPanel.svelte +263 -0
  16. package/dist/lib/components/TocPanel.svelte.d.ts +20 -0
  17. package/dist/lib/highlighter/index.d.ts +15 -0
  18. package/dist/lib/highlighter/index.js +78 -0
  19. package/dist/lib/index.d.ts +29 -0
  20. package/dist/lib/index.js +31 -0
  21. package/dist/lib/parser/extensions.d.ts +75 -0
  22. package/dist/lib/parser/extensions.js +311 -0
  23. package/dist/lib/parser/index.d.ts +57 -0
  24. package/dist/lib/parser/index.js +150 -0
  25. package/dist/lib/styles/markdown.css +516 -0
  26. package/dist/lib/types/docs.d.ts +72 -0
  27. package/dist/lib/types/docs.js +4 -0
  28. package/dist/lib/types/frontmatter.d.ts +68 -0
  29. package/dist/lib/types/frontmatter.js +4 -0
  30. package/dist/lib/types/index.d.ts +5 -0
  31. package/dist/lib/types/index.js +4 -0
  32. package/dist/lib/utils.d.ts +6 -0
  33. package/dist/lib/utils.js +9 -0
  34. package/package.json +68 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Marked Extensions for Enhanced Markdown Features
3
+ *
4
+ * Provides support for:
5
+ * - Admonitions/Callouts (note, tip, warning, important, caution)
6
+ * - Footnotes
7
+ * - Definition Lists
8
+ * - Mermaid diagram placeholders (rendered client-side)
9
+ */
10
+ import type { MarkedExtension } from 'marked';
11
+ /**
12
+ * Admonition types and their corresponding icons/classes
13
+ */
14
+ export declare const ADMONITION_TYPES: readonly ["note", "tip", "warning", "important", "caution"];
15
+ export type AdmonitionType = (typeof ADMONITION_TYPES)[number];
16
+ /**
17
+ * Admonition extension for marked
18
+ *
19
+ * Syntax:
20
+ * :::note
21
+ * This is a note
22
+ * :::
23
+ *
24
+ * :::warning Title Here
25
+ * This is a warning with a custom title
26
+ * :::
27
+ */
28
+ export declare function admonitionExtension(): MarkedExtension;
29
+ /**
30
+ * Reset footnote store (call before parsing a new document)
31
+ */
32
+ export declare function resetFootnoteStore(): void;
33
+ /**
34
+ * Footnotes extension for marked
35
+ *
36
+ * Syntax:
37
+ * Here is some text[^1] with a footnote.
38
+ *
39
+ * [^1]: This is the footnote content.
40
+ */
41
+ export declare function footnoteExtension(): MarkedExtension;
42
+ /**
43
+ * Render collected footnotes as a section
44
+ * Call this after parsing to get the footnotes HTML
45
+ */
46
+ export declare function renderFootnotes(): string;
47
+ /**
48
+ * Definition list extension for marked
49
+ *
50
+ * Syntax:
51
+ * Term 1
52
+ * : Definition for term 1
53
+ *
54
+ * Term 2
55
+ * : Definition for term 2
56
+ * : Another definition for term 2
57
+ */
58
+ export declare function definitionListExtension(): MarkedExtension;
59
+ /**
60
+ * Mermaid code block extension for marked
61
+ *
62
+ * Converts mermaid code blocks to placeholder divs that can be
63
+ * rendered client-side by the Mermaid library.
64
+ *
65
+ * Syntax:
66
+ * ```mermaid
67
+ * graph TD
68
+ * A --> B
69
+ * ```
70
+ */
71
+ export declare function mermaidExtension(): MarkedExtension;
72
+ /**
73
+ * Get all markdown extensions
74
+ */
75
+ export declare function getAllExtensions(): MarkedExtension[];
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Marked Extensions for Enhanced Markdown Features
3
+ *
4
+ * Provides support for:
5
+ * - Admonitions/Callouts (note, tip, warning, important, caution)
6
+ * - Footnotes
7
+ * - Definition Lists
8
+ * - Mermaid diagram placeholders (rendered client-side)
9
+ */
10
+ /**
11
+ * Admonition types and their corresponding icons/classes
12
+ */
13
+ export const ADMONITION_TYPES = ['note', 'tip', 'warning', 'important', 'caution'];
14
+ /**
15
+ * Admonition extension for marked
16
+ *
17
+ * Syntax:
18
+ * :::note
19
+ * This is a note
20
+ * :::
21
+ *
22
+ * :::warning Title Here
23
+ * This is a warning with a custom title
24
+ * :::
25
+ */
26
+ export function admonitionExtension() {
27
+ return {
28
+ extensions: [
29
+ {
30
+ name: 'admonition',
31
+ level: 'block',
32
+ start(src) {
33
+ return src.match(/^:::/)?.index;
34
+ },
35
+ tokenizer(src) {
36
+ // Match :::type optional-title\ncontent\n:::
37
+ const match = src.match(/^:::(note|tip|warning|important|caution)(?:\s+([^\n]*))?\n([\s\S]*?)\n:::/);
38
+ if (match) {
39
+ const token = {
40
+ type: 'admonition',
41
+ raw: match[0],
42
+ admonitionType: match[1],
43
+ title: match[2]?.trim() || match[1].charAt(0).toUpperCase() + match[1].slice(1),
44
+ content: match[3].trim(),
45
+ tokens: [],
46
+ };
47
+ // Parse the inner content as markdown
48
+ this.lexer.blockTokens(token.content, token.tokens);
49
+ return token;
50
+ }
51
+ return undefined;
52
+ },
53
+ renderer(token) {
54
+ const innerHtml = this.parser.parse(token.tokens ?? []);
55
+ const iconMap = {
56
+ note: `<svg class="admonition-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
57
+ tip: `<svg class="admonition-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>`,
58
+ warning: `<svg class="admonition-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
59
+ important: `<svg class="admonition-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>`,
60
+ caution: `<svg class="admonition-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7.86 2h8.28L22 7.86v8.28L16.14 22H7.86L2 16.14V7.86L7.86 2z"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
61
+ };
62
+ return `<div class="admonition admonition-${token.admonitionType}">
63
+ <div class="admonition-heading">
64
+ ${iconMap[token.admonitionType]}
65
+ <span class="admonition-title">${token.title}</span>
66
+ </div>
67
+ <div class="admonition-content">${innerHtml}</div>
68
+ </div>`;
69
+ },
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ let footnoteStore = {
75
+ definitions: new Map(),
76
+ references: new Set(),
77
+ };
78
+ /**
79
+ * Reset footnote store (call before parsing a new document)
80
+ */
81
+ export function resetFootnoteStore() {
82
+ footnoteStore = {
83
+ definitions: new Map(),
84
+ references: new Set(),
85
+ };
86
+ }
87
+ /**
88
+ * Footnotes extension for marked
89
+ *
90
+ * Syntax:
91
+ * Here is some text[^1] with a footnote.
92
+ *
93
+ * [^1]: This is the footnote content.
94
+ */
95
+ export function footnoteExtension() {
96
+ return {
97
+ extensions: [
98
+ // Footnote definition: [^id]: content
99
+ {
100
+ name: 'footnoteDefinition',
101
+ level: 'block',
102
+ start(src) {
103
+ return src.match(/^\[\^[^\]]+\]:/)?.index;
104
+ },
105
+ tokenizer(src) {
106
+ // Match [^id]: content (can span multiple lines if indented)
107
+ const match = src.match(/^\[\^([^\]]+)\]:\s*([\s\S]*?)(?=\n\[\^|\n\n|\n(?=[^\s])|\s*$)/);
108
+ if (match) {
109
+ const id = match[1];
110
+ const content = match[2].trim();
111
+ const token = {
112
+ type: 'footnoteDefinition',
113
+ raw: match[0],
114
+ id,
115
+ content,
116
+ tokens: [],
117
+ };
118
+ this.lexer.inline(content, token.tokens);
119
+ footnoteStore.definitions.set(id, { content, tokens: token.tokens });
120
+ return token;
121
+ }
122
+ return undefined;
123
+ },
124
+ renderer() {
125
+ // Definitions are rendered at the end, not inline
126
+ return '';
127
+ },
128
+ },
129
+ // Footnote reference: [^id]
130
+ {
131
+ name: 'footnoteRef',
132
+ level: 'inline',
133
+ start(src) {
134
+ return src.match(/\[\^[^\]]+\](?!:)/)?.index;
135
+ },
136
+ tokenizer(src) {
137
+ const match = src.match(/^\[\^([^\]]+)\](?!:)/);
138
+ if (match) {
139
+ const id = match[1];
140
+ footnoteStore.references.add(id);
141
+ return {
142
+ type: 'footnoteRef',
143
+ raw: match[0],
144
+ id,
145
+ };
146
+ }
147
+ return undefined;
148
+ },
149
+ renderer(token) {
150
+ return `<sup class="footnote-ref"><a href="#fn-${token.id}" id="fnref-${token.id}">[${token.id}]</a></sup>`;
151
+ },
152
+ },
153
+ ],
154
+ };
155
+ }
156
+ /**
157
+ * Render collected footnotes as a section
158
+ * Call this after parsing to get the footnotes HTML
159
+ */
160
+ export function renderFootnotes() {
161
+ if (footnoteStore.definitions.size === 0) {
162
+ return '';
163
+ }
164
+ const items = [];
165
+ for (const [id, { content }] of footnoteStore.definitions) {
166
+ if (footnoteStore.references.has(id)) {
167
+ items.push(`<li id="fn-${id}" class="footnote-item">
168
+ <p>${content} <a href="#fnref-${id}" class="footnote-backref">↩</a></p>
169
+ </li>`);
170
+ }
171
+ }
172
+ if (items.length === 0) {
173
+ return '';
174
+ }
175
+ return `<section class="footnotes">
176
+ <hr class="footnotes-separator" />
177
+ <ol class="footnotes-list">${items.join('')}</ol>
178
+ </section>`;
179
+ }
180
+ /**
181
+ * Definition list extension for marked
182
+ *
183
+ * Syntax:
184
+ * Term 1
185
+ * : Definition for term 1
186
+ *
187
+ * Term 2
188
+ * : Definition for term 2
189
+ * : Another definition for term 2
190
+ */
191
+ export function definitionListExtension() {
192
+ return {
193
+ extensions: [
194
+ {
195
+ name: 'defList',
196
+ level: 'block',
197
+ start(src) {
198
+ // Look for a term followed by : definition pattern
199
+ return src.match(/^[^\n]+\n:\s/)?.index;
200
+ },
201
+ tokenizer(src) {
202
+ // Match definition list blocks
203
+ const match = src.match(/^((?:[^\n]+\n(?::\s+[^\n]*\n?)+\n?)+)/);
204
+ if (match) {
205
+ const raw = match[0];
206
+ const items = [];
207
+ // Parse individual term/definition pairs
208
+ const lines = raw.split('\n');
209
+ let currentTerm = '';
210
+ let currentDefs = [];
211
+ for (const line of lines) {
212
+ if (line.startsWith(': ')) {
213
+ // This is a definition
214
+ currentDefs.push(line.slice(2).trim());
215
+ }
216
+ else if (line.trim() && !line.startsWith(':')) {
217
+ // This is a new term
218
+ if (currentTerm && currentDefs.length > 0) {
219
+ items.push({ term: currentTerm, definitions: [...currentDefs] });
220
+ currentDefs = [];
221
+ }
222
+ currentTerm = line.trim();
223
+ }
224
+ }
225
+ // Don't forget the last item
226
+ if (currentTerm && currentDefs.length > 0) {
227
+ items.push({ term: currentTerm, definitions: currentDefs });
228
+ }
229
+ if (items.length === 0) {
230
+ return undefined;
231
+ }
232
+ return {
233
+ type: 'defList',
234
+ raw,
235
+ items,
236
+ };
237
+ }
238
+ return undefined;
239
+ },
240
+ renderer(token) {
241
+ const itemsHtml = token.items
242
+ .map((item) => {
243
+ const defsHtml = item.definitions.map((def) => `<dd>${def}</dd>`).join('');
244
+ return `<dt>${item.term}</dt>${defsHtml}`;
245
+ })
246
+ .join('');
247
+ return `<dl class="definition-list">${itemsHtml}</dl>`;
248
+ },
249
+ },
250
+ ],
251
+ };
252
+ }
253
+ /**
254
+ * Mermaid code block extension for marked
255
+ *
256
+ * Converts mermaid code blocks to placeholder divs that can be
257
+ * rendered client-side by the Mermaid library.
258
+ *
259
+ * Syntax:
260
+ * ```mermaid
261
+ * graph TD
262
+ * A --> B
263
+ * ```
264
+ */
265
+ export function mermaidExtension() {
266
+ return {
267
+ extensions: [
268
+ {
269
+ name: 'mermaidBlock',
270
+ level: 'block',
271
+ start(src) {
272
+ return src.match(/^```mermaid/)?.index;
273
+ },
274
+ tokenizer(src) {
275
+ const match = src.match(/^```mermaid\n([\s\S]*?)```/);
276
+ if (match) {
277
+ return {
278
+ type: 'mermaidBlock',
279
+ raw: match[0],
280
+ code: match[1].trim(),
281
+ };
282
+ }
283
+ return undefined;
284
+ },
285
+ renderer(token) {
286
+ // Render as a placeholder div with the mermaid code
287
+ // The client-side Mermaid library will pick this up
288
+ const escapedCode = token.code
289
+ .replace(/&/g, '&amp;')
290
+ .replace(/</g, '&lt;')
291
+ .replace(/>/g, '&gt;')
292
+ .replace(/"/g, '&quot;');
293
+ return `<div class="mermaid-diagram" data-mermaid="${escapedCode}">
294
+ <pre class="mermaid">${token.code}</pre>
295
+ </div>`;
296
+ },
297
+ },
298
+ ],
299
+ };
300
+ }
301
+ /**
302
+ * Get all markdown extensions
303
+ */
304
+ export function getAllExtensions() {
305
+ return [
306
+ admonitionExtension(),
307
+ footnoteExtension(),
308
+ definitionListExtension(),
309
+ mermaidExtension(),
310
+ ];
311
+ }
@@ -0,0 +1,57 @@
1
+ import type { ParsedMarkdown, ParseOptions } from '../types/index.js';
2
+ /**
3
+ * Parse markdown content with YAML frontmatter extraction and syntax highlighting.
4
+ *
5
+ * Extracts YAML frontmatter from the beginning of the content (between `---` delimiters),
6
+ * applies Shiki syntax highlighting to code blocks, and optionally generates heading IDs.
7
+ *
8
+ * Supported extensions:
9
+ * - Admonitions/Callouts: :::note, :::tip, :::warning, :::important, :::caution
10
+ * - Footnotes: [^1] references and [^1]: definitions
11
+ * - Definition Lists: Term followed by : Definition
12
+ * - Mermaid Diagrams: ```mermaid code blocks (rendered client-side)
13
+ *
14
+ * @param content - Raw markdown string, optionally with YAML frontmatter
15
+ * @param options - Parsing options
16
+ * @param options.theme - Shiki theme for code highlighting ('github-dark' | 'github-light' | 'one-dark-pro')
17
+ * @param options.generateHeadingIds - Whether to generate IDs for headings (default: true)
18
+ * @returns Parsed markdown with frontmatter data, cleaned markdown, and rendered HTML
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const result = await parseMarkdown(`---
23
+ * title: My Document
24
+ * ---
25
+ * # Hello World
26
+ *
27
+ * :::tip Pro Tip
28
+ * This is a helpful tip!
29
+ * :::
30
+ * `);
31
+ * console.log(result.frontmatter?.title); // "My Document"
32
+ * console.log(result.html); // Contains heading and admonition HTML
33
+ * ```
34
+ */
35
+ export declare function parseMarkdown(content: string, options?: ParseOptions): Promise<ParsedMarkdown>;
36
+ /**
37
+ * Extract table of contents entries from rendered HTML content.
38
+ *
39
+ * Searches for heading elements (h1-h6) with `id` attributes and returns
40
+ * a flat array of entries. Headings without IDs are ignored.
41
+ *
42
+ * @param html - Rendered HTML string containing headings with IDs
43
+ * @param maxDepth - Maximum heading level to include (1-6, default: 3)
44
+ * @returns Flat array of TOC entries with text, level, and id
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const html = '<h1 id="intro">Introduction</h1><h2 id="setup">Setup</h2>';
49
+ * const toc = extractToc(html, 2);
50
+ * // Returns: [{ text: 'Introduction', level: 1, id: 'intro' }, { text: 'Setup', level: 2, id: 'setup' }]
51
+ * ```
52
+ */
53
+ export declare function extractToc(html: string, maxDepth?: number): {
54
+ text: string;
55
+ level: number;
56
+ id: string;
57
+ }[];
@@ -0,0 +1,150 @@
1
+ import { marked, Renderer } from 'marked';
2
+ import yaml from 'js-yaml';
3
+ import { getHighlighter, escapeHtml } from '../highlighter/index.js';
4
+ import { getAllExtensions, resetFootnoteStore, renderFootnotes } from './extensions.js';
5
+ /**
6
+ * SECURITY NOTE: This parser renders markdown to HTML without sanitization.
7
+ * Only use with trusted markdown content from your own codebase or CMS.
8
+ * Do NOT use with user-generated content without adding a sanitization step.
9
+ */
10
+ /**
11
+ * Extract frontmatter from markdown content
12
+ * Browser-compatible implementation using js-yaml
13
+ */
14
+ function extractFrontmatter(content) {
15
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
16
+ const match = content.match(frontmatterRegex);
17
+ if (!match) {
18
+ return { data: null, content };
19
+ }
20
+ try {
21
+ const data = yaml.load(match[1]);
22
+ return { data, content: match[2] };
23
+ }
24
+ catch (err) {
25
+ // Log warning for debugging - YAML parsing failed
26
+ console.warn('[docs] Failed to parse YAML frontmatter:', err instanceof Error ? err.message : err);
27
+ return { data: null, content };
28
+ }
29
+ }
30
+ /**
31
+ * Parse markdown content with YAML frontmatter extraction and syntax highlighting.
32
+ *
33
+ * Extracts YAML frontmatter from the beginning of the content (between `---` delimiters),
34
+ * applies Shiki syntax highlighting to code blocks, and optionally generates heading IDs.
35
+ *
36
+ * Supported extensions:
37
+ * - Admonitions/Callouts: :::note, :::tip, :::warning, :::important, :::caution
38
+ * - Footnotes: [^1] references and [^1]: definitions
39
+ * - Definition Lists: Term followed by : Definition
40
+ * - Mermaid Diagrams: ```mermaid code blocks (rendered client-side)
41
+ *
42
+ * @param content - Raw markdown string, optionally with YAML frontmatter
43
+ * @param options - Parsing options
44
+ * @param options.theme - Shiki theme for code highlighting ('github-dark' | 'github-light' | 'one-dark-pro')
45
+ * @param options.generateHeadingIds - Whether to generate IDs for headings (default: true)
46
+ * @returns Parsed markdown with frontmatter data, cleaned markdown, and rendered HTML
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const result = await parseMarkdown(`---
51
+ * title: My Document
52
+ * ---
53
+ * # Hello World
54
+ *
55
+ * :::tip Pro Tip
56
+ * This is a helpful tip!
57
+ * :::
58
+ * `);
59
+ * console.log(result.frontmatter?.title); // "My Document"
60
+ * console.log(result.html); // Contains heading and admonition HTML
61
+ * ```
62
+ */
63
+ export async function parseMarkdown(content, options = {}) {
64
+ const { theme = 'github-dark', generateHeadingIds = true } = options;
65
+ // Extract frontmatter
66
+ const { data: frontmatter, content: markdownContent } = extractFrontmatter(content);
67
+ // Get Shiki highlighter
68
+ const highlighter = await getHighlighter();
69
+ // Reset footnote store for this document
70
+ resetFootnoteStore();
71
+ // Configure marked with custom code renderer
72
+ const renderer = new Renderer();
73
+ renderer.code = ({ text, lang }) => {
74
+ const language = lang || 'text';
75
+ // Skip mermaid blocks - they're handled by the mermaid extension
76
+ if (language === 'mermaid') {
77
+ return '';
78
+ }
79
+ try {
80
+ const highlighted = highlighter.codeToHtml(text, {
81
+ lang: language,
82
+ theme: theme,
83
+ });
84
+ return highlighted;
85
+ }
86
+ catch {
87
+ // Fallback for unsupported languages
88
+ return `<pre class="shiki"><code class="language-${language}">${escapeHtml(text)}</code></pre>`;
89
+ }
90
+ };
91
+ if (generateHeadingIds) {
92
+ renderer.heading = ({ text, depth }) => {
93
+ const id = text
94
+ .toLowerCase()
95
+ .replace(/\./g, '-') // Convert periods to hyphens (preserves version numbers like v2.0 → v2-0)
96
+ .replace(/[^\w\s-]+/g, '') // Remove other special characters
97
+ .replace(/\s+/g, '-') // Convert spaces to hyphens
98
+ .replace(/-+/g, '-') // Collapse multiple hyphens
99
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
100
+ return `<h${depth} id="${id}">${text}</h${depth}>`;
101
+ };
102
+ }
103
+ // Apply all markdown extensions (admonitions, footnotes, definition lists, mermaid)
104
+ marked.use(...getAllExtensions());
105
+ marked.use({ renderer, gfm: true });
106
+ let html = await marked(markdownContent);
107
+ // Append footnotes section if any were referenced
108
+ const footnotesHtml = renderFootnotes();
109
+ if (footnotesHtml) {
110
+ html += footnotesHtml;
111
+ }
112
+ return {
113
+ frontmatter,
114
+ markdown: markdownContent,
115
+ html,
116
+ };
117
+ }
118
+ /**
119
+ * Extract table of contents entries from rendered HTML content.
120
+ *
121
+ * Searches for heading elements (h1-h6) with `id` attributes and returns
122
+ * a flat array of entries. Headings without IDs are ignored.
123
+ *
124
+ * @param html - Rendered HTML string containing headings with IDs
125
+ * @param maxDepth - Maximum heading level to include (1-6, default: 3)
126
+ * @returns Flat array of TOC entries with text, level, and id
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const html = '<h1 id="intro">Introduction</h1><h2 id="setup">Setup</h2>';
131
+ * const toc = extractToc(html, 2);
132
+ * // Returns: [{ text: 'Introduction', level: 1, id: 'intro' }, { text: 'Setup', level: 2, id: 'setup' }]
133
+ * ```
134
+ */
135
+ export function extractToc(html, maxDepth = 3) {
136
+ const headingRegex = /<h([1-6])[^>]*id="([^"]*)"[^>]*>([^<]*)<\/h[1-6]>/gi;
137
+ const toc = [];
138
+ let match;
139
+ while ((match = headingRegex.exec(html)) !== null) {
140
+ const level = parseInt(match[1], 10);
141
+ if (level <= maxDepth) {
142
+ toc.push({
143
+ level,
144
+ id: match[2],
145
+ text: match[3],
146
+ });
147
+ }
148
+ }
149
+ return toc;
150
+ }