reallysimpledocs 1.0.0-beta.3 → 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.
- package/CHANGELOG.md +35 -0
- package/README.md +10 -8
- package/package.json +10 -2
- package/src/astro/index.js +56 -1
- package/src/css/custom.css +35 -24
- package/src/runtime/components/CommandDialog.astro +19 -2
- package/src/runtime/components/DefaultContentHeader.astro +19 -0
- package/src/runtime/components/DefaultSidebarFooter.astro +2 -0
- package/src/runtime/components/DocsLayout.astro +100 -81
- package/src/runtime/components/Sidebar.astro +4 -0
- package/src/runtime/components/mdx/Callout.astro +28 -5
- package/src/runtime/components/mdx/Preview.astro +2 -1
- package/src/runtime/lib/docs.js +92 -14
- package/src/runtime/lib/macros.js +5 -1
- package/src/runtime/lib/markdown-export.js +200 -0
- package/src/runtime/lib/markdown-exporters/index.js +144 -0
- package/src/runtime/lib/mdx-source.js +19 -0
- package/src/runtime/lib/preview-source-plugin.js +33 -0
- package/src/runtime/lib/site.js +0 -1
- package/src/runtime/pages/doc.astro +3 -1
- package/src/runtime/pages/llms-full.txt.ts +2 -2
- package/src/runtime/pages/llms.txt.ts +2 -2
- package/src/runtime/pages/markdown.ts +2 -2
- package/src/runtime/pages/search-index.ts +2 -2
- package/src/runtime/scripts/copy-code.js +28 -7
- package/src/site/ContentHeader.astro +33 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import config from "virtual:reallysimpledocs/config";
|
|
3
3
|
import SidebarHeader from "virtual:reallysimpledocs/components/SidebarHeader";
|
|
4
|
+
import SidebarFooter from "virtual:reallysimpledocs/components/SidebarFooter";
|
|
4
5
|
import { renderSidebarContent } from "../lib/macros.js";
|
|
5
6
|
import { getSite } from "../lib/site.js";
|
|
6
7
|
|
|
@@ -16,5 +17,8 @@ const site = getSite(config);
|
|
|
16
17
|
<section class="scrollbar">
|
|
17
18
|
<Fragment set:html={renderSidebarContent(menu, "sidebar-content")} />
|
|
18
19
|
</section>
|
|
20
|
+
<footer>
|
|
21
|
+
<SidebarFooter site={site} />
|
|
22
|
+
</footer>
|
|
19
23
|
</nav>
|
|
20
24
|
</aside>
|
|
@@ -2,10 +2,22 @@
|
|
|
2
2
|
import * as lucideIcons from "lucide-static";
|
|
3
3
|
import { addSvgClass } from "../../lib/html.js";
|
|
4
4
|
|
|
5
|
-
const { type, title = "Note", icon: iconName, class: className = "" } = Astro.props;
|
|
5
|
+
const { type, title = "Note", icon: iconName, action, class: className = "" } = Astro.props;
|
|
6
6
|
const normalizedType = String(type || "").toLowerCase();
|
|
7
|
-
const calloutType = normalizedType === "warning" || normalizedType === "
|
|
8
|
-
const
|
|
7
|
+
const calloutType = normalizedType === "warning" || normalizedType === "destructive" ? normalizedType : "";
|
|
8
|
+
const destructive = calloutType === "destructive";
|
|
9
|
+
const actionLabel = action?.label;
|
|
10
|
+
const actionClass = action?.class || "btn-sm-secondary";
|
|
11
|
+
const actionAttrs = action
|
|
12
|
+
? {
|
|
13
|
+
...action.attrs,
|
|
14
|
+
href: action.href,
|
|
15
|
+
target: action.target,
|
|
16
|
+
rel: action.rel,
|
|
17
|
+
onclick: action.onclick,
|
|
18
|
+
class: actionClass,
|
|
19
|
+
}
|
|
20
|
+
: {};
|
|
9
21
|
|
|
10
22
|
function resolveNamedIcon(name) {
|
|
11
23
|
if (!name || typeof name !== "string") return "";
|
|
@@ -27,15 +39,26 @@ const heading = title;
|
|
|
27
39
|
const variantClass =
|
|
28
40
|
calloutType === "warning"
|
|
29
41
|
? "[&>svg]:text-amber-700 [&>h3]:text-amber-700 [&>section]:text-amber-700 dark:[&>svg]:text-amber-300 dark:[&>h3]:text-amber-300 dark:[&>section]:text-amber-300"
|
|
30
|
-
:
|
|
42
|
+
: destructive
|
|
31
43
|
? "[&>svg]:text-destructive [&>h3]:text-destructive [&>section]:text-destructive"
|
|
32
44
|
: "";
|
|
33
45
|
---
|
|
34
46
|
|
|
35
|
-
<div class:list={[
|
|
47
|
+
<div class:list={[destructive ? "alert-destructive" : "alert", "my-6", variantClass, className]}>
|
|
36
48
|
{icon && <Fragment set:html={icon} />}
|
|
37
49
|
<h3>{heading}</h3>
|
|
38
50
|
<section>
|
|
39
51
|
<slot />
|
|
40
52
|
</section>
|
|
53
|
+
{
|
|
54
|
+
actionLabel && (
|
|
55
|
+
<footer>
|
|
56
|
+
{action?.href ? (
|
|
57
|
+
<a {...actionAttrs}>{actionLabel}</a>
|
|
58
|
+
) : (
|
|
59
|
+
<button type="button" {...actionAttrs}>{actionLabel}</button>
|
|
60
|
+
)}
|
|
61
|
+
</footer>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
41
64
|
</div>
|
|
@@ -7,7 +7,7 @@ const html = await highlightCode(code, lang);
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
<section
|
|
10
|
-
class="relative my-6 max-w-full overflow-
|
|
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
11
|
data-preview
|
|
12
12
|
>
|
|
13
13
|
<header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
|
|
@@ -43,6 +43,7 @@ const html = await highlightCode(code, lang);
|
|
|
43
43
|
aria-labelledby={`${id}-preview-tab`}
|
|
44
44
|
tabindex="-1"
|
|
45
45
|
aria-selected="true"
|
|
46
|
+
class="flex min-h-64 w-full items-center justify-center p-6"
|
|
46
47
|
>
|
|
47
48
|
<div class={className}>
|
|
48
49
|
<slot />
|
package/src/runtime/lib/docs.js
CHANGED
|
@@ -4,6 +4,7 @@ import { marked } from "marked";
|
|
|
4
4
|
import GithubSlugger from "github-slugger";
|
|
5
5
|
import * as lucideIcons from "lucide-static";
|
|
6
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
|
-
|
|
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) {
|
|
@@ -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();
|
|
@@ -187,10 +234,30 @@ export function buildMenus(config, currentSlug) {
|
|
|
187
234
|
};
|
|
188
235
|
};
|
|
189
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
|
+
|
|
190
256
|
const processItem = (item) => {
|
|
191
257
|
if (typeof item === "string") return processSlug(item);
|
|
192
258
|
const slug = getPageItemSlug(item);
|
|
193
259
|
if (slug) return processSlug(slug, item);
|
|
260
|
+
if (item?.url) return processExternalLink(item);
|
|
194
261
|
if (item?.type === "submenu") {
|
|
195
262
|
const items = (item.items || []).map(processItem).filter(Boolean);
|
|
196
263
|
return {
|
|
@@ -220,6 +287,8 @@ export function buildMenus(config, currentSlug) {
|
|
|
220
287
|
icon: item.icon,
|
|
221
288
|
url: item.url,
|
|
222
289
|
label: item.label,
|
|
290
|
+
external: item.external,
|
|
291
|
+
attrs: item.attrs,
|
|
223
292
|
keywords: item.keywords,
|
|
224
293
|
})),
|
|
225
294
|
}));
|
|
@@ -227,23 +296,28 @@ export function buildMenus(config, currentSlug) {
|
|
|
227
296
|
return { sidebar, command };
|
|
228
297
|
}
|
|
229
298
|
|
|
230
|
-
export function getLlmDocs(config) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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;
|
|
235
308
|
}
|
|
236
309
|
|
|
237
|
-
export function getSearchDocs(config) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
310
|
+
export async function getSearchDocs(config) {
|
|
311
|
+
const docs = [];
|
|
312
|
+
for (const page of getPages(config)) {
|
|
313
|
+
docs.push({
|
|
241
314
|
slug: page.slug,
|
|
242
315
|
title: page.title,
|
|
243
316
|
path: page.path,
|
|
244
|
-
body: markdownPlainText(
|
|
245
|
-
};
|
|
246
|
-
}
|
|
317
|
+
body: markdownPlainText(await getMarkdownBodyExport(config, page)),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return docs;
|
|
247
321
|
}
|
|
248
322
|
|
|
249
323
|
function flattenSidebarItems(items = []) {
|
|
@@ -255,6 +329,10 @@ export function getPageItemSlug(item) {
|
|
|
255
329
|
return item.slug || item.page || "";
|
|
256
330
|
}
|
|
257
331
|
|
|
332
|
+
function isExternalUrl(url) {
|
|
333
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(String(url || ""));
|
|
334
|
+
}
|
|
335
|
+
|
|
258
336
|
function parseDocMarkdown(source, fallbackTitle) {
|
|
259
337
|
const content = normalizeDocSource(source);
|
|
260
338
|
const tokens = marked.lexer(content);
|
|
@@ -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
|
|
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
|
+
}
|