@classic-homes/theme-docs 0.0.49 → 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/dist/lib/components/mount.d.ts +14 -0
- package/dist/lib/components/mount.js +36 -0
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.js +2 -0
- package/dist/lib/parser/extensions.d.ts +25 -3
- package/dist/lib/parser/extensions.js +78 -10
- package/dist/lib/parser/index.d.ts +5 -1
- package/dist/lib/parser/index.js +25 -12
- package/dist/lib/parser/slug.d.ts +19 -0
- package/dist/lib/parser/slug.js +55 -0
- package/dist/lib/styles/markdown.css +23 -1
- package/dist/lib/types/frontmatter.d.ts +11 -0
- package/package.json +2 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Component } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Mount components into the placeholders `parseMarkdown` emits for its `components` option.
|
|
4
|
+
*
|
|
5
|
+
* Call it after the rendered HTML is in the DOM (e.g. from `onMount` or an `{@attach}`).
|
|
6
|
+
* Placeholders naming a component missing from `registry`, or carrying unreadable props,
|
|
7
|
+
* are left empty. Returns a cleanup function that unmounts everything it mounted.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const cleanup = mountComponents(article, { RequestForm });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export declare function mountComponents(root: ParentNode, registry: Record<string, Component<any>>): () => void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { mount, unmount } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* Mount components into the placeholders `parseMarkdown` emits for its `components` option.
|
|
4
|
+
*
|
|
5
|
+
* Call it after the rendered HTML is in the DOM (e.g. from `onMount` or an `{@attach}`).
|
|
6
|
+
* Placeholders naming a component missing from `registry`, or carrying unreadable props,
|
|
7
|
+
* are left empty. Returns a cleanup function that unmounts everything it mounted.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const cleanup = mountComponents(article, { RequestForm });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function mountComponents(root,
|
|
15
|
+
// Props arrive from markdown as strings; each component declares its own types.
|
|
16
|
+
registry) {
|
|
17
|
+
const mounted = [];
|
|
18
|
+
for (const target of root.querySelectorAll('[data-component]')) {
|
|
19
|
+
const component = registry[target.dataset.component ?? ''];
|
|
20
|
+
if (!component)
|
|
21
|
+
continue;
|
|
22
|
+
let props;
|
|
23
|
+
try {
|
|
24
|
+
props = JSON.parse(target.dataset.props ?? '{}');
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
target.replaceChildren();
|
|
30
|
+
mounted.push(mount(component, { target, props }));
|
|
31
|
+
}
|
|
32
|
+
return () => {
|
|
33
|
+
for (const instance of mounted)
|
|
34
|
+
void unmount(instance);
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -20,9 +20,11 @@ export { default as TableOfContents } from './components/TableOfContents.svelte'
|
|
|
20
20
|
export { default as TocPanel } from './components/TocPanel.svelte';
|
|
21
21
|
export { default as MermaidDiagram } from './components/MermaidDiagram.svelte';
|
|
22
22
|
export { default as MermaidInit } from './components/MermaidInit.svelte';
|
|
23
|
+
export { mountComponents } from './components/mount.js';
|
|
23
24
|
export type { AuthorInfo, FrontmatterData, ParsedMarkdown, ParseOptions, MarkdownPageProps, } from './types/frontmatter.js';
|
|
24
25
|
export type { DocsItem, DocsSection, DocsHubConfig, DocsHubProps, DocsCardProps, TocEntry, TableOfContentsProps, } from './types/docs.js';
|
|
25
26
|
export { parseMarkdown, extractToc } from './parser/index.js';
|
|
27
|
+
export { createSlugger, type HeadingIdStyle } from './parser/slug.js';
|
|
26
28
|
export { highlightCode, getHighlighter, escapeHtml } from './highlighter/index.js';
|
|
27
29
|
export { cn } from './utils.js';
|
|
28
30
|
export { ADMONITION_TYPES, type AdmonitionType, admonitionExtension, footnoteExtension, definitionListExtension, mermaidExtension, getAllExtensions, resetFootnoteStore, renderFootnotes, } from './parser/extensions.js';
|
package/dist/lib/index.js
CHANGED
|
@@ -21,8 +21,10 @@ export { default as TableOfContents } from './components/TableOfContents.svelte'
|
|
|
21
21
|
export { default as TocPanel } from './components/TocPanel.svelte';
|
|
22
22
|
export { default as MermaidDiagram } from './components/MermaidDiagram.svelte';
|
|
23
23
|
export { default as MermaidInit } from './components/MermaidInit.svelte';
|
|
24
|
+
export { mountComponents } from './components/mount.js';
|
|
24
25
|
// Utilities
|
|
25
26
|
export { parseMarkdown, extractToc } from './parser/index.js';
|
|
27
|
+
export { createSlugger } from './parser/slug.js';
|
|
26
28
|
export { highlightCode, getHighlighter, escapeHtml } from './highlighter/index.js';
|
|
27
29
|
export { cn } from './utils.js';
|
|
28
30
|
// Markdown Extensions
|
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
* Marked Extensions for Enhanced Markdown Features
|
|
3
3
|
*
|
|
4
4
|
* Provides support for:
|
|
5
|
-
* - Admonitions/Callouts (note, tip, warning, important, caution)
|
|
5
|
+
* - Admonitions/Callouts (note, info, tip, warning, important, caution, danger)
|
|
6
6
|
* - Footnotes
|
|
7
7
|
* - Definition Lists
|
|
8
8
|
* - Mermaid diagram placeholders (rendered client-side)
|
|
9
|
+
* - Component placeholders for allowlisted `<Name prop="…" />` tags (mounted client-side)
|
|
9
10
|
*/
|
|
10
11
|
import type { MarkedExtension } from 'marked';
|
|
11
12
|
/**
|
|
12
13
|
* Admonition types and their corresponding icons/classes
|
|
13
14
|
*/
|
|
14
|
-
export declare const ADMONITION_TYPES: readonly ["note", "tip", "warning", "important", "caution"];
|
|
15
|
+
export declare const ADMONITION_TYPES: readonly ["note", "info", "tip", "warning", "important", "caution", "danger"];
|
|
15
16
|
export type AdmonitionType = (typeof ADMONITION_TYPES)[number];
|
|
16
17
|
/**
|
|
17
18
|
* Admonition extension for marked
|
|
@@ -24,6 +25,12 @@ export type AdmonitionType = (typeof ADMONITION_TYPES)[number];
|
|
|
24
25
|
* :::warning Title Here
|
|
25
26
|
* This is a warning with a custom title
|
|
26
27
|
* :::
|
|
28
|
+
*
|
|
29
|
+
* :::tip[Title Here]
|
|
30
|
+
* The bracketed title form, as written for Docusaurus (remark-directive)
|
|
31
|
+
* :::
|
|
32
|
+
*
|
|
33
|
+
* Titles are inline markdown, so `code` and *emphasis* render.
|
|
27
34
|
*/
|
|
28
35
|
export declare function admonitionExtension(): MarkedExtension;
|
|
29
36
|
/**
|
|
@@ -69,7 +76,22 @@ export declare function definitionListExtension(): MarkedExtension;
|
|
|
69
76
|
* ```
|
|
70
77
|
*/
|
|
71
78
|
export declare function mermaidExtension(): MarkedExtension;
|
|
79
|
+
/**
|
|
80
|
+
* Component placeholder extension for marked
|
|
81
|
+
*
|
|
82
|
+
* Syntax (on a line of its own, string props only):
|
|
83
|
+
* <RequestForm id="ai-use-request" />
|
|
84
|
+
*
|
|
85
|
+
* Renders an empty placeholder that `mountComponents` fills with the real component:
|
|
86
|
+
* <div data-component="RequestForm" data-props="{"id":"ai-use-request"}"></div>
|
|
87
|
+
*
|
|
88
|
+
* Only names in `names` are recognised, so any other tag stays ordinary HTML. This lets
|
|
89
|
+
* MDX-style pages render without an MDX compiler.
|
|
90
|
+
*/
|
|
91
|
+
export declare function componentExtension(names: readonly string[]): MarkedExtension;
|
|
72
92
|
/**
|
|
73
93
|
* Get all markdown extensions
|
|
94
|
+
*
|
|
95
|
+
* @param components - Names of components to render as placeholders (see `componentExtension`)
|
|
74
96
|
*/
|
|
75
|
-
export declare function getAllExtensions(): MarkedExtension[];
|
|
97
|
+
export declare function getAllExtensions(components?: readonly string[]): MarkedExtension[];
|
|
@@ -2,15 +2,27 @@
|
|
|
2
2
|
* Marked Extensions for Enhanced Markdown Features
|
|
3
3
|
*
|
|
4
4
|
* Provides support for:
|
|
5
|
-
* - Admonitions/Callouts (note, tip, warning, important, caution)
|
|
5
|
+
* - Admonitions/Callouts (note, info, tip, warning, important, caution, danger)
|
|
6
6
|
* - Footnotes
|
|
7
7
|
* - Definition Lists
|
|
8
8
|
* - Mermaid diagram placeholders (rendered client-side)
|
|
9
|
+
* - Component placeholders for allowlisted `<Name prop="…" />` tags (mounted client-side)
|
|
9
10
|
*/
|
|
11
|
+
import { escapeHtml } from '../highlighter/index.js';
|
|
10
12
|
/**
|
|
11
13
|
* Admonition types and their corresponding icons/classes
|
|
12
14
|
*/
|
|
13
|
-
export const ADMONITION_TYPES = [
|
|
15
|
+
export const ADMONITION_TYPES = [
|
|
16
|
+
'note',
|
|
17
|
+
'info',
|
|
18
|
+
'tip',
|
|
19
|
+
'warning',
|
|
20
|
+
'important',
|
|
21
|
+
'caution',
|
|
22
|
+
'danger',
|
|
23
|
+
];
|
|
24
|
+
/** `:::type`, then an optional `[Title]` or ` Title`, the body, and a closing `:::`. */
|
|
25
|
+
const ADMONITION_PATTERN = new RegExp(`^:::(${ADMONITION_TYPES.join('|')})(?:\\[([^\\]\\n]*)\\]|[ \\t]+([^\\n]*))?[ \\t]*\\n([\\s\\S]*?)\\n:::`);
|
|
14
26
|
/**
|
|
15
27
|
* Admonition extension for marked
|
|
16
28
|
*
|
|
@@ -22,6 +34,12 @@ export const ADMONITION_TYPES = ['note', 'tip', 'warning', 'important', 'caution
|
|
|
22
34
|
* :::warning Title Here
|
|
23
35
|
* This is a warning with a custom title
|
|
24
36
|
* :::
|
|
37
|
+
*
|
|
38
|
+
* :::tip[Title Here]
|
|
39
|
+
* The bracketed title form, as written for Docusaurus (remark-directive)
|
|
40
|
+
* :::
|
|
41
|
+
*
|
|
42
|
+
* Titles are inline markdown, so `code` and *emphasis* render.
|
|
25
43
|
*/
|
|
26
44
|
export function admonitionExtension() {
|
|
27
45
|
return {
|
|
@@ -33,18 +51,22 @@ export function admonitionExtension() {
|
|
|
33
51
|
return src.match(/^:::/)?.index;
|
|
34
52
|
},
|
|
35
53
|
tokenizer(src) {
|
|
36
|
-
// Match :::type optional
|
|
37
|
-
const match = src.match(
|
|
54
|
+
// Match :::type[optional title] or :::type optional title, then content, then :::
|
|
55
|
+
const match = src.match(ADMONITION_PATTERN);
|
|
38
56
|
if (match) {
|
|
57
|
+
const type = match[1];
|
|
58
|
+
const title = (match[2] ?? match[3])?.trim() || type.charAt(0).toUpperCase() + type.slice(1);
|
|
39
59
|
const token = {
|
|
40
60
|
type: 'admonition',
|
|
41
61
|
raw: match[0],
|
|
42
|
-
admonitionType:
|
|
43
|
-
title
|
|
44
|
-
|
|
62
|
+
admonitionType: type,
|
|
63
|
+
title,
|
|
64
|
+
titleTokens: [],
|
|
65
|
+
content: match[4].trim(),
|
|
45
66
|
tokens: [],
|
|
46
67
|
};
|
|
47
|
-
// Parse the inner content as markdown
|
|
68
|
+
// Parse the title as inline markdown and the inner content as block markdown
|
|
69
|
+
this.lexer.inlineTokens(title, token.titleTokens);
|
|
48
70
|
this.lexer.blockTokens(token.content, token.tokens);
|
|
49
71
|
return token;
|
|
50
72
|
}
|
|
@@ -52,17 +74,20 @@ export function admonitionExtension() {
|
|
|
52
74
|
},
|
|
53
75
|
renderer(token) {
|
|
54
76
|
const innerHtml = this.parser.parse(token.tokens ?? []);
|
|
77
|
+
const titleHtml = this.parser.parseInline(token.titleTokens ?? []);
|
|
55
78
|
const iconMap = {
|
|
56
79
|
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>`,
|
|
80
|
+
info: `<svg class="admonition-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`,
|
|
57
81
|
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
82
|
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
83
|
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
84
|
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>`,
|
|
85
|
+
danger: `<svg class="admonition-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>`,
|
|
61
86
|
};
|
|
62
87
|
return `<div class="admonition admonition-${token.admonitionType}">
|
|
63
88
|
<div class="admonition-heading">
|
|
64
89
|
${iconMap[token.admonitionType]}
|
|
65
|
-
<span class="admonition-title">${
|
|
90
|
+
<span class="admonition-title">${titleHtml}</span>
|
|
66
91
|
</div>
|
|
67
92
|
<div class="admonition-content">${innerHtml}</div>
|
|
68
93
|
</div>`;
|
|
@@ -298,14 +323,57 @@ export function mermaidExtension() {
|
|
|
298
323
|
],
|
|
299
324
|
};
|
|
300
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Component placeholder extension for marked
|
|
328
|
+
*
|
|
329
|
+
* Syntax (on a line of its own, string props only):
|
|
330
|
+
* <RequestForm id="ai-use-request" />
|
|
331
|
+
*
|
|
332
|
+
* Renders an empty placeholder that `mountComponents` fills with the real component:
|
|
333
|
+
* <div data-component="RequestForm" data-props="{"id":"ai-use-request"}"></div>
|
|
334
|
+
*
|
|
335
|
+
* Only names in `names` are recognised, so any other tag stays ordinary HTML. This lets
|
|
336
|
+
* MDX-style pages render without an MDX compiler.
|
|
337
|
+
*/
|
|
338
|
+
export function componentExtension(names) {
|
|
339
|
+
const allowed = new Set(names);
|
|
340
|
+
return {
|
|
341
|
+
extensions: [
|
|
342
|
+
{
|
|
343
|
+
name: 'component',
|
|
344
|
+
level: 'block',
|
|
345
|
+
start(src) {
|
|
346
|
+
return src.match(/^ {0,3}<[A-Z]/m)?.index;
|
|
347
|
+
},
|
|
348
|
+
tokenizer(src) {
|
|
349
|
+
const match = src.match(/^ {0,3}<([A-Z][A-Za-z0-9]*)((?:\s+[A-Za-z][\w-]*="[^"]*")*)\s*\/>[ \t]*(?:\n+|$)/);
|
|
350
|
+
if (!match || !allowed.has(match[1]))
|
|
351
|
+
return undefined;
|
|
352
|
+
const props = {};
|
|
353
|
+
for (const [, key, value] of match[2].matchAll(/([A-Za-z][\w-]*)="([^"]*)"/g)) {
|
|
354
|
+
props[key] = value;
|
|
355
|
+
}
|
|
356
|
+
return { type: 'component', raw: match[0], name: match[1], props };
|
|
357
|
+
},
|
|
358
|
+
renderer(token) {
|
|
359
|
+
const props = escapeHtml(JSON.stringify(token.props));
|
|
360
|
+
return `<div data-component="${token.name}" data-props="${props}"></div>\n`;
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
301
366
|
/**
|
|
302
367
|
* Get all markdown extensions
|
|
368
|
+
*
|
|
369
|
+
* @param components - Names of components to render as placeholders (see `componentExtension`)
|
|
303
370
|
*/
|
|
304
|
-
export function getAllExtensions() {
|
|
371
|
+
export function getAllExtensions(components = []) {
|
|
305
372
|
return [
|
|
306
373
|
admonitionExtension(),
|
|
307
374
|
footnoteExtension(),
|
|
308
375
|
definitionListExtension(),
|
|
309
376
|
mermaidExtension(),
|
|
377
|
+
...(components.length > 0 ? [componentExtension(components)] : []),
|
|
310
378
|
];
|
|
311
379
|
}
|
|
@@ -6,7 +6,9 @@ import type { ParsedMarkdown, ParseOptions } from '../types/index.js';
|
|
|
6
6
|
* applies Shiki syntax highlighting to code blocks, and optionally generates heading IDs.
|
|
7
7
|
*
|
|
8
8
|
* Supported extensions:
|
|
9
|
-
* - Admonitions/Callouts: :::note, :::tip, :::warning, :::important, :::caution
|
|
9
|
+
* - Admonitions/Callouts: :::note, :::info, :::tip, :::warning, :::important, :::caution, :::danger
|
|
10
|
+
* - Explicit heading IDs: ## Heading {#custom-id}
|
|
11
|
+
* - Component placeholders: <RequestForm id="x" /> when "RequestForm" is in options.components
|
|
10
12
|
* - Footnotes: [^1] references and [^1]: definitions
|
|
11
13
|
* - Definition Lists: Term followed by : Definition
|
|
12
14
|
* - Mermaid Diagrams: ```mermaid code blocks (rendered client-side)
|
|
@@ -15,6 +17,8 @@ import type { ParsedMarkdown, ParseOptions } from '../types/index.js';
|
|
|
15
17
|
* @param options - Parsing options
|
|
16
18
|
* @param options.theme - Shiki theme for code highlighting ('github-dark' | 'github-light' | 'one-dark-pro')
|
|
17
19
|
* @param options.generateHeadingIds - Whether to generate IDs for headings (default: true)
|
|
20
|
+
* @param options.headingIdStyle - 'default', or 'github' for GitHub/Docusaurus-compatible IDs
|
|
21
|
+
* @param options.components - Tag names to render as component placeholders (see `mountComponents`)
|
|
18
22
|
* @returns Parsed markdown with frontmatter data, cleaned markdown, and rendered HTML
|
|
19
23
|
*
|
|
20
24
|
* @example
|
package/dist/lib/parser/index.js
CHANGED
|
@@ -2,6 +2,16 @@ import { Marked, Renderer } from 'marked';
|
|
|
2
2
|
import yaml from 'js-yaml';
|
|
3
3
|
import { getHighlighter, escapeHtml } from '../highlighter/index.js';
|
|
4
4
|
import { getAllExtensions, resetFootnoteStore, renderFootnotes } from './extensions.js';
|
|
5
|
+
import { createSlugger, EXPLICIT_ID_PATTERN } from './slug.js';
|
|
6
|
+
/** Undo the entity escaping marked applies to heading text, so `A & B` slugs as `A & B`. */
|
|
7
|
+
function decodeEntities(text) {
|
|
8
|
+
return text
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/�?39;/g, "'")
|
|
13
|
+
.replace(/&/g, '&');
|
|
14
|
+
}
|
|
5
15
|
/**
|
|
6
16
|
* SECURITY NOTE: This parser renders markdown to HTML without sanitization.
|
|
7
17
|
* Only use with trusted markdown content from your own codebase or CMS.
|
|
@@ -34,7 +44,9 @@ function extractFrontmatter(content) {
|
|
|
34
44
|
* applies Shiki syntax highlighting to code blocks, and optionally generates heading IDs.
|
|
35
45
|
*
|
|
36
46
|
* Supported extensions:
|
|
37
|
-
* - Admonitions/Callouts: :::note, :::tip, :::warning, :::important, :::caution
|
|
47
|
+
* - Admonitions/Callouts: :::note, :::info, :::tip, :::warning, :::important, :::caution, :::danger
|
|
48
|
+
* - Explicit heading IDs: ## Heading {#custom-id}
|
|
49
|
+
* - Component placeholders: <RequestForm id="x" /> when "RequestForm" is in options.components
|
|
38
50
|
* - Footnotes: [^1] references and [^1]: definitions
|
|
39
51
|
* - Definition Lists: Term followed by : Definition
|
|
40
52
|
* - Mermaid Diagrams: ```mermaid code blocks (rendered client-side)
|
|
@@ -43,6 +55,8 @@ function extractFrontmatter(content) {
|
|
|
43
55
|
* @param options - Parsing options
|
|
44
56
|
* @param options.theme - Shiki theme for code highlighting ('github-dark' | 'github-light' | 'one-dark-pro')
|
|
45
57
|
* @param options.generateHeadingIds - Whether to generate IDs for headings (default: true)
|
|
58
|
+
* @param options.headingIdStyle - 'default', or 'github' for GitHub/Docusaurus-compatible IDs
|
|
59
|
+
* @param options.components - Tag names to render as component placeholders (see `mountComponents`)
|
|
46
60
|
* @returns Parsed markdown with frontmatter data, cleaned markdown, and rendered HTML
|
|
47
61
|
*
|
|
48
62
|
* @example
|
|
@@ -61,7 +75,7 @@ function extractFrontmatter(content) {
|
|
|
61
75
|
* ```
|
|
62
76
|
*/
|
|
63
77
|
export async function parseMarkdown(content, options = {}) {
|
|
64
|
-
const { theme = 'github-dark', generateHeadingIds = true } = options;
|
|
78
|
+
const { theme = 'github-dark', generateHeadingIds = true, headingIdStyle = 'default', components = [], } = options;
|
|
65
79
|
// Extract frontmatter
|
|
66
80
|
const { data: frontmatter, content: markdownContent } = extractFrontmatter(content);
|
|
67
81
|
// Get Shiki highlighter
|
|
@@ -89,16 +103,15 @@ export async function parseMarkdown(content, options = {}) {
|
|
|
89
103
|
}
|
|
90
104
|
};
|
|
91
105
|
if (generateHeadingIds) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
99
|
-
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
106
|
+
const slug = createSlugger(headingIdStyle);
|
|
107
|
+
renderer.heading = function ({ tokens, depth }) {
|
|
108
|
+
// Slug the heading's text, not its markdown: `## Use **sudo**` → `use-sudo`
|
|
109
|
+
const plain = this.parser.parseInline(tokens, this.parser.textRenderer);
|
|
110
|
+
const explicitId = plain.match(EXPLICIT_ID_PATTERN)?.[1];
|
|
111
|
+
const id = slug(decodeEntities(plain.replace(EXPLICIT_ID_PATTERN, '')), explicitId);
|
|
100
112
|
// Render inline tokens so markdown inside headings (bold, code, links) works
|
|
101
|
-
|
|
113
|
+
const inner = this.parser.parseInline(tokens).replace(EXPLICIT_ID_PATTERN, '');
|
|
114
|
+
return `<h${depth} id="${escapeHtml(id)}">${inner}</h${depth}>`;
|
|
102
115
|
};
|
|
103
116
|
}
|
|
104
117
|
// Use a per-call Marked instance: the renderer captures per-call state
|
|
@@ -106,7 +119,7 @@ export async function parseMarkdown(content, options = {}) {
|
|
|
106
119
|
// accumulates extensions across calls.
|
|
107
120
|
const md = new Marked();
|
|
108
121
|
// Apply all markdown extensions (admonitions, footnotes, definition lists, mermaid)
|
|
109
|
-
md.use(...getAllExtensions());
|
|
122
|
+
md.use(...getAllExtensions(components));
|
|
110
123
|
md.use({ renderer, gfm: true });
|
|
111
124
|
let html = await md.parse(markdownContent);
|
|
112
125
|
// Append footnotes section if any were referenced
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading ID generation.
|
|
3
|
+
*
|
|
4
|
+
* Two styles:
|
|
5
|
+
* - `default`: this package's original slugs (periods become hyphens, so `v2.0` → `v2-0`).
|
|
6
|
+
* - `github`: GitHub's algorithm (github-slugger), which Docusaurus, VitePress and GitHub
|
|
7
|
+
* itself use. Pick it when migrating content whose `#anchor` links must keep working.
|
|
8
|
+
*
|
|
9
|
+
* Both styles dedupe repeated headings with a `-1`, `-2`… suffix (an explicit ID is never
|
|
10
|
+
* suffixed), and both honour an explicit `{#custom-id}` at the end of a heading.
|
|
11
|
+
*/
|
|
12
|
+
export type HeadingIdStyle = 'default' | 'github';
|
|
13
|
+
/** Trailing `{#custom-id}` on a heading, as Docusaurus and Pandoc write it. */
|
|
14
|
+
export declare const EXPLICIT_ID_PATTERN: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* Create a slugger for one document. IDs are unique within it: the second
|
|
17
|
+
* `## Setup` becomes `setup-1`, matching github-slugger.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createSlugger(style?: HeadingIdStyle): (text: string, explicitId?: string) => string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading ID generation.
|
|
3
|
+
*
|
|
4
|
+
* Two styles:
|
|
5
|
+
* - `default`: this package's original slugs (periods become hyphens, so `v2.0` → `v2-0`).
|
|
6
|
+
* - `github`: GitHub's algorithm (github-slugger), which Docusaurus, VitePress and GitHub
|
|
7
|
+
* itself use. Pick it when migrating content whose `#anchor` links must keep working.
|
|
8
|
+
*
|
|
9
|
+
* Both styles dedupe repeated headings with a `-1`, `-2`… suffix (an explicit ID is never
|
|
10
|
+
* suffixed), and both honour an explicit `{#custom-id}` at the end of a heading.
|
|
11
|
+
*/
|
|
12
|
+
/** Trailing `{#custom-id}` on a heading, as Docusaurus and Pandoc write it. */
|
|
13
|
+
export const EXPLICIT_ID_PATTERN = /\s*\{#([^}\s]+)\}\s*$/;
|
|
14
|
+
function defaultSlug(text) {
|
|
15
|
+
return text
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/\./g, '-') // Convert periods to hyphens (preserves version numbers like v2.0 → v2-0)
|
|
18
|
+
.replace(/[^\w\s-]+/g, '') // Remove other special characters
|
|
19
|
+
.replace(/\s+/g, '-') // Convert spaces to hyphens
|
|
20
|
+
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
21
|
+
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* github-slugger: lowercase, drop everything but letters, marks, numbers, connector
|
|
25
|
+
* punctuation, hyphens and spaces, then turn each space into a hyphen. Hyphens are
|
|
26
|
+
* deliberately not collapsed — `A & B` is `a--b` on GitHub too.
|
|
27
|
+
*/
|
|
28
|
+
function githubSlug(text) {
|
|
29
|
+
return text
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^\p{L}\p{M}\p{N}\p{Pc} -]/gu, '')
|
|
32
|
+
.replace(/ /g, '-');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a slugger for one document. IDs are unique within it: the second
|
|
36
|
+
* `## Setup` becomes `setup-1`, matching github-slugger.
|
|
37
|
+
*/
|
|
38
|
+
export function createSlugger(style = 'default') {
|
|
39
|
+
const slugify = style === 'github' ? githubSlug : defaultSlug;
|
|
40
|
+
const seen = new Map();
|
|
41
|
+
return function slug(text, explicitId) {
|
|
42
|
+
// An explicit ID is the author's choice: used verbatim and not counted, as in Docusaurus.
|
|
43
|
+
if (explicitId)
|
|
44
|
+
return explicitId;
|
|
45
|
+
const base = slugify(text);
|
|
46
|
+
let id = base;
|
|
47
|
+
let count = seen.get(base) ?? 0;
|
|
48
|
+
while (seen.has(id)) {
|
|
49
|
+
id = `${base}-${++count}`;
|
|
50
|
+
}
|
|
51
|
+
seen.set(base, count);
|
|
52
|
+
seen.set(id, 0);
|
|
53
|
+
return id;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
.markdown-content h1 {
|
|
36
36
|
font-size: 1.875rem;
|
|
37
37
|
line-height: 2.25rem;
|
|
38
|
-
font-weight:
|
|
38
|
+
font-weight: 500; /* Spectral italic is loaded at 400/500/600 only */
|
|
39
39
|
color: hsl(var(--foreground));
|
|
40
40
|
margin-top: 2rem;
|
|
41
41
|
margin-bottom: 1rem;
|
|
@@ -321,6 +321,17 @@
|
|
|
321
321
|
color: hsl(210 100% 40%);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/* Info - Sky */
|
|
325
|
+
.markdown-content .admonition-info {
|
|
326
|
+
border-color: hsl(199 89% 48% / 0.3);
|
|
327
|
+
background: hsl(199 89% 48% / 0.05);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.markdown-content .admonition-info .admonition-heading {
|
|
331
|
+
background: hsl(199 89% 48% / 0.1);
|
|
332
|
+
color: hsl(199 89% 32%);
|
|
333
|
+
}
|
|
334
|
+
|
|
324
335
|
/* Tip - Green */
|
|
325
336
|
.markdown-content .admonition-tip {
|
|
326
337
|
border-color: hsl(142 71% 45% / 0.3);
|
|
@@ -365,6 +376,17 @@
|
|
|
365
376
|
color: hsl(0 84% 45%);
|
|
366
377
|
}
|
|
367
378
|
|
|
379
|
+
/* Danger - Deep red */
|
|
380
|
+
.markdown-content .admonition-danger {
|
|
381
|
+
border-color: hsl(0 72% 42% / 0.45);
|
|
382
|
+
background: hsl(0 72% 42% / 0.08);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.markdown-content .admonition-danger .admonition-heading {
|
|
386
|
+
background: hsl(0 72% 42% / 0.16);
|
|
387
|
+
color: hsl(0 72% 35%);
|
|
388
|
+
}
|
|
389
|
+
|
|
368
390
|
/* ==========================================================================
|
|
369
391
|
Footnotes
|
|
370
392
|
========================================================================== */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Frontmatter Types for Documentation
|
|
3
3
|
*/
|
|
4
|
+
import type { HeadingIdStyle } from '../parser/slug.js';
|
|
4
5
|
/** Author information as an object */
|
|
5
6
|
export interface AuthorInfo {
|
|
6
7
|
/** Author's display name */
|
|
@@ -56,6 +57,16 @@ export interface ParseOptions {
|
|
|
56
57
|
theme?: string;
|
|
57
58
|
/** Whether to generate heading IDs */
|
|
58
59
|
generateHeadingIds?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Heading ID algorithm. `github` matches GitHub and Docusaurus (github-slugger), for
|
|
62
|
+
* content whose existing `#anchor` links must keep working. Default: `default`.
|
|
63
|
+
*/
|
|
64
|
+
headingIdStyle?: HeadingIdStyle;
|
|
65
|
+
/**
|
|
66
|
+
* Component names to render as placeholders when written as a self-closing tag on a
|
|
67
|
+
* line of its own (`<RequestForm id="x" />`). Mount them with `mountComponents`.
|
|
68
|
+
*/
|
|
69
|
+
components?: readonly string[];
|
|
59
70
|
}
|
|
60
71
|
/** MarkdownPage component props */
|
|
61
72
|
export interface MarkdownPageProps {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classic-homes/theme-docs",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Markdown documentation components for the Classic theme system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/lib/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@classic-homes/theme-tokens": "*",
|
|
57
57
|
"clsx": "^2.1.0",
|
|
58
|
-
"js-yaml": "^4.
|
|
58
|
+
"js-yaml": "^4.3.2",
|
|
59
59
|
"marked": "^17.0.0",
|
|
60
60
|
"shiki": "^1.0.0",
|
|
61
61
|
"tailwind-merge": "^2.2.0",
|