reallysimpledocs 1.0.0-beta.4 → 1.0.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.
Files changed (53) hide show
  1. package/README.md +59 -39
  2. package/package.json +11 -11
  3. package/src/astro/index.js +64 -98
  4. package/src/astro/preview-scripts.js +140 -0
  5. package/src/astro/preview-source.js +150 -0
  6. package/src/astro/rehype-basecoat-tables.js +62 -0
  7. package/src/components/index.js +1 -0
  8. package/src/css/custom.css +197 -58
  9. package/src/css/styles/luma.css +9 -0
  10. package/src/css/styles/lyra.css +9 -0
  11. package/src/css/styles/maia.css +9 -0
  12. package/src/css/styles/mira.css +9 -0
  13. package/src/css/styles/nova.css +9 -0
  14. package/src/css/styles/rhea.css +9 -0
  15. package/src/css/styles/sera.css +9 -0
  16. package/src/css/styles/vega.css +9 -0
  17. package/src/runtime/components/BaseLayout.astro +22 -28
  18. package/src/runtime/components/CommandDialog.astro +52 -35
  19. package/src/runtime/components/DefaultContentHeader.astro +0 -17
  20. package/src/runtime/components/DefaultHead.astro +110 -0
  21. package/src/runtime/components/DefaultHeadContent.astro +2 -0
  22. package/src/runtime/components/DefaultSidebarHeader.astro +1 -1
  23. package/src/runtime/components/DocsLayout.astro +99 -166
  24. package/src/runtime/components/DropdownMenu.astro +1 -1
  25. package/src/runtime/components/ThemeToggle.astro +21 -0
  26. package/src/runtime/components/mdx/Callout.astro +2 -32
  27. package/src/runtime/components/mdx/Card.astro +3 -8
  28. package/src/runtime/components/mdx/CardGrid.astro +1 -1
  29. package/src/runtime/components/mdx/Code.astro +21 -27
  30. package/src/runtime/components/mdx/CodeGroup.astro +114 -24
  31. package/src/runtime/components/mdx/Preview.astro +7 -13
  32. package/src/runtime/components/mdx/Steps.astro +1 -5
  33. package/src/runtime/components/mdx/Tab.astro +6 -0
  34. package/src/runtime/components/mdx/Tabs.astro +7 -7
  35. package/src/runtime/lib/code-meta.js +8 -0
  36. package/src/runtime/lib/docs.js +141 -119
  37. package/src/runtime/lib/highlight.js +1 -14
  38. package/src/runtime/lib/html.js +9 -0
  39. package/src/runtime/lib/macros.js +10 -5
  40. package/src/runtime/lib/markdown-export.js +240 -169
  41. package/src/runtime/pages/doc.astro +1 -3
  42. package/src/runtime/pages/llms-full.txt.ts +2 -2
  43. package/src/runtime/pages/llms.txt.ts +2 -2
  44. package/src/runtime/pages/markdown.ts +2 -2
  45. package/src/runtime/pages/search-index.ts +12 -3
  46. package/CHANGELOG.md +0 -35
  47. package/src/css/overrides.css +0 -1
  48. package/src/css/sources.css +0 -1
  49. package/src/runtime/lib/markdown-exporters/index.js +0 -144
  50. package/src/runtime/lib/mdx-source.js +0 -19
  51. package/src/runtime/lib/preview-source-plugin.js +0 -33
  52. package/src/runtime/scripts/copy-code.js +0 -130
  53. package/src/site/ContentHeader.astro +0 -33
@@ -1,45 +1,134 @@
1
1
  ---
2
- const { defaultTab, class: className = "" } = Astro.props;
2
+ import { decodeHtml } from "../../lib/html.js";
3
+
4
+ const { class: className = "" } = Astro.props;
3
5
  const id = `code-group-${Math.random().toString(36).slice(2)}`;
4
6
  const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
5
7
  const normalizedTabs = parseCodeTabs(slotHtml);
