@classic-homes/theme-docs 0.0.50 → 0.2.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/Breadcrumbs.svelte +55 -0
- package/dist/lib/components/Breadcrumbs.svelte.d.ts +14 -0
- package/dist/lib/components/CategoryIndex.svelte +51 -0
- package/dist/lib/components/CategoryIndex.svelte.d.ts +13 -0
- package/dist/lib/components/DocPage.svelte +172 -0
- package/dist/lib/components/DocPage.svelte.d.ts +60 -0
- package/dist/lib/components/DocPager.svelte +49 -0
- package/dist/lib/components/DocPager.svelte.d.ts +12 -0
- package/dist/lib/components/MarkdownPage.svelte +3 -1
- package/dist/lib/components/MermaidDiagram.svelte +2 -0
- package/dist/lib/components/MermaidInit.svelte +2 -0
- package/dist/lib/components/TableOfContents.svelte +114 -125
- package/dist/lib/components/TableOfContents.svelte.d.ts +11 -4
- package/dist/lib/components/TagIndex.svelte +42 -0
- package/dist/lib/components/TagIndex.svelte.d.ts +14 -0
- package/dist/lib/components/TagList.svelte +45 -0
- package/dist/lib/components/TagList.svelte.d.ts +15 -0
- package/dist/lib/components/TocPanel.svelte +27 -9
- package/dist/lib/components/TocPanel.svelte.d.ts +10 -3
- package/dist/lib/components/enhance.d.ts +29 -0
- package/dist/lib/components/enhance.js +179 -0
- package/dist/lib/components/mount.d.ts +14 -0
- package/dist/lib/components/mount.js +36 -0
- package/dist/lib/components/sidebar.d.ts +33 -0
- package/dist/lib/components/sidebar.js +84 -0
- package/dist/lib/content/browser.d.ts +6 -0
- package/dist/lib/content/browser.js +5 -0
- package/dist/lib/content/index.d.ts +13 -0
- package/dist/lib/content/index.js +12 -0
- package/dist/lib/content/load.d.ts +77 -0
- package/dist/lib/content/load.js +366 -0
- package/dist/lib/content/nav.d.ts +36 -0
- package/dist/lib/content/nav.js +81 -0
- package/dist/lib/content/render.d.ts +38 -0
- package/dist/lib/content/render.js +84 -0
- package/dist/lib/content/types.d.ts +90 -0
- package/dist/lib/content/types.js +5 -0
- package/dist/lib/index.d.ts +16 -2
- package/dist/lib/index.js +16 -2
- package/dist/lib/parser/api.d.ts +12 -0
- package/dist/lib/parser/api.js +10 -0
- package/dist/lib/parser/extensions.d.ts +51 -17
- package/dist/lib/parser/extensions.js +133 -60
- package/dist/lib/parser/index.d.ts +9 -2
- package/dist/lib/parser/index.js +125 -35
- package/dist/lib/parser/slug.d.ts +19 -0
- package/dist/lib/parser/slug.js +55 -0
- package/dist/lib/sanitize/index.d.ts +11 -0
- package/dist/lib/sanitize/index.js +122 -0
- package/dist/lib/search/index.d.ts +57 -0
- package/dist/lib/search/index.js +82 -0
- package/dist/lib/styles/markdown.css +161 -1
- package/dist/lib/types/frontmatter.d.ts +32 -0
- package/dist/lib/vite/index.d.ts +17 -0
- package/dist/lib/vite/index.js +38 -0
- package/package.json +51 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface SanitizeResult {
|
|
2
|
+
/** The cleaned HTML */
|
|
3
|
+
html: string;
|
|
4
|
+
/**
|
|
5
|
+
* What was removed, e.g. `<script>`, `p[onclick]`, `a[href=javascript:]`. Report these
|
|
6
|
+
* from a build so a page that loses markup says so instead of quietly rendering differently.
|
|
7
|
+
*/
|
|
8
|
+
removed: string[];
|
|
9
|
+
}
|
|
10
|
+
/** Sanitize one page's rendered HTML. */
|
|
11
|
+
export declare function sanitizeDocsHtml(html: string): SanitizeResult;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@classic-homes/theme-docs/sanitize` — allowlist sanitizer for HTML from `parseMarkdown`.
|
|
3
|
+
*
|
|
4
|
+
* `parseMarkdown` passes raw HTML in markdown straight through. When pages are written by
|
|
5
|
+
* more than a few trusted people (a git repo with many authors, a browser editor), run its
|
|
6
|
+
* output through `sanitizeDocsHtml` so markup cannot run script, embed frames or set event
|
|
7
|
+
* handlers for every reader.
|
|
8
|
+
*
|
|
9
|
+
* The allowlist is what the parser itself produces — GFM tables and task lists, footnotes,
|
|
10
|
+
* definition lists, Shiki highlighting, admonition icons, heading permalinks, mermaid and
|
|
11
|
+
* component placeholders — plus the safe inline HTML authors commonly write
|
|
12
|
+
* (details/summary, kbd, sub/sup, br, mark).
|
|
13
|
+
*
|
|
14
|
+
* Server/build-time only: it depends on `sanitize-html` (and through it, postcss), which
|
|
15
|
+
* you don't want in a browser bundle.
|
|
16
|
+
*/
|
|
17
|
+
import sanitizeHtml from 'sanitize-html';
|
|
18
|
+
const SVG = ['svg', 'path', 'circle', 'line', 'polyline', 'polygon', 'rect', 'g'];
|
|
19
|
+
const OPTIONS = {
|
|
20
|
+
allowedTags: [
|
|
21
|
+
...sanitizeHtml.defaults.allowedTags, // p, a, lists, tables, code, pre, headings, blockquote, …
|
|
22
|
+
'img',
|
|
23
|
+
'details',
|
|
24
|
+
'summary',
|
|
25
|
+
'kbd',
|
|
26
|
+
'sup',
|
|
27
|
+
'sub',
|
|
28
|
+
'del',
|
|
29
|
+
's',
|
|
30
|
+
'ins',
|
|
31
|
+
'mark',
|
|
32
|
+
'dl',
|
|
33
|
+
'dt',
|
|
34
|
+
'dd',
|
|
35
|
+
'section',
|
|
36
|
+
'input',
|
|
37
|
+
'br',
|
|
38
|
+
'hr',
|
|
39
|
+
...SVG,
|
|
40
|
+
],
|
|
41
|
+
allowedAttributes: {
|
|
42
|
+
'*': ['class', 'id', 'title', 'aria-hidden', 'aria-label', 'role'],
|
|
43
|
+
a: ['href', 'name', 'target', 'rel'],
|
|
44
|
+
img: ['src', 'alt', 'width', 'height', 'loading', 'decoding'],
|
|
45
|
+
pre: ['style', 'tabindex'],
|
|
46
|
+
span: ['style'],
|
|
47
|
+
code: ['style'],
|
|
48
|
+
div: ['data-component', 'data-props', 'data-mermaid'],
|
|
49
|
+
input: ['type', 'checked', 'disabled'],
|
|
50
|
+
ol: ['start', 'type'],
|
|
51
|
+
td: ['align', 'colspan', 'rowspan'],
|
|
52
|
+
th: ['align', 'colspan', 'rowspan', 'scope'],
|
|
53
|
+
details: ['open'],
|
|
54
|
+
svg: [
|
|
55
|
+
'viewBox',
|
|
56
|
+
'fill',
|
|
57
|
+
'stroke',
|
|
58
|
+
'stroke-width',
|
|
59
|
+
'stroke-linecap',
|
|
60
|
+
'stroke-linejoin',
|
|
61
|
+
'width',
|
|
62
|
+
'height',
|
|
63
|
+
'xmlns',
|
|
64
|
+
],
|
|
65
|
+
path: ['d', 'fill', 'stroke'],
|
|
66
|
+
circle: ['cx', 'cy', 'r', 'fill', 'stroke'],
|
|
67
|
+
line: ['x1', 'x2', 'y1', 'y2'],
|
|
68
|
+
polyline: ['points'],
|
|
69
|
+
polygon: ['points'],
|
|
70
|
+
rect: ['x', 'y', 'width', 'height', 'rx', 'ry'],
|
|
71
|
+
},
|
|
72
|
+
// Shiki colours code by inline style; nothing else may set styles.
|
|
73
|
+
allowedStyles: {
|
|
74
|
+
'*': {
|
|
75
|
+
color: [/^#[0-9a-f]{3,8}$/i, /^var\(--[\w-]+\)$/],
|
|
76
|
+
'background-color': [/^#[0-9a-f]{3,8}$/i, /^var\(--[\w-]+\)$/],
|
|
77
|
+
'font-style': [/^italic$/],
|
|
78
|
+
'font-weight': [/^(bold|\d{3})$/],
|
|
79
|
+
'text-decoration': [/^(underline|line-through)$/],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
|
|
83
|
+
allowProtocolRelative: false,
|
|
84
|
+
// Keep the text of a dropped element (e.g. an unknown wrapper) but never of script/style.
|
|
85
|
+
disallowedTagsMode: 'discard',
|
|
86
|
+
nonTextTags: ['script', 'style', 'textarea', 'option', 'noscript', 'iframe', 'object', 'embed'],
|
|
87
|
+
parser: { lowerCaseAttributeNames: false },
|
|
88
|
+
};
|
|
89
|
+
const SVG_TAGS = new Set(SVG);
|
|
90
|
+
const allowedTags = new Set(OPTIONS.allowedTags);
|
|
91
|
+
const allowedAttributes = OPTIONS.allowedAttributes;
|
|
92
|
+
const allowedAttrs = (tag) => [
|
|
93
|
+
...allowedAttributes['*'],
|
|
94
|
+
...(allowedAttributes[tag] ?? []),
|
|
95
|
+
];
|
|
96
|
+
/** Sanitize one page's rendered HTML. */
|
|
97
|
+
export function sanitizeDocsHtml(html) {
|
|
98
|
+
const removed = new Set();
|
|
99
|
+
const clean = sanitizeHtml(html, {
|
|
100
|
+
...OPTIONS,
|
|
101
|
+
transformTags: {
|
|
102
|
+
'*': (tagName, rawAttribs) => {
|
|
103
|
+
// HTML attribute names are case-insensitive, and content written as MDX uses JSX
|
|
104
|
+
// casing (`colSpan`, `rowSpan`). Lowercase them so the allowlist matches; SVG keeps
|
|
105
|
+
// its case-sensitive names (`viewBox`).
|
|
106
|
+
const attribs = SVG_TAGS.has(tagName)
|
|
107
|
+
? rawAttribs
|
|
108
|
+
: Object.fromEntries(Object.entries(rawAttribs).map(([name, value]) => [name.toLowerCase(), value]));
|
|
109
|
+
if (!allowedTags.has(tagName))
|
|
110
|
+
removed.add(`<${tagName}>`);
|
|
111
|
+
else
|
|
112
|
+
for (const name of Object.keys(attribs))
|
|
113
|
+
if (!allowedAttrs(tagName).includes(name))
|
|
114
|
+
removed.add(`${tagName}[${name}]`);
|
|
115
|
+
if (attribs.href && /^\s*(javascript|data|vbscript):/i.test(attribs.href))
|
|
116
|
+
removed.add(`${tagName}[href=${attribs.href.split(':')[0].trim()}:]`);
|
|
117
|
+
return { tagName, attribs };
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return { html: clean, removed: [...removed] };
|
|
122
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@classic-homes/theme-docs/search` — full-text search over rendered docs (MiniSearch).
|
|
3
|
+
*
|
|
4
|
+
* Build the index at build time with `buildSearchIndex`, ship its JSON, and query it on
|
|
5
|
+
* the server or in the browser with `createSearch`. A `filter` runs inside MiniSearch,
|
|
6
|
+
* before results and snippets are built, so access rules can drop hits a reader may not
|
|
7
|
+
* see without their text ever leaving the index.
|
|
8
|
+
*/
|
|
9
|
+
import { type Options } from 'minisearch';
|
|
10
|
+
import type { RenderedDoc } from '../content/types.js';
|
|
11
|
+
/** Fields stored with each hit, available to `filter` and in results. */
|
|
12
|
+
export interface StoredFields {
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
sidebar: string | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Index options. `buildSearchIndex` and `createSearch` must use the same ones, so both
|
|
19
|
+
* read them from here.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SEARCH_OPTIONS: Options;
|
|
22
|
+
type Indexable = Pick<RenderedDoc, 'route' | 'title' | 'description' | 'sidebar' | 'toc' | 'text'>;
|
|
23
|
+
/** Index every page. Returns MiniSearch's serialized form; write it out as JSON. */
|
|
24
|
+
export declare function buildSearchIndex(pages: Indexable[]): string;
|
|
25
|
+
export interface SearchHit {
|
|
26
|
+
route: string;
|
|
27
|
+
title: string;
|
|
28
|
+
description: string;
|
|
29
|
+
sidebar: string | null;
|
|
30
|
+
/** About 160 characters of the page around the first match (or its description) */
|
|
31
|
+
snippet: string;
|
|
32
|
+
/** Terms that matched, for highlighting */
|
|
33
|
+
terms: string[];
|
|
34
|
+
score: number;
|
|
35
|
+
}
|
|
36
|
+
export interface SearchQueryOptions {
|
|
37
|
+
/** Keep a hit only when this returns true. Runs before snippets are built. */
|
|
38
|
+
filter?: (hit: StoredFields & {
|
|
39
|
+
route: string;
|
|
40
|
+
}) => boolean;
|
|
41
|
+
/** Maximum hits. Default: 12 */
|
|
42
|
+
limit?: number;
|
|
43
|
+
/** Queries shorter than this return nothing. Default: 2 */
|
|
44
|
+
minLength?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Load a serialized index for querying.
|
|
48
|
+
*
|
|
49
|
+
* @param indexJson - What `buildSearchIndex` returned (string or parsed object)
|
|
50
|
+
* @param texts - Page text by route, for snippets. Without it, snippets use descriptions.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createSearch(indexJson: string | object, texts?: Map<string, string> | Record<string, string>): {
|
|
53
|
+
search(query: string, options?: SearchQueryOptions): SearchHit[];
|
|
54
|
+
};
|
|
55
|
+
/** About 160 characters around the first matched term. */
|
|
56
|
+
export declare function snippet(text: string, terms: string[]): string;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@classic-homes/theme-docs/search` — full-text search over rendered docs (MiniSearch).
|
|
3
|
+
*
|
|
4
|
+
* Build the index at build time with `buildSearchIndex`, ship its JSON, and query it on
|
|
5
|
+
* the server or in the browser with `createSearch`. A `filter` runs inside MiniSearch,
|
|
6
|
+
* before results and snippets are built, so access rules can drop hits a reader may not
|
|
7
|
+
* see without their text ever leaving the index.
|
|
8
|
+
*/
|
|
9
|
+
import MiniSearch from 'minisearch';
|
|
10
|
+
/**
|
|
11
|
+
* Index options. `buildSearchIndex` and `createSearch` must use the same ones, so both
|
|
12
|
+
* read them from here.
|
|
13
|
+
*/
|
|
14
|
+
export const SEARCH_OPTIONS = {
|
|
15
|
+
fields: ['title', 'headings', 'description', 'text'],
|
|
16
|
+
storeFields: ['title', 'description', 'sidebar'],
|
|
17
|
+
searchOptions: {
|
|
18
|
+
boost: { title: 4, headings: 2, description: 1.5 },
|
|
19
|
+
prefix: true,
|
|
20
|
+
fuzzy: 0.15,
|
|
21
|
+
combineWith: 'AND',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
/** Index every page. Returns MiniSearch's serialized form; write it out as JSON. */
|
|
25
|
+
export function buildSearchIndex(pages) {
|
|
26
|
+
const index = new MiniSearch(SEARCH_OPTIONS);
|
|
27
|
+
index.addAll(pages.map((page) => ({
|
|
28
|
+
id: page.route,
|
|
29
|
+
title: page.title,
|
|
30
|
+
description: page.description,
|
|
31
|
+
sidebar: page.sidebar,
|
|
32
|
+
headings: page.toc.map((entry) => entry.text).join(' '),
|
|
33
|
+
text: page.text,
|
|
34
|
+
})));
|
|
35
|
+
return JSON.stringify(index);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load a serialized index for querying.
|
|
39
|
+
*
|
|
40
|
+
* @param indexJson - What `buildSearchIndex` returned (string or parsed object)
|
|
41
|
+
* @param texts - Page text by route, for snippets. Without it, snippets use descriptions.
|
|
42
|
+
*/
|
|
43
|
+
export function createSearch(indexJson, texts) {
|
|
44
|
+
const index = MiniSearch.loadJSON(typeof indexJson === 'string' ? indexJson : JSON.stringify(indexJson), SEARCH_OPTIONS);
|
|
45
|
+
const textOf = (route) => texts instanceof Map ? texts.get(route) : texts?.[route];
|
|
46
|
+
return {
|
|
47
|
+
search(query, options = {}) {
|
|
48
|
+
const { filter, limit = 12, minLength = 2 } = options;
|
|
49
|
+
const trimmed = query.trim();
|
|
50
|
+
if (trimmed.length < minLength)
|
|
51
|
+
return [];
|
|
52
|
+
const results = index.search(trimmed, {
|
|
53
|
+
...(filter && {
|
|
54
|
+
filter: (hit) => filter({ ...hit, route: String(hit.id) }),
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
return results.slice(0, limit).map((hit) => {
|
|
58
|
+
const route = String(hit.id);
|
|
59
|
+
const stored = hit;
|
|
60
|
+
return {
|
|
61
|
+
route,
|
|
62
|
+
title: stored.title,
|
|
63
|
+
description: stored.description,
|
|
64
|
+
sidebar: stored.sidebar,
|
|
65
|
+
snippet: snippet(textOf(route) ?? stored.description ?? '', hit.terms),
|
|
66
|
+
terms: hit.terms,
|
|
67
|
+
score: hit.score,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** About 160 characters around the first matched term. */
|
|
74
|
+
export function snippet(text, terms) {
|
|
75
|
+
const lower = text.toLowerCase();
|
|
76
|
+
const at = Math.min(...terms.map((term) => lower.indexOf(term.toLowerCase())).filter((i) => i >= 0));
|
|
77
|
+
if (!Number.isFinite(at))
|
|
78
|
+
return text.length > 160 ? `${text.slice(0, 160).trim()}…` : text;
|
|
79
|
+
const start = Math.max(0, at - 60);
|
|
80
|
+
const end = start + 160;
|
|
81
|
+
return `${start > 0 ? '…' : ''}${text.slice(start, end).trim()}${end < text.length ? '…' : ''}`;
|
|
82
|
+
}
|
|
@@ -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;
|
|
@@ -272,6 +272,135 @@
|
|
|
272
272
|
border-top-right-radius: 0;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/* Fence titles (```bash title="install.sh") rendered by parseMarkdown */
|
|
276
|
+
.markdown-content .code-block-filename {
|
|
277
|
+
padding: 0.5rem 1rem;
|
|
278
|
+
font-size: 0.875rem;
|
|
279
|
+
font-family:
|
|
280
|
+
ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
281
|
+
color: hsl(var(--muted-foreground));
|
|
282
|
+
background-color: hsl(var(--muted) / 0.5);
|
|
283
|
+
border: 1px solid hsl(var(--border));
|
|
284
|
+
border-bottom: 0;
|
|
285
|
+
border-top-left-radius: 0.5rem;
|
|
286
|
+
border-top-right-radius: 0.5rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Copy buttons (enhanceCodeBlocks / DocPage) */
|
|
290
|
+
.code-block-wrapper.has-copy-button {
|
|
291
|
+
position: relative;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.code-copy-button {
|
|
295
|
+
position: absolute;
|
|
296
|
+
top: 0.5rem;
|
|
297
|
+
right: 0.5rem;
|
|
298
|
+
display: inline-flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
justify-content: center;
|
|
301
|
+
width: 2rem;
|
|
302
|
+
height: 2rem;
|
|
303
|
+
padding: 0.375rem;
|
|
304
|
+
border-radius: 0.375rem;
|
|
305
|
+
border: 1px solid hsl(0 0% 100% / 0.2);
|
|
306
|
+
background-color: hsl(0 0% 0% / 0.4);
|
|
307
|
+
color: hsl(0 0% 100% / 0.85);
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
opacity: 0;
|
|
310
|
+
transition: opacity 150ms;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.code-block-filename + .shiki + .code-copy-button {
|
|
314
|
+
top: calc(0.5rem + 2.3rem);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.code-block-wrapper:hover .code-copy-button,
|
|
318
|
+
.code-copy-button:focus-visible,
|
|
319
|
+
.code-copy-button[data-copied] {
|
|
320
|
+
opacity: 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.code-copy-button:focus-visible {
|
|
324
|
+
outline: 2px solid hsl(var(--ring));
|
|
325
|
+
outline-offset: 2px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.code-copy-button svg {
|
|
329
|
+
width: 100%;
|
|
330
|
+
height: 100%;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
@media (hover: none) {
|
|
334
|
+
.code-copy-button {
|
|
335
|
+
opacity: 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.docs-sr-only {
|
|
340
|
+
position: absolute;
|
|
341
|
+
width: 1px;
|
|
342
|
+
height: 1px;
|
|
343
|
+
margin: -1px;
|
|
344
|
+
overflow: hidden;
|
|
345
|
+
clip: rect(0, 0, 0, 0);
|
|
346
|
+
white-space: nowrap;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* Search terms carried over from a search result (?highlight=) */
|
|
350
|
+
.markdown-content mark.search-highlight {
|
|
351
|
+
background-color: hsl(var(--warning) / 0.35);
|
|
352
|
+
color: inherit;
|
|
353
|
+
border-radius: 0.125rem;
|
|
354
|
+
padding: 0 0.0625rem;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ==========================================================================
|
|
358
|
+
Heading permalinks (headingAnchors option)
|
|
359
|
+
========================================================================== */
|
|
360
|
+
|
|
361
|
+
/* Anchor targets clear a fixed header when jumped to (DocPage sets the variable) */
|
|
362
|
+
.markdown-content [id] {
|
|
363
|
+
scroll-margin-top: var(--docs-header-offset, 5rem);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.markdown-content .hash-link {
|
|
367
|
+
margin-left: 0.5rem;
|
|
368
|
+
color: hsl(var(--primary));
|
|
369
|
+
text-decoration: none;
|
|
370
|
+
opacity: 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.markdown-content .hash-link::before {
|
|
374
|
+
content: '#';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.markdown-content :is(h1, h2, h3, h4, h5, h6):hover .hash-link,
|
|
378
|
+
.markdown-content .hash-link:focus-visible {
|
|
379
|
+
opacity: 1;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ==========================================================================
|
|
383
|
+
External links (externalLinks option)
|
|
384
|
+
========================================================================== */
|
|
385
|
+
|
|
386
|
+
.markdown-content .external-link::after {
|
|
387
|
+
content: '\2197';
|
|
388
|
+
margin-left: 0.125rem;
|
|
389
|
+
font-size: 0.75em;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.markdown-content .external-link-hint {
|
|
393
|
+
position: absolute;
|
|
394
|
+
width: 1px;
|
|
395
|
+
height: 1px;
|
|
396
|
+
padding: 0;
|
|
397
|
+
margin: -1px;
|
|
398
|
+
overflow: hidden;
|
|
399
|
+
clip: rect(0, 0, 0, 0);
|
|
400
|
+
white-space: nowrap;
|
|
401
|
+
border: 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
275
404
|
/* ==========================================================================
|
|
276
405
|
Admonitions / Callouts
|
|
277
406
|
========================================================================== */
|
|
@@ -321,6 +450,17 @@
|
|
|
321
450
|
color: hsl(210 100% 40%);
|
|
322
451
|
}
|
|
323
452
|
|
|
453
|
+
/* Info - Sky */
|
|
454
|
+
.markdown-content .admonition-info {
|
|
455
|
+
border-color: hsl(199 89% 48% / 0.3);
|
|
456
|
+
background: hsl(199 89% 48% / 0.05);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.markdown-content .admonition-info .admonition-heading {
|
|
460
|
+
background: hsl(199 89% 48% / 0.1);
|
|
461
|
+
color: hsl(199 89% 32%);
|
|
462
|
+
}
|
|
463
|
+
|
|
324
464
|
/* Tip - Green */
|
|
325
465
|
.markdown-content .admonition-tip {
|
|
326
466
|
border-color: hsl(142 71% 45% / 0.3);
|
|
@@ -365,6 +505,17 @@
|
|
|
365
505
|
color: hsl(0 84% 45%);
|
|
366
506
|
}
|
|
367
507
|
|
|
508
|
+
/* Danger - Deep red */
|
|
509
|
+
.markdown-content .admonition-danger {
|
|
510
|
+
border-color: hsl(0 72% 42% / 0.45);
|
|
511
|
+
background: hsl(0 72% 42% / 0.08);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.markdown-content .admonition-danger .admonition-heading {
|
|
515
|
+
background: hsl(0 72% 42% / 0.16);
|
|
516
|
+
color: hsl(0 72% 35%);
|
|
517
|
+
}
|
|
518
|
+
|
|
368
519
|
/* ==========================================================================
|
|
369
520
|
Footnotes
|
|
370
521
|
========================================================================== */
|
|
@@ -455,6 +606,15 @@
|
|
|
455
606
|
height: auto;
|
|
456
607
|
}
|
|
457
608
|
|
|
609
|
+
/* Mermaid writes a diagram's classDef names onto its SVG nodes, so a diagram
|
|
610
|
+
with `classDef hidden …` gets nodes classed `hidden` — which Tailwind's
|
|
611
|
+
.hidden utility collapses. Not scoped to .markdown-content: mermaid measures
|
|
612
|
+
labels in a temporary SVG appended to <body> before placing the diagram. */
|
|
613
|
+
svg g.node.hidden,
|
|
614
|
+
svg g.cluster.hidden {
|
|
615
|
+
display: inline;
|
|
616
|
+
}
|
|
617
|
+
|
|
458
618
|
.markdown-content .mermaid-diagram pre.mermaid {
|
|
459
619
|
margin: 0;
|
|
460
620
|
padding: 0;
|
|
@@ -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 */
|
|
@@ -49,6 +50,12 @@ export interface ParsedMarkdown {
|
|
|
49
50
|
markdown: string;
|
|
50
51
|
/** Rendered HTML content */
|
|
51
52
|
html: string;
|
|
53
|
+
/** Every heading with an ID, in document order (all levels; filter by `level` as needed) */
|
|
54
|
+
toc: {
|
|
55
|
+
text: string;
|
|
56
|
+
level: number;
|
|
57
|
+
id: string;
|
|
58
|
+
}[];
|
|
52
59
|
}
|
|
53
60
|
/** Parser options */
|
|
54
61
|
export interface ParseOptions {
|
|
@@ -56,6 +63,31 @@ export interface ParseOptions {
|
|
|
56
63
|
theme?: string;
|
|
57
64
|
/** Whether to generate heading IDs */
|
|
58
65
|
generateHeadingIds?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Heading ID algorithm. `github` matches GitHub and Docusaurus (github-slugger), for
|
|
68
|
+
* content whose existing `#anchor` links must keep working. Default: `default`.
|
|
69
|
+
*/
|
|
70
|
+
headingIdStyle?: HeadingIdStyle;
|
|
71
|
+
/**
|
|
72
|
+
* Component names to render as placeholders when written as a self-closing tag on a
|
|
73
|
+
* line of its own (`<RequestForm id="x" />`). Mount them with `mountComponents`.
|
|
74
|
+
*/
|
|
75
|
+
components?: readonly string[];
|
|
76
|
+
/** Append a `#` permalink (`a.hash-link`) to each heading, as Docusaurus does. Default: false */
|
|
77
|
+
headingAnchors?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Open `http(s)` links in a new tab with a visually hidden "(opens in new tab)" note.
|
|
80
|
+
* `true` uses `target="_blank" rel="noopener noreferrer"`. Default: false
|
|
81
|
+
*/
|
|
82
|
+
externalLinks?: boolean | {
|
|
83
|
+
target?: string;
|
|
84
|
+
rel?: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Map code fence names to Shiki languages, e.g. `{ ios: 'text', caddyfile: 'nginx' }`.
|
|
88
|
+
* Languages Shiki doesn't know render as plain code.
|
|
89
|
+
*/
|
|
90
|
+
langAlias?: Record<string, string>;
|
|
59
91
|
}
|
|
60
92
|
/** MarkdownPage component props */
|
|
61
93
|
export interface MarkdownPageProps {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@classic-homes/theme-docs/vite` — regenerate docs content while you write.
|
|
3
|
+
*
|
|
4
|
+
* Runs your `generate` step (typically `loadDocs` → `renderDocs` → write JSON) when the
|
|
5
|
+
* dev server starts and whenever a file under `watch` changes, then reloads the page.
|
|
6
|
+
* Builds run it once before bundling, so the generated files are always current.
|
|
7
|
+
*/
|
|
8
|
+
import type { Plugin } from 'vite';
|
|
9
|
+
export interface DocsContentPluginOptions {
|
|
10
|
+
/** Directories (or files) whose changes trigger `generate` */
|
|
11
|
+
watch: string | string[];
|
|
12
|
+
/** Produce the content the app imports. Throw to report a failure. */
|
|
13
|
+
generate: () => void | Promise<void>;
|
|
14
|
+
/** Wait this long after the last change before regenerating. Default: 200ms */
|
|
15
|
+
debounce?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function docsContent(options: DocsContentPluginOptions): Plugin;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@classic-homes/theme-docs/vite` — regenerate docs content while you write.
|
|
3
|
+
*
|
|
4
|
+
* Runs your `generate` step (typically `loadDocs` → `renderDocs` → write JSON) when the
|
|
5
|
+
* dev server starts and whenever a file under `watch` changes, then reloads the page.
|
|
6
|
+
* Builds run it once before bundling, so the generated files are always current.
|
|
7
|
+
*/
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
export function docsContent(options) {
|
|
10
|
+
const { generate, debounce = 200 } = options;
|
|
11
|
+
const watched = (Array.isArray(options.watch) ? options.watch : [options.watch]).map((p) => path.resolve(p));
|
|
12
|
+
const isWatched = (file) => watched.some((w) => file === w || file.startsWith(w + path.sep));
|
|
13
|
+
let timer;
|
|
14
|
+
return {
|
|
15
|
+
name: 'classic-theme-docs:content',
|
|
16
|
+
async buildStart() {
|
|
17
|
+
await generate();
|
|
18
|
+
},
|
|
19
|
+
configureServer(server) {
|
|
20
|
+
server.watcher.add(watched);
|
|
21
|
+
server.watcher.on('all', (_event, file) => {
|
|
22
|
+
if (!isWatched(path.resolve(file)))
|
|
23
|
+
return;
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
timer = setTimeout(async () => {
|
|
26
|
+
try {
|
|
27
|
+
await generate();
|
|
28
|
+
server.config.logger.info('[docs] content regenerated', { timestamp: true });
|
|
29
|
+
server.ws.send({ type: 'full-reload' });
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
server.config.logger.error(`[docs] content regeneration failed:\n${err.stack ?? err}`);
|
|
33
|
+
}
|
|
34
|
+
}, debounce);
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|