@dogsbay/format-astro 0.2.0-beta.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.
- package/dist/base-path.d.ts +83 -0
- package/dist/base-path.d.ts.map +1 -0
- package/dist/base-path.js +110 -0
- package/dist/base-path.js.map +1 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +53 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/lead.d.ts +39 -0
- package/dist/lead.d.ts.map +1 -0
- package/dist/lead.js +38 -0
- package/dist/lead.js.map +1 -0
- package/dist/llms-txt.d.ts +81 -0
- package/dist/llms-txt.d.ts.map +1 -0
- package/dist/llms-txt.js +288 -0
- package/dist/llms-txt.js.map +1 -0
- package/dist/plugins.d.ts +40 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +339 -0
- package/dist/plugins.js.map +1 -0
- package/dist/project.d.ts +320 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +1858 -0
- package/dist/project.js.map +1 -0
- package/dist/serialize.d.ts +30 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +1197 -0
- package/dist/serialize.js.map +1 -0
- package/dist/taxonomy.d.ts +87 -0
- package/dist/taxonomy.d.ts.map +1 -0
- package/dist/taxonomy.js +467 -0
- package/dist/taxonomy.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tone palette for `:::grid-item{label="..." tone="..."}` cells.
|
|
3
|
+
* Each tone is a `bg + text` Tailwind class pair. Used to make grid demos
|
|
4
|
+
* declarative — authors specify a tone instead of writing the styling.
|
|
5
|
+
*/
|
|
6
|
+
const TONE_CLASSES = {
|
|
7
|
+
// Primary scale (intensity)
|
|
8
|
+
"primary": "bg-primary text-primary-foreground",
|
|
9
|
+
"primary-strong": "bg-primary/80 text-primary-foreground",
|
|
10
|
+
"primary-medium": "bg-primary/60 text-primary-foreground",
|
|
11
|
+
"primary-half": "bg-primary/50 text-primary-foreground",
|
|
12
|
+
"primary-light": "bg-primary/20 text-primary",
|
|
13
|
+
"primary-soft": "bg-primary/10 text-primary",
|
|
14
|
+
// Neutral
|
|
15
|
+
"muted": "bg-muted text-muted-foreground",
|
|
16
|
+
// Named colors (for layout demos with distinct cells)
|
|
17
|
+
"blue": "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
18
|
+
"amber": "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
|
19
|
+
"green": "bg-green-500/15 text-green-700 dark:text-green-300",
|
|
20
|
+
"purple": "bg-purple-500/15 text-purple-700 dark:text-purple-300",
|
|
21
|
+
"red": "bg-red-500/15 text-red-700 dark:text-red-300",
|
|
22
|
+
};
|
|
23
|
+
const COMPONENT_IMPORTS = {
|
|
24
|
+
"markdown-example": [
|
|
25
|
+
'import MarkdownExample from "@ui/markdown-example/MarkdownExample.astro";',
|
|
26
|
+
],
|
|
27
|
+
callout: [
|
|
28
|
+
'import Alert from "@ui/alert/Alert.astro";',
|
|
29
|
+
'import AlertTitle from "@ui/alert/AlertTitle.astro";',
|
|
30
|
+
'import AlertDescription from "@ui/alert/AlertDescription.astro";',
|
|
31
|
+
],
|
|
32
|
+
details: [
|
|
33
|
+
'import CollapsibleAlert from "@ui/alert/CollapsibleAlert.astro";',
|
|
34
|
+
],
|
|
35
|
+
code: [
|
|
36
|
+
'import CodeBlock from "@ui/code-block/CodeBlock.astro";',
|
|
37
|
+
],
|
|
38
|
+
"code-rich": [
|
|
39
|
+
'import CodeRich from "@ui/code-rich/CodeRich.astro";',
|
|
40
|
+
'import "@ui/code-rich/code-rich.css";',
|
|
41
|
+
],
|
|
42
|
+
tabs: [
|
|
43
|
+
'import Tabs from "@ui/tabs/Tabs.astro";',
|
|
44
|
+
'import TabsList from "@ui/tabs/TabsList.astro";',
|
|
45
|
+
'import TabsTrigger from "@ui/tabs/TabsTrigger.astro";',
|
|
46
|
+
'import TabsContent from "@ui/tabs/TabsContent.astro";',
|
|
47
|
+
],
|
|
48
|
+
table: [
|
|
49
|
+
'import Table from "@ui/table/Table.astro";',
|
|
50
|
+
'import TableHeader from "@ui/table/TableHeader.astro";',
|
|
51
|
+
'import TableBody from "@ui/table/TableBody.astro";',
|
|
52
|
+
'import TableRow from "@ui/table/TableRow.astro";',
|
|
53
|
+
'import TableHead from "@ui/table/TableHead.astro";',
|
|
54
|
+
'import TableCell from "@ui/table/TableCell.astro";',
|
|
55
|
+
],
|
|
56
|
+
diagram: ['import Diagram from "@ui/diagram/Diagram.astro";'],
|
|
57
|
+
youtube: ['import YouTube from "@ui/youtube/YouTube.astro";'],
|
|
58
|
+
"math-block": ['import MathKatex from "@ui/math/MathKatex.astro";'],
|
|
59
|
+
image: ['import BlockImage from "@ui/image/Image.astro";'],
|
|
60
|
+
"api-class": ['import ApiSymbol from "@ui/api-symbol/ApiSymbol.astro";'],
|
|
61
|
+
"api-doc": ['import ApiDoc from "@ui/api-doc/ApiDoc.astro";'],
|
|
62
|
+
"api-params": ['import ApiParams from "@ui/api-params/ApiParams.astro";'],
|
|
63
|
+
"api-source": ['import ApiSource from "@ui/api-source/ApiSource.astro";'],
|
|
64
|
+
endpoint: [
|
|
65
|
+
'import ApiLayout from "@ui/api-layout/ApiLayout.astro";',
|
|
66
|
+
'import ApiCodePanel from "@ui/api-layout/ApiCodePanel.astro";',
|
|
67
|
+
'import ExampleBlock from "@ui/api-layout/ExampleBlock.astro";',
|
|
68
|
+
'import EndpointCard from "@ui/endpoint-card/EndpointCard.astro";',
|
|
69
|
+
'import EndpointSection from "@ui/endpoint-card/EndpointSection.astro";',
|
|
70
|
+
'import PropertyList from "@ui/property-list/PropertyList.astro";',
|
|
71
|
+
'import ResponseTabs from "@ui/response-tabs/ResponseTabs.astro";',
|
|
72
|
+
'import SchemaViewer from "@ui/schema-viewer/SchemaViewer.astro";',
|
|
73
|
+
'import CodeSamples from "@ui/code-samples/CodeSamples.astro";',
|
|
74
|
+
],
|
|
75
|
+
steps: [
|
|
76
|
+
'import Steps from "@ui/steps/Steps.astro";',
|
|
77
|
+
'import Step from "@ui/steps/Step.astro";',
|
|
78
|
+
],
|
|
79
|
+
substeps: [
|
|
80
|
+
'import SubSteps from "@ui/steps/SubSteps.astro";',
|
|
81
|
+
],
|
|
82
|
+
grid: [
|
|
83
|
+
'import Grid from "@ui/grid/Grid.astro";',
|
|
84
|
+
'import GridItem from "@ui/grid/GridItem.astro";',
|
|
85
|
+
],
|
|
86
|
+
card: [
|
|
87
|
+
'import Card from "@ui/card/Card.astro";',
|
|
88
|
+
'import CardContent from "@ui/card/CardContent.astro";',
|
|
89
|
+
],
|
|
90
|
+
accordion: [
|
|
91
|
+
'import Accordion from "@ui/accordion/Accordion.astro";',
|
|
92
|
+
'import AccordionItem from "@ui/accordion/AccordionItem.astro";',
|
|
93
|
+
'import AccordionTrigger from "@ui/accordion/AccordionTrigger.astro";',
|
|
94
|
+
'import AccordionContent from "@ui/accordion/AccordionContent.astro";',
|
|
95
|
+
],
|
|
96
|
+
"link-card": [
|
|
97
|
+
'import LinkCard from "@ui/link-card/LinkCard.astro";',
|
|
98
|
+
],
|
|
99
|
+
avatar: [
|
|
100
|
+
'import Avatar from "@ui/avatar/Avatar.astro";',
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
// ─── Escaping helpers ─────────────────────────────────────────────
|
|
104
|
+
/** Escape for use inside an Astro {expression}. Uses JSON.stringify for safety. */
|
|
105
|
+
export function escapeExpr(s) {
|
|
106
|
+
// JSON.stringify handles all special characters (quotes, backslashes, newlines, etc.)
|
|
107
|
+
// and produces a valid JS string literal. This avoids template literal edge cases
|
|
108
|
+
// where backtick + brace combinations create false expression boundaries.
|
|
109
|
+
return JSON.stringify(s);
|
|
110
|
+
}
|
|
111
|
+
/** Escape for use in an HTML attribute value (double-quoted). */
|
|
112
|
+
export function escapeAttr(s) {
|
|
113
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
114
|
+
}
|
|
115
|
+
/** Escape text for use directly in an Astro template (not inside an expression). */
|
|
116
|
+
export function escapeTemplate(s) {
|
|
117
|
+
let result = "";
|
|
118
|
+
for (const ch of s) {
|
|
119
|
+
switch (ch) {
|
|
120
|
+
case "&":
|
|
121
|
+
result += "&";
|
|
122
|
+
break;
|
|
123
|
+
case "<":
|
|
124
|
+
result += "<";
|
|
125
|
+
break;
|
|
126
|
+
case ">":
|
|
127
|
+
result += ">";
|
|
128
|
+
break;
|
|
129
|
+
case "{":
|
|
130
|
+
result += '{"{"}';
|
|
131
|
+
break;
|
|
132
|
+
case "}":
|
|
133
|
+
result += '{"}"}';
|
|
134
|
+
break;
|
|
135
|
+
default: result += ch;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
// ─── Main entry point ─────────────────────────────────────────────
|
|
141
|
+
export function treeToAstro(nodes, options) {
|
|
142
|
+
const ctx = {
|
|
143
|
+
imports: new Set(),
|
|
144
|
+
indent: 0,
|
|
145
|
+
scripts: new Set(),
|
|
146
|
+
mode: options?.mode ?? "template",
|
|
147
|
+
imageOptimization: options?.imageOptimization ?? false,
|
|
148
|
+
codeBlockTitle: options?.codeBlockTitle ?? true,
|
|
149
|
+
};
|
|
150
|
+
const body = nodes.map((n) => nodeToAstro(n, ctx)).filter(Boolean).join("\n");
|
|
151
|
+
const imports = [];
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
for (const key of ctx.imports) {
|
|
154
|
+
const stmts = COMPONENT_IMPORTS[key];
|
|
155
|
+
if (stmts) {
|
|
156
|
+
for (const stmt of stmts) {
|
|
157
|
+
if (!seen.has(stmt)) {
|
|
158
|
+
seen.add(stmt);
|
|
159
|
+
imports.push(stmt);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
body,
|
|
166
|
+
imports: imports.sort(),
|
|
167
|
+
scripts: [...ctx.scripts],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// ─── Node dispatcher ──────────────────────────────────────────────
|
|
171
|
+
function nodeToAstro(node, ctx) {
|
|
172
|
+
switch (node.type) {
|
|
173
|
+
case "prose":
|
|
174
|
+
return proseToAstro(node, ctx);
|
|
175
|
+
case "heading":
|
|
176
|
+
return headingToAstro(node, ctx);
|
|
177
|
+
case "paragraph":
|
|
178
|
+
return paragraphToAstro(node, ctx);
|
|
179
|
+
case "code":
|
|
180
|
+
return codeToAstro(node, ctx);
|
|
181
|
+
case "callout":
|
|
182
|
+
return calloutToAstro(node, ctx);
|
|
183
|
+
case "details":
|
|
184
|
+
return detailsToAstro(node, ctx);
|
|
185
|
+
case "tabs":
|
|
186
|
+
return tabsToAstro(node, ctx);
|
|
187
|
+
case "ordered-list":
|
|
188
|
+
return wrapBlock("ol", "", node, childrenToAstro(node, ctx), ["start"])
|
|
189
|
+
.replace(/^<ol(\s[^>]*)?>/, (m) => {
|
|
190
|
+
const start = node.props?.start;
|
|
191
|
+
if (start && start !== 1) {
|
|
192
|
+
return m.replace(/^<ol/, `<ol start="${start}"`);
|
|
193
|
+
}
|
|
194
|
+
return m;
|
|
195
|
+
});
|
|
196
|
+
case "unordered-list":
|
|
197
|
+
return wrapBlock("ul", "", node, childrenToAstro(node, ctx));
|
|
198
|
+
case "list-item":
|
|
199
|
+
return wrapBlock("li", "", node, leafContent(node, ctx));
|
|
200
|
+
case "blockquote":
|
|
201
|
+
return wrapBlock("blockquote", "border-l-4 border-border pl-4 italic text-muted-foreground", node, childrenToAstro(node, ctx));
|
|
202
|
+
case "hr": {
|
|
203
|
+
const classAttr = mergeClassAttr("my-8 border-border", node.props?.class);
|
|
204
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
205
|
+
return `<hr${classAttr}${passthrough} />`;
|
|
206
|
+
}
|
|
207
|
+
case "html":
|
|
208
|
+
// Raw HTML always uses set:html — it may contain anything
|
|
209
|
+
return `<Fragment set:html={${escapeExpr(node.html ?? "")}} />`;
|
|
210
|
+
case "table":
|
|
211
|
+
return tableToAstro(node, ctx);
|
|
212
|
+
case "thead":
|
|
213
|
+
ctx.imports.add("table");
|
|
214
|
+
return `<TableHeader>\n${childrenToAstro(node, ctx)}\n</TableHeader>`;
|
|
215
|
+
case "tbody":
|
|
216
|
+
ctx.imports.add("table");
|
|
217
|
+
return `<TableBody>\n${childrenToAstro(node, ctx)}\n</TableBody>`;
|
|
218
|
+
case "tr":
|
|
219
|
+
ctx.imports.add("table");
|
|
220
|
+
return `<TableRow>\n${childrenToAstro(node, ctx)}\n</TableRow>`;
|
|
221
|
+
case "th":
|
|
222
|
+
ctx.imports.add("table");
|
|
223
|
+
return `<TableHead>${leafContent(node, ctx)}</TableHead>`;
|
|
224
|
+
case "td":
|
|
225
|
+
ctx.imports.add("table");
|
|
226
|
+
return `<TableCell>${leafContent(node, ctx)}</TableCell>`;
|
|
227
|
+
case "deflist":
|
|
228
|
+
return wrapBlock("dl", "my-4 space-y-2", node, childrenToAstro(node, ctx));
|
|
229
|
+
case "dt":
|
|
230
|
+
return wrapBlock("dt", "font-semibold", node, leafContent(node, ctx));
|
|
231
|
+
case "dd":
|
|
232
|
+
return wrapBlock("dd", "ml-6 text-muted-foreground", node, leafContent(node, ctx));
|
|
233
|
+
case "diagram":
|
|
234
|
+
return diagramToAstro(node, ctx);
|
|
235
|
+
case "youtube":
|
|
236
|
+
return youtubeToAstro(node, ctx);
|
|
237
|
+
case "math-block":
|
|
238
|
+
return mathBlockToAstro(node, ctx);
|
|
239
|
+
case "footnote-list":
|
|
240
|
+
return footnoteListToAstro(node, ctx);
|
|
241
|
+
case "footnote-def":
|
|
242
|
+
return footnoteDefToAstro(node, ctx);
|
|
243
|
+
case "api-class":
|
|
244
|
+
case "api-member":
|
|
245
|
+
return apiSymbolToAstro(node, ctx);
|
|
246
|
+
case "api-doc":
|
|
247
|
+
return apiDocToAstro(node, ctx);
|
|
248
|
+
case "api-params":
|
|
249
|
+
return apiParamsToAstro(node, ctx);
|
|
250
|
+
case "api-source":
|
|
251
|
+
return apiSourceToAstro(node, ctx);
|
|
252
|
+
case "endpoint":
|
|
253
|
+
return endpointToAstro(node, ctx);
|
|
254
|
+
case "html-container":
|
|
255
|
+
return htmlContainerToAstro(node, ctx);
|
|
256
|
+
case "example":
|
|
257
|
+
return exampleToAstro(node, ctx);
|
|
258
|
+
case "figure": {
|
|
259
|
+
const caption = node.props?.caption;
|
|
260
|
+
const inner = childrenToAstro(node, ctx);
|
|
261
|
+
const body = caption
|
|
262
|
+
? `${inner}\n<figcaption class="text-center text-sm text-muted-foreground mt-2">${escapeTemplate(caption)}</figcaption>`
|
|
263
|
+
: inner;
|
|
264
|
+
return wrapBlock("figure", "my-4", node, body, ["caption"]);
|
|
265
|
+
}
|
|
266
|
+
case "steps":
|
|
267
|
+
ctx.imports.add("steps");
|
|
268
|
+
return `<Steps>\n${childrenToAstro(node, ctx)}\n</Steps>`;
|
|
269
|
+
case "step":
|
|
270
|
+
ctx.imports.add("steps");
|
|
271
|
+
return `<Step>\n${leafContent(node, ctx)}\n</Step>`;
|
|
272
|
+
case "substeps":
|
|
273
|
+
ctx.imports.add("substeps");
|
|
274
|
+
ctx.imports.add("steps");
|
|
275
|
+
return `<SubSteps>\n${childrenToAstro(node, ctx)}\n</SubSteps>`;
|
|
276
|
+
case "annotation-list":
|
|
277
|
+
return wrapBlock("div", "my-2 rounded-md border-l-4 border-info/50 bg-info/5 p-4", node, childrenToAstro(node, ctx));
|
|
278
|
+
case "cards":
|
|
279
|
+
return cardsToAstro(node, ctx);
|
|
280
|
+
case "card":
|
|
281
|
+
// Standalone card — outside :::cards. Renders as a single styled card,
|
|
282
|
+
// optionally clickable when href is present. Cards inside :::cards are
|
|
283
|
+
// handled by cardsToAstro and never reach this case.
|
|
284
|
+
return standaloneCardToAstro(node, ctx);
|
|
285
|
+
case "link-card":
|
|
286
|
+
return linkCardToAstro(node, ctx);
|
|
287
|
+
case "accordion":
|
|
288
|
+
return accordionToAstro(node, ctx);
|
|
289
|
+
case "accordion-item":
|
|
290
|
+
// Bare accordion-item outside an :::accordion wrapper. Wrap it in an
|
|
291
|
+
// Accordion shell so the output is still a valid widget.
|
|
292
|
+
return accordionItemToAstro(node, ctx, /*standalone*/ true);
|
|
293
|
+
case "avatar":
|
|
294
|
+
return avatarToAstro(node, ctx);
|
|
295
|
+
case "grid": {
|
|
296
|
+
ctx.imports.add("grid");
|
|
297
|
+
// Grid component takes cols/gap/rows as props. User class goes through
|
|
298
|
+
// as the outer class attribute. Numeric-looking string values (e.g.
|
|
299
|
+
// cols="3") are coerced to numbers for the JSX prop.
|
|
300
|
+
const cols = coerceNumericList(node.props?.cols, [1, 2]);
|
|
301
|
+
const gap = coerceNumeric(node.props?.gap, 4);
|
|
302
|
+
const rows = node.props?.rows;
|
|
303
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
304
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
305
|
+
"cols", "gap", "rows", "class",
|
|
306
|
+
]);
|
|
307
|
+
const rowsAttr = rows !== undefined ? ` rows={${JSON.stringify(coerceNumeric(rows, 1))}}` : "";
|
|
308
|
+
return `<Grid cols={${JSON.stringify(cols)}} gap={${JSON.stringify(gap)}}${rowsAttr}${classAttr}${passthrough}>
|
|
309
|
+
${childrenToAstro(node, ctx)}
|
|
310
|
+
</Grid>`;
|
|
311
|
+
}
|
|
312
|
+
case "grid-item": {
|
|
313
|
+
ctx.imports.add("grid");
|
|
314
|
+
const span = coerceNumeric(node.props?.span, 1);
|
|
315
|
+
const rowSpan = node.props?.rowSpan;
|
|
316
|
+
const start = node.props?.start;
|
|
317
|
+
const label = node.props?.label;
|
|
318
|
+
const tone = node.props?.tone ?? "primary-light";
|
|
319
|
+
const attrParts = [`span={${span}}`];
|
|
320
|
+
if (rowSpan !== undefined)
|
|
321
|
+
attrParts.push(`rowSpan={${coerceNumeric(rowSpan, 1)}}`);
|
|
322
|
+
if (start !== undefined)
|
|
323
|
+
attrParts.push(`start={${coerceNumeric(start, 1)}}`);
|
|
324
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
325
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
326
|
+
"span", "rowSpan", "start", "label", "tone", "class",
|
|
327
|
+
]);
|
|
328
|
+
// When `label` is set, auto-render a styled placeholder cell. The tone
|
|
329
|
+
// controls the bg/text combo (see TONE_CLASSES). Authors can also pass
|
|
330
|
+
// their own `class` to override.
|
|
331
|
+
if (label !== undefined) {
|
|
332
|
+
const toneClasses = TONE_CLASSES[tone] ?? TONE_CLASSES["primary-light"];
|
|
333
|
+
return `<GridItem ${attrParts.join(" ")}${classAttr}${passthrough}>
|
|
334
|
+
<div class="flex h-full min-h-16 items-center justify-center rounded-lg font-mono text-sm font-medium ${toneClasses}">${escapeTemplate(label)}</div>
|
|
335
|
+
</GridItem>`;
|
|
336
|
+
}
|
|
337
|
+
return `<GridItem ${attrParts.join(" ")}${classAttr}${passthrough}>
|
|
338
|
+
${childrenToAstro(node, ctx)}
|
|
339
|
+
</GridItem>`;
|
|
340
|
+
}
|
|
341
|
+
default:
|
|
342
|
+
if (node.html)
|
|
343
|
+
return `<Fragment set:html={${escapeExpr(node.html)}} />`;
|
|
344
|
+
if (node.children)
|
|
345
|
+
return childrenToAstro(node, ctx);
|
|
346
|
+
return "";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// ─── Inline rendering ─────────────────────────────────────────────
|
|
350
|
+
/** Render inline nodes as Astro template markup (clean, editable). */
|
|
351
|
+
function inlineToTemplate(nodes) {
|
|
352
|
+
return nodes.map(inlineNodeToTemplate).join("");
|
|
353
|
+
}
|
|
354
|
+
function inlineNodeToTemplate(node) {
|
|
355
|
+
switch (node.type) {
|
|
356
|
+
case "text": {
|
|
357
|
+
let text = escapeTemplate(node.text);
|
|
358
|
+
if (node.bold)
|
|
359
|
+
text = `<strong>${text}</strong>`;
|
|
360
|
+
if (node.italic)
|
|
361
|
+
text = `<em>${text}</em>`;
|
|
362
|
+
if (node.strikethrough)
|
|
363
|
+
text = `<s>${text}</s>`;
|
|
364
|
+
return text;
|
|
365
|
+
}
|
|
366
|
+
case "link": {
|
|
367
|
+
const titleAttr = node.title ? ` title="${escapeAttr(node.title)}"` : "";
|
|
368
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}>${inlineToTemplate(node.children)}</a>`;
|
|
369
|
+
}
|
|
370
|
+
case "image":
|
|
371
|
+
return `<img src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt ?? "")}" loading="lazy" />`;
|
|
372
|
+
case "code":
|
|
373
|
+
return `<code>${escapeTemplate(node.text)}</code>`;
|
|
374
|
+
case "footnote-ref":
|
|
375
|
+
return `<sup><a href="#fn-${escapeAttr(node.label)}" id="fnref-${escapeAttr(node.label)}" class="text-primary hover:underline">[${escapeTemplate(node.label)}]</a></sup>`;
|
|
376
|
+
case "kbd":
|
|
377
|
+
return node.keys.map((k) => `<kbd>${escapeTemplate(k)}</kbd>`).join("+");
|
|
378
|
+
case "math":
|
|
379
|
+
return `<code class="math-inline">${escapeTemplate(node.latex)}</code>`;
|
|
380
|
+
case "icon":
|
|
381
|
+
return `:${node.name}:`;
|
|
382
|
+
case "html-inline":
|
|
383
|
+
// Raw HTML inline — must use set:html to avoid brace issues
|
|
384
|
+
return `<Fragment set:html={${escapeExpr(node.html)}} />`;
|
|
385
|
+
case "break":
|
|
386
|
+
return "<br />";
|
|
387
|
+
default:
|
|
388
|
+
return "";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** Render inline nodes as HTML string (for hybrid mode set:html). */
|
|
392
|
+
function inlineToHtml(nodes) {
|
|
393
|
+
return nodes.map(inlineNodeToHtml).join("");
|
|
394
|
+
}
|
|
395
|
+
function inlineNodeToHtml(node) {
|
|
396
|
+
switch (node.type) {
|
|
397
|
+
case "text": {
|
|
398
|
+
let text = escapeHtml(node.text);
|
|
399
|
+
if (node.bold)
|
|
400
|
+
text = `<strong>${text}</strong>`;
|
|
401
|
+
if (node.italic)
|
|
402
|
+
text = `<em>${text}</em>`;
|
|
403
|
+
if (node.strikethrough)
|
|
404
|
+
text = `<s>${text}</s>`;
|
|
405
|
+
return text;
|
|
406
|
+
}
|
|
407
|
+
case "link": {
|
|
408
|
+
const titleAttr = node.title ? ` title="${escapeAttr(node.title)}"` : "";
|
|
409
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}>${inlineToHtml(node.children)}</a>`;
|
|
410
|
+
}
|
|
411
|
+
case "image":
|
|
412
|
+
return `<img src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt ?? "")}" loading="lazy" />`;
|
|
413
|
+
case "code":
|
|
414
|
+
return `<code>${escapeHtml(node.text)}</code>`;
|
|
415
|
+
case "footnote-ref":
|
|
416
|
+
return `<sup><a href="#fn-${escapeAttr(node.label)}" id="fnref-${escapeAttr(node.label)}" class="text-primary hover:underline">[${escapeHtml(node.label)}]</a></sup>`;
|
|
417
|
+
case "kbd":
|
|
418
|
+
return node.keys.map((k) => `<kbd>${escapeHtml(k)}</kbd>`).join("+");
|
|
419
|
+
case "math":
|
|
420
|
+
return `<code class="math-inline">${escapeHtml(node.latex)}</code>`;
|
|
421
|
+
case "icon":
|
|
422
|
+
return `:${node.name}:`;
|
|
423
|
+
case "html-inline":
|
|
424
|
+
return node.html;
|
|
425
|
+
case "break":
|
|
426
|
+
return "<br />";
|
|
427
|
+
default:
|
|
428
|
+
return "";
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** Render inline content in the current mode. */
|
|
432
|
+
function renderInline(nodes, ctx) {
|
|
433
|
+
if (ctx.mode === "hybrid") {
|
|
434
|
+
const html = inlineToHtml(nodes);
|
|
435
|
+
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
436
|
+
}
|
|
437
|
+
return inlineToTemplate(nodes);
|
|
438
|
+
}
|
|
439
|
+
/** Render an HTML string in the current mode. */
|
|
440
|
+
function renderHtml(html, ctx) {
|
|
441
|
+
// Always use set:html for pre-rendered HTML — it may contain anything
|
|
442
|
+
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
443
|
+
}
|
|
444
|
+
// ─── Node type handlers ───────────────────────────────────────────
|
|
445
|
+
function proseToAstro(node, ctx) {
|
|
446
|
+
if (node.inline)
|
|
447
|
+
return renderInline(node.inline, ctx);
|
|
448
|
+
if (node.html)
|
|
449
|
+
return renderHtml(node.html, ctx);
|
|
450
|
+
return "";
|
|
451
|
+
}
|
|
452
|
+
function headingToAstro(node, ctx) {
|
|
453
|
+
const level = node.props?.level ?? 1;
|
|
454
|
+
// `id` takes precedence over `slug` (slug is auto-generated, id is explicit user override)
|
|
455
|
+
const id = node.props?.id ?? node.props?.slug ?? "";
|
|
456
|
+
const anchor = id
|
|
457
|
+
? `<a href="#${escapeAttr(id)}" class="ml-2 text-muted-foreground opacity-0 group-hover:opacity-100 no-underline" aria-hidden="true" tabindex="-1">¶</a>`
|
|
458
|
+
: "";
|
|
459
|
+
const idAttr = id ? ` id="${escapeAttr(id)}"` : "";
|
|
460
|
+
const classAttr = mergeClassAttr("group scroll-mt-20", node.props?.class);
|
|
461
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
462
|
+
"level", "slug", "id", "class", "text",
|
|
463
|
+
]);
|
|
464
|
+
if (ctx.mode === "template" && node.inline) {
|
|
465
|
+
const text = inlineToTemplate(node.inline);
|
|
466
|
+
return `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
467
|
+
}
|
|
468
|
+
// Hybrid or no inline — use set:html for the whole heading
|
|
469
|
+
const text = node.inline ? inlineToHtml(node.inline) : node.props?.text ?? "";
|
|
470
|
+
const html = `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
471
|
+
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
472
|
+
}
|
|
473
|
+
function paragraphToAstro(node, ctx) {
|
|
474
|
+
// Two shapes supported: paragraph.inline (flat) or paragraph.children[prose.inline] (Starlight)
|
|
475
|
+
const inlineNodes = node.inline
|
|
476
|
+
?? (node.children?.length === 1 && node.children[0].type === "prose"
|
|
477
|
+
? node.children[0].inline
|
|
478
|
+
: undefined);
|
|
479
|
+
// Block image detection
|
|
480
|
+
if (inlineNodes) {
|
|
481
|
+
const meaningful = inlineNodes.filter((n) => !(n.type === "text" && !n.text.trim()));
|
|
482
|
+
if (meaningful.length === 1 && meaningful[0].type === "image") {
|
|
483
|
+
const img = meaningful[0];
|
|
484
|
+
ctx.imports.add("image");
|
|
485
|
+
const imgDataAttr = ctx.imageOptimization
|
|
486
|
+
? ` imageData={imageMap[${escapeExpr(img.src)}]}`
|
|
487
|
+
: "";
|
|
488
|
+
// Forward arbitrary `data-*` / `aria-*` attrs that plugins
|
|
489
|
+
// may have attached to the wrapping paragraph (e.g.
|
|
490
|
+
// @dogsbay/plugin-image-zoom tags `data-zoomable="true"` so
|
|
491
|
+
// the runtime can attach handlers without scanning the DOM).
|
|
492
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
493
|
+
return `<BlockImage src="${escapeAttr(img.src)}" alt="${escapeAttr(img.alt ?? "")}"${passthrough}${imgDataAttr} />`;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
497
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
498
|
+
const hasPAttrs = classAttr || passthrough;
|
|
499
|
+
if (ctx.mode === "template") {
|
|
500
|
+
// If we have flat inline, render it directly; otherwise fall back to children dispatch
|
|
501
|
+
if (node.inline && node.inline.length > 0) {
|
|
502
|
+
return `<p${classAttr}${passthrough}>${inlineToTemplate(node.inline)}</p>`;
|
|
503
|
+
}
|
|
504
|
+
return `<p${classAttr}${passthrough}>${childrenToAstro(node, ctx)}</p>`;
|
|
505
|
+
}
|
|
506
|
+
// Hybrid mode
|
|
507
|
+
if (node.inline && node.inline.length > 0) {
|
|
508
|
+
if (hasPAttrs) {
|
|
509
|
+
return `<p${classAttr}${passthrough} set:html={${escapeExpr(inlineToHtml(node.inline))}} />`;
|
|
510
|
+
}
|
|
511
|
+
return `<p set:html={${escapeExpr(inlineToHtml(node.inline))}} />`;
|
|
512
|
+
}
|
|
513
|
+
const html = (node.children ?? []).map((c) => {
|
|
514
|
+
if (c.inline)
|
|
515
|
+
return inlineToHtml(c.inline);
|
|
516
|
+
return c.html ?? "";
|
|
517
|
+
}).join("");
|
|
518
|
+
return `<p${classAttr}${passthrough} set:html={${escapeExpr(html)}} />`;
|
|
519
|
+
}
|
|
520
|
+
function codeToAstro(node, ctx) {
|
|
521
|
+
// Code content may live on props.code (Starlight convention) or node.html
|
|
522
|
+
// (dogsbay-md convention). Same for lang — Starlight uses props.lang,
|
|
523
|
+
// dogsbay-md uses props.language.
|
|
524
|
+
const lang = node.props?.lang
|
|
525
|
+
|| node.props?.language
|
|
526
|
+
|| "plaintext";
|
|
527
|
+
const code = node.props?.code
|
|
528
|
+
?? node.html
|
|
529
|
+
?? "";
|
|
530
|
+
const title = node.props?.title;
|
|
531
|
+
const highlights = node.props?.highlights;
|
|
532
|
+
const lineNumbers = node.props?.lineNumbers;
|
|
533
|
+
const isRich = lineNumbers || highlights || code.includes("[!code");
|
|
534
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
535
|
+
const consumed = [
|
|
536
|
+
"lang", "language", "code", "title", "highlights", "lineNumbers",
|
|
537
|
+
"class", "ins", "del", "mark", "collapse",
|
|
538
|
+
];
|
|
539
|
+
const passthrough = renderPassthroughAttrs(node.props, consumed);
|
|
540
|
+
if (isRich) {
|
|
541
|
+
ctx.imports.add("code-rich");
|
|
542
|
+
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
543
|
+
if (title)
|
|
544
|
+
attrs.push(`title="${escapeAttr(title)}"`);
|
|
545
|
+
if (lineNumbers)
|
|
546
|
+
attrs.push("lineNumbers");
|
|
547
|
+
if (highlights)
|
|
548
|
+
attrs.push(`highlights="${escapeAttr(highlights)}"`);
|
|
549
|
+
return `<CodeRich ${attrs.join(" ")}${classAttr}${passthrough} />`;
|
|
550
|
+
}
|
|
551
|
+
ctx.imports.add("code");
|
|
552
|
+
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
553
|
+
if (title)
|
|
554
|
+
attrs.push(`title="${escapeAttr(title)}"`);
|
|
555
|
+
if (ctx.codeBlockTitle !== true) {
|
|
556
|
+
attrs.push(`showTitle="${ctx.codeBlockTitle}"`);
|
|
557
|
+
}
|
|
558
|
+
return `<CodeBlock ${attrs.join(" ")}${classAttr}${passthrough} />`;
|
|
559
|
+
}
|
|
560
|
+
// Map GitHub alert names (and other aliases) to Alert component variants.
|
|
561
|
+
// The Alert variant palette follows MkDocs Material; not every GitHub name
|
|
562
|
+
// maps 1:1, so we translate at the render boundary.
|
|
563
|
+
const CALLOUT_VARIANT_MAP = {
|
|
564
|
+
important: "info", // GitHub IMPORTANT (purple) → info (blue/purple accent)
|
|
565
|
+
caution: "danger", // GitHub CAUTION (red) → danger (red)
|
|
566
|
+
error: "failure",
|
|
567
|
+
hint: "tip",
|
|
568
|
+
attention: "warning",
|
|
569
|
+
};
|
|
570
|
+
function calloutToAstro(node, ctx) {
|
|
571
|
+
ctx.imports.add("callout");
|
|
572
|
+
// Callout variant may live on props.variant (Starlight / MkDocs importers)
|
|
573
|
+
// or props.type (dogsbay-md parser). Support both.
|
|
574
|
+
const rawVariant = (node.props?.variant
|
|
575
|
+
?? node.props?.type
|
|
576
|
+
?? "note").toLowerCase();
|
|
577
|
+
const variant = CALLOUT_VARIANT_MAP[rawVariant] ?? rawVariant;
|
|
578
|
+
const title = node.props?.title ?? "";
|
|
579
|
+
const icon = node.props?.icon;
|
|
580
|
+
const iconAttr = icon ? ` icon="${escapeAttr(icon)}"` : "";
|
|
581
|
+
// User classes and passthrough attributes (id, data-*, aria-*, style)
|
|
582
|
+
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
583
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
584
|
+
"type", "variant", "title", "icon", "class",
|
|
585
|
+
]);
|
|
586
|
+
const inner = childrenToAstro(node, ctx);
|
|
587
|
+
return `<Alert variant="${escapeAttr(variant)}"${iconAttr}${classAttr}${passthrough}>
|
|
588
|
+
<AlertTitle>${escapeTemplate(title)}</AlertTitle>
|
|
589
|
+
<AlertDescription>
|
|
590
|
+
${indentStr(inner, 4)}
|
|
591
|
+
</AlertDescription>
|
|
592
|
+
</Alert>`;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Merge a default class string with an optional user-provided class string.
|
|
596
|
+
* Concatenates (user classes appended) so utility frameworks like Tailwind
|
|
597
|
+
* resolve conflicts via component-internal class-variance-authority / tv().
|
|
598
|
+
*/
|
|
599
|
+
function mergeClassAttr(defaults, userClass) {
|
|
600
|
+
const combined = [defaults, userClass].filter(Boolean).join(" ").trim();
|
|
601
|
+
return combined ? ` class="${escapeAttr(combined)}"` : "";
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Render attributes not consumed as component props — `id`, `data-*`, `aria-*`,
|
|
605
|
+
* and `style`. These pass through to the rendered element.
|
|
606
|
+
*/
|
|
607
|
+
function renderPassthroughAttrs(props, consumed) {
|
|
608
|
+
if (!props)
|
|
609
|
+
return "";
|
|
610
|
+
const skip = new Set([...consumed, "source", "children"]);
|
|
611
|
+
const parts = [];
|
|
612
|
+
for (const [key, value] of Object.entries(props)) {
|
|
613
|
+
if (skip.has(key))
|
|
614
|
+
continue;
|
|
615
|
+
if (value === undefined || value === null || value === false)
|
|
616
|
+
continue;
|
|
617
|
+
if (!/^(id|data-[\w-]+|aria-[\w-]+|style|role|title)$/.test(key))
|
|
618
|
+
continue;
|
|
619
|
+
if (value === true) {
|
|
620
|
+
parts.push(key);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
parts.push(`${key}="${escapeAttr(String(value))}"`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return parts.length ? " " + parts.join(" ") : "";
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Coerce an attribute value to a number, falling back to default.
|
|
630
|
+
* Accepts: number, numeric string ("3"), or anything → fallback.
|
|
631
|
+
*/
|
|
632
|
+
function coerceNumeric(value, fallback) {
|
|
633
|
+
if (typeof value === "number")
|
|
634
|
+
return value;
|
|
635
|
+
if (typeof value === "string") {
|
|
636
|
+
const n = Number(value);
|
|
637
|
+
if (!Number.isNaN(n))
|
|
638
|
+
return n;
|
|
639
|
+
}
|
|
640
|
+
return fallback;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Coerce a value to a numeric array.
|
|
644
|
+
* Accepts: number → [n], number[], numeric string → [n], JSON-array string,
|
|
645
|
+
* comma-separated string → split, otherwise fallback.
|
|
646
|
+
*/
|
|
647
|
+
function coerceNumericList(value, fallback) {
|
|
648
|
+
if (Array.isArray(value))
|
|
649
|
+
return value.map((v) => coerceNumeric(v, 0));
|
|
650
|
+
if (typeof value === "number")
|
|
651
|
+
return [value];
|
|
652
|
+
if (typeof value === "string") {
|
|
653
|
+
const trimmed = value.trim();
|
|
654
|
+
if (trimmed.startsWith("[")) {
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(trimmed);
|
|
657
|
+
if (Array.isArray(parsed))
|
|
658
|
+
return parsed.map((v) => coerceNumeric(v, 0));
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
// fallthrough
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (trimmed.includes(",")) {
|
|
665
|
+
return trimmed.split(",").map((s) => coerceNumeric(s.trim(), 0));
|
|
666
|
+
}
|
|
667
|
+
return [coerceNumeric(trimmed, fallback[0])];
|
|
668
|
+
}
|
|
669
|
+
return fallback;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Wrap content in an HTML tag with user-mergeable class + passthrough attrs.
|
|
673
|
+
* Use for simple block elements like `<ol>`, `<blockquote>`, `<figure>` where
|
|
674
|
+
* the default class is a utility string.
|
|
675
|
+
*
|
|
676
|
+
* `consumed` lists prop keys that are used elsewhere (as component props or
|
|
677
|
+
* semantic fields) and shouldn't leak into passthrough attrs.
|
|
678
|
+
*/
|
|
679
|
+
function wrapBlock(tag, defaultClass, node, content, consumed = []) {
|
|
680
|
+
const classAttr = mergeClassAttr(defaultClass, node.props?.class);
|
|
681
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class", ...consumed]);
|
|
682
|
+
const needsNewline = /<\/?[a-zA-Z]/.test(content) && !content.startsWith("<");
|
|
683
|
+
const sep = content.includes("\n") ? "\n" : "";
|
|
684
|
+
return `<${tag}${classAttr}${passthrough}>${sep}${content}${sep}</${tag}>`;
|
|
685
|
+
void needsNewline;
|
|
686
|
+
}
|
|
687
|
+
function detailsToAstro(node, ctx) {
|
|
688
|
+
ctx.imports.add("details");
|
|
689
|
+
const variant = node.props?.variant ?? "note";
|
|
690
|
+
const title = node.props?.title ?? "Details";
|
|
691
|
+
const open = node.props?.open;
|
|
692
|
+
const icon = node.props?.icon;
|
|
693
|
+
const inner = childrenToAstro(node, ctx);
|
|
694
|
+
const attrs = [`variant="${escapeAttr(variant)}"`, `title="${escapeAttr(title)}"`];
|
|
695
|
+
if (open)
|
|
696
|
+
attrs.push("open");
|
|
697
|
+
if (icon)
|
|
698
|
+
attrs.push(`icon="${escapeAttr(icon)}"`);
|
|
699
|
+
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
700
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
701
|
+
"variant", "title", "open", "icon", "class", "summary",
|
|
702
|
+
]);
|
|
703
|
+
return `<CollapsibleAlert ${attrs.join(" ")}${classAttr}${passthrough}>
|
|
704
|
+
${indentStr(inner, 2)}
|
|
705
|
+
</CollapsibleAlert>`;
|
|
706
|
+
}
|
|
707
|
+
function tabsToAstro(node, ctx) {
|
|
708
|
+
ctx.imports.add("tabs");
|
|
709
|
+
const tabs = node.children ?? [];
|
|
710
|
+
if (tabs.length === 0)
|
|
711
|
+
return "";
|
|
712
|
+
const tabItems = tabs.map((tab, i) => {
|
|
713
|
+
const title = tab.props?.title || tab.props?.label || tab.props?.value || `Tab ${i + 1}`;
|
|
714
|
+
const value = tab.props?.value || title;
|
|
715
|
+
const label = title;
|
|
716
|
+
return { value, label, node: tab };
|
|
717
|
+
});
|
|
718
|
+
const defaultValue = tabItems[0].value;
|
|
719
|
+
const triggers = tabItems
|
|
720
|
+
.map((t) => ` <TabsTrigger value="${escapeAttr(t.value)}">${escapeTemplate(t.label)}</TabsTrigger>`)
|
|
721
|
+
.join("\n");
|
|
722
|
+
const contents = tabItems
|
|
723
|
+
.map((t) => {
|
|
724
|
+
const inner = childrenToAstro(t.node, ctx);
|
|
725
|
+
return ` <TabsContent value="${escapeAttr(t.value)}">\n${indentStr(inner, 4)}\n </TabsContent>`;
|
|
726
|
+
})
|
|
727
|
+
.join("\n");
|
|
728
|
+
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
729
|
+
const passthrough = renderPassthroughAttrs(node.props, ["sync", "default", "class"]);
|
|
730
|
+
return `<Tabs defaultValue="${escapeAttr(defaultValue)}"${classAttr}${passthrough}>
|
|
731
|
+
<TabsList>
|
|
732
|
+
${triggers}
|
|
733
|
+
</TabsList>
|
|
734
|
+
${contents}
|
|
735
|
+
</Tabs>`;
|
|
736
|
+
}
|
|
737
|
+
function tableToAstro(node, ctx) {
|
|
738
|
+
ctx.imports.add("table");
|
|
739
|
+
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
740
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
741
|
+
return `<Table${classAttr}${passthrough}>\n${childrenToAstro(node, ctx)}\n</Table>`;
|
|
742
|
+
}
|
|
743
|
+
function diagramToAstro(node, ctx) {
|
|
744
|
+
ctx.imports.add("diagram");
|
|
745
|
+
const lang = node.props?.lang || "mermaid";
|
|
746
|
+
const code = node.props?.code ?? "";
|
|
747
|
+
const svg = node.props?.svg ?? "";
|
|
748
|
+
const title = node.props?.title;
|
|
749
|
+
const attrs = [`lang="${escapeAttr(lang)}"`, `code={${escapeExpr(code)}}`, `svg={${escapeExpr(svg)}}`];
|
|
750
|
+
if (title)
|
|
751
|
+
attrs.push(`title="${escapeAttr(title)}"`);
|
|
752
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
753
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
754
|
+
"lang", "code", "svg", "title", "class",
|
|
755
|
+
]);
|
|
756
|
+
return `<Diagram ${attrs.join(" ")}${classAttr}${passthrough} />`;
|
|
757
|
+
}
|
|
758
|
+
function youtubeToAstro(node, ctx) {
|
|
759
|
+
ctx.imports.add("youtube");
|
|
760
|
+
const id = node.props?.id ?? "";
|
|
761
|
+
const title = node.props?.title ?? "";
|
|
762
|
+
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
763
|
+
const passthrough = renderPassthroughAttrs(node.props, ["id", "title", "class"]);
|
|
764
|
+
return `<YouTube id="${escapeAttr(id)}" title="${escapeAttr(title)}"${classAttr}${passthrough} />`;
|
|
765
|
+
}
|
|
766
|
+
function mathBlockToAstro(node, ctx) {
|
|
767
|
+
ctx.imports.add("math-block");
|
|
768
|
+
const latex = node.props?.latex ?? "";
|
|
769
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
770
|
+
const passthrough = renderPassthroughAttrs(node.props, ["latex", "class"]);
|
|
771
|
+
return `<MathKatex latex={${escapeExpr(latex)}} display${classAttr}${passthrough} />`;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Render a footnote list as a GitHub-style `<section>` with a
|
|
775
|
+
* separator rule, an accessibility-only heading, and the ordered
|
|
776
|
+
* list of definitions. Without the wrapper, the bare `<ol>` reads
|
|
777
|
+
* as a continuation of whatever bulleted/numbered list precedes
|
|
778
|
+
* it — which we hit on /docs/ccms/heretto/ where the "Related"
|
|
779
|
+
* section flowed straight into the footnotes with no break.
|
|
780
|
+
*
|
|
781
|
+
* Structure follows GitHub's convention:
|
|
782
|
+
* <hr> separator (visible)
|
|
783
|
+
* <section data-footnotes aria-labelledby="footnote-label">
|
|
784
|
+
* <h2 id="footnote-label" class="sr-only">Footnotes</h2>
|
|
785
|
+
* <ol data-footnote-list>...</ol>
|
|
786
|
+
* </section>
|
|
787
|
+
*
|
|
788
|
+
* The `sr-only` h2 is announced by screen readers as a section
|
|
789
|
+
* heading without adding visible chrome — writers who want a
|
|
790
|
+
* visible "Sources" / "References" heading still author it
|
|
791
|
+
* manually above their `[^N]:` definitions.
|
|
792
|
+
*/
|
|
793
|
+
function footnoteListToAstro(node, ctx) {
|
|
794
|
+
return [
|
|
795
|
+
`<hr class="my-8 border-t border-border" data-footnotes-separator />`,
|
|
796
|
+
`<section class="footnotes" data-footnotes aria-labelledby="footnote-label">`,
|
|
797
|
+
` <h2 id="footnote-label" class="sr-only">Footnotes</h2>`,
|
|
798
|
+
` <ol class="list-decimal pl-6 space-y-1 text-sm text-muted-foreground" data-footnote-list>`,
|
|
799
|
+
childrenToAstro(node, ctx),
|
|
800
|
+
` </ol>`,
|
|
801
|
+
`</section>`,
|
|
802
|
+
].join("\n");
|
|
803
|
+
}
|
|
804
|
+
function footnoteDefToAstro(node, ctx) {
|
|
805
|
+
const id = node.props?.id ?? "";
|
|
806
|
+
const backrefId = node.props?.backrefId ?? "";
|
|
807
|
+
if (ctx.mode === "template" && node.inline) {
|
|
808
|
+
const content = inlineToTemplate(node.inline);
|
|
809
|
+
const backref = backrefId ? ` <a href="#${escapeAttr(backrefId)}" class="text-primary hover:underline">↩</a>` : "";
|
|
810
|
+
return `<li id="${escapeAttr(id)}" class="text-sm text-muted-foreground">${content}${backref}</li>`;
|
|
811
|
+
}
|
|
812
|
+
const content = node.inline ? inlineToHtml(node.inline) : (node.html ?? "");
|
|
813
|
+
const backref = backrefId ? ` <a href="#${escapeAttr(backrefId)}" class="text-primary hover:underline">↩</a>` : "";
|
|
814
|
+
return `<li id="${escapeAttr(id)}" class="text-sm text-muted-foreground" set:html={${escapeExpr(content + backref)}} />`;
|
|
815
|
+
}
|
|
816
|
+
function apiSymbolToAstro(node, ctx) {
|
|
817
|
+
ctx.imports.add("api-class");
|
|
818
|
+
const name = node.props?.name ?? "";
|
|
819
|
+
const kind = node.props?.kind ?? "func";
|
|
820
|
+
const signature = node.props?.signature;
|
|
821
|
+
const bases = node.props?.bases || [];
|
|
822
|
+
const level = node.type === "api-class" ? 2 : 3;
|
|
823
|
+
const attrs = [`name="${escapeAttr(name)}"`, `kind="${escapeAttr(kind)}"`, `level={${level}}`];
|
|
824
|
+
if (signature)
|
|
825
|
+
attrs.push(`signature={${escapeExpr(signature)}}`);
|
|
826
|
+
if (bases.length > 0)
|
|
827
|
+
attrs.push(`bases={${JSON.stringify(bases)}}`);
|
|
828
|
+
const inner = childrenToAstro(node, ctx);
|
|
829
|
+
return `<ApiSymbol ${attrs.join(" ")}>\n${indentStr(inner, 2)}\n</ApiSymbol>`;
|
|
830
|
+
}
|
|
831
|
+
function apiDocToAstro(node, ctx) {
|
|
832
|
+
ctx.imports.add("api-doc");
|
|
833
|
+
const html = node.props?.html ?? "";
|
|
834
|
+
return `<ApiDoc html={${escapeExpr(html)}} />`;
|
|
835
|
+
}
|
|
836
|
+
function apiParamsToAstro(node, ctx) {
|
|
837
|
+
ctx.imports.add("api-params");
|
|
838
|
+
const params = node.props?.params ?? [];
|
|
839
|
+
return `<ApiParams params={${JSON.stringify(params)}} />`;
|
|
840
|
+
}
|
|
841
|
+
function apiSourceToAstro(node, ctx) {
|
|
842
|
+
ctx.imports.add("api-source");
|
|
843
|
+
const code = node.props?.code ?? "";
|
|
844
|
+
const file = node.props?.file;
|
|
845
|
+
const line = node.props?.line;
|
|
846
|
+
const attrs = [`code={${escapeExpr(code)}}`];
|
|
847
|
+
if (file)
|
|
848
|
+
attrs.push(`file="${escapeAttr(file)}"`);
|
|
849
|
+
if (line)
|
|
850
|
+
attrs.push(`line={${line}}`);
|
|
851
|
+
return `<ApiSource ${attrs.join(" ")} />`;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Render an OpenAPI operation as `<ApiLayout>` with two slots:
|
|
855
|
+
* - `description` — `<EndpointCard>` containing collapsible
|
|
856
|
+
* `<EndpointSection>`s for parameters / request body / responses,
|
|
857
|
+
* using `<ParameterTable>` / `<SchemaViewer>` / `<ResponseTabs>`.
|
|
858
|
+
* - `code` — `<CodeSamples>` and (when present) request / response
|
|
859
|
+
* example panels.
|
|
860
|
+
*
|
|
861
|
+
* All structured data (parameters, schemas, samples) is JSON-stringified
|
|
862
|
+
* and passed as Astro props — the components handle the rendering. Only
|
|
863
|
+
* top-level summary / description text is interpolated into the markup.
|
|
864
|
+
*/
|
|
865
|
+
function endpointToAstro(node, ctx) {
|
|
866
|
+
ctx.imports.add("endpoint");
|
|
867
|
+
const props = (node.props ?? {});
|
|
868
|
+
const method = (props.method || "get").toLowerCase();
|
|
869
|
+
const path = props.path ?? "";
|
|
870
|
+
const baseUrl = props.baseUrl ?? "";
|
|
871
|
+
const summary = props.summary;
|
|
872
|
+
const description = props.description;
|
|
873
|
+
const deprecated = props.deprecated === true;
|
|
874
|
+
const parameters = props.parameters ?? [];
|
|
875
|
+
const requestBody = props.requestBody ?? [];
|
|
876
|
+
const responses = props.responses ?? [];
|
|
877
|
+
const codeSamples = props.codeSamples ?? [];
|
|
878
|
+
const requestBodyExample = props.requestBodyExample;
|
|
879
|
+
const cardAttrs = [
|
|
880
|
+
'slot="description"',
|
|
881
|
+
`method="${escapeAttr(method)}"`,
|
|
882
|
+
`path={${escapeExpr(path)}}`,
|
|
883
|
+
];
|
|
884
|
+
if (baseUrl)
|
|
885
|
+
cardAttrs.push(`baseUrl={${escapeExpr(baseUrl)}}`);
|
|
886
|
+
if (summary)
|
|
887
|
+
cardAttrs.push(`summary={${escapeExpr(summary)}}`);
|
|
888
|
+
if (description)
|
|
889
|
+
cardAttrs.push(`description={${escapeExpr(description)}}`);
|
|
890
|
+
if (deprecated)
|
|
891
|
+
cardAttrs.push("deprecated");
|
|
892
|
+
// Description column — collapsible sections for parameters, request
|
|
893
|
+
// body schema, and response schemas. PropertyList (Stripe-style,
|
|
894
|
+
// grouped by location with per-type colour badges) reads better than
|
|
895
|
+
// ParameterTable at every viewport width — see
|
|
896
|
+
// packages/ui/src/property-list/PropertyList.astro.
|
|
897
|
+
const sections = [];
|
|
898
|
+
if (parameters.length > 0) {
|
|
899
|
+
sections.push(`<EndpointSection title="Parameters">\n` +
|
|
900
|
+
` <PropertyList parameters={${JSON.stringify(parameters)}} />\n` +
|
|
901
|
+
`</EndpointSection>`);
|
|
902
|
+
}
|
|
903
|
+
if (requestBody.length > 0) {
|
|
904
|
+
sections.push(`<EndpointSection title="Request body">\n` +
|
|
905
|
+
` <SchemaViewer properties={${JSON.stringify(requestBody)}} />\n` +
|
|
906
|
+
`</EndpointSection>`);
|
|
907
|
+
}
|
|
908
|
+
if (responses.length > 0) {
|
|
909
|
+
sections.push(`<EndpointSection title="Responses">\n` +
|
|
910
|
+
` <ResponseTabs responses={${JSON.stringify(responses)}} />\n` +
|
|
911
|
+
`</EndpointSection>`);
|
|
912
|
+
}
|
|
913
|
+
const sectionsBody = sections.length > 0
|
|
914
|
+
? `\n${indentStr(sections.join("\n"), 4)}\n `
|
|
915
|
+
: "";
|
|
916
|
+
// Code column — wrapped in <ApiCodePanel> for consistent vertical
|
|
917
|
+
// rhythm. Code samples first, then a request body example, then one
|
|
918
|
+
// ExampleBlock per response status that carries a concrete example.
|
|
919
|
+
// Each ExampleBlock auto-detects language from the response's
|
|
920
|
+
// contentType (application/json → "json", application/xml → "xml",
|
|
921
|
+
// etc.).
|
|
922
|
+
const codePanels = [];
|
|
923
|
+
if (codeSamples.length > 0) {
|
|
924
|
+
codePanels.push(`<div>\n` +
|
|
925
|
+
` <h3 class="mb-3 text-xs font-semibold uppercase tracking-wider text-white/60">Code samples</h3>\n` +
|
|
926
|
+
` <CodeSamples samples={${JSON.stringify(codeSamples)}} />\n` +
|
|
927
|
+
`</div>`);
|
|
928
|
+
}
|
|
929
|
+
if (requestBodyExample !== undefined) {
|
|
930
|
+
const json = JSON.stringify(requestBodyExample, null, 2);
|
|
931
|
+
codePanels.push(`<ExampleBlock title="Request body" code={${escapeExpr(json)}} lang="json" />`);
|
|
932
|
+
}
|
|
933
|
+
for (const r of responses) {
|
|
934
|
+
if (r.example === undefined)
|
|
935
|
+
continue;
|
|
936
|
+
const status = String(r.status ?? "");
|
|
937
|
+
const contentType = r.contentType;
|
|
938
|
+
const exampleJson = JSON.stringify(r.example, null, 2);
|
|
939
|
+
const ctAttr = contentType ? ` contentType={${escapeExpr(contentType)}}` : "";
|
|
940
|
+
codePanels.push(`<ExampleBlock title="Response ${escapeAttr(status)}" code={${escapeExpr(exampleJson)}}${ctAttr} />`);
|
|
941
|
+
}
|
|
942
|
+
const codeBody = codePanels.length > 0
|
|
943
|
+
? `\n${indentStr(codePanels.join("\n"), 4)}\n `
|
|
944
|
+
: "";
|
|
945
|
+
return (`<ApiLayout>\n` +
|
|
946
|
+
` <EndpointCard ${cardAttrs.join(" ")}>${sectionsBody}</EndpointCard>\n` +
|
|
947
|
+
` <ApiCodePanel slot="code">${codeBody}</ApiCodePanel>\n` +
|
|
948
|
+
`</ApiLayout>`);
|
|
949
|
+
}
|
|
950
|
+
function cardsToAstro(node, ctx) {
|
|
951
|
+
ctx.imports.add("grid");
|
|
952
|
+
ctx.imports.add("card");
|
|
953
|
+
// Grid-level attributes on the :::cards container
|
|
954
|
+
const outerClass = mergeClassAttr("", node.props?.class);
|
|
955
|
+
const outerPass = renderPassthroughAttrs(node.props, [
|
|
956
|
+
"cols", "gap", "rows", "class",
|
|
957
|
+
]);
|
|
958
|
+
// MDX pattern: children are card nodes with title/href/icon props
|
|
959
|
+
const cardNodes = (node.children ?? []).filter((c) => c.type === "card");
|
|
960
|
+
if (cardNodes.length > 0) {
|
|
961
|
+
const cards = cardNodes.map((card) => {
|
|
962
|
+
const title = card.props?.title ?? "";
|
|
963
|
+
const href = card.props?.href ?? "";
|
|
964
|
+
const description = card.props?.description;
|
|
965
|
+
// Description sources: inline content, children, or props.description.
|
|
966
|
+
const inner = childrenToAstro(card, ctx)
|
|
967
|
+
|| (card.inline ? inlineToTemplate(card.inline) : "")
|
|
968
|
+
|| (description ? escapeTemplate(description) : "");
|
|
969
|
+
const titleHtml = title ? `<h3 class="text-base font-semibold">${escapeTemplate(title)}</h3>` : "";
|
|
970
|
+
const desc = inner
|
|
971
|
+
? `<p class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${inner}</p>`
|
|
972
|
+
: "";
|
|
973
|
+
const cardDefaultClass = href
|
|
974
|
+
? "p-4 hover:border-foreground/20 transition-colors"
|
|
975
|
+
: "p-4";
|
|
976
|
+
const cardClassAttr = mergeClassAttr(cardDefaultClass, card.props?.class);
|
|
977
|
+
const cardPass = renderPassthroughAttrs(card.props, [
|
|
978
|
+
"title", "href", "description", "icon", "variant", "class",
|
|
979
|
+
]);
|
|
980
|
+
if (href) {
|
|
981
|
+
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="[text-decoration:none] text-foreground">\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
982
|
+
}
|
|
983
|
+
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n${titleHtml}\n${desc}\n</CardContent>\n</Card>`;
|
|
984
|
+
}).join("\n");
|
|
985
|
+
return `<Grid cols={[1, 2]} gap={4}${outerClass}${outerPass}>\n${cards}\n</Grid>`;
|
|
986
|
+
}
|
|
987
|
+
// MkDocs pattern: children are unordered-list items
|
|
988
|
+
const items = (node.children ?? [])
|
|
989
|
+
.filter((c) => c.type === "unordered-list")
|
|
990
|
+
.flatMap((ul) => ul.children ?? []);
|
|
991
|
+
if (items.length > 0) {
|
|
992
|
+
const cards = items
|
|
993
|
+
.map((item) => `<Card>\n<CardContent>\n${childrenToAstro(item, ctx)}\n</CardContent>\n</Card>`)
|
|
994
|
+
.join("\n");
|
|
995
|
+
return `<Grid cols={[1, 2]} gap={4}>\n${cards}\n</Grid>`;
|
|
996
|
+
}
|
|
997
|
+
// Fallback: render children directly in grid
|
|
998
|
+
return `<Grid cols={[1, 2]} gap={4}>\n${childrenToAstro(node, ctx)}\n</Grid>`;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Render a standalone `card` TreeNode (outside `:::cards`).
|
|
1002
|
+
*
|
|
1003
|
+
* Same shape as the cards-list path uses for each card, but emitted as a
|
|
1004
|
+
* single block. Title becomes an h3; description is the body. When href is
|
|
1005
|
+
* present the whole card becomes a link target.
|
|
1006
|
+
*/
|
|
1007
|
+
function standaloneCardToAstro(node, ctx) {
|
|
1008
|
+
ctx.imports.add("card");
|
|
1009
|
+
const title = node.props?.title ?? "";
|
|
1010
|
+
const href = node.props?.href ?? "";
|
|
1011
|
+
const description = node.props?.description;
|
|
1012
|
+
const inner = childrenToAstro(node, ctx)
|
|
1013
|
+
|| (node.inline ? inlineToTemplate(node.inline) : "")
|
|
1014
|
+
|| (description ? escapeTemplate(description) : "");
|
|
1015
|
+
const titleHtml = title
|
|
1016
|
+
? `<h3 class="text-base font-semibold">${escapeTemplate(title)}</h3>`
|
|
1017
|
+
: "";
|
|
1018
|
+
const desc = inner
|
|
1019
|
+
? `<p class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${inner}</p>`
|
|
1020
|
+
: "";
|
|
1021
|
+
const cardDefaultClass = href
|
|
1022
|
+
? "p-4 hover:border-foreground/20 transition-colors"
|
|
1023
|
+
: "p-4";
|
|
1024
|
+
const classAttr = mergeClassAttr(cardDefaultClass, node.props?.class);
|
|
1025
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1026
|
+
"title", "href", "description", "icon", "variant", "class",
|
|
1027
|
+
]);
|
|
1028
|
+
if (href) {
|
|
1029
|
+
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="[text-decoration:none] text-foreground">\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1030
|
+
}
|
|
1031
|
+
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n${titleHtml}\n${desc}\n</CardContent>\n</Card>`;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Render a `link-card` TreeNode as the dedicated `<LinkCard>` component.
|
|
1035
|
+
*
|
|
1036
|
+
* LinkCard takes title + description + href as props (no slot for body).
|
|
1037
|
+
* If the parser kept rich body children instead of folding into description,
|
|
1038
|
+
* we fall back to the standalone-card path which can hold arbitrary content.
|
|
1039
|
+
*/
|
|
1040
|
+
function linkCardToAstro(node, ctx) {
|
|
1041
|
+
const hasRichChildren = (node.children ?? []).length > 0;
|
|
1042
|
+
if (hasRichChildren) {
|
|
1043
|
+
return standaloneCardToAstro(node, ctx);
|
|
1044
|
+
}
|
|
1045
|
+
ctx.imports.add("link-card");
|
|
1046
|
+
const title = node.props?.title ?? "";
|
|
1047
|
+
const description = node.props?.description;
|
|
1048
|
+
const href = node.props?.href ?? "";
|
|
1049
|
+
const titleAttr = ` title="${escapeAttr(title)}"`;
|
|
1050
|
+
const hrefAttr = ` href="${escapeAttr(href)}"`;
|
|
1051
|
+
const descAttr = description ? ` description="${escapeAttr(description)}"` : "";
|
|
1052
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1053
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1054
|
+
"title", "description", "href", "icon", "class",
|
|
1055
|
+
]);
|
|
1056
|
+
return `<LinkCard${titleAttr}${descAttr}${hrefAttr}${classAttr}${passthrough} />`;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Render an `accordion` TreeNode and its `accordion-item` children.
|
|
1060
|
+
*
|
|
1061
|
+
* Output mirrors the canonical Starwind/Dogsbay accordion shape:
|
|
1062
|
+
* <Accordion type defaultValue>
|
|
1063
|
+
* <AccordionItem value>
|
|
1064
|
+
* <AccordionTrigger>label</AccordionTrigger>
|
|
1065
|
+
* <AccordionContent>body</AccordionContent>
|
|
1066
|
+
* </AccordionItem>
|
|
1067
|
+
* </Accordion>
|
|
1068
|
+
*/
|
|
1069
|
+
function accordionToAstro(node, ctx) {
|
|
1070
|
+
ctx.imports.add("accordion");
|
|
1071
|
+
const type = node.props?.type ?? "single";
|
|
1072
|
+
const defaultValue = node.props?.defaultValue;
|
|
1073
|
+
const typeAttr = ` type="${escapeAttr(type)}"`;
|
|
1074
|
+
const defaultAttr = defaultValue !== undefined
|
|
1075
|
+
? ` defaultValue=${typeof defaultValue === "string"
|
|
1076
|
+
? `"${escapeAttr(defaultValue)}"`
|
|
1077
|
+
: `{${JSON.stringify(defaultValue)}}`}`
|
|
1078
|
+
: "";
|
|
1079
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1080
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1081
|
+
"type", "defaultValue", "class",
|
|
1082
|
+
]);
|
|
1083
|
+
const items = (node.children ?? [])
|
|
1084
|
+
.filter((c) => c.type === "accordion-item")
|
|
1085
|
+
.map((item) => accordionItemToAstro(item, ctx, /*standalone*/ false))
|
|
1086
|
+
.join("\n");
|
|
1087
|
+
return `<Accordion${typeAttr}${defaultAttr}${classAttr}${passthrough}>\n${items}\n</Accordion>`;
|
|
1088
|
+
}
|
|
1089
|
+
function accordionItemToAstro(node, ctx, standalone) {
|
|
1090
|
+
ctx.imports.add("accordion");
|
|
1091
|
+
const value = node.props?.value ?? "item-1";
|
|
1092
|
+
const label = node.props?.label ?? node.props?.title ?? "Item";
|
|
1093
|
+
const valueAttr = ` value="${escapeAttr(value)}"`;
|
|
1094
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1095
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1096
|
+
"value", "label", "title", "class",
|
|
1097
|
+
]);
|
|
1098
|
+
const body = childrenToAstro(node, ctx)
|
|
1099
|
+
|| (node.inline ? inlineToTemplate(node.inline) : "");
|
|
1100
|
+
const itemMarkup = `<AccordionItem${valueAttr}${classAttr}${passthrough}>
|
|
1101
|
+
<AccordionTrigger>${escapeTemplate(label)}</AccordionTrigger>
|
|
1102
|
+
<AccordionContent>
|
|
1103
|
+
${body}
|
|
1104
|
+
</AccordionContent>
|
|
1105
|
+
</AccordionItem>`;
|
|
1106
|
+
// Bare accordion-item: wrap so the output is still a working widget.
|
|
1107
|
+
if (standalone) {
|
|
1108
|
+
return `<Accordion type="single">\n${itemMarkup}\n</Accordion>`;
|
|
1109
|
+
}
|
|
1110
|
+
return itemMarkup;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Render an `avatar` TreeNode as the `<Avatar>` component.
|
|
1114
|
+
*/
|
|
1115
|
+
function avatarToAstro(node, ctx) {
|
|
1116
|
+
ctx.imports.add("avatar");
|
|
1117
|
+
const src = node.props?.src ?? "";
|
|
1118
|
+
const alt = node.props?.alt ?? "";
|
|
1119
|
+
const fallback = node.props?.fallback;
|
|
1120
|
+
const srcAttr = src ? ` src="${escapeAttr(src)}"` : "";
|
|
1121
|
+
const altAttr = ` alt="${escapeAttr(alt)}"`;
|
|
1122
|
+
const fbAttr = fallback ? ` fallback="${escapeAttr(fallback)}"` : "";
|
|
1123
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1124
|
+
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1125
|
+
"src", "alt", "fallback", "class",
|
|
1126
|
+
]);
|
|
1127
|
+
return `<Avatar${srcAttr}${altAttr}${fbAttr}${classAttr}${passthrough} />`;
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Render a :::example directive as a MarkdownExample component.
|
|
1131
|
+
*
|
|
1132
|
+
* The parser captured the inner content as `node.props.source` (re-serialized
|
|
1133
|
+
* back to Dogsbay MD text) and kept `node.children` for live rendering.
|
|
1134
|
+
*
|
|
1135
|
+
* The consuming app is expected to provide a `MarkdownExample` component via
|
|
1136
|
+
* its imports (see apps/md-docs/ for a reference implementation).
|
|
1137
|
+
*/
|
|
1138
|
+
function exampleToAstro(node, ctx) {
|
|
1139
|
+
ctx.imports.add("markdown-example");
|
|
1140
|
+
const source = String(node.props?.source ?? "");
|
|
1141
|
+
const title = node.props?.title;
|
|
1142
|
+
const showFallback = node.props?.fallback === true || node.props?.fallback === "true";
|
|
1143
|
+
const titleAttr = title ? ` title="${escapeAttr(title)}"` : "";
|
|
1144
|
+
const fallbackAttr = showFallback ? " showFallback" : "";
|
|
1145
|
+
const inner = childrenToAstro(node, ctx);
|
|
1146
|
+
return `<MarkdownExample source={${escapeExpr(source)}}${titleAttr}${fallbackAttr}>\n${inner}\n</MarkdownExample>`;
|
|
1147
|
+
}
|
|
1148
|
+
function htmlContainerToAstro(node, ctx) {
|
|
1149
|
+
const attrs = node.props?.attrs ?? {};
|
|
1150
|
+
const classes = attrs.class ?? "";
|
|
1151
|
+
if (classes.includes("grid") && classes.includes("cards")) {
|
|
1152
|
+
return cardsToAstro(node, ctx);
|
|
1153
|
+
}
|
|
1154
|
+
if (classes.includes("mdx-columns")) {
|
|
1155
|
+
const inner = childrenToAstro(node, ctx);
|
|
1156
|
+
return `<div class="columns-1 sm:columns-2 gap-4 [&_ul]:m-0">\n${inner}\n</div>`;
|
|
1157
|
+
}
|
|
1158
|
+
const inner = childrenToAstro(node, ctx);
|
|
1159
|
+
return `<div>\n${inner}\n</div>`;
|
|
1160
|
+
}
|
|
1161
|
+
// ─── Utilities ────────────────────────────────────────────────────
|
|
1162
|
+
/**
|
|
1163
|
+
* Render a node's content whether it's attached as flat inline (dogsbay-md
|
|
1164
|
+
* parser shape) or as wrapped children (Starlight importer shape). Both
|
|
1165
|
+
* shapes supported; inline comes before children if both present.
|
|
1166
|
+
*
|
|
1167
|
+
* Used for list-item, table cells, step, dt, dd — types that commonly
|
|
1168
|
+
* carry leaf text content directly on the node.
|
|
1169
|
+
*/
|
|
1170
|
+
function leafContent(node, ctx) {
|
|
1171
|
+
const parts = [];
|
|
1172
|
+
if (node.inline && node.inline.length > 0) {
|
|
1173
|
+
parts.push(inlineToTemplate(node.inline));
|
|
1174
|
+
}
|
|
1175
|
+
if (node.children && node.children.length > 0) {
|
|
1176
|
+
const rendered = childrenToAstro(node, ctx);
|
|
1177
|
+
if (rendered)
|
|
1178
|
+
parts.push(rendered);
|
|
1179
|
+
}
|
|
1180
|
+
return parts.join("\n");
|
|
1181
|
+
}
|
|
1182
|
+
function childrenToAstro(node, ctx) {
|
|
1183
|
+
if (!node.children || node.children.length === 0)
|
|
1184
|
+
return "";
|
|
1185
|
+
return node.children.map((n) => nodeToAstro(n, ctx)).filter(Boolean).join("\n");
|
|
1186
|
+
}
|
|
1187
|
+
function indentStr(text, spaces) {
|
|
1188
|
+
const prefix = " ".repeat(spaces);
|
|
1189
|
+
return text
|
|
1190
|
+
.split("\n")
|
|
1191
|
+
.map((line) => (line ? `${prefix}${line}` : line))
|
|
1192
|
+
.join("\n");
|
|
1193
|
+
}
|
|
1194
|
+
function escapeHtml(s) {
|
|
1195
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1196
|
+
}
|
|
1197
|
+
//# sourceMappingURL=serialize.js.map
|