@classic-homes/theme-docs 0.0.50 → 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.
@@ -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
+ }
@@ -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="{&quot;id&quot;:&quot;ai-use-request&quot;}"></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 = ['note', 'tip', 'warning', 'important', 'caution'];
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-title\ncontent\n:::
37
- const match = src.match(/^:::(note|tip|warning|important|caution)(?:\s+([^\n]*))?\n([\s\S]*?)\n:::/);
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: match[1],
43
- title: match[2]?.trim() || match[1].charAt(0).toUpperCase() + match[1].slice(1),
44
- content: match[3].trim(),
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">${token.title}</span>
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="{&quot;id&quot;:&quot;ai-use-request&quot;}"></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
@@ -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 &amp; B` slugs as `A & B`. */
7
+ function decodeEntities(text) {
8
+ return text
9
+ .replace(/&lt;/g, '<')
10
+ .replace(/&gt;/g, '>')
11
+ .replace(/&quot;/g, '"')
12
+ .replace(/&#0?39;/g, "'")
13
+ .replace(/&amp;/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
- renderer.heading = function ({ tokens, 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
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
- return `<h${depth} id="${id}">${this.parser.parseInline(tokens)}</h${depth}>`;
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: 700;
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.50",
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",