reallysimpledocs 1.0.0-beta.2 → 1.0.0-beta.4

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 (36) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +11 -11
  3. package/package.json +10 -2
  4. package/src/astro/index.js +94 -3
  5. package/src/components/index.js +0 -2
  6. package/src/css/custom.css +45 -205
  7. package/src/runtime/components/CommandDialog.astro +20 -3
  8. package/src/runtime/components/DefaultContentHeader.astro +19 -0
  9. package/src/runtime/components/DefaultSidebarFooter.astro +2 -0
  10. package/src/runtime/components/DocsLayout.astro +100 -81
  11. package/src/runtime/components/Sidebar.astro +4 -0
  12. package/src/runtime/components/mdx/Callout.astro +36 -26
  13. package/src/runtime/components/mdx/Card.astro +8 -7
  14. package/src/runtime/components/mdx/CardGrid.astro +1 -1
  15. package/src/runtime/components/mdx/Code.astro +33 -13
  16. package/src/runtime/components/mdx/CodeGroup.astro +38 -14
  17. package/src/runtime/components/mdx/Preview.astro +16 -6
  18. package/src/runtime/components/mdx/Steps.astro +5 -1
  19. package/src/runtime/components/mdx/Tabs.astro +1 -1
  20. package/src/runtime/lib/docs.js +107 -36
  21. package/src/runtime/lib/highlight.js +27 -0
  22. package/src/runtime/lib/macros.js +5 -1
  23. package/src/runtime/lib/markdown-export.js +200 -0
  24. package/src/runtime/lib/markdown-exporters/index.js +144 -0
  25. package/src/runtime/lib/mdx-source.js +19 -0
  26. package/src/runtime/lib/preview-source-plugin.js +33 -0
  27. package/src/runtime/lib/site.js +0 -1
  28. package/src/runtime/pages/doc.astro +4 -1
  29. package/src/runtime/pages/llms-full.txt.ts +2 -2
  30. package/src/runtime/pages/llms.txt.ts +2 -2
  31. package/src/runtime/pages/markdown.ts +2 -2
  32. package/src/runtime/pages/search-index.ts +2 -2
  33. package/src/runtime/scripts/copy-code.js +51 -6
  34. package/src/site/ContentHeader.astro +33 -0
  35. package/src/runtime/components/mdx/Badge.astro +0 -15
  36. package/src/runtime/components/mdx/LinkButton.astro +0 -14
@@ -2,8 +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";
6
5
  import * as lucideIcons from "lucide-static";
6
+ import { highlightCode as renderHighlightedCode } from "./highlight.js";
7
+ import { mdxToMarkdown } from "./markdown-export.js";
7
8
  import { markdownPath, routePath } from "./paths.js";
8
9
 
9
10
  export function getManifest(config) {
@@ -82,8 +83,14 @@ export function getDocMarkdown(config, page) {
82
83
  return fs.readFileSync(filePath, "utf8");
83
84
  }
84
85
 
85
- export function getMarkdownExport(config, page) {
86
- return `${getDocMarkdown(config, page).trim()}\n`;
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();
87
94
  }
88
95
 
89
96
  export async function renderDoc(config, page) {
@@ -117,7 +124,7 @@ export async function renderDoc(config, page) {
117
124
  };
118
125
 
119
126
  const tokens = marked.lexer(content);
120
- await highlightCode(tokens);
127
+ await highlightMarkdownCode(tokens);
121
128
  const html = marked.parser(tokens, { renderer });
122
129
  return { page, html, headings };
123
130
  }
@@ -152,6 +159,46 @@ export function getNavigation(config, page) {
152
159
  };
153
160
  }