6
- const defaultValue = defaultTab || normalizedTabs[0]?.value;
7
- const classes = [
8
- "relative my-6 w-full max-w-full overflow-hidden rounded-lg [&>[role=tabpanel]]:border-t-0 [&_[data-code-block]]:my-0 [&_[data-code-block]]:min-w-0 [&_[data-code-block]]:rounded-none [&_[data-code-block]>header]:hidden",
9
- className,
10
- ];
8
+ const defaultValue = normalizedTabs[0]?.value;
11
9
 
12
10
  function parseCodeTabs(html) {
13
11
  const blocks = [];
14
- const pattern =
15
- /<div\b(?=[^>]*\bdata-code-block\b)(?=[^>]*\bdata-code-title="([^"]*)")[^>]*>([\s\S]*?)<\/div>/g;
16
- let match;
17
- while ((match = pattern.exec(html))) {
18
- const label = decodeHtml(match[1]).trim();
19
- if (!label) continue;
12
+ for (const block of findCodeFrames(html)) {
13
+ const label = decodeHtml(block.title).trim();
14
+ const code = findCodeBlock(block.html);
15
+ if (!label || !code) continue;
20
16
  blocks.push({
21
17
  label,
22
18
  value: label,
23
- html: match[0],
19
+ html: code.html,
24
20
  });
25
21
  }
26
22
  return blocks;
27
23
  }
28
24
 
