@dogsbay/format-astro 0.2.0-beta.16 → 0.2.0-beta.161
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/api-markdown.d.ts +7 -0
- package/dist/api-markdown.js +49 -0
- package/dist/base-path.d.ts +93 -8
- package/dist/base-path.js +122 -9
- package/dist/blog.d.ts +133 -0
- package/dist/blog.js +325 -0
- package/dist/browser.d.ts +13 -0
- package/dist/browser.js +13 -0
- package/dist/canonical.d.ts +35 -0
- package/dist/canonical.js +110 -0
- package/dist/capabilities.d.ts +28 -0
- package/dist/capabilities.js +94 -0
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +3 -1
- package/dist/diff-decoration.d.ts +35 -0
- package/dist/diff-decoration.js +101 -0
- package/dist/granularity.d.ts +82 -0
- package/dist/granularity.js +246 -0
- package/dist/head-metadata.d.ts +25 -0
- package/dist/head-metadata.js +55 -0
- package/dist/index.d.ts +26 -6
- package/dist/index.js +34 -5
- package/dist/lead.d.ts +1 -39
- package/dist/lead.js +3 -38
- package/dist/llms-txt.d.ts +47 -11
- package/dist/llms-txt.js +83 -275
- package/dist/plugins.d.ts +0 -1
- package/dist/plugins.js +4 -3
- package/dist/project.d.ts +502 -13
- package/dist/project.js +2506 -697
- package/dist/redirects.d.ts +117 -0
- package/dist/redirects.js +223 -0
- package/dist/serialize.d.ts +14 -14
- package/dist/serialize.js +908 -270
- package/dist/site-chrome.d.ts +62 -0
- package/dist/site-chrome.js +56 -0
- package/dist/sitemap.d.ts +62 -4
- package/dist/sitemap.js +63 -56
- package/dist/taxonomy.d.ts +48 -2
- package/dist/taxonomy.js +50 -12
- package/dist/theme-css.d.ts +33 -0
- package/dist/theme-css.js +686 -0
- package/dist/tone-classes.d.ts +52 -0
- package/dist/tone-classes.js +84 -0
- package/dist/write-if-changed.d.ts +20 -0
- package/dist/write-if-changed.js +57 -0
- package/package.json +17 -7
- package/dist/base-path.d.ts.map +0 -1
- package/dist/base-path.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lead.d.ts.map +0 -1
- package/dist/lead.js.map +0 -1
- package/dist/llms-txt.d.ts.map +0 -1
- package/dist/llms-txt.js.map +0 -1
- package/dist/plugins.d.ts.map +0 -1
- package/dist/plugins.js.map +0 -1
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js.map +0 -1
- package/dist/serialize.d.ts.map +0 -1
- package/dist/serialize.js.map +0 -1
- package/dist/sitemap.d.ts.map +0 -1
- package/dist/sitemap.js.map +0 -1
- package/dist/taxonomy.d.ts.map +0 -1
- package/dist/taxonomy.js.map +0 -1
package/dist/serialize.js
CHANGED
|
@@ -1,32 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* markdown-generated `.astro` pages, which Tailwind's content
|
|
9
|
-
* scanner doesn't always pick up reliably (and consumers who put
|
|
10
|
-
* their content/ outside the scanned globs won't pick them up at
|
|
11
|
-
* all). See generateGlobalCss in project.ts.
|
|
12
|
-
*/
|
|
13
|
-
export const TONE_CLASSES = {
|
|
14
|
-
// Primary scale (intensity)
|
|
15
|
-
"primary": "bg-primary text-primary-foreground",
|
|
16
|
-
"primary-strong": "bg-primary/80 text-primary-foreground",
|
|
17
|
-
"primary-medium": "bg-primary/60 text-primary-foreground",
|
|
18
|
-
"primary-half": "bg-primary/50 text-primary-foreground",
|
|
19
|
-
"primary-light": "bg-primary/20 text-primary",
|
|
20
|
-
"primary-soft": "bg-primary/10 text-primary",
|
|
21
|
-
// Neutral
|
|
22
|
-
"muted": "bg-muted text-muted-foreground",
|
|
23
|
-
// Named colors (for layout demos with distinct cells)
|
|
24
|
-
"blue": "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
25
|
-
"amber": "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
|
26
|
-
"green": "bg-green-500/15 text-green-700 dark:text-green-300",
|
|
27
|
-
"purple": "bg-purple-500/15 text-purple-700 dark:text-purple-300",
|
|
28
|
-
"red": "bg-red-500/15 text-red-700 dark:text-red-300",
|
|
29
|
-
};
|
|
1
|
+
import { walkInline, applyTextFlags, renderLeaf, indent, isSafeHref, apiPageLayout, apiSymbolLevels, renderAroundGroup, isDetailsSheet, isEmptyTableHead, cardBodyHasLink, stepLead, stepTitle, } from "@dogsbay/serialize-core";
|
|
2
|
+
import { parseInline } from "@dogsbay/format-dogsbay-md";
|
|
3
|
+
import { decorateDiff, hasDiffMarks } from "./diff-decoration.js";
|
|
4
|
+
import { TAB_GLYPH, tabDiffSignal } from "@dogsbay/render-html/diff";
|
|
5
|
+
import { TONE_CLASSES, CARD_TONE_CLASSES, TAB_VARIANTS, THEME_SCOPES, lookup } from "./tone-classes.js";
|
|
6
|
+
import { apiBlockHtml, apiPropertiesHtml } from "./api-markdown.js";
|
|
7
|
+
export { TONE_CLASSES };
|
|
30
8
|
const COMPONENT_IMPORTS = {
|
|
31
9
|
"markdown-example": [
|
|
32
10
|
'import MarkdownExample from "@ui/markdown-example/MarkdownExample.astro";',
|
|
@@ -39,6 +17,9 @@ const COMPONENT_IMPORTS = {
|
|
|
39
17
|
details: [
|
|
40
18
|
'import CollapsibleAlert from "@ui/alert/CollapsibleAlert.astro";',
|
|
41
19
|
],
|
|
20
|
+
"details-sheet": [
|
|
21
|
+
'import DetailsSheet from "@ui/details-sheet/DetailsSheet.astro";',
|
|
22
|
+
],
|
|
42
23
|
code: [
|
|
43
24
|
'import CodeBlock from "@ui/code-block/CodeBlock.astro";',
|
|
44
25
|
],
|
|
@@ -62,6 +43,9 @@ const COMPONENT_IMPORTS = {
|
|
|
62
43
|
],
|
|
63
44
|
diagram: ['import Diagram from "@ui/diagram/Diagram.astro";'],
|
|
64
45
|
youtube: ['import YouTube from "@ui/youtube/YouTube.astro";'],
|
|
46
|
+
badge: ['import Badge from "@ui/badge/Badge.astro";'],
|
|
47
|
+
kbd: ['import Kbd from "@ui/kbd/Kbd.astro";'],
|
|
48
|
+
files: ['import FileTree from "@ui/files/FileTree.astro";'],
|
|
65
49
|
"math-block": ['import MathKatex from "@ui/math/MathKatex.astro";'],
|
|
66
50
|
image: ['import BlockImage from "@ui/image/Image.astro";'],
|
|
67
51
|
"api-class": ['import ApiSymbol from "@ui/api-symbol/ApiSymbol.astro";'],
|
|
@@ -103,6 +87,9 @@ const COMPONENT_IMPORTS = {
|
|
|
103
87
|
"link-card": [
|
|
104
88
|
'import LinkCard from "@ui/link-card/LinkCard.astro";',
|
|
105
89
|
],
|
|
90
|
+
"link-button": [
|
|
91
|
+
'import Button from "@ui/button/Button.astro";',
|
|
92
|
+
],
|
|
106
93
|
avatar: [
|
|
107
94
|
'import Avatar from "@ui/avatar/Avatar.astro";',
|
|
108
95
|
],
|
|
@@ -116,12 +103,50 @@ export function escapeExpr(s) {
|
|
|
116
103
|
// JSON.stringify handles all special characters (quotes, backslashes, newlines, etc.)
|
|
117
104
|
// and produces a valid JS string literal. This avoids template literal edge cases
|
|
118
105
|
// where backtick + brace combinations create false expression boundaries.
|
|
119
|
-
|
|
106
|
+
//
|
|
107
|
+
// `<` becomes \u003c: Astro parses the template as HTML before the expression is
|
|
108
|
+
// JavaScript, so a `<script>` inside a string (a code block showing one, say) would
|
|
109
|
+
// be taken for a real script tag and its text compiled as JavaScript. Vite's
|
|
110
|
+
// dependency scan then fails to parse it and a production build breaks.
|
|
111
|
+
return JSON.stringify(s).replace(/</g, "\\u003c");
|
|
120
112
|
}
|
|
121
113
|
/** Escape for use in an HTML attribute value (double-quoted). */
|
|
122
114
|
export function escapeAttr(s) {
|
|
123
115
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
124
116
|
}
|
|
117
|
+
export function componentAttr(name, value) {
|
|
118
|
+
return /[&"<>]/.test(value)
|
|
119
|
+
? ` ${name}={${escapeExpr(value)}}`
|
|
120
|
+
: ` ${name}="${value}"`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Render extra attributes from an inline element's `attrs` field
|
|
124
|
+
* (links and images today; spans in a future phase) as ` key="val"`
|
|
125
|
+
* pairs prefixed with a leading space, suitable for splicing into an
|
|
126
|
+
* open tag immediately after the element's first-class attrs (href,
|
|
127
|
+
* src, etc.). `id`, `class`, and `style` are first-class; any other
|
|
128
|
+
* key passes through verbatim.
|
|
129
|
+
*/
|
|
130
|
+
/** `{…}` attributes on a COMPONENT (Badge, Button): `componentAttr`, which Astro does not entity-decode. */
|
|
131
|
+
function renderInlineComponentAttrs(attrs) {
|
|
132
|
+
if (!attrs)
|
|
133
|
+
return "";
|
|
134
|
+
return Object.entries(attrs)
|
|
135
|
+
.filter(([, value]) => value !== undefined)
|
|
136
|
+
.map(([key, value]) => componentAttr(key, String(value)))
|
|
137
|
+
.join("");
|
|
138
|
+
}
|
|
139
|
+
function renderInlineElementAttrs(attrs) {
|
|
140
|
+
if (!attrs)
|
|
141
|
+
return "";
|
|
142
|
+
const parts = [];
|
|
143
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
144
|
+
if (value === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
parts.push(` ${key}="${escapeAttr(String(value))}"`);
|
|
147
|
+
}
|
|
148
|
+
return parts.join("");
|
|
149
|
+
}
|
|
125
150
|
/** Escape text for use directly in an Astro template (not inside an expression). */
|
|
126
151
|
export function escapeTemplate(s) {
|
|
127
152
|
let result = "";
|
|
@@ -151,11 +176,19 @@ export function escapeTemplate(s) {
|
|
|
151
176
|
export function treeToAstro(nodes, options) {
|
|
152
177
|
const ctx = {
|
|
153
178
|
imports: new Set(),
|
|
179
|
+
diagramIds: new Map(),
|
|
154
180
|
indent: 0,
|
|
155
181
|
scripts: new Set(),
|
|
156
182
|
mode: options?.mode ?? "template",
|
|
157
183
|
imageOptimization: options?.imageOptimization ?? false,
|
|
158
184
|
codeBlockTitle: options?.codeBlockTitle ?? true,
|
|
185
|
+
imageCaptions: options?.imageCaptions ?? false,
|
|
186
|
+
combinedPrefix: options?.combinedPrefix ?? "",
|
|
187
|
+
// Mixed prose+endpoint pages render endpoints as embedded blocks;
|
|
188
|
+
// pages that are nothing but endpoints/headings keep the
|
|
189
|
+
// full-viewport operation layout (the OpenAPI page shape).
|
|
190
|
+
embeddedEndpoints: apiPageLayout(nodes).embeddedEndpoints,
|
|
191
|
+
apiSymbolLevels: apiSymbolLevels(nodes),
|
|
159
192
|
};
|
|
160
193
|
// Pre-scan for inline `icon` nodes anywhere in the tree.
|
|
161
194
|
// `inlineNodeToTemplate` is intentionally pure (no ctx) — it
|
|
@@ -164,9 +197,9 @@ export function treeToAstro(nodes, options) {
|
|
|
164
197
|
// (card.props.icon) registers `icon` directly at the call
|
|
165
198
|
// site; this pre-scan picks up the inline `:icon[name]`
|
|
166
199
|
// directives the writer can drop into prose.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
200
|
+
// Badges and inline buttons are components too, registered the same way.
|
|
201
|
+
for (const key of inlineComponentsIn(nodes))
|
|
202
|
+
ctx.imports.add(key);
|
|
170
203
|
const body = nodes.map((n) => nodeToAstro(n, ctx)).filter(Boolean).join("\n");
|
|
171
204
|
const imports = [];
|
|
172
205
|
const seen = new Set();
|
|
@@ -188,7 +221,18 @@ export function treeToAstro(nodes, options) {
|
|
|
188
221
|
};
|
|
189
222
|
}
|
|
190
223
|
// ─── Node dispatcher ──────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Dispatch + diff decoration: nodes carrying DiffTree marks (from
|
|
226
|
+
* @dogsbay/tree-diff, read structurally) decorate the rendered
|
|
227
|
+
* snippet; unmarked nodes render byte-identically to before.
|
|
228
|
+
*/
|
|
191
229
|
function nodeToAstro(node, ctx) {
|
|
230
|
+
const rendered = nodeToAstroBase(node, ctx);
|
|
231
|
+
if (!rendered || !hasDiffMarks(node))
|
|
232
|
+
return rendered;
|
|
233
|
+
return decorateDiff(node, rendered, (peer) => nodeToAstroBase(peer, ctx));
|
|
234
|
+
}
|
|
235
|
+
function nodeToAstroBase(node, ctx) {
|
|
192
236
|
switch (node.type) {
|
|
193
237
|
case "prose":
|
|
194
238
|
return proseToAstro(node, ctx);
|
|
@@ -200,10 +244,27 @@ function nodeToAstro(node, ctx) {
|
|
|
200
244
|
return codeToAstro(node, ctx);
|
|
201
245
|
case "callout":
|
|
202
246
|
return calloutToAstro(node, ctx);
|
|
247
|
+
case "files":
|
|
248
|
+
return filesToAstro(node, ctx);
|
|
249
|
+
case "container":
|
|
250
|
+
return genericContainerToAstro(node, ctx);
|
|
203
251
|
case "details":
|
|
204
252
|
return detailsToAstro(node, ctx);
|
|
253
|
+
case "theme": {
|
|
254
|
+
// A scoped theme: `.dark` re-declares the tokens for its subtree, so a
|
|
255
|
+
// plain wrapper is enough (THEME_SCOPES); an unknown name keeps the content
|
|
256
|
+
const name = node.props?.name;
|
|
257
|
+
const scope = lookup(THEME_SCOPES, name);
|
|
258
|
+
const inner = childrenToAstro(node, ctx);
|
|
259
|
+
if (!scope)
|
|
260
|
+
return inner;
|
|
261
|
+
const passthrough = renderPassthroughAttrs(node.props, ["name", "class", "data-theme"]);
|
|
262
|
+
// The default (expression) form: an entity-escaped `[&>…]` is not a
|
|
263
|
+
// class Tailwind's scanner recognises, so the rule was never generated
|
|
264
|
+
return `<div${mergeClassAttr(scope, node.props?.class)} data-theme="${escapeAttr(name)}"${passthrough}>\n${indentStr(inner, 2)}\n</div>`;
|
|
265
|
+
}
|
|
205
266
|
case "tabs":
|
|
206
|
-
return
|
|
267
|
+
return renderAroundGroup(node, "tab", (n) => tabsToAstro(n, ctx), (c) => nodeToAstro(c, ctx), "\n");
|
|
207
268
|
case "ordered-list":
|
|
208
269
|
return wrapBlock("ol", "", node, childrenToAstro(node, ctx), ["start"])
|
|
209
270
|
.replace(/^<ol(\s[^>]*)?>/, (m) => {
|
|
@@ -215,21 +276,43 @@ function nodeToAstro(node, ctx) {
|
|
|
215
276
|
});
|
|
216
277
|
case "unordered-list":
|
|
217
278
|
return wrapBlock("ul", "", node, childrenToAstro(node, ctx));
|
|
218
|
-
case "list-item":
|
|
219
|
-
|
|
279
|
+
case "list-item": {
|
|
280
|
+
// a task item: a disabled checkbox before its text, as render-html draws it
|
|
281
|
+
const c = node.props?.checked;
|
|
282
|
+
if (typeof c !== "boolean")
|
|
283
|
+
return wrapBlock("li", "", node, leafContent(node, ctx));
|
|
284
|
+
// the checkbox labelled by the item's text (axe: a form control needs a label); nested blocks after
|
|
285
|
+
const text = node.inline && node.inline.length ? leafContent({ ...node, children: undefined }, ctx) : "";
|
|
286
|
+
const rest = node.children?.length ? childrenToAstro({ ...node, inline: undefined }, ctx) : "";
|
|
287
|
+
// no text of its own: the checkbox is named "Task"
|
|
288
|
+
const lead = text ? `<label><input type="checkbox" disabled${c ? " checked" : ""} /> ${text}</label>` : `<input type="checkbox" disabled${c ? " checked" : ""} aria-label="Task" />`;
|
|
289
|
+
return wrapBlock("li", "", node, `${lead}${rest}`);
|
|
290
|
+
}
|
|
220
291
|
case "blockquote":
|
|
221
292
|
return wrapBlock("blockquote", "border-l-4 border-border pl-4 italic text-muted-foreground", node, childrenToAstro(node, ctx));
|
|
293
|
+
// A transparent grouping wrapper: renders its children inside a plain
|
|
294
|
+
// <div> and adds no semantics of its own. Exists so a caller can carry
|
|
295
|
+
// an attribute (a class, a data-* flag) for a whole run of blocks —
|
|
296
|
+
// the release comparison marks a wholly added/removed PAGE once, at
|
|
297
|
+
// the page level, instead of putting a bar on every block inside it.
|
|
298
|
+
case "group":
|
|
299
|
+
return wrapBlock("div", "", node, childrenToAstro(node, ctx));
|
|
222
300
|
case "hr": {
|
|
223
301
|
const classAttr = mergeClassAttr("my-8 border-border", node.props?.class);
|
|
224
302
|
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
225
303
|
return `<hr${classAttr}${passthrough} />`;
|
|
226
304
|
}
|
|
227
305
|
case "html":
|
|
228
|
-
// Raw HTML always uses set:html — it may contain anything
|
|
229
|
-
|
|
306
|
+
// Raw HTML always uses set:html — it may contain anything. Callouts inside it
|
|
307
|
+
// (dogsbay-md's HTML table fallback writes `<div role="note" data-callout>`)
|
|
308
|
+
// render as real callouts
|
|
309
|
+
return htmlToAstro(node.html ?? "", ctx);
|
|
230
310
|
case "table":
|
|
231
311
|
return tableToAstro(node, ctx);
|
|
232
312
|
case "thead":
|
|
313
|
+
// a header row with no text (`| | |`, a headerless table) is no header (as render-html)
|
|
314
|
+
if (isEmptyTableHead(node))
|
|
315
|
+
return "";
|
|
233
316
|
ctx.imports.add("table");
|
|
234
317
|
return `<TableHeader>\n${childrenToAstro(node, ctx)}\n</TableHeader>`;
|
|
235
318
|
case "tbody":
|
|
@@ -238,9 +321,13 @@ function nodeToAstro(node, ctx) {
|
|
|
238
321
|
case "tr":
|
|
239
322
|
ctx.imports.add("table");
|
|
240
323
|
return `<TableRow>\n${childrenToAstro(node, ctx)}\n</TableRow>`;
|
|
241
|
-
case "th":
|
|
324
|
+
case "th": {
|
|
242
325
|
ctx.imports.add("table");
|
|
243
|
-
|
|
326
|
+
// An empty header cell is no header (axe `empty-table-header`): a plain cell in the header row
|
|
327
|
+
const content = leafContent(node, ctx);
|
|
328
|
+
// Written here, not through TableHead: a site keeps the component it was scaffolded with
|
|
329
|
+
return content.trim() ? `<TableHead>${content}</TableHead>` : `<td class="${EMPTY_HEADER_CELL_CLASS}"></td>`;
|
|
330
|
+
}
|
|
244
331
|
case "td":
|
|
245
332
|
ctx.imports.add("table");
|
|
246
333
|
return `<TableCell>${leafContent(node, ctx)}</TableCell>`;
|
|
@@ -277,18 +364,36 @@ function nodeToAstro(node, ctx) {
|
|
|
277
364
|
return exampleToAstro(node, ctx);
|
|
278
365
|
case "figure": {
|
|
279
366
|
const caption = node.props?.caption;
|
|
367
|
+
// A figure of one code block: the caption goes in the code block's header bar,
|
|
368
|
+
// which then is the figure's caption
|
|
369
|
+
const only = node.children?.length === 1 ? node.children[0] : undefined;
|
|
370
|
+
if (caption && only?.type === "code") {
|
|
371
|
+
return codeToAstro(only, ctx, {
|
|
372
|
+
caption: captionToAstro(caption, ctx),
|
|
373
|
+
// The figure's attributes, except those the code block has itself (an id twice is invalid)
|
|
374
|
+
attrs: renderPassthroughAttrs(node.props, ["caption", "class", ...Object.keys(only.props ?? {})]),
|
|
375
|
+
classes: ["my-4", node.props?.class].filter(Boolean).join(" "),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
280
378
|
const inner = childrenToAstro(node, ctx);
|
|
281
379
|
const body = caption
|
|
282
|
-
? `${inner}\n<figcaption class="text-center text-sm text-muted-foreground mt-2">${
|
|
380
|
+
? `${inner}\n<figcaption class="text-center text-sm text-muted-foreground mt-2">${captionToAstro(caption, ctx)}</figcaption>`
|
|
283
381
|
: inner;
|
|
284
382
|
return wrapBlock("figure", "my-4", node, body, ["caption"]);
|
|
285
383
|
}
|
|
286
384
|
case "steps":
|
|
287
385
|
ctx.imports.add("steps");
|
|
288
386
|
return `<Steps>\n${childrenToAstro(node, ctx)}\n</Steps>`;
|
|
289
|
-
case "step":
|
|
387
|
+
case "step": {
|
|
290
388
|
ctx.imports.add("steps");
|
|
291
|
-
|
|
389
|
+
// A `title` prop (Mintlify `<Step title>`) leads the step in bold, as render-html draws it
|
|
390
|
+
// The title on its own line (Steps.astro: `.step-title` is a block): from the prop, or the
|
|
391
|
+
// step's leading bold run (stepLead), which Markdown otherwise runs into the detail
|
|
392
|
+
const title = stepTitle(node);
|
|
393
|
+
const split = title ? undefined : stepLead(node.inline);
|
|
394
|
+
const lead = title ? `<strong class="step-title">${escapeTemplate(title)}</strong>\n` : split ? `<strong class="step-title">${renderInline(split.title, ctx)}</strong>\n` : "";
|
|
395
|
+
return `<Step>\n${lead}${leafContent(split ? { ...node, inline: split.rest } : node, ctx)}\n</Step>`;
|
|
396
|
+
}
|
|
292
397
|
case "substeps":
|
|
293
398
|
ctx.imports.add("substeps");
|
|
294
399
|
ctx.imports.add("steps");
|
|
@@ -304,8 +409,10 @@ function nodeToAstro(node, ctx) {
|
|
|
304
409
|
return standaloneCardToAstro(node, ctx);
|
|
305
410
|
case "link-card":
|
|
306
411
|
return linkCardToAstro(node, ctx);
|
|
412
|
+
case "link-button":
|
|
413
|
+
return linkButtonToAstro(node, ctx);
|
|
307
414
|
case "accordion":
|
|
308
|
-
return
|
|
415
|
+
return renderAroundGroup(node, "accordion-item", (n) => accordionToAstro(n, ctx), (c) => nodeToAstro(c, ctx), "\n");
|
|
309
416
|
case "accordion-item":
|
|
310
417
|
// Bare accordion-item outside an :::accordion wrapper. Wrap it in an
|
|
311
418
|
// Accordion shell so the output is still a valid widget.
|
|
@@ -349,7 +456,7 @@ ${childrenToAstro(node, ctx)}
|
|
|
349
456
|
// controls the bg/text combo (see TONE_CLASSES). Authors can also pass
|
|
350
457
|
// their own `class` to override.
|
|
351
458
|
if (label !== undefined) {
|
|
352
|
-
const toneClasses = TONE_CLASSES
|
|
459
|
+
const toneClasses = lookup(TONE_CLASSES, tone) ?? TONE_CLASSES["primary-light"];
|
|
353
460
|
return `<GridItem ${attrParts.join(" ")}${classAttr}${passthrough}>
|
|
354
461
|
<div class="flex h-full min-h-16 items-center justify-center rounded-lg font-mono text-sm font-medium ${toneClasses}">${escapeTemplate(label)}</div>
|
|
355
462
|
</GridItem>`;
|
|
@@ -358,6 +465,28 @@ ${childrenToAstro(node, ctx)}
|
|
|
358
465
|
${childrenToAstro(node, ctx)}
|
|
359
466
|
</GridItem>`;
|
|
360
467
|
}
|
|
468
|
+
case "related": {
|
|
469
|
+
// #384 R3: AsciiDoc [role="_additional-resources"] + .Title + list folds
|
|
470
|
+
// to this semantic node (maps to DITA <related-links>). Render a titled
|
|
471
|
+
// section, not a TOC heading. The title is a caption (block title), so a
|
|
472
|
+
// styled <p>, not an <h2>.
|
|
473
|
+
const title = node.props?.title;
|
|
474
|
+
const classAttr = mergeClassAttr("related-resources", node.props?.class);
|
|
475
|
+
const passthrough = renderPassthroughAttrs(node.props, ["title", "class"]);
|
|
476
|
+
const titleHtml = title
|
|
477
|
+
? `<p class="related-resources-title">${escapeTemplate(title)}</p>\n`
|
|
478
|
+
: "";
|
|
479
|
+
return `<section${classAttr}${passthrough}>\n${titleHtml}${childrenToAstro(node, ctx)}\n</section>`;
|
|
480
|
+
}
|
|
481
|
+
case "mdx-raw": {
|
|
482
|
+
// Preserved-but-unmapped MDX component (format-mdx never-drop; see
|
|
483
|
+
// docs-dev/format-mdx-adapters.md). Rendered as a visible source
|
|
484
|
+
// block — the content must not silently disappear from the HTML
|
|
485
|
+
// output. Map the component (adapter / mdx.components config) to
|
|
486
|
+
// upgrade it to a real widget.
|
|
487
|
+
const source = String(node.props?.source ?? "");
|
|
488
|
+
return `<pre class="mdx-raw my-4 overflow-x-auto rounded-md border bg-muted p-4 text-sm"><code>{${escapeExpr(source)}}</code></pre>`;
|
|
489
|
+
}
|
|
361
490
|
default:
|
|
362
491
|
if (node.html)
|
|
363
492
|
return `<Fragment set:html={${escapeExpr(node.html)}} />`;
|
|
@@ -367,37 +496,77 @@ ${childrenToAstro(node, ctx)}
|
|
|
367
496
|
}
|
|
368
497
|
}
|
|
369
498
|
// ─── Inline rendering ─────────────────────────────────────────────
|
|
370
|
-
/**
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Astro's two inline dialects, as emitters over serialize-core's shared walk.
|
|
501
|
+
*
|
|
502
|
+
* These were the THIRD and FOURTH copies of the 11-variant switch (obsidian and
|
|
503
|
+
* dogsbay-md's html fallback were the other two), and the duplication is
|
|
504
|
+
* precisely why `highlight` was missing from BOTH of them: adding an arm meant
|
|
505
|
+
* remembering four places, so `:highlight[…]` rendered as nothing at all on
|
|
506
|
+
* every Astro page. The walk is now shared, so a variant is handled in one
|
|
507
|
+
* place per format instead of being silently skippable.
|
|
508
|
+
*
|
|
509
|
+
* Only the leaf syntax differs between the two maps: template mode escapes for
|
|
510
|
+
* Astro's `{}` and emits real components (`<Icon>`, `<Fragment set:html>`),
|
|
511
|
+
* hybrid mode escapes for plain HTML and emits inert markup.
|
|
512
|
+
*/
|
|
513
|
+
/** Canonical highlight styling — keep in sync with InlineRenderer.astro. */
|
|
514
|
+
const HIGHLIGHT_CLASS = "rounded-sm bg-yellow-200/60 px-0.5 dark:bg-yellow-500/30";
|
|
515
|
+
function templateInlineEmitters() {
|
|
516
|
+
return {
|
|
517
|
+
text: (node, ctx) => applyTextFlags(ctx.escape(node.text), node, {
|
|
518
|
+
bold: (s) => `<strong>${s}</strong>`,
|
|
519
|
+
italic: (s) => `<em>${s}</em>`,
|
|
520
|
+
strike: (s) => `<s>${s}</s>`,
|
|
521
|
+
}),
|
|
522
|
+
link: (node, ctx) => {
|
|
523
|
+
// Inside a whole-card link, a link is its text (wholeCardLink)
|
|
524
|
+
if (cardLinkDepth > 0)
|
|
525
|
+
return ctx.recurse(node.children);
|
|
526
|
+
// A link node an importer built directly never passed markdown-it's
|
|
527
|
+
// scheme check: a `javascript:` href keeps its text and loses the link.
|
|
528
|
+
if (!isSafeHref(node.href)) {
|
|
529
|
+
ctx.report("link", `unsafe link target dropped (${node.href.slice(0, 40)})`);
|
|
530
|
+
return ctx.recurse(node.children);
|
|
531
|
+
}
|
|
532
|
+
const titleAttr = node.title ? componentAttr("title", node.title) : "";
|
|
533
|
+
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
534
|
+
// An inline `:button`: the Button component, small, keeping its variant ("primary" is the filled default).
|
|
535
|
+
if (node.button) {
|
|
536
|
+
const variant = buttonVariant(node.button.variant);
|
|
537
|
+
return `<Button${componentAttr("href", node.href)}${componentAttr("variant", variant)} size="sm"${titleAttr}${renderInlineComponentAttrs(node.attrs)}>${ctx.recurse(node.children)}</Button>`;
|
|
538
|
+
}
|
|
539
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}${extraAttrs}>${ctx.recurse(node.children)}</a>`;
|
|
540
|
+
},
|
|
541
|
+
badge: (node, ctx) => `<Badge${componentAttr("variant", node.variant ?? "default")}${renderInlineComponentAttrs(node.attrs)}>${ctx.escape(node.text)}</Badge>`,
|
|
542
|
+
image: (node, ctx) => {
|
|
543
|
+
if (!isSafeHref(node.src, "image")) {
|
|
544
|
+
ctx.report("image", `unsafe image source dropped (${node.src.slice(0, 40)})`);
|
|
545
|
+
return ctx.escape(node.alt ?? "");
|
|
546
|
+
}
|
|
547
|
+
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
548
|
+
return `<img src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt ?? "")}" loading="lazy"${extraAttrs} />`;
|
|
549
|
+
},
|
|
550
|
+
// Code inherits the emphasis around it (`*using the \`dita\` command*`): the parser
|
|
551
|
+
// flags the code span, and dropping the flags split the italics around the code
|
|
552
|
+
code: (node, ctx) => applyTextFlags(`<code>${ctx.escape(node.text)}</code>`, node, {
|
|
553
|
+
bold: (s) => `<strong>${s}</strong>`,
|
|
554
|
+
italic: (s) => `<em>${s}</em>`,
|
|
555
|
+
strike: (s) => `<s>${s}</s>`,
|
|
556
|
+
}),
|
|
557
|
+
// The arm that did not exist — `:highlight[…]` / `:mark[…]` rendered as
|
|
558
|
+
// nothing at all. The class list mirrors the canonical rendering in
|
|
559
|
+
// `packages/ui/src/content-renderer/InlineRenderer.astro`, so a highlight
|
|
560
|
+
// looks the same however a page is produced. A BARE `<mark>` would have
|
|
561
|
+
// been "fixed" but unstyled: the generated `.docs-prose` stylesheet had no
|
|
562
|
+
// `mark` rule, so browsers paint their default yellow — including on dark
|
|
563
|
+
// pages, where nothing declares `color-scheme`.
|
|
564
|
+
highlight: (node, ctx) => `<mark class="${HIGHLIGHT_CLASS}">${ctx.recurse(node.children)}</mark>`,
|
|
565
|
+
"footnote-ref": (node, ctx) => `<sup><a href="#fn-${escapeAttr(node.label)}" id="fnref-${escapeAttr(node.label)}" class="text-primary hover:underline">[${ctx.escape(node.label)}]</a></sup>`,
|
|
566
|
+
// The Kbd component (keys styled as keycaps); plain <kbd> had no style beyond a monospace font.
|
|
567
|
+
kbd: (node, ctx) => node.keys.map((k) => `<Kbd>${ctx.escape(k)}</Kbd>`).join("+"),
|
|
568
|
+
math: (node, ctx) => `<code class="math-inline">${ctx.escape(node.latex)}</code>`,
|
|
569
|
+
icon: (node) => {
|
|
401
570
|
// Inline icon — emits the platform `<Icon>` component which
|
|
402
571
|
// resolves at build time against Lucide (default) plus any
|
|
403
572
|
// other Iconify pack via the `pack:name` shorthand. Caller
|
|
@@ -411,47 +580,39 @@ function inlineNodeToTemplate(node) {
|
|
|
411
580
|
// to Lucide for everything.
|
|
412
581
|
const fullName = node.library ? `${node.library}:${node.name}` : node.name;
|
|
413
582
|
return `<Icon name="${escapeAttr(fullName)}" class="inline-block size-[1em] align-[-0.125em]" />`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return "<br />";
|
|
420
|
-
default:
|
|
421
|
-
return "";
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
/** Render inline nodes as HTML string (for hybrid mode set:html). */
|
|
425
|
-
function inlineToHtml(nodes) {
|
|
426
|
-
return nodes.map(inlineNodeToHtml).join("");
|
|
583
|
+
},
|
|
584
|
+
// Raw HTML inline — must use set:html to avoid brace issues
|
|
585
|
+
"html-inline": (node) => `<Fragment set:html={${escapeExpr(node.html)}} />`,
|
|
586
|
+
break: () => "<br />",
|
|
587
|
+
};
|
|
427
588
|
}
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
589
|
+
function htmlModeInlineEmitters() {
|
|
590
|
+
return {
|
|
591
|
+
...templateInlineEmitters(),
|
|
592
|
+
// Inherited from the template map, this emitted `title={"…"}` — an
|
|
593
|
+
// EXPRESSION, inside a string that becomes a `set:html` value. Astro never
|
|
594
|
+
// parses it as markup, so the reader saw the braces and quotes.
|
|
595
|
+
link: (node, ctx) => {
|
|
596
|
+
if (cardLinkDepth > 0)
|
|
597
|
+
return ctx.recurse(node.children);
|
|
598
|
+
if (!isSafeHref(node.href)) {
|
|
599
|
+
ctx.report("link", `unsafe link target dropped (${node.href.slice(0, 40)})`);
|
|
600
|
+
return ctx.recurse(node.children);
|
|
601
|
+
}
|
|
441
602
|
const titleAttr = node.title ? ` title="${escapeAttr(node.title)}"` : "";
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
return `<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
603
|
+
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
604
|
+
// No components in HTML mode: a marked link (and a marked span for a badge below).
|
|
605
|
+
const button = node.button ? ` data-button data-variant="${escapeAttr(buttonVariant(node.button.variant))}"` : "";
|
|
606
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}${button}${extraAttrs}>${ctx.recurse(node.children)}</a>`;
|
|
607
|
+
},
|
|
608
|
+
kbd: (node, ctx) => node.keys.map((k) => `<kbd>${ctx.escape(k)}</kbd>`).join("+"),
|
|
609
|
+
badge: (node, ctx) => `<span data-badge data-variant="${escapeAttr(node.variant ?? "default")}"${renderInlineElementAttrs(node.attrs)}>${ctx.escape(node.text)}</span>`,
|
|
610
|
+
text: (node, ctx) => applyTextFlags(ctx.escape(node.text), node, {
|
|
611
|
+
bold: (s) => `<strong>${s}</strong>`,
|
|
612
|
+
italic: (s) => `<em>${s}</em>`,
|
|
613
|
+
strike: (s) => `<s>${s}</s>`,
|
|
614
|
+
}),
|
|
615
|
+
icon: (node) => {
|
|
455
616
|
// Hybrid-mode HTML rendering — fall back to a textual
|
|
456
617
|
// shortcode reference when we can't inline the SVG. Real
|
|
457
618
|
// resolution happens in template mode via the <Icon>
|
|
@@ -459,14 +620,19 @@ function inlineNodeToHtml(node) {
|
|
|
459
620
|
// production output.
|
|
460
621
|
const fullName = node.library ? `${node.library}:${node.name}` : node.name;
|
|
461
622
|
return `<span class="dogsbay-icon" data-icon="${escapeAttr(fullName)}"></span>`;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
623
|
+
},
|
|
624
|
+
"html-inline": (node) => node.html,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const TEMPLATE_INLINE = templateInlineEmitters();
|
|
628
|
+
const HTML_INLINE = htmlModeInlineEmitters();
|
|
629
|
+
/** Render inline nodes as Astro template markup (clean, editable). */
|
|
630
|
+
function inlineToTemplate(nodes) {
|
|
631
|
+
return walkInline(nodes, { emitters: TEMPLATE_INLINE, escape: escapeTemplate });
|
|
632
|
+
}
|
|
633
|
+
/** Render inline nodes as HTML string (for hybrid mode set:html). */
|
|
634
|
+
function inlineToHtml(nodes) {
|
|
635
|
+
return walkInline(nodes, { emitters: HTML_INLINE, escape: escapeHtml });
|
|
470
636
|
}
|
|
471
637
|
/** Render inline content in the current mode. */
|
|
472
638
|
function renderInline(nodes, ctx) {
|
|
@@ -474,6 +640,8 @@ function renderInline(nodes, ctx) {
|
|
|
474
640
|
const html = inlineToHtml(nodes);
|
|
475
641
|
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
476
642
|
}
|
|
643
|
+
// Inline parsed from a prop (a caption) is not in the page-level pre-scan; its components are imported here.
|
|
644
|
+
inlineComponentsIn([{ type: "paragraph", inline: nodes }], ctx.imports);
|
|
477
645
|
return inlineToTemplate(nodes);
|
|
478
646
|
}
|
|
479
647
|
/** Render an HTML string in the current mode. */
|
|
@@ -489,23 +657,39 @@ function proseToAstro(node, ctx) {
|
|
|
489
657
|
return renderHtml(node.html, ctx);
|
|
490
658
|
return "";
|
|
491
659
|
}
|
|
660
|
+
/** TableHead's classes, for a header row's empty cell written as a plain `td`. */
|
|
661
|
+
const EMPTY_HEADER_CELL_CLASS = "px-2 py-2 text-left align-bottom font-semibold text-muted-foreground";
|
|
662
|
+
/**
|
|
663
|
+
* A card's title is text styled as one, not a heading (as render-html): a grid of cards is not a
|
|
664
|
+
* section of the page, and as headings its titles filled the outline and a reader's heading list.
|
|
665
|
+
*/
|
|
666
|
+
function cardTitleTag(_ctx) {
|
|
667
|
+
return "div";
|
|
668
|
+
}
|
|
492
669
|
function headingToAstro(node, ctx) {
|
|
493
670
|
const level = node.props?.level ?? 1;
|
|
671
|
+
ctx.lastHeading = level;
|
|
494
672
|
// `id` takes precedence over `slug` (slug is auto-generated, id is explicit user override)
|
|
495
673
|
const id = node.props?.id ?? node.props?.slug ?? "";
|
|
496
674
|
const anchor = id
|
|
497
|
-
? `<a href="#${escapeAttr(id)}" class="ml-2 text-muted-foreground opacity-0 group-hover:opacity-100 no-underline" aria-hidden="true" tabindex="-1">¶</a>`
|
|
675
|
+
? `<a href="#${escapeAttr(id)}" class="ml-2 text-muted-foreground opacity-0 group-hover:opacity-100 no-underline" aria-hidden="true" tabindex="-1" data-pagefind-ignore>¶</a>`
|
|
498
676
|
: "";
|
|
499
677
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : "";
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
"level", "slug", "id", "class", "text",
|
|
503
|
-
]);
|
|
678
|
+
const consumed = ["level", "slug", "id", "class", "text"];
|
|
679
|
+
const userClass = node.props?.class;
|
|
504
680
|
if (ctx.mode === "template" && node.inline) {
|
|
681
|
+
// Real Astro markup: expressions are parsed.
|
|
682
|
+
const classAttr = mergeClassAttr("group scroll-mt-20", userClass);
|
|
683
|
+
const passthrough = renderPassthroughAttrs(node.props, consumed);
|
|
505
684
|
const text = inlineToTemplate(node.inline);
|
|
506
685
|
return `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
507
686
|
}
|
|
508
|
-
// Hybrid or no inline —
|
|
687
|
+
// Hybrid or no inline — the whole heading becomes a set:html STRING, where an
|
|
688
|
+
// expression would be literal text. Every MDX/Mintlify heading takes this
|
|
689
|
+
// branch (that importer emits `props.text` + children, never `inline`), so
|
|
690
|
+
// this is the common path, not an edge case.
|
|
691
|
+
const classAttr = mergeClassAttr("group scroll-mt-20", userClass, "html");
|
|
692
|
+
const passthrough = renderPassthroughAttrs(node.props, consumed, "html");
|
|
509
693
|
const text = node.inline ? inlineToHtml(node.inline) : node.props?.text ?? "";
|
|
510
694
|
const html = `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
511
695
|
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
@@ -522,15 +706,28 @@ function paragraphToAstro(node, ctx) {
|
|
|
522
706
|
if (meaningful.length === 1 && meaningful[0].type === "image") {
|
|
523
707
|
const img = meaningful[0];
|
|
524
708
|
ctx.imports.add("image");
|
|
709
|
+
// imageMap is keyed by the unprefixed publicPath built from
|
|
710
|
+
// `import.meta.glob("/src/assets/**", ...)` at runtime, so
|
|
711
|
+
// strip the URL prefix back off if rewriteTreeImageSrcs has
|
|
712
|
+
// already applied it. Keeps the optimized-image lookup working
|
|
713
|
+
// alongside the prefix-on-emit fix.
|
|
714
|
+
const lookupKey = ctx.combinedPrefix && img.src.startsWith(`${ctx.combinedPrefix}/`)
|
|
715
|
+
? img.src.slice(ctx.combinedPrefix.length)
|
|
716
|
+
: img.src;
|
|
525
717
|
const imgDataAttr = ctx.imageOptimization
|
|
526
|
-
? ` imageData={imageMap[${escapeExpr(
|
|
718
|
+
? ` imageData={imageMap[${escapeExpr(lookupKey)}]}`
|
|
527
719
|
: "";
|
|
528
720
|
// Forward arbitrary `data-*` / `aria-*` attrs that plugins
|
|
529
721
|
// may have attached to the wrapping paragraph (e.g.
|
|
530
722
|
// @dogsbay/plugin-image-zoom tags `data-zoomable="true"` so
|
|
531
723
|
// the runtime can attach handlers without scanning the DOM).
|
|
532
724
|
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
533
|
-
|
|
725
|
+
// Author-supplied {.class #id ...} on the image itself
|
|
726
|
+
// (Phase 1.5 of plans/inline-attrs.md).
|
|
727
|
+
const imageAttrs = renderInlineElementAttrs(img.attrs);
|
|
728
|
+
// `content.imageCaptions: title`: the figure form of BlockImage, captioned by the title (as dogsbay-ssg).
|
|
729
|
+
const figure = ctx.imageCaptions && img.title ? ` block${componentAttr("title", img.title)}` : "";
|
|
730
|
+
return `<BlockImage${componentAttr("src", img.src)}${componentAttr("alt", img.alt ?? "")}${figure}${imageAttrs}${passthrough}${imgDataAttr} />`;
|
|
534
731
|
}
|
|
535
732
|
}
|
|
536
733
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
@@ -557,7 +754,7 @@ function paragraphToAstro(node, ctx) {
|
|
|
557
754
|
}).join("");
|
|
558
755
|
return `<p${classAttr}${passthrough} set:html={${escapeExpr(html)}} />`;
|
|
559
756
|
}
|
|
560
|
-
function codeToAstro(node, ctx) {
|
|
757
|
+
function codeToAstro(node, ctx, figure) {
|
|
561
758
|
// Code content may live on props.code (Starlight convention) or node.html
|
|
562
759
|
// (dogsbay-md convention). Same for lang — Starlight uses props.lang,
|
|
563
760
|
// dogsbay-md uses props.language.
|
|
@@ -568,34 +765,52 @@ function codeToAstro(node, ctx) {
|
|
|
568
765
|
?? node.html
|
|
569
766
|
?? "";
|
|
570
767
|
const title = node.props?.title;
|
|
571
|
-
|
|
768
|
+
// The fence's documented `mark`, `ins` and `del` are CodeRich's highlights, diffAdd and diffRemove.
|
|
769
|
+
const lineSpec = (...values) => values.filter((v) => typeof v === "string" || typeof v === "number").map(String).filter(Boolean).join(",") || undefined;
|
|
770
|
+
const highlights = lineSpec(node.props?.highlights, node.props?.mark);
|
|
572
771
|
const lineNumbers = node.props?.lineNumbers;
|
|
573
|
-
const
|
|
574
|
-
const
|
|
772
|
+
const copyText = node.props?.copyText;
|
|
773
|
+
const diffAdd = lineSpec(node.props?.diffAdd, node.props?.ins);
|
|
774
|
+
const diffRemove = lineSpec(node.props?.diffRemove, node.props?.del);
|
|
775
|
+
// Expressive Code's `collapse`: line runs folded behind a toggle (`{2-4, 9}` or `2-4,9`)
|
|
776
|
+
const collapse = lineSpec(node.props?.collapse)?.replace(/[{}\s]/g, "") || undefined;
|
|
777
|
+
const isRich = lineNumbers || highlights || code.includes("[!code") || Boolean(diffAdd || diffRemove) || Boolean(collapse);
|
|
778
|
+
const classAttr = mergeClassAttr(figure?.classes ?? "", node.props?.class);
|
|
779
|
+
const figureAttrs = figure?.attrs ?? "";
|
|
780
|
+
// The component with its caption slot filled, or self-closing
|
|
781
|
+
const close = (tag, open) => figure ? `${open}>\n<Fragment slot="caption">${figure.caption}</Fragment>\n</${tag}>` : `${open} />`;
|
|
575
782
|
const consumed = [
|
|
576
783
|
"lang", "language", "code", "title", "highlights", "lineNumbers",
|
|
577
|
-
"class", "ins", "del", "mark", "collapse",
|
|
784
|
+
"class", "ins", "del", "mark", "collapse", "copyText", "diffAdd", "diffRemove",
|
|
578
785
|
];
|
|
579
786
|
const passthrough = renderPassthroughAttrs(node.props, consumed);
|
|
580
787
|
if (isRich) {
|
|
581
788
|
ctx.imports.add("code-rich");
|
|
582
789
|
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
583
790
|
if (title)
|
|
584
|
-
attrs.push(
|
|
791
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
585
792
|
if (lineNumbers)
|
|
586
793
|
attrs.push("lineNumbers");
|
|
587
794
|
if (highlights)
|
|
588
795
|
attrs.push(`highlights="${escapeAttr(highlights)}"`);
|
|
589
|
-
|
|
796
|
+
if (copyText !== undefined)
|
|
797
|
+
attrs.push(`copyText={${escapeExpr(copyText)}}`);
|
|
798
|
+
if (diffAdd)
|
|
799
|
+
attrs.push(`diffAdd="${escapeAttr(diffAdd)}"`);
|
|
800
|
+
if (diffRemove)
|
|
801
|
+
attrs.push(`diffRemove="${escapeAttr(diffRemove)}"`);
|
|
802
|
+
if (collapse)
|
|
803
|
+
attrs.push(`collapse="${escapeAttr(collapse)}"`);
|
|
804
|
+
return close("CodeRich", `<CodeRich ${attrs.join(" ")}${classAttr}${passthrough}${figureAttrs}`);
|
|
590
805
|
}
|
|
591
806
|
ctx.imports.add("code");
|
|
592
807
|
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
593
808
|
if (title)
|
|
594
|
-
attrs.push(
|
|
809
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
595
810
|
if (ctx.codeBlockTitle !== true) {
|
|
596
811
|
attrs.push(`showTitle="${ctx.codeBlockTitle}"`);
|
|
597
812
|
}
|
|
598
|
-
return `<CodeBlock ${attrs.join(" ")}${classAttr}${passthrough}
|
|
813
|
+
return close("CodeBlock", `<CodeBlock ${attrs.join(" ")}${classAttr}${passthrough}${figureAttrs}`);
|
|
599
814
|
}
|
|
600
815
|
// Map GitHub alert names (and other aliases) to Alert component variants.
|
|
601
816
|
// The Alert variant palette follows MkDocs Material; not every GitHub name
|
|
@@ -607,6 +822,71 @@ const CALLOUT_VARIANT_MAP = {
|
|
|
607
822
|
hint: "tip",
|
|
608
823
|
attention: "warning",
|
|
609
824
|
};
|
|
825
|
+
// Default title shown when a callout has no explicit one, keyed by the type
|
|
826
|
+
// the author wrote: aliases share a look but keep their own name (`:::info` is
|
|
827
|
+
// "Info", not the "Important" of the variant it shares). The same labels as
|
|
828
|
+
// dogsbay-ssg (src/render/icons.ts CALLOUTS). Anything else: the type, capitalised.
|
|
829
|
+
const CALLOUT_LABELS = {
|
|
830
|
+
note: "Note",
|
|
831
|
+
info: "Info",
|
|
832
|
+
tip: "Tip",
|
|
833
|
+
hint: "Hint",
|
|
834
|
+
success: "Success",
|
|
835
|
+
important: "Important",
|
|
836
|
+
warning: "Warning",
|
|
837
|
+
attention: "Attention",
|
|
838
|
+
caution: "Caution",
|
|
839
|
+
danger: "Danger",
|
|
840
|
+
error: "Error",
|
|
841
|
+
failure: "Failure",
|
|
842
|
+
question: "Question",
|
|
843
|
+
bug: "Bug",
|
|
844
|
+
example: "Example",
|
|
845
|
+
abstract: "Abstract",
|
|
846
|
+
quote: "Quote",
|
|
847
|
+
};
|
|
848
|
+
const CALLOUT_OPEN = /<div role="note" data-callout="([^"]*)"(?: data-title="([^"]*)")?>/g;
|
|
849
|
+
/**
|
|
850
|
+
* Raw HTML as set:html fragments, with each marked callout div rendered by
|
|
851
|
+
* calloutToAstro. The fragments around a callout may hold unbalanced tags
|
|
852
|
+
* (`<table><tr><td>` before, `</td></tr></table>` after); set:html writes
|
|
853
|
+
* them verbatim, so the page's HTML is whole again.
|
|
854
|
+
*/
|
|
855
|
+
function htmlToAstro(html, ctx) {
|
|
856
|
+
const raw = (s) => (s ? `<Fragment set:html={${escapeExpr(s)}} />` : "");
|
|
857
|
+
const out = [];
|
|
858
|
+
let rest = html;
|
|
859
|
+
for (;;) {
|
|
860
|
+
CALLOUT_OPEN.lastIndex = 0;
|
|
861
|
+
const open = CALLOUT_OPEN.exec(rest);
|
|
862
|
+
if (!open)
|
|
863
|
+
break;
|
|
864
|
+
// The callout ends at its matching </div>
|
|
865
|
+
const divs = /<div\b|<\/div>/g;
|
|
866
|
+
divs.lastIndex = open.index + open[0].length;
|
|
867
|
+
let depth = 1;
|
|
868
|
+
let end = -1;
|
|
869
|
+
for (let m = divs.exec(rest); m; m = divs.exec(rest)) {
|
|
870
|
+
depth += m[0] === "</div>" ? -1 : 1;
|
|
871
|
+
if (depth === 0) {
|
|
872
|
+
end = m.index;
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (end < 0)
|
|
877
|
+
break;
|
|
878
|
+
const unescape = (s) => s.replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
879
|
+
out.push(raw(rest.slice(0, open.index)));
|
|
880
|
+
out.push(calloutToAstro({
|
|
881
|
+
type: "callout",
|
|
882
|
+
props: { type: unescape(open[1]), ...(open[2] !== undefined ? { title: unescape(open[2]) } : {}) },
|
|
883
|
+
children: [{ type: "html", html: rest.slice(open.index + open[0].length, end) }],
|
|
884
|
+
}, ctx));
|
|
885
|
+
rest = rest.slice(end + "</div>".length);
|
|
886
|
+
}
|
|
887
|
+
out.push(raw(rest));
|
|
888
|
+
return out.filter(Boolean).join("\n") || raw(html);
|
|
889
|
+
}
|
|
610
890
|
function calloutToAstro(node, ctx) {
|
|
611
891
|
ctx.imports.add("callout");
|
|
612
892
|
// Callout variant may live on props.variant (Starlight / MkDocs importers)
|
|
@@ -615,36 +895,90 @@ function calloutToAstro(node, ctx) {
|
|
|
615
895
|
?? node.props?.type
|
|
616
896
|
?? "note").toLowerCase();
|
|
617
897
|
const variant = CALLOUT_VARIANT_MAP[rawVariant] ?? rawVariant;
|
|
618
|
-
|
|
898
|
+
// When the source gives no explicit title, fall back to the type label
|
|
899
|
+
// (Note / Warning / …) instead of an empty <AlertTitle> — AsciiDoc
|
|
900
|
+
// admonitions and GitHub-style alerts both show the type next to the icon,
|
|
901
|
+
// and an empty title leaves the icon standing alone. Issue #004.
|
|
902
|
+
const explicitTitle = node.props?.title;
|
|
903
|
+
// The default title names the authored type, not the variant it maps to.
|
|
904
|
+
const title = explicitTitle && explicitTitle.length > 0
|
|
905
|
+
? explicitTitle
|
|
906
|
+
: CALLOUT_LABELS[rawVariant] ?? rawVariant.charAt(0).toUpperCase() + rawVariant.slice(1);
|
|
619
907
|
const icon = node.props?.icon;
|
|
620
|
-
|
|
908
|
+
// The authored type (`hint`, `caution`) beside the variant it shares, as dogsbay-ssg writes it.
|
|
909
|
+
const iconAttr = (icon ? ` icon="${escapeAttr(icon)}"` : "") + (node.props?.["data-type"] === undefined ? ` data-type="${escapeAttr(rawVariant)}"` : "");
|
|
621
910
|
// User classes and passthrough attributes (id, data-*, aria-*, style)
|
|
622
911
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
623
912
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
624
913
|
"type", "variant", "title", "icon", "class",
|
|
625
914
|
]);
|
|
626
915
|
const inner = childrenToAstro(node, ctx);
|
|
916
|
+
// An authored title is inline Markdown (`Before \`harbor init\``), as render-html reads it (MD-002);
|
|
917
|
+
// the default label is text
|
|
918
|
+
const titleNodes = explicitTitle && explicitTitle.length > 0 ? titleInline(title) : [{ type: "text", text: title }];
|
|
919
|
+
const plainTitle = isPlainCaption(titleNodes);
|
|
920
|
+
// `collapsed` / `collapsible`: a disclosure, as render-html draws it (MD-001)
|
|
921
|
+
if (isCollapsibleCallout(node.props)) {
|
|
922
|
+
ctx.imports.add("details");
|
|
923
|
+
const attrs = [`variant="${escapeAttr(collapsibleVariant(variant))}"`, componentAttr("title", plainTitle ? captionText(titleNodes) : title).trimStart()];
|
|
924
|
+
if (calloutStartsOpen(node.props))
|
|
925
|
+
attrs.push("open");
|
|
926
|
+
const slot = plainTitle ? "" : `\n <Fragment slot="title">${renderInline(titleNodes, ctx)}</Fragment>`;
|
|
927
|
+
const collapsiblePassthrough = renderPassthroughAttrs(node.props, ["type", "variant", "title", "icon", "class", "collapsed", "collapsible", "open"]);
|
|
928
|
+
return `<CollapsibleAlert ${attrs.join(" ")}${iconAttr}${classAttr}${collapsiblePassthrough}>${slot}
|
|
929
|
+
${indentStr(inner, 2)}
|
|
930
|
+
</CollapsibleAlert>`;
|
|
931
|
+
}
|
|
627
932
|
return `<Alert variant="${escapeAttr(variant)}"${iconAttr}${classAttr}${passthrough}>
|
|
628
|
-
<AlertTitle>${escapeTemplate(
|
|
933
|
+
<AlertTitle>${plainTitle ? escapeTemplate(captionText(titleNodes)) : renderInline(titleNodes, ctx)}</AlertTitle>
|
|
629
934
|
<AlertDescription>
|
|
630
935
|
${indentStr(inner, 4)}
|
|
631
936
|
</AlertDescription>
|
|
632
937
|
</Alert>`;
|
|
633
938
|
}
|
|
939
|
+
/**
|
|
940
|
+
* A presence flag: `open`, `open=""`, `open="true"` and `true` are on; absent, `false` and `"false"` are
|
|
941
|
+
* off. A bare `{open}` parses as "" (falsy), and "false" is truthy, so a plain truth test got both wrong
|
|
942
|
+
* (MD-003). render-html reads flags with the same rule (`flagOn`).
|
|
943
|
+
*/
|
|
944
|
+
function flagOn(v) {
|
|
945
|
+
return v !== undefined && v !== null && v !== false && v !== "false";
|
|
946
|
+
}
|
|
947
|
+
/** CollapsibleAlert's variants; `plain` is chrome (no tint, icon or role). */
|
|
948
|
+
const COLLAPSIBLE_VARIANTS = new Set(["plain", "destructive", "note", "abstract", "info", "tip", "success", "question", "warning", "failure", "danger", "bug", "example", "quote"]);
|
|
949
|
+
/** A disclosure's variant: lower-cased, aliases mapped, anything CollapsibleAlert does not draw → note. */
|
|
950
|
+
function collapsibleVariant(raw) {
|
|
951
|
+
const v = raw.toLowerCase();
|
|
952
|
+
const mapped = CALLOUT_VARIANT_MAP[v] ?? v;
|
|
953
|
+
return COLLAPSIBLE_VARIANTS.has(mapped) ? mapped : "note";
|
|
954
|
+
}
|
|
955
|
+
/** A callout with `collapsible` on or any `collapsed` is a disclosure (render-html's `callout`). */
|
|
956
|
+
function isCollapsibleCallout(props) {
|
|
957
|
+
return flagOn(props?.collapsible) || (props?.collapsed !== undefined && props?.collapsed !== null);
|
|
958
|
+
}
|
|
959
|
+
/** It starts open when `collapsed` is off (`collapsed="false"`) or `open` is on. */
|
|
960
|
+
function calloutStartsOpen(props) {
|
|
961
|
+
const c = props?.collapsed;
|
|
962
|
+
return c === false || c === "false" || flagOn(props?.open);
|
|
963
|
+
}
|
|
634
964
|
/**
|
|
635
965
|
* Merge a default class string with an optional user-provided class string.
|
|
636
966
|
* Concatenates (user classes appended) so utility frameworks like Tailwind
|
|
637
967
|
* resolve conflicts via component-internal class-variance-authority / tv().
|
|
638
968
|
*/
|
|
639
|
-
function mergeClassAttr(defaults, userClass) {
|
|
969
|
+
function mergeClassAttr(defaults, userClass, target = "astro") {
|
|
640
970
|
const combined = [defaults, userClass].filter(Boolean).join(" ").trim();
|
|
641
|
-
|
|
971
|
+
if (!combined)
|
|
972
|
+
return "";
|
|
973
|
+
return target === "html"
|
|
974
|
+
? ` class="${escapeAttr(combined)}"`
|
|
975
|
+
: componentAttr("class", combined);
|
|
642
976
|
}
|
|
643
977
|
/**
|
|
644
978
|
* Render attributes not consumed as component props — `id`, `data-*`, `aria-*`,
|
|
645
979
|
* and `style`. These pass through to the rendered element.
|
|
646
980
|
*/
|
|
647
|
-
function renderPassthroughAttrs(props, consumed) {
|
|
981
|
+
function renderPassthroughAttrs(props, consumed, target = "astro") {
|
|
648
982
|
if (!props)
|
|
649
983
|
return "";
|
|
650
984
|
const skip = new Set([...consumed, "source", "children"]);
|
|
@@ -659,9 +993,12 @@ function renderPassthroughAttrs(props, consumed) {
|
|
|
659
993
|
if (value === true) {
|
|
660
994
|
parts.push(key);
|
|
661
995
|
}
|
|
662
|
-
else {
|
|
996
|
+
else if (target === "html") {
|
|
663
997
|
parts.push(`${key}="${escapeAttr(String(value))}"`);
|
|
664
998
|
}
|
|
999
|
+
else {
|
|
1000
|
+
parts.push(componentAttr(key, String(value)).trimStart());
|
|
1001
|
+
}
|
|
665
1002
|
}
|
|
666
1003
|
return parts.length ? " " + parts.join(" ") : "";
|
|
667
1004
|
}
|
|
@@ -724,25 +1061,78 @@ function wrapBlock(tag, defaultClass, node, content, consumed = []) {
|
|
|
724
1061
|
return `<${tag}${classAttr}${passthrough}>${sep}${content}${sep}</${tag}>`;
|
|
725
1062
|
void needsNewline;
|
|
726
1063
|
}
|
|
1064
|
+
/**
|
|
1065
|
+
* `:::files` around a nested bullet list: FileTree, whose markup (files/markup.ts) dogsbay-ssg
|
|
1066
|
+
* shares. An entry is a folder when it has entries or its name ends in "/".
|
|
1067
|
+
*/
|
|
1068
|
+
function filesToAstro(node, ctx) {
|
|
1069
|
+
ctx.imports.add("files");
|
|
1070
|
+
const isList = (n) => n.type === "unordered-list" || n.type === "ordered-list";
|
|
1071
|
+
const items = (list) => (list?.children ?? [])
|
|
1072
|
+
.filter((li) => li.type === "list-item")
|
|
1073
|
+
.map((li) => {
|
|
1074
|
+
const inline = li.inline ?? (li.children ?? []).find((c) => c.type === "paragraph")?.inline ?? [];
|
|
1075
|
+
const sub = (li.children ?? []).find(isList);
|
|
1076
|
+
const text = inline.map((n) => ("text" in n && typeof n.text === "string" ? n.text : "")).join("");
|
|
1077
|
+
return { label: inlineToHtml(inline), folder: !!sub || /\/\s*$/.test(text), children: items(sub) };
|
|
1078
|
+
});
|
|
1079
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1080
|
+
// Each list is a tree; anything else in the block (an intro, a note) renders as it is.
|
|
1081
|
+
// `<` is escaped in the JSON: a label may hold inline HTML, and a literal `</script>` in an
|
|
1082
|
+
// expression breaks Vite's dependency scan (see escapeExpr).
|
|
1083
|
+
return (node.children ?? [])
|
|
1084
|
+
.map((child) => isList(child)
|
|
1085
|
+
? `<FileTree items={${JSON.stringify(items(child)).replace(/</g, "\\u003c")}}${classAttr} />`
|
|
1086
|
+
: nodeToAstro(child, ctx))
|
|
1087
|
+
.filter(Boolean)
|
|
1088
|
+
.join("\n");
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* A `:::name` block with a name no renderer knows (a generic container): its content in a
|
|
1092
|
+
* marked <div>, the title (text after the name) first.
|
|
1093
|
+
*/
|
|
1094
|
+
function genericContainerToAstro(node, ctx) {
|
|
1095
|
+
const name = typeof node.meta?.name === "string" ? node.meta.name : "container";
|
|
1096
|
+
const title = typeof node.meta?.title === "string" ? `<p class="font-semibold">${escapeTemplate(node.meta.title)}</p>\n` : "";
|
|
1097
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1098
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
1099
|
+
// Nested props (a `---` block on an unknown directive) reach the page as one JSON attribute,
|
|
1100
|
+
// so a site's own component or script can read them; scalar props pass through as before.
|
|
1101
|
+
const nested = Object.fromEntries(Object.entries(node.props ?? {}).filter(([, v]) => v !== null && typeof v === "object"));
|
|
1102
|
+
const dataProps = Object.keys(nested).length ? ` data-props="${escapeAttr(JSON.stringify(nested))}"` : "";
|
|
1103
|
+
return `<div data-directive="${escapeAttr(name)}"${classAttr}${passthrough}${dataProps}>\n${title}${childrenToAstro(node, ctx)}\n</div>`;
|
|
1104
|
+
}
|
|
727
1105
|
function detailsToAstro(node, ctx) {
|
|
728
1106
|
ctx.imports.add("details");
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
1107
|
+
// `variant` as documented; `type` as dogsbay-ssg (and callouts) read it.
|
|
1108
|
+
// Aliases (`caution` → danger) and unknown names (→ note) as render-html maps them
|
|
1109
|
+
const variant = collapsibleVariant(node.props?.variant ?? node.props?.type ?? "note");
|
|
1110
|
+
// `summary` is dogsbay-md's alias for the title (`:::details{summary="…"}`)
|
|
1111
|
+
const title = node.props?.title ?? node.props?.summary ?? "Details";
|
|
1112
|
+
const open = flagOn(node.props?.open);
|
|
732
1113
|
const icon = node.props?.icon;
|
|
733
1114
|
const inner = childrenToAstro(node, ctx);
|
|
734
|
-
|
|
1115
|
+
// The title is inline Markdown, as in render-html: plain text stays the `title` prop
|
|
1116
|
+
const titleNodes = titleInline(title);
|
|
1117
|
+
const plainTitle = isPlainCaption(titleNodes);
|
|
1118
|
+
const attrs = [`variant="${escapeAttr(variant)}"`, componentAttr("title", plainTitle ? captionText(titleNodes) : title).trimStart()];
|
|
735
1119
|
if (open)
|
|
736
1120
|
attrs.push("open");
|
|
737
1121
|
if (icon)
|
|
738
1122
|
attrs.push(`icon="${escapeAttr(icon)}"`);
|
|
739
1123
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
740
1124
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
741
|
-
"variant", "title", "open", "icon", "class", "summary",
|
|
1125
|
+
"variant", "type", "title", "open", "icon", "class", "summary", "sheet",
|
|
742
1126
|
]);
|
|
743
|
-
|
|
1127
|
+
const slot = plainTitle ? "" : `\n <Fragment slot="title">${renderInline(titleNodes, ctx)}</Fragment>`;
|
|
1128
|
+
const alert = `<CollapsibleAlert ${attrs.join(" ")}${classAttr}${passthrough}>${slot}
|
|
744
1129
|
${indentStr(inner, 2)}
|
|
745
1130
|
</CollapsibleAlert>`;
|
|
1131
|
+
// `sheet`: the same details, opened as a side sheet by <db-details-sheet>
|
|
1132
|
+
if (!isDetailsSheet(node.props))
|
|
1133
|
+
return alert;
|
|
1134
|
+
ctx.imports.add("details-sheet");
|
|
1135
|
+
return `<DetailsSheet>\n${indentStr(alert, 2)}\n</DetailsSheet>`;
|
|
746
1136
|
}
|
|
747
1137
|
function tabsToAstro(node, ctx) {
|
|
748
1138
|
ctx.imports.add("tabs");
|
|
@@ -750,45 +1140,162 @@ function tabsToAstro(node, ctx) {
|
|
|
750
1140
|
if (tabs.length === 0)
|
|
751
1141
|
return "";
|
|
752
1142
|
const tabItems = tabs.map((tab, i) => {
|
|
753
|
-
|
|
1143
|
+
// The same order as render-html: `label` is ours, `title` what a Comark import wrote
|
|
1144
|
+
const title = tab.props?.label || tab.props?.title || tab.props?.value || `Tab ${i + 1}`;
|
|
754
1145
|
const value = tab.props?.value || title;
|
|
755
1146
|
const label = title;
|
|
756
1147
|
return { value, label, node: tab };
|
|
757
1148
|
});
|
|
758
1149
|
const defaultValue = tabItems[0].value;
|
|
1150
|
+
// Diff-trigger indicator: a marked tab panel is invisible until
|
|
1151
|
+
// opened, so its TRIGGER carries a data-diff-tab attribute (styled
|
|
1152
|
+
// as a colored dot by DIFF_CSS) telling the reader WHICH tab to
|
|
1153
|
+
// open. Structural read of @dogsbay/tree-diff marks, same as
|
|
1154
|
+
// diff-decoration; unmarked trees are unaffected.
|
|
1155
|
+
// The indicator must not live in COLOUR alone (WCAG 1.4.1), and a
|
|
1156
|
+
// background-tinted dot disappears in forced-colors mode. So it is a
|
|
1157
|
+
// GLYPH (shape carries the meaning) plus visually-hidden text, so the
|
|
1158
|
+
// trigger announces "Astro, changed in this comparison" rather than
|
|
1159
|
+
// relying on a `title` screen readers may never speak. (TAB_GLYPH and
|
|
1160
|
+
// tabDiffSignal are shared with render-html.)
|
|
1161
|
+
// A variant's class strings are complete (TAB_VARIANTS): TabsList and
|
|
1162
|
+
// TabsTrigger merge a `class` over their base with tailwind-variants
|
|
1163
|
+
const variant = lookup(TAB_VARIANTS, node.props?.variant);
|
|
1164
|
+
const triggerClass = variant ? ` class="${escapeAttr(variant.trigger)}"` : "";
|
|
759
1165
|
const triggers = tabItems
|
|
760
|
-
.map((t) =>
|
|
1166
|
+
.map((t) => {
|
|
1167
|
+
const signal = tabDiffSignal(t.node);
|
|
1168
|
+
if (!signal) {
|
|
1169
|
+
return ` <TabsTrigger${componentAttr("value", t.value)}${triggerClass}>${escapeTemplate(t.label)}</TabsTrigger>`;
|
|
1170
|
+
}
|
|
1171
|
+
const glyph = TAB_GLYPH[signal] ?? "•";
|
|
1172
|
+
// The glyph and its announced text are ONE element: "Hide
|
|
1173
|
+
// highlights" hides `.db-tab-mark`, and a sibling sr-only span
|
|
1174
|
+
// would survive that — leaving screen-reader users still hearing
|
|
1175
|
+
// "changed in this comparison" on every tab while the visible
|
|
1176
|
+
// marker is gone (code review, 2026-07-13).
|
|
1177
|
+
const marker = `<span class="db-tab-mark" data-diff-tab="${escapeAttr(signal)}">` +
|
|
1178
|
+
`<span aria-hidden="true">${glyph}</span>` +
|
|
1179
|
+
`<span class="sr-only">${escapeTemplate(signal)} in this comparison</span>` +
|
|
1180
|
+
`</span>`;
|
|
1181
|
+
// `value` MUST match TabsContent's, which is a componentAttr expression:
|
|
1182
|
+
// a title containing `&` was emitted `&` here and `&` there, so the
|
|
1183
|
+
// trigger and its panel no longer paired and the tab rendered empty.
|
|
1184
|
+
// `data-diff-tab` is a plain data attribute — entity escaping is right.
|
|
1185
|
+
return ` <TabsTrigger${componentAttr("value", t.value)}${triggerClass} data-diff-tab="${escapeAttr(signal)}">${escapeTemplate(t.label)}<Fragment set:html={${escapeExpr(marker)}} /></TabsTrigger>`;
|
|
1186
|
+
})
|
|
761
1187
|
.join("\n");
|
|
762
1188
|
const contents = tabItems
|
|
763
1189
|
.map((t) => {
|
|
764
1190
|
const inner = childrenToAstro(t.node, ctx);
|
|
765
|
-
return ` <TabsContent
|
|
1191
|
+
return ` <TabsContent${componentAttr("value", t.value)}>\n${indentStr(inner, 4)}\n </TabsContent>`;
|
|
766
1192
|
})
|
|
767
1193
|
.join("\n");
|
|
768
1194
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
769
1195
|
const passthrough = renderPassthroughAttrs(node.props, ["sync", "default", "class"]);
|
|
770
|
-
|
|
771
|
-
|
|
1196
|
+
// Groups with the same key switch together (db-tabs). Written `syncKey`, `sync-key` or `group`, as dogsbay-ssg reads it.
|
|
1197
|
+
const syncKey = [node.props?.syncKey, node.props?.["sync-key"], node.props?.group].find((v) => typeof v === "string" && v !== "");
|
|
1198
|
+
const syncAttr = syncKey ? componentAttr("syncKey", syncKey) : "";
|
|
1199
|
+
// Same encoding as the trigger/content `value` it selects — see above.
|
|
1200
|
+
return `<Tabs${componentAttr("defaultValue", defaultValue)}${syncAttr}${classAttr}${passthrough}>
|
|
1201
|
+
<TabsList${variant ? ` class="${escapeAttr(variant.list)}"` : ""}>
|
|
772
1202
|
${triggers}
|
|
773
1203
|
</TabsList>
|
|
774
1204
|
${contents}
|
|
775
1205
|
</Tabs>`;
|
|
776
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* A caption is inline Markdown. Plain text renders exactly as before;
|
|
1209
|
+
* formatting (`Sample \`<plugin>\` file`) renders as inline markup.
|
|
1210
|
+
*/
|
|
1211
|
+
function captionInline(caption) {
|
|
1212
|
+
return parseInline(caption);
|
|
1213
|
+
}
|
|
1214
|
+
const isPlainCaption = (nodes) => nodes.every((n) => n.type === "text" && !n.bold && !n.italic && !n.strikethrough);
|
|
1215
|
+
/**
|
|
1216
|
+
* Formatting tags with no attributes, the HTML a title keeps (a raw `<details><summary><code>x</code>`
|
|
1217
|
+
* becomes the title `<code>x</code>`). render-html has the same list.
|
|
1218
|
+
*/
|
|
1219
|
+
const TITLE_TAG = /^<\/?(?:code|kbd|b|strong|i|em|s|del|mark|sub|sup|br)\s*\/?>$/i;
|
|
1220
|
+
/**
|
|
1221
|
+
* A callout or details title: inline Markdown, and any other raw HTML is TEXT. Importers write
|
|
1222
|
+
* plain-text titles with angle brackets (`Map<K, V>`, `List<String>`), which as HTML dropped words,
|
|
1223
|
+
* and a title with `<img onerror=…>` ran a script in the summary. render-html reads titles the same way.
|
|
1224
|
+
*/
|
|
1225
|
+
function titleInline(title) {
|
|
1226
|
+
const nodes = captionInline(title);
|
|
1227
|
+
const kept = balancedTitleTags(nodes);
|
|
1228
|
+
return nodes.map((n, i) => (n.type === "html-inline" && !kept.has(i) ? { type: "text", text: n.html } : n));
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* The indexes of the formatting tags a title keeps: a bare `TITLE_TAG` that is one half of a
|
|
1232
|
+
* matched pair (or `<br>`). An unclosed `<em>` was kept, and the browser carried it past the
|
|
1233
|
+
* title's element into the rest of the page (render-html has the same rule).
|
|
1234
|
+
*/
|
|
1235
|
+
function balancedTitleTags(nodes) {
|
|
1236
|
+
const kept = new Set();
|
|
1237
|
+
const open = [];
|
|
1238
|
+
nodes.forEach((n, i) => {
|
|
1239
|
+
if (n.type !== "html-inline")
|
|
1240
|
+
return;
|
|
1241
|
+
const html = n.html.trim();
|
|
1242
|
+
if (!TITLE_TAG.test(html))
|
|
1243
|
+
return;
|
|
1244
|
+
const name = /^<\/?\s*(\w+)/.exec(html)[1].toLowerCase();
|
|
1245
|
+
if (name === "br")
|
|
1246
|
+
kept.add(i);
|
|
1247
|
+
else if (!html.startsWith("</"))
|
|
1248
|
+
open.push({ name, at: i });
|
|
1249
|
+
else if (open.length && open[open.length - 1].name === name) {
|
|
1250
|
+
kept.add(open.pop().at);
|
|
1251
|
+
kept.add(i);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
return kept;
|
|
1255
|
+
}
|
|
1256
|
+
/** A plain caption's text (escapes undone by the parse). */
|
|
1257
|
+
const captionText = (nodes) => nodes.map((n) => (n.type === "text" ? n.text : "")).join("");
|
|
1258
|
+
function captionToAstro(caption, ctx) {
|
|
1259
|
+
// Raw HTML other than balanced formatting tags is text, as in a title (an imported repo's caption
|
|
1260
|
+
// is someone else's; render-html draws captions the same way).
|
|
1261
|
+
const nodes = titleInline(caption);
|
|
1262
|
+
return isPlainCaption(nodes)
|
|
1263
|
+
? escapeTemplate(captionText(nodes))
|
|
1264
|
+
: renderInline(nodes, ctx);
|
|
1265
|
+
}
|
|
777
1266
|
function tableToAstro(node, ctx) {
|
|
778
1267
|
ctx.imports.add("table");
|
|
779
1268
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
780
|
-
const
|
|
781
|
-
|
|
1269
|
+
const caption = node.props?.caption;
|
|
1270
|
+
const nodes = caption ? captionInline(caption) : [];
|
|
1271
|
+
const plain = isPlainCaption(nodes);
|
|
1272
|
+
// A plain caption stays the `caption` prop; a formatted one fills the table's caption slot
|
|
1273
|
+
const captionAttr = caption && plain
|
|
1274
|
+
? ` caption={${escapeExpr(nodes.map((n) => (n.type === "text" ? n.text : "")).join(""))}}`
|
|
1275
|
+
: "";
|
|
1276
|
+
const captionSlot = caption && !plain ? `<Fragment slot="caption">${renderInline(nodes, ctx)}</Fragment>\n` : "";
|
|
1277
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class", "caption"]);
|
|
1278
|
+
return `<Table${classAttr}${captionAttr}${passthrough}>\n${captionSlot}${childrenToAstro(node, ctx)}\n</Table>`;
|
|
782
1279
|
}
|
|
783
1280
|
function diagramToAstro(node, ctx) {
|
|
784
1281
|
ctx.imports.add("diagram");
|
|
785
1282
|
const lang = node.props?.lang || "mermaid";
|
|
786
1283
|
const code = node.props?.code ?? "";
|
|
787
|
-
|
|
1284
|
+
let svg = node.props?.svg ?? "";
|
|
788
1285
|
const title = node.props?.title;
|
|
1286
|
+
// A Pele SVG's ids (`<prefix>-title`, `<prefix>-desc`, for aria-labelledby) are unique within
|
|
1287
|
+
// its source page. A book view or an include can show the same diagram twice on one page, so
|
|
1288
|
+
// the later copies are renamed; the prefix appears nowhere else in the SVG.
|
|
1289
|
+
const prefix = /^<svg[^>]*\bclass="pele\b/.test(svg) ? /\sid="(dg-[0-9a-f]{12}(?:-\d+)?)-/.exec(svg)?.[1] : undefined;
|
|
1290
|
+
if (prefix) {
|
|
1291
|
+
const n = (ctx.diagramIds.get(prefix) ?? 0) + 1;
|
|
1292
|
+
ctx.diagramIds.set(prefix, n);
|
|
1293
|
+
if (n > 1)
|
|
1294
|
+
svg = svg.split(prefix).join(`${prefix}-p${n}`);
|
|
1295
|
+
}
|
|
789
1296
|
const attrs = [`lang="${escapeAttr(lang)}"`, `code={${escapeExpr(code)}}`, `svg={${escapeExpr(svg)}}`];
|
|
790
1297
|
if (title)
|
|
791
|
-
attrs.push(
|
|
1298
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
792
1299
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
793
1300
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
794
1301
|
"lang", "code", "svg", "title", "class",
|
|
@@ -801,7 +1308,7 @@ function youtubeToAstro(node, ctx) {
|
|
|
801
1308
|
const title = node.props?.title ?? "";
|
|
802
1309
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
803
1310
|
const passthrough = renderPassthroughAttrs(node.props, ["id", "title", "class"]);
|
|
804
|
-
return `<YouTube
|
|
1311
|
+
return `<YouTube${componentAttr("id", id)}${componentAttr("title", title)}${classAttr}${passthrough} />`;
|
|
805
1312
|
}
|
|
806
1313
|
function mathBlockToAstro(node, ctx) {
|
|
807
1314
|
ctx.imports.add("math-block");
|
|
@@ -844,6 +1351,11 @@ function footnoteListToAstro(node, ctx) {
|
|
|
844
1351
|
function footnoteDefToAstro(node, ctx) {
|
|
845
1352
|
const id = node.props?.id ?? "";
|
|
846
1353
|
const backrefId = node.props?.backrefId ?? "";
|
|
1354
|
+
// Block content (a list, code): the blocks, with the backref after them
|
|
1355
|
+
if (node.children?.length) {
|
|
1356
|
+
const backref = backrefId ? `\n<a href="#${escapeAttr(backrefId)}" class="text-primary hover:underline">↩</a>` : "";
|
|
1357
|
+
return `<li id="${escapeAttr(id)}" class="text-sm text-muted-foreground">\n${childrenToAstro(node, ctx)}${backref}\n</li>`;
|
|
1358
|
+
}
|
|
847
1359
|
if (ctx.mode === "template" && node.inline) {
|
|
848
1360
|
const content = inlineToTemplate(node.inline);
|
|
849
1361
|
const backref = backrefId ? ` <a href="#${escapeAttr(backrefId)}" class="text-primary hover:underline">↩</a>` : "";
|
|
@@ -859,8 +1371,15 @@ function apiSymbolToAstro(node, ctx) {
|
|
|
859
1371
|
const kind = node.props?.kind ?? "func";
|
|
860
1372
|
const signature = node.props?.signature;
|
|
861
1373
|
const bases = node.props?.bases || [];
|
|
862
|
-
|
|
863
|
-
|
|
1374
|
+
// By position, not by type (apiSymbolLevels): a module-level function was
|
|
1375
|
+
// an h3 straight under the page's h1, and a class inside a class an h2.
|
|
1376
|
+
const level = ctx.apiSymbolLevels.get(node) ?? (node.type === "api-class" ? 2 : 3);
|
|
1377
|
+
ctx.lastHeading = level;
|
|
1378
|
+
const attrs = [
|
|
1379
|
+
componentAttr("name", name).trimStart(),
|
|
1380
|
+
componentAttr("kind", kind).trimStart(),
|
|
1381
|
+
`level={${level}}`,
|
|
1382
|
+
];
|
|
864
1383
|
if (signature)
|
|
865
1384
|
attrs.push(`signature={${escapeExpr(signature)}}`);
|
|
866
1385
|
if (bases.length > 0)
|
|
@@ -909,11 +1428,17 @@ function endpointToAstro(node, ctx) {
|
|
|
909
1428
|
const path = props.path ?? "";
|
|
910
1429
|
const baseUrl = props.baseUrl ?? "";
|
|
911
1430
|
const summary = props.summary;
|
|
912
|
-
|
|
1431
|
+
// Descriptions are CommonMark, rendered with raw HTML off: the components
|
|
1432
|
+
// put them in the page with set:html (api-markdown.ts).
|
|
1433
|
+
// (Compared as text first: the card hides a description equal to the
|
|
1434
|
+
// summary, and the rendered HTML never equals it.)
|
|
1435
|
+
const description = typeof props.description === "string" && props.description !== props.summary
|
|
1436
|
+
? apiBlockHtml(props.description)
|
|
1437
|
+
: undefined;
|
|
913
1438
|
const deprecated = props.deprecated === true;
|
|
914
|
-
const parameters = props.parameters ?? [];
|
|
915
|
-
const requestBody = props.requestBody ?? [];
|
|
916
|
-
const responses = props.responses ?? [];
|
|
1439
|
+
const parameters = apiPropertiesHtml(props.parameters) ?? [];
|
|
1440
|
+
const requestBody = apiPropertiesHtml(props.requestBody) ?? [];
|
|
1441
|
+
const responses = (props.responses ?? []).map((r) => r && Array.isArray(r.schema) ? { ...r, schema: apiPropertiesHtml(r.schema) } : r);
|
|
917
1442
|
const codeSamples = props.codeSamples ?? [];
|
|
918
1443
|
const requestBodyExample = props.requestBodyExample;
|
|
919
1444
|
const cardAttrs = [
|
|
@@ -953,7 +1478,9 @@ function endpointToAstro(node, ctx) {
|
|
|
953
1478
|
const sectionsBody = sections.length > 0
|
|
954
1479
|
? `\n${indentStr(sections.join("\n"), 4)}\n `
|
|
955
1480
|
: "";
|
|
956
|
-
// Code column —
|
|
1481
|
+
// Code column — its titles are labels (<p>), not headings: an operation
|
|
1482
|
+
// page has only its h1 above them, so an h3 skipped a level.
|
|
1483
|
+
// Wrapped in <ApiCodePanel> for consistent vertical
|
|
957
1484
|
// rhythm. Code samples first, then a request body example, then one
|
|
958
1485
|
// ExampleBlock per response status that carries a concrete example.
|
|
959
1486
|
// Each ExampleBlock auto-detects language from the response's
|
|
@@ -962,7 +1489,7 @@ function endpointToAstro(node, ctx) {
|
|
|
962
1489
|
const codePanels = [];
|
|
963
1490
|
if (codeSamples.length > 0) {
|
|
964
1491
|
codePanels.push(`<div>\n` +
|
|
965
|
-
` <
|
|
1492
|
+
` <p class="mb-3 text-xs font-semibold uppercase tracking-wider text-white/60">Code samples</p>\n` +
|
|
966
1493
|
` <CodeSamples samples={${JSON.stringify(codeSamples)}} />\n` +
|
|
967
1494
|
`</div>`);
|
|
968
1495
|
}
|
|
@@ -982,31 +1509,75 @@ function endpointToAstro(node, ctx) {
|
|
|
982
1509
|
const codeBody = codePanels.length > 0
|
|
983
1510
|
? `\n${indentStr(codePanels.join("\n"), 4)}\n `
|
|
984
1511
|
: "";
|
|
985
|
-
|
|
1512
|
+
const layoutAttrs = ctx.embeddedEndpoints ? ' variant="embedded"' : "";
|
|
1513
|
+
return (`<ApiLayout${layoutAttrs}>\n` +
|
|
986
1514
|
` <EndpointCard ${cardAttrs.join(" ")}>${sectionsBody}</EndpointCard>\n` +
|
|
987
1515
|
` <ApiCodePanel slot="code">${codeBody}</ApiCodePanel>\n` +
|
|
988
1516
|
`</ApiLayout>`);
|
|
989
1517
|
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Whole-card links being emitted. Inside one, a link in the card's body is its text: a browser
|
|
1520
|
+
* ends the outer <a> at an inner one, so the rest of the card fell out of the link (and lost
|
|
1521
|
+
* not-prose). The serializer is synchronous, so a depth count is enough.
|
|
1522
|
+
*/
|
|
1523
|
+
let cardLinkDepth = 0;
|
|
1524
|
+
function wholeCardLink(yes, fn) {
|
|
1525
|
+
if (!yes)
|
|
1526
|
+
return fn();
|
|
1527
|
+
cardLinkDepth++;
|
|
1528
|
+
try {
|
|
1529
|
+
return fn();
|
|
1530
|
+
}
|
|
1531
|
+
finally {
|
|
1532
|
+
cardLinkDepth--;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
/** A card's title: not-prose, or docs.css's h3 rule (1.25rem, 2rem above) applies inside the card. */
|
|
1536
|
+
const CARD_TITLE_CLASS = "not-prose m-0 border-0 p-0 text-base font-semibold";
|
|
1537
|
+
/** Marks a card title for styles and tests (it is a div, not a heading). */
|
|
1538
|
+
const CARD_TITLE_ATTR = "data-card-title";
|
|
1539
|
+
/**
|
|
1540
|
+
* A card group's columns, responsive as render-html draws them (its GRID_COLS): one column on a
|
|
1541
|
+
* phone, then up to `cols` (1–6) by Grid's breakpoints (0, 640, 768, 1024). Default 2. A list
|
|
1542
|
+
* (`cols="1,2,3"`) is the author's own breakpoints and passes through.
|
|
1543
|
+
*/
|
|
1544
|
+
function cardCols(raw) {
|
|
1545
|
+
const list = coerceNumericList(raw, [2]);
|
|
1546
|
+
if (list.length > 1)
|
|
1547
|
+
return list;
|
|
1548
|
+
const n = Math.min(6, Math.max(1, Math.round(list[0]) || 2));
|
|
1549
|
+
return n === 1 ? [1] : n === 2 ? [1, 2] : n <= 4 ? [1, 2, 2, n] : [1, 3, 3, n];
|
|
1550
|
+
}
|
|
990
1551
|
function cardsToAstro(node, ctx) {
|
|
991
1552
|
ctx.imports.add("grid");
|
|
992
1553
|
ctx.imports.add("card");
|
|
993
1554
|
// Grid-level attributes on the :::cards container
|
|
994
1555
|
const outerClass = mergeClassAttr("", node.props?.class);
|
|
995
1556
|
const outerPass = renderPassthroughAttrs(node.props, [
|
|
996
|
-
"cols", "gap", "rows", "class",
|
|
1557
|
+
"cols", "gap", "rows", "class", "tone",
|
|
997
1558
|
]);
|
|
998
1559
|
// MDX pattern: children are card nodes with title/href/icon props
|
|
999
1560
|
const cardNodes = (node.children ?? []).filter((c) => c.type === "card");
|
|
1000
1561
|
if (cardNodes.length > 0) {
|
|
1001
1562
|
const cards = cardNodes.map((card) => {
|
|
1002
1563
|
const title = card.props?.title ?? "";
|
|
1003
|
-
const href = card.props?.href
|
|
1564
|
+
const href = safeHref(card.props?.href);
|
|
1004
1565
|
const description = card.props?.description;
|
|
1005
1566
|
const icon = card.props?.icon;
|
|
1567
|
+
// The title's level before the body is written: a heading inside it must not move this
|
|
1568
|
+
// card's title, nor the next card's (each starts from the level before the group)
|
|
1569
|
+
const tag = cardTitleTag(ctx);
|
|
1570
|
+
const before = ctx.lastHeading;
|
|
1006
1571
|
// Description sources: inline content, children, or props.description.
|
|
1007
|
-
|
|
1008
|
-
|
|
1572
|
+
// Child blocks carry their own <p>, so they get a <div>: <p><p>…</p></p> is invalid HTML.
|
|
1573
|
+
// A body with links of its own: the title links, not the whole card (cardBodyHasLink)
|
|
1574
|
+
const whole = !!href && (!title || !cardBodyHasLink([card.children, card.inline]));
|
|
1575
|
+
const blocks = wholeCardLink(whole, () => childrenToAstro(card, ctx));
|
|
1576
|
+
ctx.lastHeading = before;
|
|
1577
|
+
const inner = blocks
|
|
1578
|
+
|| (card.inline ? wholeCardLink(whole, () => inlineToTemplate(card.inline)) : "")
|
|
1009
1579
|
|| (description ? escapeTemplate(description) : "");
|
|
1580
|
+
const descTag = blocks ? "div" : "p";
|
|
1010
1581
|
// Icon renders inline before the title — picks up Lucide
|
|
1011
1582
|
// SVG via the `icon` import. Resolved at build time; nothing
|
|
1012
1583
|
// emitted when the name doesn't resolve (silent fallback).
|
|
@@ -1014,26 +1585,33 @@ function cardsToAstro(node, ctx) {
|
|
|
1014
1585
|
// top of the page.
|
|
1015
1586
|
let iconHtml = "";
|
|
1016
1587
|
if (icon) {
|
|
1017
|
-
|
|
1018
|
-
iconHtml = `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1588
|
+
iconHtml = cardIconHtml(icon, title, ctx);
|
|
1019
1589
|
}
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1590
|
+
const titleText = href && !whole ? `<a href="${escapeAttr(href)}">${escapeTemplate(title)}</a>` : escapeTemplate(title);
|
|
1591
|
+
const titleHtml = title ? `<${tag} class="${CARD_TITLE_CLASS}" ${CARD_TITLE_ATTR}>${titleText}</${tag}>` : "";
|
|
1592
|
+
// A description and a body are both shown (the description used to be dropped when a body existed).
|
|
1593
|
+
const lead = blocks && description
|
|
1594
|
+
? `<p class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${escapeTemplate(description)}</p>`
|
|
1023
1595
|
: "";
|
|
1024
|
-
const
|
|
1025
|
-
? "
|
|
1026
|
-
: "
|
|
1596
|
+
const desc = lead + (inner
|
|
1597
|
+
? `<${descTag} class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${inner}</${descTag}>`
|
|
1598
|
+
: "");
|
|
1599
|
+
// A tone (TONES; the card's own, else the group's) sets the card's
|
|
1600
|
+
// border and tint: Card's tailwind-variants merge replaces bg-card
|
|
1601
|
+
const toned = lookup(CARD_TONE_CLASSES, card.props?.tone ?? node.props?.tone);
|
|
1602
|
+
const cardDefaultClass = whole
|
|
1603
|
+
? toned ? `p-4 ${toned} hover:shadow-md transition-colors` : "p-4 hover:border-foreground/20 transition-colors"
|
|
1604
|
+
: toned ? `p-4 ${toned}` : "p-4";
|
|
1027
1605
|
const cardClassAttr = mergeClassAttr(cardDefaultClass, card.props?.class);
|
|
1028
1606
|
const cardPass = renderPassthroughAttrs(card.props, [
|
|
1029
|
-
"title", "href", "description", "icon", "variant", "class",
|
|
1607
|
+
"title", "href", "description", "icon", "variant", "class", "tone",
|
|
1030
1608
|
]);
|
|
1031
|
-
if (
|
|
1032
|
-
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="[text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1609
|
+
if (whole) {
|
|
1610
|
+
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" data-variant="card" class="[text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1033
1611
|
}
|
|
1034
1612
|
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n${iconHtml}\n${titleHtml}\n${desc}\n</CardContent>\n</Card>`;
|
|
1035
1613
|
}).join("\n");
|
|
1036
|
-
return `<Grid cols={[
|
|
1614
|
+
return `<Grid cols={[${cardCols(node.props?.cols).join(", ")}]} gap={4}${outerClass}${outerPass}>\n${cards}\n</Grid>`;
|
|
1037
1615
|
}
|
|
1038
1616
|
// MkDocs pattern: children are unordered-list items
|
|
1039
1617
|
const items = (node.children ?? [])
|
|
@@ -1043,10 +1621,10 @@ function cardsToAstro(node, ctx) {
|
|
|
1043
1621
|
const cards = items
|
|
1044
1622
|
.map((item) => `<Card>\n<CardContent>\n${childrenToAstro(item, ctx)}\n</CardContent>\n</Card>`)
|
|
1045
1623
|
.join("\n");
|
|
1046
|
-
return `<Grid cols={[
|
|
1624
|
+
return `<Grid cols={[${cardCols(node.props?.cols).join(", ")}]} gap={4}>\n${cards}\n</Grid>`;
|
|
1047
1625
|
}
|
|
1048
1626
|
// Fallback: render children directly in grid
|
|
1049
|
-
return `<Grid cols={[
|
|
1627
|
+
return `<Grid cols={[${cardCols(node.props?.cols).join(", ")}]} gap={4}>\n${childrenToAstro(node, ctx)}\n</Grid>`;
|
|
1050
1628
|
}
|
|
1051
1629
|
/**
|
|
1052
1630
|
* Render a standalone `card` TreeNode (outside `:::cards`).
|
|
@@ -1058,34 +1636,43 @@ function cardsToAstro(node, ctx) {
|
|
|
1058
1636
|
function standaloneCardToAstro(node, ctx) {
|
|
1059
1637
|
ctx.imports.add("card");
|
|
1060
1638
|
const title = node.props?.title ?? "";
|
|
1061
|
-
const href = node.props?.href
|
|
1639
|
+
const href = safeHref(node.props?.href);
|
|
1062
1640
|
const description = node.props?.description;
|
|
1063
1641
|
const icon = node.props?.icon;
|
|
1064
|
-
|
|
1065
|
-
|
|
1642
|
+
// The title's level before the body is written (a heading inside it must not move it)
|
|
1643
|
+
const tag = cardTitleTag(ctx);
|
|
1644
|
+
const before = ctx.lastHeading;
|
|
1645
|
+
// Child blocks carry their own <p>, so they get a <div>: <p><p>…</p></p> is invalid HTML.
|
|
1646
|
+
const whole = !!href && (!title || !cardBodyHasLink([node.children, node.inline]));
|
|
1647
|
+
const blocks = wholeCardLink(whole, () => childrenToAstro(node, ctx));
|
|
1648
|
+
ctx.lastHeading = before;
|
|
1649
|
+
const inner = blocks
|
|
1650
|
+
|| (node.inline ? wholeCardLink(whole, () => inlineToTemplate(node.inline)) : "")
|
|
1066
1651
|
|| (description ? escapeTemplate(description) : "");
|
|
1067
|
-
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1070
|
-
if (icon) {
|
|
1071
|
-
ctx.imports.add("icon");
|
|
1072
|
-
iconHtml = `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1073
|
-
}
|
|
1652
|
+
const descTag = blocks ? "div" : "p";
|
|
1653
|
+
// Icon renders inline before the title, a name or an image path, as in a card group
|
|
1654
|
+
const iconHtml = icon ? cardIconHtml(icon, title, ctx) : "";
|
|
1074
1655
|
const titleHtml = title
|
|
1075
|
-
?
|
|
1656
|
+
? `<${tag} class="${CARD_TITLE_CLASS}" ${CARD_TITLE_ATTR}>${href && !whole ? `<a href="${escapeAttr(href)}">${escapeTemplate(title)}</a>` : escapeTemplate(title)}</${tag}>`
|
|
1076
1657
|
: "";
|
|
1077
|
-
|
|
1078
|
-
|
|
1658
|
+
// A description and a body are both shown (the description used to be dropped when a body existed).
|
|
1659
|
+
const lead = blocks && description
|
|
1660
|
+
? `<p class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${escapeTemplate(description)}</p>`
|
|
1079
1661
|
: "";
|
|
1080
|
-
const
|
|
1081
|
-
? "
|
|
1082
|
-
: "
|
|
1662
|
+
const desc = lead + (inner
|
|
1663
|
+
? `<${descTag} class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${inner}</${descTag}>`
|
|
1664
|
+
: "");
|
|
1665
|
+
// A tone (TONES), as on a card in a group (cardsToAstro)
|
|
1666
|
+
const toned = lookup(CARD_TONE_CLASSES, node.props?.tone);
|
|
1667
|
+
const cardDefaultClass = whole
|
|
1668
|
+
? toned ? `p-4 ${toned} hover:shadow-md transition-colors` : "p-4 hover:border-foreground/20 transition-colors"
|
|
1669
|
+
: toned ? `p-4 ${toned}` : "p-4";
|
|
1083
1670
|
const classAttr = mergeClassAttr(cardDefaultClass, node.props?.class);
|
|
1084
1671
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1085
|
-
"title", "href", "description", "icon", "variant", "class",
|
|
1672
|
+
"title", "href", "description", "icon", "variant", "class", "tone",
|
|
1086
1673
|
]);
|
|
1087
|
-
if (
|
|
1088
|
-
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="[text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1674
|
+
if (whole) {
|
|
1675
|
+
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" data-variant="card" class="[text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1089
1676
|
}
|
|
1090
1677
|
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n${iconHtml}\n${titleHtml}\n${desc}\n</CardContent>\n</Card>`;
|
|
1091
1678
|
}
|
|
@@ -1096,6 +1683,59 @@ function standaloneCardToAstro(node, ctx) {
|
|
|
1096
1683
|
* If the parser kept rich body children instead of folding into description,
|
|
1097
1684
|
* we fall back to the standalone-card path which can hold arbitrary content.
|
|
1098
1685
|
*/
|
|
1686
|
+
/**
|
|
1687
|
+
* `link-button` — a call-to-action link styled as a button.
|
|
1688
|
+
*
|
|
1689
|
+
* There was no case for this at all, so it fell through to rendering the
|
|
1690
|
+
* node's CHILDREN: `<p>Get started</p>`. The label survived, the href did not,
|
|
1691
|
+
* and nothing warned — the reader saw a stray sentence where the page's
|
|
1692
|
+
* primary call to action should be. Every importer that produces one was
|
|
1693
|
+
* affected; `Button.astro` has taken an `href` all along.
|
|
1694
|
+
*/
|
|
1695
|
+
/**
|
|
1696
|
+
* A card icon is either a registry NAME or a path to an image file.
|
|
1697
|
+
*
|
|
1698
|
+
* `<Icon name>` resolves names and renders NOTHING when the name does not
|
|
1699
|
+
* resolve — so a path handed to it disappeared silently, which is how every
|
|
1700
|
+
* "Related products" card lost its logo without anyone noticing.
|
|
1701
|
+
*/
|
|
1702
|
+
function cardIconHtml(icon, title, ctx) {
|
|
1703
|
+
if (/\.(svg|png|jpe?g|webp|avif)$/i.test(icon)) {
|
|
1704
|
+
return `<img src="${escapeAttr(icon)}" alt="" class="size-5 mb-2" loading="lazy" />`;
|
|
1705
|
+
}
|
|
1706
|
+
ctx.imports.add("icon");
|
|
1707
|
+
return `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* A button's variant as `Button.astro` names it. The spec's `primary` is its
|
|
1711
|
+
* filled `default` style; `Button.astro` has no `primary`, so it rendered
|
|
1712
|
+
* with no fill at all.
|
|
1713
|
+
*/
|
|
1714
|
+
function buttonVariant(variant) {
|
|
1715
|
+
return !variant || variant === "primary" ? "default" : variant;
|
|
1716
|
+
}
|
|
1717
|
+
function linkButtonToAstro(node, ctx) {
|
|
1718
|
+
ctx.imports.add("link-button");
|
|
1719
|
+
const href = safeHref(node.props?.href);
|
|
1720
|
+
const written = node.props?.variant;
|
|
1721
|
+
const variant = written === undefined ? undefined : buttonVariant(written);
|
|
1722
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1723
|
+
const passthrough = renderPassthroughAttrs(node.props, ["href", "variant", "class"]);
|
|
1724
|
+
// A button's label is phrasing content, so a lone paragraph child is
|
|
1725
|
+
// unwrapped — `<Button><p>Get started</p></Button>` is invalid markup and
|
|
1726
|
+
// renders with the paragraph's block spacing inside the button.
|
|
1727
|
+
const kids = node.children ?? [];
|
|
1728
|
+
const soleParagraph = kids.length === 1 && kids[0].type === "paragraph" ? kids[0] : undefined;
|
|
1729
|
+
const label = soleParagraph
|
|
1730
|
+
? leafContent(soleParagraph, ctx)
|
|
1731
|
+
: leafContent(node, ctx) || childrenToAstro(node, ctx);
|
|
1732
|
+
return `<Button${componentAttr("href", href)}${variant ? componentAttr("variant", variant) : ""}${classAttr}${passthrough}>${label}</Button>`;
|
|
1733
|
+
}
|
|
1734
|
+
/** A card or button `href` from an importer, or "" (no link) when its
|
|
1735
|
+
* scheme could run script: these never passed markdown-it's link check. */
|
|
1736
|
+
function safeHref(href) {
|
|
1737
|
+
return typeof href === "string" && isSafeHref(href) ? href : "";
|
|
1738
|
+
}
|
|
1099
1739
|
function linkCardToAstro(node, ctx) {
|
|
1100
1740
|
const hasRichChildren = (node.children ?? []).length > 0;
|
|
1101
1741
|
if (hasRichChildren) {
|
|
@@ -1104,15 +1744,17 @@ function linkCardToAstro(node, ctx) {
|
|
|
1104
1744
|
ctx.imports.add("link-card");
|
|
1105
1745
|
const title = node.props?.title ?? "";
|
|
1106
1746
|
const description = node.props?.description;
|
|
1107
|
-
const href = node.props?.href
|
|
1108
|
-
const titleAttr =
|
|
1109
|
-
const hrefAttr =
|
|
1110
|
-
const descAttr = description ?
|
|
1747
|
+
const href = safeHref(node.props?.href);
|
|
1748
|
+
const titleAttr = componentAttr("title", title);
|
|
1749
|
+
const hrefAttr = componentAttr("href", href);
|
|
1750
|
+
const descAttr = description ? componentAttr("description", description) : "";
|
|
1751
|
+
const icon = node.props?.icon;
|
|
1752
|
+
const iconAttr = icon ? componentAttr("icon", icon) : "";
|
|
1111
1753
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1112
1754
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1113
1755
|
"title", "description", "href", "icon", "class",
|
|
1114
1756
|
]);
|
|
1115
|
-
return `<LinkCard${titleAttr}${descAttr}${hrefAttr}${classAttr}${passthrough} />`;
|
|
1757
|
+
return `<LinkCard${titleAttr}${descAttr}${hrefAttr}${iconAttr}${classAttr}${passthrough} />`;
|
|
1116
1758
|
}
|
|
1117
1759
|
/**
|
|
1118
1760
|
* Render an `accordion` TreeNode and its `accordion-item` children.
|
|
@@ -1131,9 +1773,9 @@ function accordionToAstro(node, ctx) {
|
|
|
1131
1773
|
const defaultValue = node.props?.defaultValue;
|
|
1132
1774
|
const typeAttr = ` type="${escapeAttr(type)}"`;
|
|
1133
1775
|
const defaultAttr = defaultValue !== undefined
|
|
1134
|
-
?
|
|
1135
|
-
?
|
|
1136
|
-
: `{${JSON.stringify(defaultValue)}}`
|
|
1776
|
+
? typeof defaultValue === "string"
|
|
1777
|
+
? componentAttr("defaultValue", defaultValue)
|
|
1778
|
+
: ` defaultValue={${JSON.stringify(defaultValue)}}`
|
|
1137
1779
|
: "";
|
|
1138
1780
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1139
1781
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
@@ -1141,15 +1783,18 @@ function accordionToAstro(node, ctx) {
|
|
|
1141
1783
|
]);
|
|
1142
1784
|
const items = (node.children ?? [])
|
|
1143
1785
|
.filter((c) => c.type === "accordion-item")
|
|
1144
|
-
.map((item) => accordionItemToAstro(item, ctx, /*standalone*/ false))
|
|
1786
|
+
.map((item, i) => accordionItemToAstro(item, ctx, /*standalone*/ false, i + 1))
|
|
1145
1787
|
.join("\n");
|
|
1146
1788
|
return `<Accordion${typeAttr}${defaultAttr}${classAttr}${passthrough}>\n${items}\n</Accordion>`;
|
|
1147
1789
|
}
|
|
1148
|
-
function accordionItemToAstro(node, ctx, standalone
|
|
1790
|
+
function accordionItemToAstro(node, ctx, standalone,
|
|
1791
|
+
/** 1-based position: an item with no `value` must not share one with its siblings */
|
|
1792
|
+
index = 1) {
|
|
1149
1793
|
ctx.imports.add("accordion");
|
|
1150
|
-
|
|
1794
|
+
// Items are matched by value: a shared fallback opened and closed them together
|
|
1795
|
+
const value = node.props?.value ?? `item-${index}`;
|
|
1151
1796
|
const label = node.props?.label ?? node.props?.title ?? "Item";
|
|
1152
|
-
const valueAttr =
|
|
1797
|
+
const valueAttr = componentAttr("value", value);
|
|
1153
1798
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1154
1799
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1155
1800
|
"value", "label", "title", "class",
|
|
@@ -1176,9 +1821,9 @@ function avatarToAstro(node, ctx) {
|
|
|
1176
1821
|
const src = node.props?.src ?? "";
|
|
1177
1822
|
const alt = node.props?.alt ?? "";
|
|
1178
1823
|
const fallback = node.props?.fallback;
|
|
1179
|
-
const srcAttr = src ?
|
|
1180
|
-
const altAttr =
|
|
1181
|
-
const fbAttr = fallback ?
|
|
1824
|
+
const srcAttr = src ? componentAttr("src", src) : "";
|
|
1825
|
+
const altAttr = componentAttr("alt", alt);
|
|
1826
|
+
const fbAttr = fallback ? componentAttr("fallback", fallback) : "";
|
|
1182
1827
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1183
1828
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1184
1829
|
"src", "alt", "fallback", "class",
|
|
@@ -1199,7 +1844,7 @@ function exampleToAstro(node, ctx) {
|
|
|
1199
1844
|
const source = String(node.props?.source ?? "");
|
|
1200
1845
|
const title = node.props?.title;
|
|
1201
1846
|
const showFallback = node.props?.fallback === true || node.props?.fallback === "true";
|
|
1202
|
-
const titleAttr = title ?
|
|
1847
|
+
const titleAttr = title ? componentAttr("title", title) : "";
|
|
1203
1848
|
const fallbackAttr = showFallback ? " showFallback" : "";
|
|
1204
1849
|
const inner = childrenToAstro(node, ctx);
|
|
1205
1850
|
return `<MarkdownExample source={${escapeExpr(source)}}${titleAttr}${fallbackAttr}>\n${inner}\n</MarkdownExample>`;
|
|
@@ -1214,29 +1859,34 @@ function htmlContainerToAstro(node, ctx) {
|
|
|
1214
1859
|
const inner = childrenToAstro(node, ctx);
|
|
1215
1860
|
return `<div class="columns-1 sm:columns-2 gap-4 [&_ul]:m-0">\n${inner}\n</div>`;
|
|
1216
1861
|
}
|
|
1862
|
+
// A container parsed from raw HTML (a one-line `<details>` body) carries
|
|
1863
|
+
// its content in `html`, not children — rendering only children dropped it
|
|
1864
|
+
if (!node.children?.length && node.html)
|
|
1865
|
+
return htmlToAstro(node.html, ctx);
|
|
1217
1866
|
const inner = childrenToAstro(node, ctx);
|
|
1218
1867
|
return `<div>\n${inner}\n</div>`;
|
|
1219
1868
|
}
|
|
1220
1869
|
// ─── Utilities ────────────────────────────────────────────────────
|
|
1221
1870
|
/**
|
|
1222
|
-
* Render a node's content
|
|
1223
|
-
* parser
|
|
1224
|
-
*
|
|
1871
|
+
* Render a node's content across all THREE TreeNode shapes: flat `inline`
|
|
1872
|
+
* (dogsbay-md parser), wrapped `children[{prose, inline}]` (Starlight
|
|
1873
|
+
* importer), and a node carrying only `html` (MDX/Starlight/MkDocs). Inline
|
|
1874
|
+
* comes before children when both are present.
|
|
1225
1875
|
*
|
|
1226
1876
|
* Used for list-item, table cells, step, dt, dd — types that commonly
|
|
1227
1877
|
* carry leaf text content directly on the node.
|
|
1878
|
+
*
|
|
1879
|
+
* This function is where serialize-core's `renderLeaf` was extracted FROM, so
|
|
1880
|
+
* delegating to it closes the loop: the wrapper-dedup and html-shape handling
|
|
1881
|
+
* the core grew afterwards (from the Docusaurus dogfooding bug) now flow back
|
|
1882
|
+
* here instead of the two implementations continuing to drift apart.
|
|
1228
1883
|
*/
|
|
1229
1884
|
function leafContent(node, ctx) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
const rendered = childrenToAstro(node, ctx);
|
|
1236
|
-
if (rendered)
|
|
1237
|
-
parts.push(rendered);
|
|
1238
|
-
}
|
|
1239
|
-
return parts.join("\n");
|
|
1885
|
+
return renderLeaf(node, {
|
|
1886
|
+
inline: inlineToTemplate,
|
|
1887
|
+
children: () => childrenToAstro(node, ctx),
|
|
1888
|
+
html: (html) => `<Fragment set:html={${escapeExpr(html)}} />`,
|
|
1889
|
+
});
|
|
1240
1890
|
}
|
|
1241
1891
|
function childrenToAstro(node, ctx) {
|
|
1242
1892
|
if (!node.children || node.children.length === 0)
|
|
@@ -1250,40 +1900,28 @@ function childrenToAstro(node, ctx) {
|
|
|
1250
1900
|
* icon usage on cards registers `icon` at the renderer call site;
|
|
1251
1901
|
* this is the inline-only path. Returns true on first hit.
|
|
1252
1902
|
*/
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
return true;
|
|
1271
|
-
if ("children" in node && Array.isArray(node.children)) {
|
|
1272
|
-
for (const child of node.children) {
|
|
1273
|
-
if (inlineHasIcon(child))
|
|
1274
|
-
return true;
|
|
1275
|
-
}
|
|
1903
|
+
/** The component imports inline nodes need: `icon`, `badge`, and `link-button` for an inline `:button`. */
|
|
1904
|
+
function inlineComponentsIn(nodes, out = new Set()) {
|
|
1905
|
+
const visit = (node) => {
|
|
1906
|
+
if (node.type === "icon")
|
|
1907
|
+
out.add("icon");
|
|
1908
|
+
else if (node.type === "badge")
|
|
1909
|
+
out.add("badge");
|
|
1910
|
+
else if (node.type === "kbd")
|
|
1911
|
+
out.add("kbd");
|
|
1912
|
+
else if (node.type === "link" && node.button)
|
|
1913
|
+
out.add("link-button");
|
|
1914
|
+
if ("children" in node && Array.isArray(node.children))
|
|
1915
|
+
node.children.forEach(visit);
|
|
1916
|
+
};
|
|
1917
|
+
for (const node of nodes ?? []) {
|
|
1918
|
+
node.inline?.forEach(visit);
|
|
1919
|
+
inlineComponentsIn(node.children, out);
|
|
1276
1920
|
}
|
|
1277
|
-
return
|
|
1278
|
-
}
|
|
1279
|
-
function indentStr(text, spaces) {
|
|
1280
|
-
const prefix = " ".repeat(spaces);
|
|
1281
|
-
return text
|
|
1282
|
-
.split("\n")
|
|
1283
|
-
.map((line) => (line ? `${prefix}${line}` : line))
|
|
1284
|
-
.join("\n");
|
|
1921
|
+
return out;
|
|
1285
1922
|
}
|
|
1923
|
+
/** Indent, via the core's shared implementation (was a fourth local copy). */
|
|
1924
|
+
const indentStr = indent;
|
|
1286
1925
|
function escapeHtml(s) {
|
|
1287
1926
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1288
1927
|
}
|
|
1289
|
-
//# sourceMappingURL=serialize.js.map
|