reallysimpledocs 1.0.0-beta.4 → 1.0.0-beta.5

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 (49) hide show
  1. package/README.md +35 -35
  2. package/package.json +10 -13
  3. package/src/astro/index.js +46 -98
  4. package/src/astro/preview-source.js +90 -0
  5. package/src/components/index.js +1 -0
  6. package/src/css/custom.css +252 -33
  7. package/src/css/styles/luma.css +11 -0
  8. package/src/css/styles/lyra.css +11 -0
  9. package/src/css/styles/maia.css +11 -0
  10. package/src/css/styles/mira.css +11 -0
  11. package/src/css/styles/nova.css +11 -0
  12. package/src/css/styles/rhea.css +11 -0
  13. package/src/css/styles/sera.css +11 -0
  14. package/src/css/styles/vega.css +11 -0
  15. package/src/runtime/components/BaseLayout.astro +20 -27
  16. package/src/runtime/components/CommandDialog.astro +30 -32
  17. package/src/runtime/components/DefaultContentHeader.astro +0 -17
  18. package/src/runtime/components/DefaultHead.astro +120 -0
  19. package/src/runtime/components/DefaultHeadContent.astro +2 -0
  20. package/src/runtime/components/DefaultSidebarHeader.astro +1 -1
  21. package/src/runtime/components/DocsLayout.astro +114 -165
  22. package/src/runtime/components/DropdownMenu.astro +1 -1
  23. package/src/runtime/components/ThemeToggle.astro +21 -0
  24. package/src/runtime/components/mdx/Callout.astro +2 -32
  25. package/src/runtime/components/mdx/Card.astro +3 -8
  26. package/src/runtime/components/mdx/CardGrid.astro +1 -1
  27. package/src/runtime/components/mdx/Code.astro +2 -16
  28. package/src/runtime/components/mdx/CodeGroup.astro +7 -11
  29. package/src/runtime/components/mdx/Preview.astro +7 -13
  30. package/src/runtime/components/mdx/Steps.astro +1 -5
  31. package/src/runtime/components/mdx/Tab.astro +6 -0
  32. package/src/runtime/components/mdx/Tabs.astro +7 -7
  33. package/src/runtime/lib/docs.js +130 -102
  34. package/src/runtime/lib/highlight.js +1 -14
  35. package/src/runtime/lib/macros.js +10 -5
  36. package/src/runtime/lib/markdown-export.js +134 -171
  37. package/src/runtime/pages/doc.astro +1 -3
  38. package/src/runtime/pages/llms-full.txt.ts +2 -2
  39. package/src/runtime/pages/llms.txt.ts +2 -2
  40. package/src/runtime/pages/markdown.ts +2 -2
  41. package/src/runtime/pages/search-index.ts +12 -3
  42. package/CHANGELOG.md +0 -35
  43. package/src/css/overrides.css +0 -1
  44. package/src/css/sources.css +0 -1
  45. package/src/runtime/lib/markdown-exporters/index.js +0 -144
  46. package/src/runtime/lib/mdx-source.js +0 -19
  47. package/src/runtime/lib/preview-source-plugin.js +0 -33
  48. package/src/runtime/scripts/copy-code.js +0 -130
  49. package/src/site/ContentHeader.astro +0 -33
@@ -2,6 +2,6 @@
2
2
  const { class: className = "" } = Astro.props;
3
3
  ---
4
4
 
5
- <div class:list={["my-6 grid w-full gap-4 sm:grid-cols-2 [&>*]:my-0", className]}>
5
+ <div class:list={["grid w-full gap-4 sm:grid-cols-2", className]}>
6
6
  <slot />
7
7
  </div>
@@ -5,10 +5,6 @@ const { lang = "text", title, class: className = "" } = Astro.props;
5
5
  const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
6
6
  const rawCode = decodeSlotText(slotHtml);
7
7
  const html = await highlightCode(rawCode, lang);
8
- const classes = [
9
- "relative my-6 w-full max-w-full overflow-hidden rounded-lg",
10
- className,
11
- ];
12
8
 
13
9
  function decodeSlotText(value) {
14
10
  return String(value || "")
@@ -23,17 +19,7 @@ function decodeSlotText(value) {
23
19
  }
24
20
  ---
25
21
 
26
- <div
27
- data-code-block
28
- data-code-title={title || undefined}
29
- class:list={classes}
30
- >
31
- {
32
- title && (
33
- <header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
34
- <span class="mr-auto">{title}</span>
35
- </header>
36
- )
37
- }
22
+ <div class:list={["code-block", className]} data-code-title={title || undefined}>
23
+ {title && <header><span>{title}</span></header>}
38
24
  <Fragment set:html={html} />
39
25
  </div>
@@ -1,18 +1,14 @@
1
1
  ---
2
- const { defaultTab, class: className = "" } = Astro.props;
2
+ const { class: className = "" } = Astro.props;
3
3
  const id = `code-group-${Math.random().toString(36).slice(2)}`;
4
4
  const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
5
5
  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
- ];
6
+ const defaultValue = normalizedTabs[0]?.value;
11
7
 
