@dominikcz/greg 0.9.27
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/README.md +397 -0
- package/bin/greg.js +241 -0
- package/bin/init.js +351 -0
- package/bin/templates/docs/getting-started.md +47 -0
- package/bin/templates/docs/index.md +11 -0
- package/bin/templates/greg.config.js +39 -0
- package/bin/templates/greg.config.ts +38 -0
- package/bin/templates/index.html +16 -0
- package/bin/templates/src/App.svelte +5 -0
- package/bin/templates/src/app.css +20 -0
- package/bin/templates/src/main.js +9 -0
- package/bin/templates/svelte.config.js +1 -0
- package/bin/templates/tsconfig.json +21 -0
- package/bin/templates/vite.config.js +23 -0
- package/docs/__partials/markdown/examples/basic.md +4 -0
- package/docs/__partials/markdown/examples/diff.md +10 -0
- package/docs/__partials/markdown/examples/focus.md +5 -0
- package/docs/__partials/markdown/examples/language-title.md +3 -0
- package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
- package/docs/__partials/markdown/examples/line-numbers.md +5 -0
- package/docs/__partials/note.md +4 -0
- package/docs/guide/__shared-warning.md +4 -0
- package/docs/guide/asset-handling.md +88 -0
- package/docs/guide/deploying.md +162 -0
- package/docs/guide/getting-started.md +334 -0
- package/docs/guide/index.md +23 -0
- package/docs/guide/localization.md +290 -0
- package/docs/guide/markdown/code.md +95 -0
- package/docs/guide/markdown/components-and-mermaid.md +43 -0
- package/docs/guide/markdown/containers.md +110 -0
- package/docs/guide/markdown/header-anchors.md +34 -0
- package/docs/guide/markdown/includes.md +84 -0
- package/docs/guide/markdown/index.md +20 -0
- package/docs/guide/markdown/inline-attributes.md +21 -0
- package/docs/guide/markdown/links-and-toc.md +64 -0
- package/docs/guide/markdown/math.md +54 -0
- package/docs/guide/markdown/syntax-highlighting.md +75 -0
- package/docs/guide/routing.md +150 -0
- package/docs/guide/using-svelte.md +88 -0
- package/docs/guide/versioning.md +281 -0
- package/docs/incompatibilities.md +48 -0
- package/docs/index.md +43 -0
- package/docs/reference/badge.md +100 -0
- package/docs/reference/carbon-ads.md +46 -0
- package/docs/reference/code-group.md +126 -0
- package/docs/reference/home-page.md +232 -0
- package/docs/reference/index.md +18 -0
- package/docs/reference/markdowndocs.md +275 -0
- package/docs/reference/outline.md +79 -0
- package/docs/reference/search.md +263 -0
- package/docs/reference/steps.md +200 -0
- package/docs/reference/team-page.md +189 -0
- package/docs/reference/theme.md +150 -0
- package/fakeDocsGenerator/generate_docs.js +310 -0
- package/package.json +92 -0
- package/scripts/build-versions.js +609 -0
- package/scripts/generate-static.js +79 -0
- package/scripts/render-markdown.js +420 -0
- package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
- package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
- package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
- package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
- package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
- package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
- package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
- package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
- package/src/lib/MarkdownDocs/Outline.svelte +238 -0
- package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
- package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
- package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
- package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
- package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
- package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
- package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
- package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
- package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
- package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
- package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
- package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
- package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
- package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
- package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
- package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
- package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
- package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
- package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
- package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
- package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
- package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
- package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
- package/src/lib/MarkdownDocs/ai/characters.js +52 -0
- package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
- package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
- package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
- package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
- package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
- package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
- package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
- package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
- package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
- package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
- package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
- package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
- package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
- package/src/lib/MarkdownDocs/ai/types.ts +71 -0
- package/src/lib/MarkdownDocs/aiServer.js +288 -0
- package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
- package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
- package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
- package/src/lib/MarkdownDocs/common.ts +47 -0
- package/src/lib/MarkdownDocs/docsUtils.js +281 -0
- package/src/lib/MarkdownDocs/index.plugins.js +22 -0
- package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
- package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
- package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
- package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
- package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
- package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
- package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
- package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
- package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
- package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
- package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
- package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
- package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
- package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
- package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
- package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
- package/src/lib/MarkdownDocs/remarkImports.js +461 -0
- package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
- package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
- package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
- package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
- package/src/lib/MarkdownDocs/searchServer.js +263 -0
- package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
- package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
- package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
- package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
- package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
- package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
- package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
- package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
- package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
- package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
- package/src/lib/components/Badge.svelte +59 -0
- package/src/lib/components/Button.svelte +138 -0
- package/src/lib/components/CarbonAds.svelte +99 -0
- package/src/lib/components/CodeGroup.svelte +102 -0
- package/src/lib/components/Feature.svelte +209 -0
- package/src/lib/components/Features.svelte +123 -0
- package/src/lib/components/Hero.svelte +399 -0
- package/src/lib/components/Image.svelte +128 -0
- package/src/lib/components/Link.svelte +105 -0
- package/src/lib/components/SocialLink.svelte +84 -0
- package/src/lib/components/SocialLinks.svelte +33 -0
- package/src/lib/components/Steps.svelte +143 -0
- package/src/lib/components/TeamMember.svelte +273 -0
- package/src/lib/components/TeamMembers.svelte +81 -0
- package/src/lib/components/TeamPage.svelte +65 -0
- package/src/lib/components/TeamPageSection.svelte +108 -0
- package/src/lib/components/TeamPageTitle.svelte +89 -0
- package/src/lib/components/index.js +24 -0
- package/src/lib/portal/context.js +12 -0
- package/src/lib/portal/index.js +3 -0
- package/src/lib/portal/portal.svelte +14 -0
- package/src/lib/portal/slot.svelte +8 -0
- package/src/lib/scss/__code.scss +128 -0
- package/src/lib/scss/__containers.scss +99 -0
- package/src/lib/scss/__markdown.scss +447 -0
- package/src/lib/scss/__scrollbar.scss +60 -0
- package/src/lib/scss/__steps.scss +100 -0
- package/src/lib/scss/__theme.scss +238 -0
- package/src/lib/scss/__toc.scss +55 -0
- package/src/lib/scss/__utilities.scss +7 -0
- package/src/lib/scss/greg.scss +9 -0
- package/src/lib/spinner/spinner.svelte +42 -0
- package/svelte.config.js +146 -0
- package/types/index.d.ts +456 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* MarkdownRenderer
|
|
4
|
+
*
|
|
5
|
+
* Renders a raw Markdown string to HTML in the browser using the same
|
|
6
|
+
* remark/rehype pipeline as the build-time mdsvex setup (minus Svelte-specific
|
|
7
|
+
* and filesystem-specific plugins).
|
|
8
|
+
*
|
|
9
|
+
* Syntax highlighting is provided by Shiki (same engine as build-time mdsvex).
|
|
10
|
+
* Svelte components (Badge, Button, Image, Link, CodeGroup) are hydrated after render.
|
|
11
|
+
* Mermaid diagrams are rendered via mermaid.js after render.
|
|
12
|
+
*/
|
|
13
|
+
import { unified } from "unified";
|
|
14
|
+
import { visit } from "unist-util-visit";
|
|
15
|
+
import { mount, unmount, untrack } from "svelte";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
COMPONENT_REGISTRY,
|
|
19
|
+
getRuntimeRenderHandlers,
|
|
20
|
+
getThemeChangeRenderHandlers,
|
|
21
|
+
runRenderHandlers,
|
|
22
|
+
getRemarkPluginEntries,
|
|
23
|
+
getRehypePluginEntries,
|
|
24
|
+
applyPluginEntries,
|
|
25
|
+
type RenderHandler,
|
|
26
|
+
} from "./markdownRendererRuntime";
|
|
27
|
+
import {
|
|
28
|
+
parseCodeDirectives,
|
|
29
|
+
decorateHighlightedCodeHtml,
|
|
30
|
+
} from "./codeDirectives.js";
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
MERMAID_THEMES,
|
|
34
|
+
DEFAULT_MERMAID_THEME,
|
|
35
|
+
getColorSchemeTheme,
|
|
36
|
+
} from "./mermaidThemes.js";
|
|
37
|
+
import { createHighlighter, type HighlighterGeneric } from "shiki";
|
|
38
|
+
|
|
39
|
+
const shikiThemes = {
|
|
40
|
+
light: "github-light",
|
|
41
|
+
dark: "github-dark",
|
|
42
|
+
} as const;
|
|
43
|
+
const shikiDefaultLang = "txt";
|
|
44
|
+
const shikiLangAliases: Record<string, string> = {
|
|
45
|
+
js: "javascript",
|
|
46
|
+
ts: "typescript",
|
|
47
|
+
sh: "bash",
|
|
48
|
+
shell: "bash",
|
|
49
|
+
zsh: "bash",
|
|
50
|
+
yml: "yaml",
|
|
51
|
+
md: "markdown",
|
|
52
|
+
};
|
|
53
|
+
const shikiLangs = [
|
|
54
|
+
"javascript",
|
|
55
|
+
"typescript",
|
|
56
|
+
"bash",
|
|
57
|
+
"json",
|
|
58
|
+
"html",
|
|
59
|
+
"css",
|
|
60
|
+
"yaml",
|
|
61
|
+
"markdown",
|
|
62
|
+
"svelte",
|
|
63
|
+
"txt",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
let shikiHighlighterPromise: Promise<HighlighterGeneric<any, any>> | null =
|
|
67
|
+
null;
|
|
68
|
+
|
|
69
|
+
function getShikiHighlighter() {
|
|
70
|
+
if (!shikiHighlighterPromise) {
|
|
71
|
+
shikiHighlighterPromise = createHighlighter({
|
|
72
|
+
themes: [shikiThemes.light, shikiThemes.dark],
|
|
73
|
+
langs: shikiLangs,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return shikiHighlighterPromise;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseClassNameList(value: unknown): string[] {
|
|
80
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
81
|
+
if (typeof value === "string")
|
|
82
|
+
return value.split(/\s+/).filter(Boolean);
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mergeClassNames(...lists: Array<string[]>) {
|
|
87
|
+
return [...new Set(lists.flat().filter(Boolean))];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractAttr(html: string, tag: string, attr: string) {
|
|
91
|
+
const match = html.match(
|
|
92
|
+
new RegExp(`<${tag}\\b[^>]*\\b${attr}="([^"]*)"`, "i"),
|
|
93
|
+
);
|
|
94
|
+
return match?.[1] ?? "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractCodeInnerHtml(shikiHtml: string) {
|
|
98
|
+
const match = shikiHtml.match(
|
|
99
|
+
/<pre\b[^>]*>\s*<code\b[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/i,
|
|
100
|
+
);
|
|
101
|
+
return match?.[1] ?? "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveShikiLang(
|
|
105
|
+
highlighter: HighlighterGeneric<any, any>,
|
|
106
|
+
language: string,
|
|
107
|
+
) {
|
|
108
|
+
const rawLang = String(language || "")
|
|
109
|
+
.trim()
|
|
110
|
+
.toLowerCase();
|
|
111
|
+
const mapped = shikiLangAliases[rawLang] ?? rawLang;
|
|
112
|
+
const loaded = highlighter.getLoadedLanguages();
|
|
113
|
+
if (loaded.includes(mapped)) return mapped;
|
|
114
|
+
if (loaded.includes(rawLang)) return rawLang;
|
|
115
|
+
return shikiDefaultLang;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let runtimeRenderHandlers: RenderHandler[] = [];
|
|
119
|
+
let themeChangeRenderHandlers: RenderHandler[] = [];
|
|
120
|
+
|
|
121
|
+
let mountedInstances: ReturnType<typeof mount>[] = [];
|
|
122
|
+
|
|
123
|
+
function hydrateComponents(root: HTMLElement) {
|
|
124
|
+
for (const c of mountedInstances) unmount(c);
|
|
125
|
+
mountedInstances = [];
|
|
126
|
+
|
|
127
|
+
for (const [tagName, entry] of Object.entries(COMPONENT_REGISTRY)) {
|
|
128
|
+
const selector = entry.selector ?? tagName;
|
|
129
|
+
for (const el of Array.from(
|
|
130
|
+
root.querySelectorAll(selector),
|
|
131
|
+
) as HTMLElement[]) {
|
|
132
|
+
const props = entry.buildProps(el);
|
|
133
|
+
|
|
134
|
+
const wrapper = document.createElement("span");
|
|
135
|
+
el.replaceWith(wrapper);
|
|
136
|
+
mountedInstances.push(
|
|
137
|
+
mount(entry.component, { target: wrapper, props }),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function initMermaid(
|
|
144
|
+
root: HTMLElement,
|
|
145
|
+
themeConfig: Record<string, unknown> = {},
|
|
146
|
+
) {
|
|
147
|
+
const blocks = Array.from(
|
|
148
|
+
root.querySelectorAll("pre.mermaid"),
|
|
149
|
+
) as HTMLElement[];
|
|
150
|
+
if (!blocks.length) return;
|
|
151
|
+
// Save the original source text so we can restore it for re-renders.
|
|
152
|
+
for (const b of blocks) {
|
|
153
|
+
if (!b.dataset.mermaidSrc)
|
|
154
|
+
b.dataset.mermaidSrc = b.textContent ?? "";
|
|
155
|
+
}
|
|
156
|
+
const { default: mermaid } = await import("mermaid");
|
|
157
|
+
mermaid.initialize({ startOnLoad: false, ...themeConfig });
|
|
158
|
+
await mermaid.run({ nodes: blocks });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Re-renders mermaid diagrams in `root` with a new theme config.
|
|
163
|
+
* Restores original source text from `data-mermaid-src` before calling
|
|
164
|
+
* mermaid.run(), because mermaid replaces the element's innerHTML with SVG.
|
|
165
|
+
*/
|
|
166
|
+
async function rerenderMermaid(
|
|
167
|
+
root: HTMLElement,
|
|
168
|
+
themeConfig: Record<string, unknown> = {},
|
|
169
|
+
) {
|
|
170
|
+
const blocks = Array.from(
|
|
171
|
+
root.querySelectorAll("pre.mermaid[data-mermaid-src]"),
|
|
172
|
+
) as HTMLElement[];
|
|
173
|
+
if (!blocks.length) return;
|
|
174
|
+
for (const b of blocks) {
|
|
175
|
+
b.removeAttribute("data-processed");
|
|
176
|
+
b.textContent = b.dataset.mermaidSrc ?? "";
|
|
177
|
+
}
|
|
178
|
+
const { default: mermaid } = await import("mermaid");
|
|
179
|
+
mermaid.initialize({ startOnLoad: false, ...themeConfig });
|
|
180
|
+
await mermaid.run({ nodes: blocks });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Props ────────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
type Props = {
|
|
186
|
+
markdown: string;
|
|
187
|
+
/** URL path of the current .md file, e.g. '/docs/guide/getting-started.md'. Used to resolve relative imports. */
|
|
188
|
+
baseUrl?: string;
|
|
189
|
+
/** Docs URL prefix, e.g. '/docs'. */
|
|
190
|
+
docsPrefix?: string;
|
|
191
|
+
/**
|
|
192
|
+
* Key of the active Mermaid theme. Must be present in `mermaidThemes`.
|
|
193
|
+
* Defaults to `'material'`.
|
|
194
|
+
*/
|
|
195
|
+
mermaidTheme?: string;
|
|
196
|
+
/**
|
|
197
|
+
* Additional (or override) Mermaid theme configs keyed by theme name.
|
|
198
|
+
* Merged on top of the built-in `MERMAID_THEMES` object.
|
|
199
|
+
*/
|
|
200
|
+
mermaidThemes?: Record<string, Record<string, unknown>>;
|
|
201
|
+
/**
|
|
202
|
+
* Current colour scheme of the host application.
|
|
203
|
+
* When `'dark'`, automatically selects `mermaidTheme + '-dark'` if available.
|
|
204
|
+
*/
|
|
205
|
+
colorTheme?: "light" | "dark";
|
|
206
|
+
};
|
|
207
|
+
let {
|
|
208
|
+
markdown,
|
|
209
|
+
baseUrl = "/docs/index.md",
|
|
210
|
+
docsPrefix = "/docs",
|
|
211
|
+
mermaidTheme = DEFAULT_MERMAID_THEME,
|
|
212
|
+
mermaidThemes: extraThemes = {},
|
|
213
|
+
colorTheme,
|
|
214
|
+
}: Props = $props();
|
|
215
|
+
|
|
216
|
+
/** Combined theme map: built-ins overridden/extended by user-supplied themes. */
|
|
217
|
+
const allMermaidThemes = $derived({ ...MERMAID_THEMES, ...extraThemes });
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Effective theme key: switches to `<base>-dark` variant when colorTheme is dark
|
|
221
|
+
* and such a variant exists in allMermaidThemes.
|
|
222
|
+
*/
|
|
223
|
+
const effectiveMermaidTheme = $derived(
|
|
224
|
+
getColorSchemeTheme(mermaidTheme, colorTheme, allMermaidThemes),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/** Resolved MermaidConfig for the currently active theme. */
|
|
228
|
+
const activeMermaidThemeConfig = $derived(
|
|
229
|
+
(allMermaidThemes[effectiveMermaidTheme] ??
|
|
230
|
+
allMermaidThemes[DEFAULT_MERMAID_THEME] ??
|
|
231
|
+
{}) as Record<string, unknown>,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// ── Rehype plugin: mermaid code blocks ───────────────────────────────────────
|
|
235
|
+
// Transforms <pre><code class="language-mermaid"> into <pre class="mermaid">
|
|
236
|
+
// so that mermaid.run() can pick it up after render.
|
|
237
|
+
|
|
238
|
+
function rehypeMermaid() {
|
|
239
|
+
return (tree: any) => {
|
|
240
|
+
visit(tree, "element", (node: any, index: any, parent: any) => {
|
|
241
|
+
if (node.tagName !== "pre" || !parent || index == null) return;
|
|
242
|
+
const code = node.children?.[0];
|
|
243
|
+
if (!code || code.tagName !== "code") return;
|
|
244
|
+
const cls: string[] = code.properties?.className ?? [];
|
|
245
|
+
if (!cls.includes("language-mermaid")) return;
|
|
246
|
+
const text = (code.children ?? [])
|
|
247
|
+
.filter((c: any) => c.type === "text")
|
|
248
|
+
.map((c: any) => c.value)
|
|
249
|
+
.join("");
|
|
250
|
+
parent.children[index] = {
|
|
251
|
+
type: "element",
|
|
252
|
+
tagName: "pre",
|
|
253
|
+
properties: { className: ["mermaid"] },
|
|
254
|
+
children: [{ type: "text", value: text }],
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Rehype plugin: normalize <Steps> wrapper in runtime HTML ───────────────
|
|
261
|
+
// In browser rendering, unknown component tags are serialized as lowercase
|
|
262
|
+
// HTML tags (e.g. <Steps> -> <steps>). Convert them to a regular block wrapper
|
|
263
|
+
// with a stable class so CSS can style them exactly like Starlight steps.
|
|
264
|
+
function rehypeStepsWrapper() {
|
|
265
|
+
return (tree: any) => {
|
|
266
|
+
visit(tree, "element", (node: any) => {
|
|
267
|
+
if (node.tagName !== "steps") return;
|
|
268
|
+
node.tagName = "div";
|
|
269
|
+
const existing = Array.isArray(node.properties?.className)
|
|
270
|
+
? node.properties.className
|
|
271
|
+
: [];
|
|
272
|
+
node.properties = {
|
|
273
|
+
...(node.properties ?? {}),
|
|
274
|
+
className: existing.includes("greg-steps")
|
|
275
|
+
? existing
|
|
276
|
+
: [...existing, "greg-steps"],
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// With allowDangerousHtml and without rehype-raw, custom HTML tags may
|
|
281
|
+
// remain as `raw` nodes. Normalize those too so styling still applies.
|
|
282
|
+
visit(tree, "raw", (node: any) => {
|
|
283
|
+
if (typeof node.value !== "string") return;
|
|
284
|
+
node.value = node.value
|
|
285
|
+
.replace(/<\s*steps\b[^>]*>/gi, '<div class="greg-steps">')
|
|
286
|
+
.replace(/<\s*\/\s*steps\s*>/gi, "</div>");
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Rehype plugin: shiki code blocks ─────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
function rehypeShiki() {
|
|
294
|
+
return async (tree: any) => {
|
|
295
|
+
const highlighter = await getShikiHighlighter();
|
|
296
|
+
|
|
297
|
+
visit(tree, "element", (node: any, _index: any, parent: any) => {
|
|
298
|
+
if (node.tagName !== "code") return;
|
|
299
|
+
// Only highlight fenced code blocks (parent is <pre>)
|
|
300
|
+
if (parent?.tagName !== "pre") return;
|
|
301
|
+
|
|
302
|
+
const raw =
|
|
303
|
+
node.children
|
|
304
|
+
?.filter((c: any) => c.type === "text")
|
|
305
|
+
.map((c: any) => c.value)
|
|
306
|
+
.join("") ?? "";
|
|
307
|
+
const meta = String(node.properties?.["data-code-meta"] ?? "");
|
|
308
|
+
|
|
309
|
+
// Read language from class="language-xxx"
|
|
310
|
+
const cls: string =
|
|
311
|
+
(node.properties?.className ?? []).find((c: string) =>
|
|
312
|
+
c.startsWith("language-"),
|
|
313
|
+
) ?? "";
|
|
314
|
+
const lang = cls.replace("language-", "");
|
|
315
|
+
const directives = parseCodeDirectives(raw, meta, lang);
|
|
316
|
+
|
|
317
|
+
// Attach data-code-lang for rehypeCodeTitle / code group
|
|
318
|
+
if (lang) node.properties["data-code-lang"] = lang;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const safeLang = resolveShikiLang(highlighter, lang);
|
|
322
|
+
const shikiHtml = highlighter.codeToHtml(
|
|
323
|
+
directives.cleanedCode,
|
|
324
|
+
{
|
|
325
|
+
lang: safeLang,
|
|
326
|
+
themes: shikiThemes,
|
|
327
|
+
defaultColor: false,
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const preClasses = parseClassNameList(
|
|
332
|
+
extractAttr(shikiHtml, "pre", "class"),
|
|
333
|
+
);
|
|
334
|
+
const preStyle = extractAttr(shikiHtml, "pre", "style");
|
|
335
|
+
const preDir = extractAttr(shikiHtml, "pre", "dir");
|
|
336
|
+
const preTabIndex = extractAttr(
|
|
337
|
+
shikiHtml,
|
|
338
|
+
"pre",
|
|
339
|
+
"tabindex",
|
|
340
|
+
);
|
|
341
|
+
const codeClasses = parseClassNameList(
|
|
342
|
+
extractAttr(shikiHtml, "code", "class"),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
let highlighted = extractCodeInnerHtml(shikiHtml);
|
|
346
|
+
highlighted = decorateHighlightedCodeHtml(
|
|
347
|
+
highlighted,
|
|
348
|
+
directives,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const hasFocusedLines = directives.lineInfo.some(
|
|
352
|
+
(info: any) => Boolean(info?.focus),
|
|
353
|
+
);
|
|
354
|
+
const hasDiff = directives.lineInfo.some((info: any) =>
|
|
355
|
+
Boolean(
|
|
356
|
+
info?.diffAdd ||
|
|
357
|
+
info?.diffRemove ||
|
|
358
|
+
info?.diffWarning ||
|
|
359
|
+
info?.diffError,
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
parent.properties = {
|
|
364
|
+
...(parent.properties ?? {}),
|
|
365
|
+
...(preStyle ? { style: preStyle } : {}),
|
|
366
|
+
...(preDir ? { dir: preDir } : {}),
|
|
367
|
+
...(preTabIndex
|
|
368
|
+
? { tabIndex: Number(preTabIndex) }
|
|
369
|
+
: {}),
|
|
370
|
+
className: mergeClassNames(
|
|
371
|
+
parseClassNameList(parent.properties?.className),
|
|
372
|
+
preClasses,
|
|
373
|
+
hasFocusedLines ? ["has-focused-lines"] : [],
|
|
374
|
+
hasDiff ? ["has-diff"] : [],
|
|
375
|
+
),
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
node.properties = {
|
|
379
|
+
...(node.properties ?? {}),
|
|
380
|
+
className: mergeClassNames(
|
|
381
|
+
parseClassNameList(node.properties?.className),
|
|
382
|
+
codeClasses,
|
|
383
|
+
),
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
if (directives.lineNumbers?.enabled) {
|
|
387
|
+
const totalLines = directives.lineInfo.length;
|
|
388
|
+
const start = Number.isFinite(
|
|
389
|
+
directives.lineNumbers.start,
|
|
390
|
+
)
|
|
391
|
+
? directives.lineNumbers.start
|
|
392
|
+
: 1;
|
|
393
|
+
const maxDigits = String(
|
|
394
|
+
Math.max(start + totalLines - 1, start),
|
|
395
|
+
).length;
|
|
396
|
+
node.properties["data-line-numbers"] = "true";
|
|
397
|
+
node.properties["data-line-numbers-max-digits"] =
|
|
398
|
+
String(maxDigits);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
node.children = [{ type: "raw", value: highlighted }];
|
|
402
|
+
} catch {
|
|
403
|
+
return; // leave as-is on failure
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Processor ────────────────────────────────────────────────────────────────
|
|
410
|
+
// Rebuilt whenever baseUrl changes (each page navigation) so remarkImportsBrowser
|
|
411
|
+
// gets the correct relative-path context.
|
|
412
|
+
|
|
413
|
+
function buildProcessor(currentBaseUrl: string, currentDocsPrefix: string) {
|
|
414
|
+
const processor = unified();
|
|
415
|
+
applyPluginEntries(
|
|
416
|
+
processor,
|
|
417
|
+
getRemarkPluginEntries(currentBaseUrl, currentDocsPrefix),
|
|
418
|
+
);
|
|
419
|
+
applyPluginEntries(
|
|
420
|
+
processor,
|
|
421
|
+
getRehypePluginEntries({
|
|
422
|
+
rehypeStepsWrapper,
|
|
423
|
+
rehypeMermaid,
|
|
424
|
+
rehypeShiki,
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
return processor;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
runtimeRenderHandlers = getRuntimeRenderHandlers({
|
|
431
|
+
hydrateComponents,
|
|
432
|
+
initMermaid,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
themeChangeRenderHandlers = getThemeChangeRenderHandlers({
|
|
436
|
+
rerenderMermaid,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// ── Reactive rendering ────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
let html = $state("");
|
|
442
|
+
let containerEl: HTMLElement | undefined;
|
|
443
|
+
|
|
444
|
+
/** Strip YAML frontmatter block before rendering. */
|
|
445
|
+
function stripFrontmatter(src: string): string {
|
|
446
|
+
return src.replace(/^---[\r\n][\s\S]*?[\r\n]---[\r\n]?/, "");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
$effect(() => {
|
|
450
|
+
if (!markdown) {
|
|
451
|
+
html = "";
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const processor = buildProcessor(baseUrl, docsPrefix);
|
|
455
|
+
processor.process(stripFrontmatter(markdown)).then((vfile) => {
|
|
456
|
+
html = String(vfile);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Effect 1: fires when rendered HTML changes (new markdown/page navigation).
|
|
461
|
+
// Uses untrack() for the theme config so theme changes do NOT re-trigger this
|
|
462
|
+
// effect — they are handled exclusively by Effect 2 below.
|
|
463
|
+
$effect(() => {
|
|
464
|
+
const _ = html;
|
|
465
|
+
if (!_ || !containerEl) return;
|
|
466
|
+
void runRenderHandlers(containerEl, runtimeRenderHandlers, {
|
|
467
|
+
mermaidThemeConfig: untrack(() => activeMermaidThemeConfig),
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Effect 2: fires when the effective mermaid theme changes (light ↔ dark toggle).
|
|
472
|
+
// Re-renders existing SVGs so that themeVariables (background, darkMode, …)
|
|
473
|
+
// are updated. This is a no-op on initial load because initMermaid has not yet
|
|
474
|
+
// set data-mermaid-src on any element.
|
|
475
|
+
$effect(() => {
|
|
476
|
+
const config = activeMermaidThemeConfig;
|
|
477
|
+
if (!containerEl) return;
|
|
478
|
+
void runRenderHandlers(containerEl, themeChangeRenderHandlers, {
|
|
479
|
+
mermaidThemeConfig: config,
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
</script>
|
|
483
|
+
|
|
484
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
485
|
+
<div class="markdown-renderer markdown-body" bind:this={containerEl}>
|
|
486
|
+
{@html html}
|
|
487
|
+
</div>
|