29
- function decodeHtml(value) {
30
- return String(value || "")
31
- .replace(/&lt;/g, "<")
32
- .replace(/&gt;/g, ">")
33
- .replace(/&amp;/g, "&")
34
- .replace(/&quot;/g, '"')
35
- .replace(/&#39;/g, "'");
25
+ function findCodeFrames(html) {
26
+ const blocks = [];
27
+ let cursor = 0;
28
+
29
+ while (cursor < html.length) {
30
+ const open = findNextDiv(html, cursor);
31
+ if (!open) break;
32
+ const title = attrValue(open.source, "data-code-title");
33
+ if (hasClass(open.source, "code-frame") && title) {
34
+ const close = findMatchingDivClose(html, open.end);
35
+ if (!close) break;
36
+ blocks.push({ title, html: html.slice(open.start, close.end) });
37
+ cursor = close.end;
38
+ continue;
39
+ }
40
+ cursor = open.end;
41
+ }
42
+
43
+ return blocks;
36
44
  }
45
+
46
+ function findCodeBlock(html) {
47
+ let cursor = 0;
48
+
49
+ while (cursor < html.length) {
50
+ const open = findNextDiv(html, cursor);
51
+ if (!open) break;
52
+ if (hasClass(open.source, "code-block")) {
53
+ const close = findMatchingDivClose(html, open.end);
54
+ if (!close) return null;
55
+ return { html: html.slice(open.start, close.end) };
56
+ }
57
+ cursor = open.end;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ function findMatchingDivClose(html, fromIndex) {
64
+ let depth = 1;
65
+ let cursor = fromIndex;
66
+
67
+ while (cursor < html.length) {
68
+ const tag = findNextDiv(html, cursor);
69
+ if (!tag) return null;
70
+ depth += tag.closing ? -1 : 1;
71
+ if (depth === 0) return tag;
72
+ cursor = tag.end;
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ function findNextDiv(html, fromIndex) {
79
+ let cursor = fromIndex;
80
+ while (cursor < html.length) {
81
+ const start = html.indexOf("<div", cursor);
82
+ const closeStart = html.indexOf("</div", cursor);
83
+ const isClose = closeStart !== -1 && (start === -1 || closeStart < start);
84
+ const tagStart = isClose ? closeStart : start;
85
+ if (tagStart === -1) return null;
86
+
87
+ const end = findTagEnd(html, tagStart);
88
+ if (end === -1) return null;
89
+ return {
90
+ start: tagStart,
91
+ end: end + 1,
92
+ closing: isClose,
93
+ source: html.slice(tagStart, end + 1),
94
+ };
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ function findTagEnd(html, start) {
101
+ let quote = "";
102
+ for (let index = start; index < html.length; index += 1) {
103
+ const char = html[index];
104
+ if (quote) {
105
+ if (char === quote) quote = "";
106
+ continue;
107
+ }
108
+ if (char === '"' || char === "'") {
109
+ quote = char;
110
+ continue;
111
+ }
112
+ if (char === ">") return index;
113
+ }
114
+ return -1;
115
+ }
116
+
117
+ function attrValue(tag, name) {
118
+ const pattern = new RegExp(`\\s${name}=(["'])(.*?)\\1`, "s");
119
+ return tag.match(pattern)?.[2] || "";
120
+ }
121
+
122
+ function hasClass(tag, name) {
123
+ return attrValue(tag, "class").split(/\s+/).includes(name);
124
+ }
125
+
37
126
  ---
38
127
 
39
- <div class:list={classes} data-code-group>
40
- <header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
41
- <div class="tabs my-0 min-w-0 max-w-full gap-0">
42
- <nav class="h-full justify-start rounded-none bg-transparent p-0" role="tablist" aria-orientation="horizontal" data-variant="line">
128
+ <div class:list={["tabs-frame", className]}>
129
+ <header>
130
+ <div class="tabs">
131
+ <nav role="tablist" aria-orientation="horizontal" data-variant="line">
43
132
  {
44
133
  normalizedTabs.map((tab, index) => {
45
134
  const selected = tab.value === defaultValue;
@@ -66,6 +155,7 @@ function decodeHtml(value) {
66
155
  return (
67
156
  <div
68
157
  role="tabpanel"
158
+ data-code-panel
69
159
  id={`${id}-panel-${index + 1}`}
70
160
  aria-labelledby={`${id}-tab-${index + 1}`}
71
161
  tabindex="-1"
@@ -4,15 +4,13 @@ import { highlightCode } from "../../lib/highlight.js";
4
4
  const { code = "", lang = "html", title = "Preview", class: className = "" } = Astro.props;
5
5
  const id = `preview-${Math.random().toString(36).slice(2)}`;
6
6
  const html = await highlightCode(code, lang);
7
+ const previewClassName = className || "w-fit";
7
8
  ---
8
9
 
9
- <section
10
- class="relative my-6 max-w-full overflow-visible rounded-lg border [&>[role=tabpanel]]:border-t-0 [&>[data-code-panel]_[data-code-block]]:my-0 [&>[data-code-panel]_[data-code-block]]:min-w-0 [&>[data-code-panel]_[data-code-block]]:rounded-none"
11
- data-preview
12
- >
13
- <header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
14
- <div class="tabs my-0 min-w-0 max-w-full gap-0">
15
- <nav class="h-full justify-start rounded-none bg-transparent p-0" role="tablist" aria-orientation="horizontal" data-variant="line">
10
+ <section class="tabs-frame">
11
+ <header>
12
+ <div class="tabs">
13
+ <nav role="tablist" aria-orientation="horizontal" data-variant="line">
16
14
  <button
17
15
  type="button"
18
16
  role="tab"
@@ -43,9 +41,8 @@ const html = await highlightCode(code, lang);
43
41
  aria-labelledby={`${id}-preview-tab`}
44
42
  tabindex="-1"
45
43
  aria-selected="true"
46
- class="flex min-h-64 w-full items-center justify-center p-6"
47
44
  >
48
- <div class={className}>
45
+ <div class={previewClassName}>
49
46
  <slot />
50
47
  </div>
51
48
  </section>
@@ -58,10 +55,7 @@ const html = await highlightCode(code, lang);
58
55
  aria-selected="false"
59
56
  hidden
60
57
  >
61
- <div
62
- class="relative my-6 w-full max-w-full overflow-hidden rounded-lg"
63
- data-code-block
64
- >
58
+ <div class="code-block">
65
59
  <Fragment set:html={html} />
66
60
  </div>
67
61
  </section>
@@ -1,11 +1,7 @@
1
1
  ---
2
2
  const { class: className = "" } = Astro.props;
3
- const classes = [
4
- "mb-12 mt-6 [counter-reset:step] md:ml-4 md:border-l md:pl-8 [&>section]:mb-10 [&>section]:[counter-increment:step] [&>section:last-child]:mb-0 [&>section>*+*]:mt-6 [&>section>*]:leading-relaxed [&>section>*:first-child]:mt-0 [&>section>*:last-child]:mb-0 [&>section>h3+*]:mt-4 [&>section>h3]:relative [&>section>h3]:scroll-m-28 [&>section>h3]:text-lg [&>section>h3]:font-medium [&>section>h3]:tracking-tight [&>section>h3]:before:mr-4 [&>section>h3]:before:inline-flex [&>section>h3]:before:size-7 [&>section>h3]:before:items-center [&>section>h3]:before:justify-center [&>section>h3]:before:rounded-full [&>section>h3]:before:bg-muted [&>section>h3]:before:text-sm [&>section>h3]:before:font-medium [&>section>h3]:before:text-muted-foreground [&>section>h3]:before:content-[counter(step)] md:[&>section>h3]:before:absolute md:[&>section>h3]:before:left-0 md:[&>section>h3]:before:top-0 md:[&>section>h3]:before:mr-0 md:[&>section>h3]:before:-translate-x-[2.875rem]",
5
- className,
6
- ];
7
3
  ---
8
4
 
9
- <div role="list" class:list={classes}>
5
+ <div role="list" class:list={["steps", className]}>
10
6
  <slot />
11
7
  </div>
@@ -0,0 +1,6 @@
1
+ ---
2
+ const { title } = Astro.props;
3
+ void title;
4
+ ---
5
+
6
+ <slot />
@@ -1,16 +1,17 @@
1
1
  ---
2
- const { tabs = [], defaultTab, orientation = "horizontal", variant, class: className = "" } = Astro.props;
2
+ const { tabs = [], variant, class: className = "" } = Astro.props;
3
3
  const id = `tabs-${Math.random().toString(36).slice(2)}`;
4
- const normalizedTabs = tabs.map((tab, index) => ({
4
+ const configuredTabs = typeof tabs === "string" ? JSON.parse(tabs) : tabs;
5
+ const normalizedTabs = configuredTabs.map((tab, index) => ({
5
6
  ...tab,
6
7
  slot: tab.slot || String(index),
7
8
  value: tab.value || tab.slot || String(index),
8
9
  }));
9
- const defaultValue = defaultTab || normalizedTabs.find((tab) => !tab.disabled)?.value || normalizedTabs[0]?.value;
10
+ const defaultValue = normalizedTabs[0]?.value;
10
11
  ---
11
12
 
12
- <div class:list={["tabs my-6", className]}>
13
- <nav role="tablist" aria-orientation={orientation} data-variant={variant}>
13
+ <div class:list={["tabs", className]}>
14
+ <nav role="tablist" aria-orientation="horizontal" data-variant={variant}>
14
15
  {
15
16
  normalizedTabs.map((tab, index) => {
16
17
  const selected = tab.value === defaultValue;
@@ -21,8 +22,7 @@ const defaultValue = defaultTab || normalizedTabs.find((tab) => !tab.disabled)?.
21
22
  id={`${id}-tab-${index + 1}`}
22
23
  aria-controls={`${id}-panel-${index + 1}`}
23
24
  aria-selected={selected ? "true" : "false"}
24
- tabindex={selected && !tab.disabled ? "0" : "-1"}
25
- disabled={tab.disabled}
25
+ tabindex={selected ? "0" : "-1"}
26
26
  >
27
27
  {tab.label}
28
28
  </button>
@@ -0,0 +1,8 @@
1
+ export function parseCodeMeta(meta) {
2
+ const value = String(meta || "");
3
+ const title = value.match(/(?:^|\s)title=(?:"([^"]*)"|'([^']*)'|([^\s]+))/);
4
+
5
+ return {
6
+ title: title?.[1] ?? title?.[2] ?? title?.[3] ?? "",
7
+ };
8
+ }
@@ -2,9 +2,12 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { marked } from "marked";
4
4
  import GithubSlugger from "github-slugger";
5
+ import { codeToHtml } from "shiki";
5
6
  import * as lucideIcons from "lucide-static";
6
- import { highlightCode as renderHighlightedCode } from "./highlight.js";
7
- import { mdxToMarkdown } from "./markdown-export.js";
7
+ import { transformBasecoatTablesHtml } from "../../astro/rehype-basecoat-tables.js";
8
+ import { parseCodeMeta } from "./code-meta.js";
9
+ import { escapeHtml } from "./html.js";
10
+ import { normalizeDocMarkdown } from "./markdown-export.js";
8
11
  import { markdownPath, routePath } from "./paths.js";
9
12
 
10
13
  export function getManifest(config) {
@@ -83,14 +86,8 @@ export function getDocMarkdown(config, page) {
83
86
  return fs.readFileSync(filePath, "utf8");
84
87
  }
85
88
 
86
- export async function getMarkdownExport(config, page) {
87
- const source = getDocMarkdown(config, page);
88
- if (page.extension === ".mdx") return await mdxToMarkdown(source, config);
89
- return `${source.trim()}\n`;
90
- }
91
-
92
- export async function getMarkdownBodyExport(config, page) {
93
- return parseDocMarkdown(await getMarkdownExport(config, page), page.title).body.trim();
89
+ export function getMarkdownExport(config, page) {
90
+ return `${getNormalizedDocMarkdown(config, page).trim()}\n`;
94
91
  }
95
92
 
96
93
  export async function renderDoc(config, page) {
@@ -108,25 +105,10 @@ export async function renderDoc(config, page) {
108
105
  return `<h${depth} id="${id}" tabindex="-1"><a class="header-anchor" href="#${id}">${text}</a></h${depth}>`;
109
106
  };
110
107
 
111
- renderer.table = (token) => {
112
- const header = renderer.tablerow({
113
- text: token.header.map((cell) => renderer.tablecell(cell)).join(""),
114
- });
115
- const body = token.rows
116
- .map((row) =>
117
- renderer.tablerow({
118
- text: row.map((cell) => renderer.tablecell(cell)).join(""),
119
- }),
120
- )
121
- .join("");
122
- const tbody = body ? `<tbody>${body}</tbody>` : "";
123
- return `<div class="relative my-6 w-full overflow-auto"><table><thead>${header}</thead>${tbody}</table></div>`;
124
- };
125
-
126
108
  const tokens = marked.lexer(content);
127
- await highlightMarkdownCode(tokens);
109
+ await highlightCode(tokens);
128
110
  const html = marked.parser(tokens, { renderer });
129
- return { page, html, headings };
111
+ return { page, html: await transformBasecoatTablesHtml(html), headings };
130
112
  }
131
113
 
132
114
  export function getDocHeadings(config, page) {
@@ -159,46 +141,6 @@ export function getNavigation(config, page) {
159
141
  };
160
142
  }
161
143
 
162
- export function getBreadcrumbs(config, page) {
163
- const pages = getPages(config);
164
- const bySlug = new Map(pages.map((entry) => [entry.slug, entry]));
165
- const trail = [];
166
-
167
- const walk = (items = [], ancestors = []) => {
168
- for (const item of items) {
169
- if (item?.type === "group") {
170
- const result = walk(item.items || [], ancestors);
171
- if (result) return result;
172
- continue;
173
- }
174
-
175
- if (item?.type === "submenu") {
176
- const result = walk(item.items || [], item.label ? [...ancestors, { label: item.label }] : ancestors);
177
- if (result) return result;
178
- continue;
179
- }
180
-
181
- const slug = typeof item === "string" ? item : getPageItemSlug(item);
182
- if (slug !== page.slug) continue;
183
- const matchedPage = bySlug.get(slug);
184
- if (!matchedPage) return ancestors;
185
- return [
186
- ...ancestors,
187
- {
188
- label: item.label || matchedPage.title,
189
- url: matchedPage.path,
190
- },
191
- ];
192
- }
193
- return null;
194
- };
195
-
196
- const manifest = getManifest(config);
197
- const found = walk(manifest.menu || []);
198
- if (found) trail.push(...found);
199
- return trail;
200
- }
201
-
202
144
  export function resolveIcon(icon) {
203
145
  if (!icon || typeof icon !== "string") return "";
204
146
  const trimmed = icon.trim();
@@ -229,35 +171,28 @@ export function buildMenus(config, currentSlug) {
229
171
  icon,
230
172
  url: page.path,
231
173
  label: item.label || page.title,
174
+ badge: item.badge,
232
175
  current: page.slug === currentSlug,
233
176
  keywords: page.title,
234
- };
235
- };
236
-
237
- const processExternalLink = (item) => {
238
- if (!item?.url || !item?.label) return null;
239
- const external = isExternalUrl(item.url);
240
- const attrs = {
241
- ...item.attrs,
242
- target: external ? item.attrs?.target || "_blank" : item.attrs?.target,
243
- rel: external ? item.attrs?.rel || "noopener" : item.attrs?.rel,
244
- };
245
- return {
246
- type: "item",
247
- icon: resolveIcon(item.icon),
248
- url: item.url,
249
- label: item.label,
250
- external,
251
- attrs,
252
- keywords: item.label,
177
+ attrs: item.attrs,
253
178
  };
254
179
  };
255
180
 
256
181
  const processItem = (item) => {
257
182
  if (typeof item === "string") return processSlug(item);
183
+ if (item?.url && item?.label) {
184
+ return {
185
+ type: "item",
186
+ icon: resolveIcon(item.icon),
187
+ url: item.url,
188
+ label: item.label,
189
+ badge: item.badge,
190
+ external: /^https?:\/\//.test(item.url),
191
+ attrs: item.attrs,
192
+ };
193
+ }
258
194
  const slug = getPageItemSlug(item);
259
195
  if (slug) return processSlug(slug, item);
260
- if (item?.url) return processExternalLink(item);
261
196
  if (item?.type === "submenu") {
262
197
  const items = (item.items || []).map(processItem).filter(Boolean);
263
198
  return {
@@ -287,37 +222,90 @@ export function buildMenus(config, currentSlug) {
287
222
  icon: item.icon,
288
223
  url: item.url,
289
224
  label: item.label,
290
- external: item.external,
291
- attrs: item.attrs,
292
225
  keywords: item.keywords,
226
+ attrs: item.attrs,
293
227
  })),
294
228
  }));
295
229
 
296
230
  return { sidebar, command };
297
231
  }
298
232
 
299
- export async function getLlmDocs(config) {
300
- const docs = [];
301
- for (const page of getPages(config)) {
302
- docs.push({
303
- ...page,
304
- content: await getMarkdownBodyExport(config, page),
305
- });
306
- }
307
- return docs;
233
+ export function getLlmDocs(config) {
234
+ return getPages(config).map((page) => ({
235
+ ...page,
236
+ content: getNormalizedDocMarkdown(config, page, { includeTitle: false }).trim(),
237
+ }));
308
238
  }
309
239
 
310
- export async function getSearchDocs(config) {
311
- const docs = [];
312
- for (const page of getPages(config)) {
313
- docs.push({
240
+ export function getSearchDocs(config) {
241
+ const pages = getPages(config);
242
+ const trails = getPageTrails(config, pages);
243
+
244
+ return pages.map((page) => {
245
+ const markdown = getNormalizedDocMarkdown(config, page, { includeTitle: false });
246
+ const { prose, code } = splitFencedMarkdown(markdown);
247
+ const trail = trails.get(page.slug) || [page.title];
248
+
249
+ return {
314
250
  slug: page.slug,
315
251
  title: page.title,
252
+ breadcrumb: trail.length > 1 ? trail.join(" › ") : "",
316
253
  path: page.path,
317
- body: markdownPlainText(await getMarkdownBodyExport(config, page)),
254
+ body: markdownPlainText(prose),
255
+ code: markdownPlainText(code),
256
+ };
257
+ });
258
+ }
259
+
260
+ function getPageTrails(config, pages) {
261
+ const manifest = getManifest(config);
262
+ const pagesBySlug = new Map(pages.map((page) => [page.slug, page]));
263
+ const trails = new Map();
264
+
265
+ const pageLabel = (item, slug) => {
266
+ if (typeof item === "object" && item?.label) return item.label;
267
+ return pagesBySlug.get(slug)?.title || fallbackLabelFromSlug(slug);
268
+ };
269
+
270
+ const walk = (items = [], parents = []) => {
271
+ items.forEach((item) => {
272
+ if (typeof item === "string") {
273
+ trails.set(item, [...parents, pageLabel(item, item)]);
274
+ return;
275
+ }
276
+
277
+ if (item?.type === "submenu") {
278
+ walk(item.items || [], [...parents, item.label].filter(Boolean));
279
+ return;
280
+ }
281
+
282
+ const slug = getPageItemSlug(item);
283
+ if (slug) trails.set(slug, [...parents, pageLabel(item, slug)]);
318
284
  });
319
- }
320
- return docs;
285
+ };
286
+
287
+ (manifest.menu || []).forEach((group) => {
288
+ if (group?.type === "group") walk(group.items || []);
289
+ });
290
+
291
+ return trails;
292
+ }
293
+
294
+ function splitFencedMarkdown(markdown) {
295
+ const code = [];
296
+ const prose = String(markdown || "").replace(/```[^\n]*\n([\s\S]*?)```/g, (_, block) => {
297
+ code.push(block);
298
+ return "\n";
299
+ });
300
+
301
+ return { prose, code: code.join("\n\n") };
302
+ }
303
+
304
+ function getNormalizedDocMarkdown(config, page, { includeTitle = true } = {}) {
305
+ const source = getDocMarkdown(config, page);
306
+ const parsed = parseDocMarkdown(source, page.title);
307
+ const body = normalizeDocMarkdown(renderNunjucksCompat(parsed.body), collectMdxExports(source));
308
+ return includeTitle ? `# ${parsed.title}\n\n${body}` : body;
321
309
  }
322
310
 
323
311
  function flattenSidebarItems(items = []) {
@@ -329,10 +317,6 @@ export function getPageItemSlug(item) {
329
317
  return item.slug || item.page || "";
330
318
  }
331
319
 
332
- function isExternalUrl(url) {
333
- return /^[a-z][a-z0-9+.-]*:/i.test(String(url || ""));
334
- }
335
-
336
320
  function parseDocMarkdown(source, fallbackTitle) {
337
321
  const content = normalizeDocSource(source);
338
322
  const tokens = marked.lexer(content);
@@ -355,6 +339,15 @@ function markdownPlainText(markdown) {
355
339
  return tokensText(marked.lexer(renderNunjucksCompat(normalizeDocSource(markdown))));
356
340
  }
357
341
 
342
+ function collectMdxExports(source) {
343
+ const values = new Map();
344
+ const pattern = /export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*`([\s\S]*?)`;/g;
345
+ for (const match of String(source || "").matchAll(pattern)) {
346
+ values.set(match[1], match[2].replace(/\\n/g, "\n").replace(/\\`/g, "`"));
347
+ }
348
+ return values;
349
+ }
350
+
358
351
  function normalizeDocSource(source) {
359
352
  const withoutFrontmatter = String(source || "")
360
353
  .trimStart()
@@ -366,10 +359,10 @@ function normalizeDocSource(source) {
366
359
  .trimStart();
367
360
  }
368
361
 
369
- async function highlightMarkdownCode(tokens) {
362
+ async function highlightCode(tokens) {
370
363
  for (const token of tokens) {
371
364
  if (token.type === "code") {
372
- const html = await highlightToken(token);
365
+ const html = codeBlockHtml(token, await highlightToken(token));
373
366
  Object.assign(token, {
374
367
  type: "html",
375
368
  raw: html,
@@ -380,24 +373,39 @@ async function highlightMarkdownCode(tokens) {
380
373
  }
381
374
 
382
375
  for (const childTokens of nestedTokenLists(token)) {
383
- await highlightMarkdownCode(childTokens);
376
+ await highlightCode(childTokens);
384
377
  }
385
378
  }
386
379
  }
387
380
 
381
+ function codeBlockHtml(token, html) {
382
+ const title = parseCodeMeta(token.lang).title;
383
+ if (!title) return html;
384
+ return `<div class="code-frame" data-code-title="${escapeHtml(title)}"><header><span>${escapeHtml(title)}</span></header><div class="code-block">${html}</div></div>`;
385
+ }
386
+
388
387
  async function highlightToken(token) {
389
- const lang = normalizeLanguage(token.lang);
390
388
  try {
391
- return wrapCodeBlock(await renderHighlightedCode(token.text, lang));
389
+ return await codeToHtml(token.text, {
390
+ lang: normalizeLanguage(token.lang),
391
+ themes: {
392
+ light: "github-light",
393
+ dark: "github-dark",
394
+ },
395
+ defaultColor: false,
396
+ });
392
397
  } catch {
393
- return wrapCodeBlock(await renderHighlightedCode(token.text, "text"));
398
+ return codeToHtml(token.text, {
399
+ lang: "text",
400
+ themes: {
401
+ light: "github-light",
402
+ dark: "github-dark",
403
+ },
404
+ defaultColor: false,
405
+ });
394
406
  }
395
407
  }
396
408
 
397
- function wrapCodeBlock(html) {
398
- return `<div class="relative my-6 w-full max-w-full overflow-hidden rounded-lg" data-code-block>${html}</div>`;
399
- }
400
-
401
409
  function normalizeLanguage(lang) {
402
410
  const value = lang?.trim().split(/\s+/)[0].toLowerCase() || "text";
403
411
  if (value === "njk") return "html";
@@ -427,6 +435,7 @@ function plainText(tokens) {
427
435
  function tokensText(tokens) {
428
436
  return tokens
429
437
  .map((token) => {
438
+ if (token.type === "html") return htmlText(token.raw || token.text || "");
430
439
  const childTokens = [];
431
440
  if (token.tokens) childTokens.push(token.tokens);
432
441
  if (token.items) childTokens.push(...token.items.flatMap((item) => (item.tokens ? [item.tokens] : [])));
@@ -438,3 +447,16 @@ function tokensText(tokens) {
438
447
  .replace(/\s+/g, " ")
439
448
  .trim();
440
449
  }
450
+
451
+ function htmlText(html) {
452
+ return String(html || "")
453
+ .replace(/<script\b[\s\S]*?<\/script>/gi, " ")
454
+ .replace(/<style\b[\s\S]*?<\/style>/gi, " ")
455
+ .replace(/<[^>]+>/g, " ")
456
+ .replace(/&nbsp;/g, " ")
457
+ .replace(/&amp;/g, "&")
458
+ .replace(/&lt;/g, "<")
459
+ .replace(/&gt;/g, ">")
460
+ .replace(/&quot;/g, '"')
461
+ .replace(/&#39;/g, "'");
462
+ }
@@ -3,7 +3,7 @@ import { codeToHtml } from "shiki";
3
3
  export async function highlightCode(code, lang = "text") {
4
4
  const value = String(code || "");
5
5
  const language = String(lang || "text").toLowerCase() === "mdx" ? "tsx" : String(lang || "text");
6
- const html = await codeToHtml(value, {
6
+ return codeToHtml(value, {
7
7
  lang: language,
8
8
  themes: {
9
9
  light: "github-light",
@@ -11,17 +11,4 @@ export async function highlightCode(code, lang = "text") {
11
11
  },
12
12
  defaultColor: false,
13
13
  });
14
- return addCodeLayoutClasses(html);
15
- }
16
-
17
- function addCodeLayoutClasses(html) {
18
- return String(html)
19
- .replace(
20
- /<pre class="/,
21
- '<pre class="!my-0 max-w-full overflow-x-auto !rounded-none !border-0 ',
22
- )
23
- .replace(
24
- /<code>/,
25
- '<code class="block min-w-full w-max px-4 py-3 text-[13px]">',
26
- );
27
14
  }