@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.
- package/dist/lib/components/CodeBlock.svelte +73 -0
- package/dist/lib/components/CodeBlock.svelte.d.ts +16 -0
- package/dist/lib/components/DocsCard.svelte +99 -0
- package/dist/lib/components/DocsCard.svelte.d.ts +11 -0
- package/dist/lib/components/DocsHub.svelte +83 -0
- package/dist/lib/components/DocsHub.svelte.d.ts +28 -0
- package/dist/lib/components/MarkdownPage.svelte +100 -0
- package/dist/lib/components/MarkdownPage.svelte.d.ts +33 -0
- package/dist/lib/components/MermaidDiagram.svelte +87 -0
- package/dist/lib/components/MermaidDiagram.svelte.d.ts +14 -0
- package/dist/lib/components/MermaidInit.svelte +100 -0
- package/dist/lib/components/MermaidInit.svelte.d.ts +9 -0
- package/dist/lib/components/TableOfContents.svelte +179 -0
- package/dist/lib/components/TableOfContents.svelte.d.ts +16 -0
- package/dist/lib/components/TocPanel.svelte +263 -0
- package/dist/lib/components/TocPanel.svelte.d.ts +20 -0
- package/dist/lib/highlighter/index.d.ts +15 -0
- package/dist/lib/highlighter/index.js +78 -0
- package/dist/lib/index.d.ts +29 -0
- package/dist/lib/index.js +31 -0
- package/dist/lib/parser/extensions.d.ts +75 -0
- package/dist/lib/parser/extensions.js +311 -0
- package/dist/lib/parser/index.d.ts +57 -0
- package/dist/lib/parser/index.js +150 -0
- package/dist/lib/styles/markdown.css +516 -0
- package/dist/lib/types/docs.d.ts +72 -0
- package/dist/lib/types/docs.js +4 -0
- package/dist/lib/types/frontmatter.d.ts +68 -0
- package/dist/lib/types/frontmatter.js +4 -0
- package/dist/lib/types/index.d.ts +5 -0
- package/dist/lib/types/index.js +4 -0
- package/dist/lib/utils.d.ts +6 -0
- package/dist/lib/utils.js +9 -0
- 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, '&')
|
|
290
|
+
.replace(/</g, '<')
|
|
291
|
+
.replace(/>/g, '>')
|
|
292
|
+
.replace(/"/g, '"');
|
|
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
|
+
}
|