12
8
  function parseCodeTabs(html) {
13
9
  const blocks = [];
14
10
  const pattern =
15
- /<div\b(?=[^>]*\bdata-code-block\b)(?=[^>]*\bdata-code-title="([^"]*)")[^>]*>([\s\S]*?)<\/div>/g;
11
+ /<div\b(?=[^>]*\bclass="[^"]*\bcode-block\b[^"]*")(?=[^>]*\bdata-code-title="([^"]*)")[^>]*>([\s\S]*?)<\/div>/g;
16
12
  let match;
17
13
  while ((match = pattern.exec(html))) {
18
14
  const label = decodeHtml(match[1]).trim();
@@ -36,10 +32,10 @@ function decodeHtml(value) {
36
32
  }
37
33
  ---
38
34
 
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">
35
+ <div class:list={["code-group", className]}>
36
+ <header>
37
+ <div class="tabs">
38
+ <nav role="tablist" aria-orientation="horizontal" data-variant="line">
43
39
  {
44
40
  normalizedTabs.map((tab, index) => {
45
41
  const selected = tab.value === defaultValue;
@@ -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="preview">
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>
@@ -2,9 +2,9 @@ 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 { normalizeDocMarkdown } from "./markdown-export.js";
8
8
  import { markdownPath, routePath } from "./paths.js";
9
9
 
10
10
  export function getManifest(config) {
@@ -83,14 +83,8 @@ export function getDocMarkdown(config, page) {
83
83
  return fs.readFileSync(filePath, "utf8");
84
84
  }
85
85
 
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();
86
+ export function getMarkdownExport(config, page) {
87
+ return `${getNormalizedDocMarkdown(config, page).trim()}\n`;
94
88
  }
95
89
 
96
90
  export async function renderDoc(config, page) {
@@ -124,7 +118,7 @@ export async function renderDoc(config, page) {
124
118
  };
125
119
 
126
120
  const tokens = marked.lexer(content);
127
- await highlightMarkdownCode(tokens);
121
+ await highlightCode(tokens);
128
122
  const html = marked.parser(tokens, { renderer });
129
123
  return { page, html, headings };
130
124
  }
@@ -159,46 +153,6 @@ export function getNavigation(config, page) {
159
153
  };
160
154
  }
161
155
 
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
156
  export function resolveIcon(icon) {
203
157
  if (!icon || typeof icon !== "string") return "";
204
158
  const trimmed = icon.trim();
@@ -229,35 +183,28 @@ export function buildMenus(config, currentSlug) {
229
183
  icon,
230
184
  url: page.path,
231
185
  label: item.label || page.title,
186
+ badge: item.badge,
232
187
  current: page.slug === currentSlug,
233
188
  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,
189
+ attrs: item.attrs,
253
190
  };
254
191
  };
255
192
 
256
193
  const processItem = (item) => {
257
194
  if (typeof item === "string") return processSlug(item);
195
+ if (item?.url && item?.label) {
196
+ return {
197
+ type: "item",
198
+ icon: resolveIcon(item.icon),
199
+ url: item.url,
200
+ label: item.label,
201
+ badge: item.badge,
202
+ external: /^https?:\/\//.test(item.url),
203
+ attrs: item.attrs,
204
+ };
205
+ }
258
206
  const slug = getPageItemSlug(item);
259
207
  if (slug) return processSlug(slug, item);
260
- if (item?.url) return processExternalLink(item);
261
208
  if (item?.type === "submenu") {
262
209
  const items = (item.items || []).map(processItem).filter(Boolean);
263
210
  return {
@@ -287,37 +234,90 @@ export function buildMenus(config, currentSlug) {
287
234
  icon: item.icon,
288
235
  url: item.url,
289
236
  label: item.label,
290
- external: item.external,
291
- attrs: item.attrs,
292
237
  keywords: item.keywords,
238
+ attrs: item.attrs,
293
239
  })),
294
240
  }));
295
241
 
296
242
  return { sidebar, command };
297
243
  }
298
244
 
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;
245
+ export function getLlmDocs(config) {
246
+ return getPages(config).map((page) => ({
247
+ ...page,
248
+ content: getNormalizedDocMarkdown(config, page, { includeTitle: false }).trim(),
249
+ }));
308
250
  }
309
251
 
310
- export async function getSearchDocs(config) {
311
- const docs = [];
312
- for (const page of getPages(config)) {
313
- docs.push({
252
+ export function getSearchDocs(config) {
253
+ const pages = getPages(config);
254
+ const trails = getPageTrails(config, pages);
255
+
256
+ return pages.map((page) => {
257
+ const markdown = getNormalizedDocMarkdown(config, page, { includeTitle: false });
258
+ const { prose, code } = splitFencedMarkdown(markdown);
259
+ const trail = trails.get(page.slug) || [page.title];
260
+
261
+ return {
314
262
  slug: page.slug,
315
263
  title: page.title,
264
+ breadcrumb: trail.length > 1 ? trail.join(" › ") : "",
316
265
  path: page.path,
317
- body: markdownPlainText(await getMarkdownBodyExport(config, page)),
266
+ body: markdownPlainText(prose),
267
+ code: markdownPlainText(code),
268
+ };
269
+ });
270
+ }
271
+
272
+ function getPageTrails(config, pages) {
273
+ const manifest = getManifest(config);
274
+ const pagesBySlug = new Map(pages.map((page) => [page.slug, page]));
275
+ const trails = new Map();
276
+
277
+ const pageLabel = (item, slug) => {
278
+ if (typeof item === "object" && item?.label) return item.label;
279
+ return pagesBySlug.get(slug)?.title || fallbackLabelFromSlug(slug);
280
+ };
281
+
282
+ const walk = (items = [], parents = []) => {
283
+ items.forEach((item) => {
284
+ if (typeof item === "string") {
285
+ trails.set(item, [...parents, pageLabel(item, item)]);
286
+ return;
287
+ }
288
+
289
+ if (item?.type === "submenu") {
290
+ walk(item.items || [], [...parents, item.label].filter(Boolean));
291
+ return;
292
+ }
293
+
294
+ const slug = getPageItemSlug(item);
295
+ if (slug) trails.set(slug, [...parents, pageLabel(item, slug)]);
318
296
  });
319
- }
320
- return docs;
297
+ };
298
+
299
+ (manifest.menu || []).forEach((group) => {
300
+ if (group?.type === "group") walk(group.items || []);
301
+ });
302
+
303
+ return trails;
304
+ }
305
+
306
+ function splitFencedMarkdown(markdown) {
307
+ const code = [];
308
+ const prose = String(markdown || "").replace(/```[^\n]*\n([\s\S]*?)```/g, (_, block) => {
309
+ code.push(block);
310
+ return "\n";
311
+ });
312
+
313
+ return { prose, code: code.join("\n\n") };
314
+ }
315
+
316
+ function getNormalizedDocMarkdown(config, page, { includeTitle = true } = {}) {
317
+ const source = getDocMarkdown(config, page);
318
+ const parsed = parseDocMarkdown(source, page.title);
319
+ const body = normalizeDocMarkdown(renderNunjucksCompat(parsed.body), collectMdxExports(source));
320
+ return includeTitle ? `# ${parsed.title}\n\n${body}` : body;
321
321
  }
322
322
 
323
323
  function flattenSidebarItems(items = []) {
@@ -329,10 +329,6 @@ export function getPageItemSlug(item) {
329
329
  return item.slug || item.page || "";
330
330
  }
331
331
 
332
- function isExternalUrl(url) {
333
- return /^[a-z][a-z0-9+.-]*:/i.test(String(url || ""));
334
- }
335
-
336
332
  function parseDocMarkdown(source, fallbackTitle) {
337
333
  const content = normalizeDocSource(source);
338
334
  const tokens = marked.lexer(content);
@@ -355,6 +351,15 @@ function markdownPlainText(markdown) {
355
351
  return tokensText(marked.lexer(renderNunjucksCompat(normalizeDocSource(markdown))));
356
352
  }
357
353
 
354
+ function collectMdxExports(source) {
355
+ const values = new Map();
356
+ const pattern = /export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*`([\s\S]*?)`;/g;
357
+ for (const match of String(source || "").matchAll(pattern)) {
358
+ values.set(match[1], match[2].replace(/\\n/g, "\n").replace(/\\`/g, "`"));
359
+ }
360
+ return values;
361
+ }
362
+
358
363
  function normalizeDocSource(source) {
359
364
  const withoutFrontmatter = String(source || "")
360
365
  .trimStart()
@@ -366,7 +371,7 @@ function normalizeDocSource(source) {
366
371
  .trimStart();
367
372
  }
368
373
 
369
- async function highlightMarkdownCode(tokens) {
374
+ async function highlightCode(tokens) {
370
375
  for (const token of tokens) {
371
376
  if (token.type === "code") {
372
377
  const html = await highlightToken(token);
@@ -380,24 +385,33 @@ async function highlightMarkdownCode(tokens) {
380
385
  }
381
386
 
382
387
  for (const childTokens of nestedTokenLists(token)) {
383
- await highlightMarkdownCode(childTokens);
388
+ await highlightCode(childTokens);
384
389
  }
385
390
  }
386
391
  }
387
392
 
388
393
  async function highlightToken(token) {
389
- const lang = normalizeLanguage(token.lang);
390
394
  try {
391
- return wrapCodeBlock(await renderHighlightedCode(token.text, lang));
395
+ return await codeToHtml(token.text, {
396
+ lang: normalizeLanguage(token.lang),
397
+ themes: {
398
+ light: "github-light",
399
+ dark: "github-dark",
400
+ },
401
+ defaultColor: false,
402
+ });
392
403
  } catch {
393
- return wrapCodeBlock(await renderHighlightedCode(token.text, "text"));
404
+ return codeToHtml(token.text, {
405
+ lang: "text",
406
+ themes: {
407
+ light: "github-light",
408
+ dark: "github-dark",
409
+ },
410
+ defaultColor: false,
411
+ });
394
412
  }
395
413
  }
396
414
 
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
415
  function normalizeLanguage(lang) {
402
416
  const value = lang?.trim().split(/\s+/)[0].toLowerCase() || "text";
403
417
  if (value === "njk") return "html";
@@ -427,6 +441,7 @@ function plainText(tokens) {
427
441
  function tokensText(tokens) {
428
442
  return tokens
429
443
  .map((token) => {
444
+ if (token.type === "html") return htmlText(token.raw || token.text || "");
430
445
  const childTokens = [];
431
446
  if (token.tokens) childTokens.push(token.tokens);
432
447
  if (token.items) childTokens.push(...token.items.flatMap((item) => (item.tokens ? [item.tokens] : [])));
@@ -438,3 +453,16 @@ function tokensText(tokens) {
438
453
  .replace(/\s+/g, " ")
439
454
  .trim();
440
455
  }
456
+
457
+ function htmlText(html) {
458
+ return String(html || "")
459
+ .replace(/<script\b[\s\S]*?<\/script>/gi, " ")
460
+ .replace(/<style\b[\s\S]*?<\/style>/gi, " ")
461
+ .replace(/<[^>]+>/g, " ")
462
+ .replace(/&nbsp;/g, " ")
463
+ .replace(/&amp;/g, "&")
464
+ .replace(/&lt;/g, "<")
465
+ .replace(/&gt;/g, ">")
466
+ .replace(/&quot;/g, '"')
467
+ .replace(/&#39;/g, "'");
468
+ }
@@ -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
  }
@@ -1,8 +1,6 @@
1
- import ArrowUpRight from "lucide-static/icons/arrow-up-right.svg?raw";
2
1
  import Search from "lucide-static/icons/search.svg?raw";
3
- import { attrs, escapeHtml } from "./html.js";
4
-
5
- const externalLinkIcon = ArrowUpRight.replace('class="lucide', 'class="lucide ml-auto size-3.5 shrink-0');
2
+ import ArrowUpRight from "lucide-static/icons/arrow-up-right.svg?raw";
3
+ import { addSvgClass, attrs, escapeHtml } from "./html.js";
6
4
 
7
5
  export function renderSidebar({
8
6
  id = "sidebar",
@@ -61,13 +59,20 @@ export function renderSidebarContent(items = [], parentIdPrefix = "content") {
61
59
  <a${attrs({ href: item.url, "aria-current": item.current ? "page" : undefined, ...without(item.attrs, ["href", "aria-current"]) })}>
62
60
  ${item.icon || ""}
63
61
  <span>${escapeHtml(item.label)}</span>
64
- ${item.external ? externalLinkIcon : ""}
62
+ ${renderSidebarMeta(item)}
65
63
  </a>
66
64
  </li>`;
67
65
  })
68
66
  .join("");
69
67
  }
70
68
 
69
+ function renderSidebarMeta(item) {
70
+ const badge = item.badge ? `<span class="badge" data-variant="outline">${escapeHtml(item.badge)}</span>` : "";
71
+ const external = item.external ? addSvgClass(ArrowUpRight, "size-3 opacity-50") : "";
72
+ if (!badge && !external) return "";
73
+ return `<span class="ml-auto inline-flex items-center gap-1">${badge}${external}</span>`;
74
+ }
75
+
71
76
  export function renderCommandDialog({
72
77
  id = "command-search",
73
78
  items = [],