@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.
Files changed (183) hide show
  1. package/README.md +397 -0
  2. package/bin/greg.js +241 -0
  3. package/bin/init.js +351 -0
  4. package/bin/templates/docs/getting-started.md +47 -0
  5. package/bin/templates/docs/index.md +11 -0
  6. package/bin/templates/greg.config.js +39 -0
  7. package/bin/templates/greg.config.ts +38 -0
  8. package/bin/templates/index.html +16 -0
  9. package/bin/templates/src/App.svelte +5 -0
  10. package/bin/templates/src/app.css +20 -0
  11. package/bin/templates/src/main.js +9 -0
  12. package/bin/templates/svelte.config.js +1 -0
  13. package/bin/templates/tsconfig.json +21 -0
  14. package/bin/templates/vite.config.js +23 -0
  15. package/docs/__partials/markdown/examples/basic.md +4 -0
  16. package/docs/__partials/markdown/examples/diff.md +10 -0
  17. package/docs/__partials/markdown/examples/focus.md +5 -0
  18. package/docs/__partials/markdown/examples/language-title.md +3 -0
  19. package/docs/__partials/markdown/examples/line-highlighting.md +5 -0
  20. package/docs/__partials/markdown/examples/line-numbers.md +5 -0
  21. package/docs/__partials/note.md +4 -0
  22. package/docs/guide/__shared-warning.md +4 -0
  23. package/docs/guide/asset-handling.md +88 -0
  24. package/docs/guide/deploying.md +162 -0
  25. package/docs/guide/getting-started.md +334 -0
  26. package/docs/guide/index.md +23 -0
  27. package/docs/guide/localization.md +290 -0
  28. package/docs/guide/markdown/code.md +95 -0
  29. package/docs/guide/markdown/components-and-mermaid.md +43 -0
  30. package/docs/guide/markdown/containers.md +110 -0
  31. package/docs/guide/markdown/header-anchors.md +34 -0
  32. package/docs/guide/markdown/includes.md +84 -0
  33. package/docs/guide/markdown/index.md +20 -0
  34. package/docs/guide/markdown/inline-attributes.md +21 -0
  35. package/docs/guide/markdown/links-and-toc.md +64 -0
  36. package/docs/guide/markdown/math.md +54 -0
  37. package/docs/guide/markdown/syntax-highlighting.md +75 -0
  38. package/docs/guide/routing.md +150 -0
  39. package/docs/guide/using-svelte.md +88 -0
  40. package/docs/guide/versioning.md +281 -0
  41. package/docs/incompatibilities.md +48 -0
  42. package/docs/index.md +43 -0
  43. package/docs/reference/badge.md +100 -0
  44. package/docs/reference/carbon-ads.md +46 -0
  45. package/docs/reference/code-group.md +126 -0
  46. package/docs/reference/home-page.md +232 -0
  47. package/docs/reference/index.md +18 -0
  48. package/docs/reference/markdowndocs.md +275 -0
  49. package/docs/reference/outline.md +79 -0
  50. package/docs/reference/search.md +263 -0
  51. package/docs/reference/steps.md +200 -0
  52. package/docs/reference/team-page.md +189 -0
  53. package/docs/reference/theme.md +150 -0
  54. package/fakeDocsGenerator/generate_docs.js +310 -0
  55. package/package.json +92 -0
  56. package/scripts/build-versions.js +609 -0
  57. package/scripts/generate-static.js +79 -0
  58. package/scripts/render-markdown.js +420 -0
  59. package/src/lib/MarkdownDocs/AiChat.svelte +936 -0
  60. package/src/lib/MarkdownDocs/BackToTop.svelte +68 -0
  61. package/src/lib/MarkdownDocs/Breadcrumb.svelte +68 -0
  62. package/src/lib/MarkdownDocs/DocsNavigation.svelte +149 -0
  63. package/src/lib/MarkdownDocs/DocsSiteHeader.svelte +758 -0
  64. package/src/lib/MarkdownDocs/DocsVersionSwitcher.svelte +103 -0
  65. package/src/lib/MarkdownDocs/MarkdownDocs.svelte +2115 -0
  66. package/src/lib/MarkdownDocs/MarkdownRenderer.svelte +487 -0
  67. package/src/lib/MarkdownDocs/Outline.svelte +238 -0
  68. package/src/lib/MarkdownDocs/PrevNext.svelte +115 -0
  69. package/src/lib/MarkdownDocs/SearchModal.svelte +1241 -0
  70. package/src/lib/MarkdownDocs/TreeView.svelte +32 -0
  71. package/src/lib/MarkdownDocs/TreeViewItem.svelte +219 -0
  72. package/src/lib/MarkdownDocs/VersionOutdatedNotice.svelte +72 -0
  73. package/src/lib/MarkdownDocs/__tests__/codeDirectives.test.js +54 -0
  74. package/src/lib/MarkdownDocs/__tests__/common.test.js +41 -0
  75. package/src/lib/MarkdownDocs/__tests__/docsExamplesLint.test.js +77 -0
  76. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/__partial-basic.md +3 -0
  77. package/src/lib/MarkdownDocs/__tests__/fixtures/docs/markdown/snippet.js +9 -0
  78. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/part.md +11 -0
  79. package/src/lib/MarkdownDocs/__tests__/fixtures/includes/wrapper.md +5 -0
  80. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.js +8 -0
  81. package/src/lib/MarkdownDocs/__tests__/fixtures/snippets/sample.md +5 -0
  82. package/src/lib/MarkdownDocs/__tests__/helpers.js +67 -0
  83. package/src/lib/MarkdownDocs/__tests__/localeUtils.test.js +204 -0
  84. package/src/lib/MarkdownDocs/__tests__/markdown.test.js +704 -0
  85. package/src/lib/MarkdownDocs/__tests__/markdownRendererRuntime.test.js +65 -0
  86. package/src/lib/MarkdownDocs/__tests__/searchIndexBuilder.test.js +117 -0
  87. package/src/lib/MarkdownDocs/__tests__/sqliteStore.test.js +202 -0
  88. package/src/lib/MarkdownDocs/__tests__/useRouter.test.js +16 -0
  89. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.js +14 -0
  90. package/src/lib/MarkdownDocs/ai/adapters/customAdapter.ts +43 -0
  91. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.js +81 -0
  92. package/src/lib/MarkdownDocs/ai/adapters/ollamaAdapter.ts +116 -0
  93. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.js +92 -0
  94. package/src/lib/MarkdownDocs/ai/adapters/openaiAdapter.ts +137 -0
  95. package/src/lib/MarkdownDocs/ai/aiProvider.ts +31 -0
  96. package/src/lib/MarkdownDocs/ai/characters.js +52 -0
  97. package/src/lib/MarkdownDocs/ai/characters.ts +69 -0
  98. package/src/lib/MarkdownDocs/ai/chunkStore.ts +25 -0
  99. package/src/lib/MarkdownDocs/ai/chunker.js +85 -0
  100. package/src/lib/MarkdownDocs/ai/chunker.ts +135 -0
  101. package/src/lib/MarkdownDocs/ai/docLinker.js +26 -0
  102. package/src/lib/MarkdownDocs/ai/docLinker.ts +36 -0
  103. package/src/lib/MarkdownDocs/ai/promptBuilder.js +33 -0
  104. package/src/lib/MarkdownDocs/ai/promptBuilder.ts +53 -0
  105. package/src/lib/MarkdownDocs/ai/ragPipeline.js +54 -0
  106. package/src/lib/MarkdownDocs/ai/ragPipeline.ts +106 -0
  107. package/src/lib/MarkdownDocs/ai/stores/memoryStore.js +88 -0
  108. package/src/lib/MarkdownDocs/ai/stores/memoryStore.ts +112 -0
  109. package/src/lib/MarkdownDocs/ai/stores/sqliteStore.ts +372 -0
  110. package/src/lib/MarkdownDocs/ai/types.ts +71 -0
  111. package/src/lib/MarkdownDocs/aiServer.js +288 -0
  112. package/src/lib/MarkdownDocs/codeDirectives.js +191 -0
  113. package/src/lib/MarkdownDocs/codeFenceInfo.js +45 -0
  114. package/src/lib/MarkdownDocs/codeGroup.ts +46 -0
  115. package/src/lib/MarkdownDocs/common.ts +47 -0
  116. package/src/lib/MarkdownDocs/docsUtils.js +281 -0
  117. package/src/lib/MarkdownDocs/index.plugins.js +22 -0
  118. package/src/lib/MarkdownDocs/layouts/LayoutDoc.svelte +8 -0
  119. package/src/lib/MarkdownDocs/layouts/LayoutHome.svelte +58 -0
  120. package/src/lib/MarkdownDocs/layouts/LayoutPage.svelte +9 -0
  121. package/src/lib/MarkdownDocs/loadGregConfig.js +82 -0
  122. package/src/lib/MarkdownDocs/localeUtils.ts +682 -0
  123. package/src/lib/MarkdownDocs/markdownRendererRuntime.ts +314 -0
  124. package/src/lib/MarkdownDocs/mermaidThemes.js +319 -0
  125. package/src/lib/MarkdownDocs/navigationUtils.js +22 -0
  126. package/src/lib/MarkdownDocs/rehypeCodeGroup.js +326 -0
  127. package/src/lib/MarkdownDocs/rehypeCodeTitle.js +96 -0
  128. package/src/lib/MarkdownDocs/rehypeToc.js +170 -0
  129. package/src/lib/MarkdownDocs/remarkCodeMeta.js +22 -0
  130. package/src/lib/MarkdownDocs/remarkContainers.js +329 -0
  131. package/src/lib/MarkdownDocs/remarkCustomAnchors.js +42 -0
  132. package/src/lib/MarkdownDocs/remarkEscapeSvelte.js +33 -0
  133. package/src/lib/MarkdownDocs/remarkGlobalComponents.js +65 -0
  134. package/src/lib/MarkdownDocs/remarkImports.js +461 -0
  135. package/src/lib/MarkdownDocs/remarkImportsBrowser.js +349 -0
  136. package/src/lib/MarkdownDocs/remarkInlineAttrs.js +95 -0
  137. package/src/lib/MarkdownDocs/remarkMathToHtml.js +138 -0
  138. package/src/lib/MarkdownDocs/searchIndexBuilder.js +497 -0
  139. package/src/lib/MarkdownDocs/searchServer.js +263 -0
  140. package/src/lib/MarkdownDocs/treeViewTypes.ts +11 -0
  141. package/src/lib/MarkdownDocs/useRouter.svelte.ts +114 -0
  142. package/src/lib/MarkdownDocs/useSplitter.svelte.ts +33 -0
  143. package/src/lib/MarkdownDocs/versioningDefaults.js +20 -0
  144. package/src/lib/MarkdownDocs/vitePluginAiServer.js +204 -0
  145. package/src/lib/MarkdownDocs/vitePluginCopyDocs.js +153 -0
  146. package/src/lib/MarkdownDocs/vitePluginFrontmatter.js +109 -0
  147. package/src/lib/MarkdownDocs/vitePluginGregConfig.js +108 -0
  148. package/src/lib/MarkdownDocs/vitePluginSearchIndex.js +57 -0
  149. package/src/lib/MarkdownDocs/vitePluginSearchServer.js +190 -0
  150. package/src/lib/components/Badge.svelte +59 -0
  151. package/src/lib/components/Button.svelte +138 -0
  152. package/src/lib/components/CarbonAds.svelte +99 -0
  153. package/src/lib/components/CodeGroup.svelte +102 -0
  154. package/src/lib/components/Feature.svelte +209 -0
  155. package/src/lib/components/Features.svelte +123 -0
  156. package/src/lib/components/Hero.svelte +399 -0
  157. package/src/lib/components/Image.svelte +128 -0
  158. package/src/lib/components/Link.svelte +105 -0
  159. package/src/lib/components/SocialLink.svelte +84 -0
  160. package/src/lib/components/SocialLinks.svelte +33 -0
  161. package/src/lib/components/Steps.svelte +143 -0
  162. package/src/lib/components/TeamMember.svelte +273 -0
  163. package/src/lib/components/TeamMembers.svelte +81 -0
  164. package/src/lib/components/TeamPage.svelte +65 -0
  165. package/src/lib/components/TeamPageSection.svelte +108 -0
  166. package/src/lib/components/TeamPageTitle.svelte +89 -0
  167. package/src/lib/components/index.js +24 -0
  168. package/src/lib/portal/context.js +12 -0
  169. package/src/lib/portal/index.js +3 -0
  170. package/src/lib/portal/portal.svelte +14 -0
  171. package/src/lib/portal/slot.svelte +8 -0
  172. package/src/lib/scss/__code.scss +128 -0
  173. package/src/lib/scss/__containers.scss +99 -0
  174. package/src/lib/scss/__markdown.scss +447 -0
  175. package/src/lib/scss/__scrollbar.scss +60 -0
  176. package/src/lib/scss/__steps.scss +100 -0
  177. package/src/lib/scss/__theme.scss +238 -0
  178. package/src/lib/scss/__toc.scss +55 -0
  179. package/src/lib/scss/__utilities.scss +7 -0
  180. package/src/lib/scss/greg.scss +9 -0
  181. package/src/lib/spinner/spinner.svelte +42 -0
  182. package/svelte.config.js +146 -0
  183. 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>