154
161
 
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
+
155
202
  export function resolveIcon(icon) {
156
203
  if (!icon || typeof icon !== "string") return "";
157
204
  const trimmed = icon.trim();
@@ -164,7 +211,9 @@ export function resolveIcon(icon) {
164
211
  .join("")
165
212
  : trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
166
213
  const iconClass = trimmed.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
167
- return lucideIcons[pascalName]?.replace('class="lucide', `class="lucide lucide-${iconClass}`) || "";
214
+ const svg = lucideIcons[pascalName] || "";
215
+ if (!svg || svg.includes(`lucide-${iconClass}`)) return svg;
216
+ return svg.replace('class="lucide', `class="lucide lucide-${iconClass}`);
168
217
  }
169
218
 
170
219
  export function buildMenus(config, currentSlug) {
@@ -185,10 +234,30 @@ export function buildMenus(config, currentSlug) {
185
234
  };
186
235
  };
187
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,
253
+ };
254
+ };
255
+
188
256
  const processItem = (item) => {
189
257
  if (typeof item === "string") return processSlug(item);
190
258
  const slug = getPageItemSlug(item);
191
259
  if (slug) return processSlug(slug, item);
260
+ if (item?.url) return processExternalLink(item);
192
261
  if (item?.type === "submenu") {
193
262
  const items = (item.items || []).map(processItem).filter(Boolean);
194
263
  return {
@@ -218,6 +287,8 @@ export function buildMenus(config, currentSlug) {
218
287
  icon: item.icon,
219
288
  url: item.url,
220
289
  label: item.label,
290
+ external: item.external,
291
+ attrs: item.attrs,
221
292
  keywords: item.keywords,
222
293
  })),
223
294
  }));
@@ -225,23 +296,28 @@ export function buildMenus(config, currentSlug) {
225
296
  return { sidebar, command };
226
297
  }
227
298
 
228
- export function getLlmDocs(config) {
229
- return getPages(config).map((page) => ({
230
- ...page,
231
- content: parseDocMarkdown(getDocMarkdown(config, page), page.title).body.trim(),
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
308
  }
234
309
 
235
- export function getSearchDocs(config) {
236
- return getPages(config).map((page) => {
237
- const parsed = parseDocMarkdown(getDocMarkdown(config, page), page.title);
238
- return {
310
+ export async function getSearchDocs(config) {
311
+ const docs = [];
312
+ for (const page of getPages(config)) {
313
+ docs.push({
239
314
  slug: page.slug,
240
315
  title: page.title,
241
316
  path: page.path,
242
- body: markdownPlainText(parsed.body),
243
- };
244
- });
317
+ body: markdownPlainText(await getMarkdownBodyExport(config, page)),
318
+ });
319
+ }
320
+ return docs;
245
321
  }
246
322
 
247
323
  function flattenSidebarItems(items = []) {
@@ -253,6 +329,10 @@ export function getPageItemSlug(item) {
253
329
  return item.slug || item.page || "";
254
330
  }
255
331
 
332
+ function isExternalUrl(url) {
333
+ return /^[a-z][a-z0-9+.-]*:/i.test(String(url || ""));
334
+ }
335
+
256
336
  function parseDocMarkdown(source, fallbackTitle) {
257
337
  const content = normalizeDocSource(source);
258
338
  const tokens = marked.lexer(content);
@@ -282,11 +362,11 @@ function normalizeDocSource(source) {
282
362
  const firstHeading = withoutFrontmatter.search(/^#\s+/m);
283
363
  if (firstHeading > 0) return withoutFrontmatter.slice(firstHeading).trimStart();
284
364
  return withoutFrontmatter
285
- .replace(/^(\s*(?:import|export)\s+[^\n]*\n)+/m, "")
365
+ .replace(/^(?:[ \t]*(?:import|export)\s+[^\n]*\n)+/, "")
286
366
  .trimStart();
287
367
  }
288
368
 
289
- async function highlightCode(tokens) {
369
+ async function highlightMarkdownCode(tokens) {
290
370
  for (const token of tokens) {
291
371
  if (token.type === "code") {
292
372
  const html = await highlightToken(token);
@@ -300,33 +380,24 @@ async function highlightCode(tokens) {
300
380
  }
301
381
 
302
382
  for (const childTokens of nestedTokenLists(token)) {
303
- await highlightCode(childTokens);
383
+ await highlightMarkdownCode(childTokens);
304
384
  }
305
385
  }
306
386
  }
307
387
 
308
388
  async function highlightToken(token) {
389
+ const lang = normalizeLanguage(token.lang);
309
390
  try {
310
- return await codeToHtml(token.text, {
311
- lang: normalizeLanguage(token.lang),
312
- themes: {
313
- light: "github-light",
314
- dark: "github-dark",
315
- },
316
- defaultColor: false,
317
- });
391
+ return wrapCodeBlock(await renderHighlightedCode(token.text, lang));
318
392
  } catch {
319
- return codeToHtml(token.text, {
320
- lang: "text",
321
- themes: {
322
- light: "github-light",
323
- dark: "github-dark",
324
- },
325
- defaultColor: false,
326
- });
393
+ return wrapCodeBlock(await renderHighlightedCode(token.text, "text"));
327
394
  }
328
395
  }
329
396
 
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
+
330
401
  function normalizeLanguage(lang) {
331
402
  const value = lang?.trim().split(/\s+/)[0].toLowerCase() || "text";
332
403
  if (value === "njk") return "html";
@@ -0,0 +1,27 @@
1
+ import { codeToHtml } from "shiki";
2
+
3
+ export async function highlightCode(code, lang = "text") {
4
+ const value = String(code || "");
5
+ const language = String(lang || "text").toLowerCase() === "mdx" ? "tsx" : String(lang || "text");
6
+ const html = await codeToHtml(value, {
7
+ lang: language,
8
+ themes: {
9
+ light: "github-light",
10
+ dark: "github-dark",
11
+ },
12
+ defaultColor: false,
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
+ }
@@ -1,6 +1,9 @@
1
+ import ArrowUpRight from "lucide-static/icons/arrow-up-right.svg?raw";
1
2
  import Search from "lucide-static/icons/search.svg?raw";
2
3
  import { attrs, escapeHtml } from "./html.js";
3
4
 
5
+ const externalLinkIcon = ArrowUpRight.replace('class="lucide', 'class="lucide ml-auto size-3.5 shrink-0');
6
+
4
7
  export function renderSidebar({
5
8
  id = "sidebar",
6
9
  label = "Sidebar navigation",
@@ -58,6 +61,7 @@ export function renderSidebarContent(items = [], parentIdPrefix = "content") {
58
61
  <a${attrs({ href: item.url, "aria-current": item.current ? "page" : undefined, ...without(item.attrs, ["href", "aria-current"]) })}>
59
62
  ${item.icon || ""}
60
63
  <span>${escapeHtml(item.label)}</span>
64
+ ${item.external ? externalLinkIcon : ""}
61
65
  </a>
62
66
  </li>`;
63
67
  })
@@ -67,7 +71,7 @@ export function renderSidebarContent(items = [], parentIdPrefix = "content") {
67
71
  export function renderCommandDialog({
68
72
  id = "command-search",
69
73
  items = [],
70
- placeholder = "Search the docs...",
74
+ placeholder = "Search...",
71
75
  emptyText = "No results found.",
72
76
  open = false,
73
77
  commandAttrs = {},
@@ -0,0 +1,200 @@
1
+ import { gfmToMarkdown } from "mdast-util-gfm";
2
+ import { toMarkdown } from "mdast-util-to-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import remarkMdx from "remark-mdx";
5
+ import remarkParse from "remark-parse";
6
+ import { unified } from "unified";
7
+ import { serializeComponent } from "./markdown-exporters/index.js";
8
+ import { getMdxChildrenSource, trimCode } from "./mdx-source.js";
9
+
10
+ const processor = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
11
+
12
+ const customSerializersCache = new Map();
13
+
14
+ export async function mdxToMarkdown(source, options = {}) {
15
+ const tree = processor.parse(source);
16
+ const state = {
17
+ constants: collectExportedStrings(tree),
18
+ customSerializers: await loadCustomSerializers(options.markdownSerializers),
19
+ source: String(source || ""),
20
+ };
21
+ const children = await transformBlocks(tree.children || [], state);
22
+ return `${stringify({ type: "root", children }).trim()}\n`;
23
+ }
24
+
25
+ async function loadCustomSerializers(entries = {}) {
26
+ const cacheKey = JSON.stringify(entries || {});
27
+ if (customSerializersCache.has(cacheKey)) return customSerializersCache.get(cacheKey);
28
+
29
+ const serializers = {};
30
+ for (const [name, moduleId] of Object.entries(entries || {})) {
31
+ if (!moduleId) continue;
32
+ const module = await import(/* @vite-ignore */ moduleId);
33
+ const serializer = module.default || module.serialize;
34
+ if (typeof serializer !== "function") {
35
+ throw new Error(`Markdown serializer for "${name}" must export a function as default or serialize.`);
36
+ }
37
+ serializers[name] = serializer;
38
+ }
39
+
40
+ customSerializersCache.set(cacheKey, serializers);
41
+ return serializers;
42
+ }
43
+
44
+ async function transformBlocks(nodes, state) {
45
+ const blocks = [];
46
+ for (const node of nodes) {
47
+ blocks.push(...(await transformBlock(node, state)));
48
+ }
49
+ return blocks.filter(Boolean);
50
+ }
51
+
52
+ async function transformBlock(node, state) {
53
+ if (!node) return [];
54
+
55
+ if (node.type === "mdxjsEsm") return [];
56
+
57
+ if (isMdxElement(node)) {
58
+ return await transformComponent(node, state);
59
+ }
60
+
61
+ if (node.type === "paragraph") {
62
+ const children = significantChildren(node.children || []);
63
+ if (children.length === 1 && isMdxElement(children[0])) {
64
+ return await transformComponent(children[0], state);
65
+ }
66
+ }
67
+
68
+ const clone = { ...node };
69
+ if (Array.isArray(clone.children)) {
70
+ if (isBlockContainer(clone.type)) {
71
+ clone.children = await transformBlocks(clone.children, state);
72
+ } else {
73
+ clone.children = await transformInline(clone.children, state);
74
+ }
75
+ }
76
+ return [clone];
77
+ }
78
+
79
+ async function transformComponent(node, state) {
80
+ return (
81
+ (await serializeComponent(
82
+ node,
83
+ {
84
+ transformBlocks: (nodes) => transformBlocks(nodes, state),
85
+ getAttr: (entry, name) => getAttr(entry, name, state.constants),
86
+ getCodeValue: (entry) => getCodeValue(entry, state.constants),
87
+ getChildrenSource: (entry) => getMdxChildrenSource(entry, state.source),
88
+ isNamedElement,
89
+ significantChildren,
90
+ },
91
+ state.customSerializers,
92
+ )) || (await transformBlocks(node.children || [], state))
93
+ );
94
+ }
95
+
96
+ async function transformInline(nodes, state) {
97
+ const inline = [];
98
+ for (const node of nodes) {
99
+ if (isMdxElement(node)) {
100
+ const text = stringify({ type: "root", children: await transformComponent(node, state) }).trim();
101
+ if (text) inline.push({ type: "text", value: text });
102
+ continue;
103
+ }
104
+ if (node.type === "mdxTextExpression" || node.type === "mdxFlowExpression") {
105
+ const value = expressionValue(node, state.constants);
106
+ if (value) inline.push({ type: "text", value });
107
+ continue;
108
+ }
109
+ const clone = { ...node };
110
+ if (Array.isArray(clone.children)) clone.children = await transformInline(clone.children, state);
111
+ inline.push(clone);
112
+ }
113
+ return inline;
114
+ }
115
+
116
+ function getCodeValue(node, constants) {
117
+ const parts = (node.children || []).map((child) => {
118
+ if (child.type === "text") return child.value || "";
119
+ if (child.type === "mdxFlowExpression" || child.type === "mdxTextExpression") {
120
+ return expressionValue(child, constants);
121
+ }
122
+ return "";
123
+ });
124
+ return trimCode(parts.join("") || String(getAttr(node, "code", constants) || ""));
125
+ }
126
+
127
+ function getAttr(node, name, constants) {
128
+ const attr = (node.attributes || []).find((entry) => entry.type === "mdxJsxAttribute" && entry.name === name);
129
+ if (!attr) return "";
130
+ if (attr.value == null) return "true";
131
+ if (typeof attr.value === "string") return attr.value;
132
+ return expressionValue(attr.value, constants);
133
+ }
134
+
135
+ function collectExportedStrings(tree) {
136
+ const constants = new Map();
137
+ for (const node of tree.children || []) {
138
+ if (node.type !== "mdxjsEsm") continue;
139
+ for (const statement of node.data?.estree?.body || []) {
140
+ if (statement.type !== "ExportNamedDeclaration") continue;
141
+ if (statement.declaration?.type !== "VariableDeclaration") continue;
142
+ for (const declaration of statement.declaration.declarations || []) {
143
+ if (declaration.id?.type !== "Identifier") continue;
144
+ const value = staticValue(declaration.init, constants);
145
+ if (value) constants.set(declaration.id.name, value);
146
+ }
147
+ }
148
+ }
149
+ return constants;
150
+ }
151
+
152
+ function expressionValue(node, constants) {
153
+ const expression = node.data?.estree?.body?.[0]?.expression || node.data?.estree?.body?.[0];
154
+ return staticValue(expression, constants) || "";
155
+ }
156
+
157
+ function staticValue(node, constants) {
158
+ if (!node) return "";
159
+ if (node.type === "Literal") return node.value ?? "";
160
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
161
+ return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw ?? "").join("");
162
+ }
163
+ if (node.type === "Identifier") return constants.get(node.name) || "";
164
+ if (node.type === "ObjectExpression") {
165
+ return Object.fromEntries(
166
+ node.properties
167
+ .filter((property) => property.type === "Property")
168
+ .map((property) => [property.key.name || property.key.value, staticValue(property.value, constants)]),
169
+ );
170
+ }
171
+ if (node.type === "ArrayExpression") {
172
+ return node.elements.map((element) => staticValue(element, constants));
173
+ }
174
+ return "";
175
+ }
176
+
177
+ function stringify(tree) {
178
+ return toMarkdown(tree, {
179
+ bullet: "-",
180
+ fences: true,
181
+ rule: "-",
182
+ extensions: [gfmToMarkdown()],
183
+ });
184
+ }
185
+
186
+ function isMdxElement(node) {
187
+ return node?.type === "mdxJsxFlowElement" || node?.type === "mdxJsxTextElement";
188
+ }
189
+
190
+ function isNamedElement(node, name) {
191
+ return isMdxElement(node) && node.name === name;
192
+ }
193
+
194
+ function significantChildren(children) {
195
+ return children.filter((child) => child.type !== "text" || child.value.trim());
196
+ }
197
+
198
+ function isBlockContainer(type) {
199
+ return type === "root" || type === "blockquote" || type === "list" || type === "listItem";
200
+ }
@@ -0,0 +1,144 @@
1
+ const serializers = {
2
+ Callout: renderCallout,
3
+ Code: renderCode,
4
+ CodeGroup: renderCodeGroup,
5
+ Preview: renderPreview,
6
+ Steps: renderSteps,
7
+ Tabs: renderChildren,
8
+ Card: renderCard,
9
+ CardGrid: renderCardGrid,
10
+ };
11
+
12
+ export async function serializeComponent(node, context, customSerializers = {}) {
13
+ if (customSerializers[node.name]) return await customSerializers[node.name](node, context);
14
+ return serializers[node.name]?.(node, context) || null;
15
+ }
16
+
17
+ async function renderChildren(node, context) {
18
+ return await context.transformBlocks(node.children || []);
19
+ }
20
+
21
+ async function renderCallout(node, context) {
22
+ const title = context.getAttr(node, "title");
23
+ const action = context.getAttr(node, "action");
24
+ const children = await context.transformBlocks(node.children || []);
25
+ const quoteChildren = [];
26
+ if (title) quoteChildren.push(strongParagraph(title));
27
+ quoteChildren.push(...children);
28
+ if (action?.label) {
29
+ quoteChildren.push({
30
+ type: "paragraph",
31
+ children: action.href
32
+ ? [{ type: "link", url: action.href, children: [{ type: "text", value: action.label }] }]
33
+ : [{ type: "strong", children: [{ type: "text", value: action.label }] }],
34
+ });
35
+ }
36
+ return [{ type: "blockquote", children: quoteChildren }];
37
+ }
38
+
39
+ function renderCode(node, context) {
40
+ const title = context.getAttr(node, "title") || context.getAttr(node, "slot");
41
+ const lang = context.getAttr(node, "lang") || "text";
42
+ const value = context.getCodeValue(node);
43
+ const blocks = [];
44
+ if (title) blocks.push(strongParagraph(title));
45
+ blocks.push({ type: "code", lang, value });
46
+ return blocks;
47
+ }
48
+
49
+ function renderCodeGroup(node, context) {
50
+ const codeNodes = collectCodeChildren(node, context);
51
+ if (!codeNodes.length) return renderChildren(node, context);
52
+ return codeNodes.flatMap((child, index) => {
53
+ const blocks = renderCode(child, context);
54
+ return index === 0 ? blocks : [{ type: "thematicBreak" }, ...blocks];
55
+ });
56
+ }
57
+
58
+ async function renderPreview(node, context) {
59
+ const value = context.getAttr(node, "code") || context.getChildrenSource(node);
60
+ if (value) {
61
+ return [{ type: "code", lang: context.getAttr(node, "lang") || "mdx", value }];
62
+ }
63
+ return await renderChildren(node, context);
64
+ }
65
+
66
+ async function renderSteps(node, context) {
67
+ const steps = (node.children || []).filter((child) => context.isNamedElement(child, "Step"));
68
+ if (!steps.length) return await renderChildren(node, context);
69
+ const children = [];
70
+ for (const step of steps) {
71
+ children.push({
72
+ type: "listItem",
73
+ spread: true,
74
+ children: await stepToBlocks(step, context),
75
+ });
76
+ }
77
+ return [
78
+ {
79
+ type: "list",
80
+ ordered: true,
81
+ spread: true,
82
+ children,
83
+ },
84
+ ];
85
+ }
86
+
87
+ function renderCard(node, context) {
88
+ return [cardList([node], context)];
89
+ }
90
+
91
+ function renderCardGrid(node, context) {
92
+ return [cardList((node.children || []).filter((child) => context.isNamedElement(child, "Card")), context)];
93
+ }
94
+
95
+ async function stepToBlocks(node, context) {
96
+ const title = context.getAttr(node, "title");
97
+ const children = await context.transformBlocks(node.children || []);
98
+ if (!title) return children;
99
+ return [strongParagraph(title), ...children];
100
+ }
101
+
102
+ function cardList(cards, context) {
103
+ return {
104
+ type: "list",
105
+ ordered: false,
106
+ spread: true,
107
+ children: cards.map((card) => ({
108
+ type: "listItem",
109
+ spread: false,
110
+ children: [cardParagraph(card, context)],
111
+ })),
112
+ };
113
+ }
114
+
115
+ function cardParagraph(node, context) {
116
+ const title = context.getAttr(node, "title") || "Card";
117
+ const description = context.getAttr(node, "description");
118
+ const href = context.getAttr(node, "href");
119
+ const titleNode =
120
+ href && href !== "#"
121
+ ? { type: "link", url: href, children: [{ type: "text", value: title }] }
122
+ : { type: "strong", children: [{ type: "text", value: title }] };
123
+ return {
124
+ type: "paragraph",
125
+ children: description ? [titleNode, { type: "text", value: `: ${description}` }] : [titleNode],
126
+ };
127
+ }
128
+
129
+ function collectCodeChildren(node, context) {
130
+ return (node.children || []).flatMap((child) => {
131
+ if (context.isNamedElement(child, "Code")) return [child];
132
+ if (child.type === "paragraph") {
133
+ return context.significantChildren(child.children || []).filter((entry) => context.isNamedElement(entry, "Code"));
134
+ }
135
+ return [];
136
+ });
137
+ }
138
+
139
+ function strongParagraph(value) {
140
+ return {
141
+ type: "paragraph",
142
+ children: [{ type: "strong", children: [{ type: "text", value }] }],
143
+ };
144
+ }
@@ -0,0 +1,19 @@
1
+ export function getMdxChildrenSource(node, source) {
2
+ const children = node?.children || [];
3
+ const first = children[0];
4
+ const last = children.at(-1);
5
+ const start = first?.position?.start?.offset;
6
+ const end = last?.position?.end?.offset;
7
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return "";
8
+ return trimCode(String(source || "").slice(start, end));
9
+ }
10
+
11
+ export function trimCode(value) {
12
+ const lines = String(value || "").replace(/\r\n/g, "\n").split("\n");
13
+ while (lines.length && !lines[0].trim()) lines.shift();
14
+ while (lines.length && !lines.at(-1).trim()) lines.pop();
15
+ const indent = lines
16
+ .filter((line) => line.trim())
17
+ .reduce((min, line) => Math.min(min, line.match(/^\s*/)[0].length), Infinity);
18
+ return lines.map((line) => line.slice(Number.isFinite(indent) ? indent : 0)).join("\n");
19
+ }
@@ -0,0 +1,33 @@
1
+ import { getMdxChildrenSource } from "./mdx-source.js";
2
+
3
+ export function previewSourceRemarkPlugin() {
4
+ return (tree, file) => {
5
+ const source = String(file.value || "");
6
+ walk(tree, (node) => {
7
+ if (!isPreview(node) || hasAttribute(node, "code")) return;
8
+ const code = getMdxChildrenSource(node, source);
9
+ node.attributes = [
10
+ ...(node.attributes || []),
11
+ {
12
+ type: "mdxJsxAttribute",
13
+ name: "code",
14
+ value: code,
15
+ },
16
+ ];
17
+ });
18
+ };
19
+ }
20
+
21
+ function walk(node, callback) {
22
+ if (!node || typeof node !== "object") return;
23
+ callback(node);
24
+ for (const child of node.children || []) walk(child, callback);
25
+ }
26
+
27
+ function isPreview(node) {
28
+ return node?.type === "mdxJsxFlowElement" && node.name === "Preview";
29
+ }
30
+
31
+ function hasAttribute(node, name) {
32
+ return (node.attributes || []).some((attr) => attr.type === "mdxJsxAttribute" && attr.name === name);
33
+ }