@dogsbay/format-astro 0.2.0-beta.11 → 0.2.0-beta.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-path.d.ts +93 -7
- package/dist/base-path.d.ts.map +1 -1
- package/dist/base-path.js +122 -8
- package/dist/base-path.js.map +1 -1
- package/dist/blog.d.ts +134 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +319 -0
- package/dist/blog.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/diff-decoration.d.ts +79 -0
- package/dist/diff-decoration.d.ts.map +1 -0
- package/dist/diff-decoration.js +541 -0
- package/dist/diff-decoration.js.map +1 -0
- package/dist/granularity.d.ts +83 -0
- package/dist/granularity.d.ts.map +1 -0
- package/dist/granularity.js +247 -0
- package/dist/granularity.js.map +1 -0
- package/dist/index.d.ts +22 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -3
- package/dist/index.js.map +1 -1
- package/dist/lead.d.ts +19 -0
- package/dist/lead.d.ts.map +1 -1
- package/dist/lead.js +101 -6
- package/dist/lead.js.map +1 -1
- package/dist/llms-txt.d.ts +48 -2
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +131 -14
- package/dist/llms-txt.js.map +1 -1
- package/dist/plugins.js +1 -1
- package/dist/plugins.js.map +1 -1
- package/dist/project.d.ts +311 -13
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +2261 -211
- package/dist/project.js.map +1 -1
- package/dist/serialize.d.ts +23 -0
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +359 -138
- package/dist/serialize.js.map +1 -1
- package/dist/sitemap.d.ts +81 -0
- package/dist/sitemap.d.ts.map +1 -0
- package/dist/sitemap.js +200 -0
- package/dist/sitemap.js.map +1 -0
- package/dist/taxonomy.d.ts +48 -1
- package/dist/taxonomy.d.ts.map +1 -1
- package/dist/taxonomy.js +61 -23
- package/dist/taxonomy.js.map +1 -1
- package/package.json +8 -7
package/dist/serialize.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { walkInline, applyTextFlags, renderLeaf, indent, } from "@dogsbay/serialize-core";
|
|
2
|
+
import { decorateDiff, hasDiffMarks } from "./diff-decoration.js";
|
|
1
3
|
/**
|
|
2
4
|
* Tone palette for `:::grid-item{label="..." tone="..."}` cells.
|
|
3
5
|
* Each tone is a `bg + text` Tailwind class pair. Used to make grid demos
|
|
4
6
|
* declarative — authors specify a tone instead of writing the styling.
|
|
7
|
+
*
|
|
8
|
+
* Exported because the scaffolded site's `global.css` must safelist
|
|
9
|
+
* these classes via `@source inline()` — they only appear in
|
|
10
|
+
* markdown-generated `.astro` pages, which Tailwind's content
|
|
11
|
+
* scanner doesn't always pick up reliably (and consumers who put
|
|
12
|
+
* their content/ outside the scanned globs won't pick them up at
|
|
13
|
+
* all). See generateGlobalCss in project.ts.
|
|
5
14
|
*/
|
|
6
|
-
const TONE_CLASSES = {
|
|
15
|
+
export const TONE_CLASSES = {
|
|
7
16
|
// Primary scale (intensity)
|
|
8
17
|
"primary": "bg-primary text-primary-foreground",
|
|
9
18
|
"primary-strong": "bg-primary/80 text-primary-foreground",
|
|
@@ -96,6 +105,9 @@ const COMPONENT_IMPORTS = {
|
|
|
96
105
|
"link-card": [
|
|
97
106
|
'import LinkCard from "@ui/link-card/LinkCard.astro";',
|
|
98
107
|
],
|
|
108
|
+
"link-button": [
|
|
109
|
+
'import Button from "@ui/button/Button.astro";',
|
|
110
|
+
],
|
|
99
111
|
avatar: [
|
|
100
112
|
'import Avatar from "@ui/avatar/Avatar.astro";',
|
|
101
113
|
],
|
|
@@ -115,6 +127,30 @@ export function escapeExpr(s) {
|
|
|
115
127
|
export function escapeAttr(s) {
|
|
116
128
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
117
129
|
}
|
|
130
|
+
export function componentAttr(name, value) {
|
|
131
|
+
return /[&"<>]/.test(value)
|
|
132
|
+
? ` ${name}={${escapeExpr(value)}}`
|
|
133
|
+
: ` ${name}="${value}"`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Render extra attributes from an inline element's `attrs` field
|
|
137
|
+
* (links and images today; spans in a future phase) as ` key="val"`
|
|
138
|
+
* pairs prefixed with a leading space, suitable for splicing into an
|
|
139
|
+
* open tag immediately after the element's first-class attrs (href,
|
|
140
|
+
* src, etc.). `id`, `class`, and `style` are first-class; any other
|
|
141
|
+
* key passes through verbatim.
|
|
142
|
+
*/
|
|
143
|
+
function renderInlineElementAttrs(attrs) {
|
|
144
|
+
if (!attrs)
|
|
145
|
+
return "";
|
|
146
|
+
const parts = [];
|
|
147
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
148
|
+
if (value === undefined)
|
|
149
|
+
continue;
|
|
150
|
+
parts.push(` ${key}="${escapeAttr(String(value))}"`);
|
|
151
|
+
}
|
|
152
|
+
return parts.join("");
|
|
153
|
+
}
|
|
118
154
|
/** Escape text for use directly in an Astro template (not inside an expression). */
|
|
119
155
|
export function escapeTemplate(s) {
|
|
120
156
|
let result = "";
|
|
@@ -149,6 +185,12 @@ export function treeToAstro(nodes, options) {
|
|
|
149
185
|
mode: options?.mode ?? "template",
|
|
150
186
|
imageOptimization: options?.imageOptimization ?? false,
|
|
151
187
|
codeBlockTitle: options?.codeBlockTitle ?? true,
|
|
188
|
+
combinedPrefix: options?.combinedPrefix ?? "",
|
|
189
|
+
// Mixed prose+endpoint pages render endpoints as embedded blocks;
|
|
190
|
+
// pages that are nothing but endpoints/headings keep the
|
|
191
|
+
// full-viewport operation layout (the OpenAPI page shape).
|
|
192
|
+
embeddedEndpoints: nodes.some((n) => n.type === "endpoint") &&
|
|
193
|
+
nodes.some((n) => n.type !== "endpoint" && n.type !== "heading" && n.type !== "hr"),
|
|
152
194
|
};
|
|
153
195
|
// Pre-scan for inline `icon` nodes anywhere in the tree.
|
|
154
196
|
// `inlineNodeToTemplate` is intentionally pure (no ctx) — it
|
|
@@ -181,7 +223,18 @@ export function treeToAstro(nodes, options) {
|
|
|
181
223
|
};
|
|
182
224
|
}
|
|
183
225
|
// ─── Node dispatcher ──────────────────────────────────────────────
|
|
226
|
+
/**
|
|
227
|
+
* Dispatch + diff decoration: nodes carrying DiffTree marks (from
|
|
228
|
+
* @dogsbay/tree-diff, read structurally) decorate the rendered
|
|
229
|
+
* snippet; unmarked nodes render byte-identically to before.
|
|
230
|
+
*/
|
|
184
231
|
function nodeToAstro(node, ctx) {
|
|
232
|
+
const rendered = nodeToAstroBase(node, ctx);
|
|
233
|
+
if (!rendered || !hasDiffMarks(node))
|
|
234
|
+
return rendered;
|
|
235
|
+
return decorateDiff(node, rendered, (peer) => nodeToAstroBase(peer, ctx));
|
|
236
|
+
}
|
|
237
|
+
function nodeToAstroBase(node, ctx) {
|
|
185
238
|
switch (node.type) {
|
|
186
239
|
case "prose":
|
|
187
240
|
return proseToAstro(node, ctx);
|
|
@@ -212,6 +265,13 @@ function nodeToAstro(node, ctx) {
|
|
|
212
265
|
return wrapBlock("li", "", node, leafContent(node, ctx));
|
|
213
266
|
case "blockquote":
|
|
214
267
|
return wrapBlock("blockquote", "border-l-4 border-border pl-4 italic text-muted-foreground", node, childrenToAstro(node, ctx));
|
|
268
|
+
// A transparent grouping wrapper: renders its children inside a plain
|
|
269
|
+
// <div> and adds no semantics of its own. Exists so a caller can carry
|
|
270
|
+
// an attribute (a class, a data-* flag) for a whole run of blocks —
|
|
271
|
+
// the release comparison marks a wholly added/removed PAGE once, at
|
|
272
|
+
// the page level, instead of putting a bar on every block inside it.
|
|
273
|
+
case "group":
|
|
274
|
+
return wrapBlock("div", "", node, childrenToAstro(node, ctx));
|
|
215
275
|
case "hr": {
|
|
216
276
|
const classAttr = mergeClassAttr("my-8 border-border", node.props?.class);
|
|
217
277
|
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
@@ -297,6 +357,8 @@ function nodeToAstro(node, ctx) {
|
|
|
297
357
|
return standaloneCardToAstro(node, ctx);
|
|
298
358
|
case "link-card":
|
|
299
359
|
return linkCardToAstro(node, ctx);
|
|
360
|
+
case "link-button":
|
|
361
|
+
return linkButtonToAstro(node, ctx);
|
|
300
362
|
case "accordion":
|
|
301
363
|
return accordionToAstro(node, ctx);
|
|
302
364
|
case "accordion-item":
|
|
@@ -351,6 +413,28 @@ ${childrenToAstro(node, ctx)}
|
|
|
351
413
|
${childrenToAstro(node, ctx)}
|
|
352
414
|
</GridItem>`;
|
|
353
415
|
}
|
|
416
|
+
case "related": {
|
|
417
|
+
// #384 R3: AsciiDoc [role="_additional-resources"] + .Title + list folds
|
|
418
|
+
// to this semantic node (maps to DITA <related-links>). Render a titled
|
|
419
|
+
// section, not a TOC heading. The title is a caption (block title), so a
|
|
420
|
+
// styled <p>, not an <h2>.
|
|
421
|
+
const title = node.props?.title;
|
|
422
|
+
const classAttr = mergeClassAttr("related-resources", node.props?.class);
|
|
423
|
+
const passthrough = renderPassthroughAttrs(node.props, ["title", "class"]);
|
|
424
|
+
const titleHtml = title
|
|
425
|
+
? `<p class="related-resources-title">${escapeTemplate(title)}</p>\n`
|
|
426
|
+
: "";
|
|
427
|
+
return `<section${classAttr}${passthrough}>\n${titleHtml}${childrenToAstro(node, ctx)}\n</section>`;
|
|
428
|
+
}
|
|
429
|
+
case "mdx-raw": {
|
|
430
|
+
// Preserved-but-unmapped MDX component (format-mdx never-drop; see
|
|
431
|
+
// docs-dev/format-mdx-adapters.md). Rendered as a visible source
|
|
432
|
+
// block — the content must not silently disappear from the HTML
|
|
433
|
+
// output. Map the component (adapter / mdx.components config) to
|
|
434
|
+
// upgrade it to a real widget.
|
|
435
|
+
const source = String(node.props?.source ?? "");
|
|
436
|
+
return `<pre class="mdx-raw my-4 overflow-x-auto rounded-md border bg-muted p-4 text-sm"><code>{${escapeExpr(source)}}</code></pre>`;
|
|
437
|
+
}
|
|
354
438
|
default:
|
|
355
439
|
if (node.html)
|
|
356
440
|
return `<Fragment set:html={${escapeExpr(node.html)}} />`;
|
|
@@ -360,37 +444,51 @@ ${childrenToAstro(node, ctx)}
|
|
|
360
444
|
}
|
|
361
445
|
}
|
|
362
446
|
// ─── Inline rendering ─────────────────────────────────────────────
|
|
363
|
-
/**
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
447
|
+
/**
|
|
448
|
+
* Astro's two inline dialects, as emitters over serialize-core's shared walk.
|
|
449
|
+
*
|
|
450
|
+
* These were the THIRD and FOURTH copies of the 11-variant switch (obsidian and
|
|
451
|
+
* dogsbay-md's html fallback were the other two), and the duplication is
|
|
452
|
+
* precisely why `highlight` was missing from BOTH of them: adding an arm meant
|
|
453
|
+
* remembering four places, so `:highlight[…]` rendered as nothing at all on
|
|
454
|
+
* every Astro page. The walk is now shared, so a variant is handled in one
|
|
455
|
+
* place per format instead of being silently skippable.
|
|
456
|
+
*
|
|
457
|
+
* Only the leaf syntax differs between the two maps: template mode escapes for
|
|
458
|
+
* Astro's `{}` and emits real components (`<Icon>`, `<Fragment set:html>`),
|
|
459
|
+
* hybrid mode escapes for plain HTML and emits inert markup.
|
|
460
|
+
*/
|
|
461
|
+
/** Canonical highlight styling — keep in sync with InlineRenderer.astro. */
|
|
462
|
+
const HIGHLIGHT_CLASS = "rounded-sm bg-yellow-200/60 px-0.5 dark:bg-yellow-500/30";
|
|
463
|
+
function templateInlineEmitters() {
|
|
464
|
+
return {
|
|
465
|
+
text: (node, ctx) => applyTextFlags(ctx.escape(node.text), node, {
|
|
466
|
+
bold: (s) => `<strong>${s}</strong>`,
|
|
467
|
+
italic: (s) => `<em>${s}</em>`,
|
|
468
|
+
strike: (s) => `<s>${s}</s>`,
|
|
469
|
+
}),
|
|
470
|
+
link: (node, ctx) => {
|
|
471
|
+
const titleAttr = node.title ? componentAttr("title", node.title) : "";
|
|
472
|
+
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
473
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}${extraAttrs}>${ctx.recurse(node.children)}</a>`;
|
|
474
|
+
},
|
|
475
|
+
image: (node) => {
|
|
476
|
+
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
477
|
+
return `<img src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt ?? "")}" loading="lazy"${extraAttrs} />`;
|
|
478
|
+
},
|
|
479
|
+
code: (node, ctx) => `<code>${ctx.escape(node.text)}</code>`,
|
|
480
|
+
// The arm that did not exist — `:highlight[…]` / `:mark[…]` rendered as
|
|
481
|
+
// nothing at all. The class list mirrors the canonical rendering in
|
|
482
|
+
// `packages/ui/src/content-renderer/InlineRenderer.astro`, so a highlight
|
|
483
|
+
// looks the same however a page is produced. A BARE `<mark>` would have
|
|
484
|
+
// been "fixed" but unstyled: the generated `.docs-prose` stylesheet had no
|
|
485
|
+
// `mark` rule, so browsers paint their default yellow — including on dark
|
|
486
|
+
// pages, where nothing declares `color-scheme`.
|
|
487
|
+
highlight: (node, ctx) => `<mark class="${HIGHLIGHT_CLASS}">${ctx.recurse(node.children)}</mark>`,
|
|
488
|
+
"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>`,
|
|
489
|
+
kbd: (node, ctx) => node.keys.map((k) => `<kbd>${ctx.escape(k)}</kbd>`).join("+"),
|
|
490
|
+
math: (node, ctx) => `<code class="math-inline">${ctx.escape(node.latex)}</code>`,
|
|
491
|
+
icon: (node) => {
|
|
394
492
|
// Inline icon — emits the platform `<Icon>` component which
|
|
395
493
|
// resolves at build time against Lucide (default) plus any
|
|
396
494
|
// other Iconify pack via the `pack:name` shorthand. Caller
|
|
@@ -404,47 +502,29 @@ function inlineNodeToTemplate(node) {
|
|
|
404
502
|
// to Lucide for everything.
|
|
405
503
|
const fullName = node.library ? `${node.library}:${node.name}` : node.name;
|
|
406
504
|
return `<Icon name="${escapeAttr(fullName)}" class="inline-block size-[1em] align-[-0.125em]" />`;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return "<br />";
|
|
413
|
-
default:
|
|
414
|
-
return "";
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
/** Render inline nodes as HTML string (for hybrid mode set:html). */
|
|
418
|
-
function inlineToHtml(nodes) {
|
|
419
|
-
return nodes.map(inlineNodeToHtml).join("");
|
|
505
|
+
},
|
|
506
|
+
// Raw HTML inline — must use set:html to avoid brace issues
|
|
507
|
+
"html-inline": (node) => `<Fragment set:html={${escapeExpr(node.html)}} />`,
|
|
508
|
+
break: () => "<br />",
|
|
509
|
+
};
|
|
420
510
|
}
|
|
421
|
-
function
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
text = `<em>${text}</em>`;
|
|
429
|
-
if (node.strikethrough)
|
|
430
|
-
text = `<s>${text}</s>`;
|
|
431
|
-
return text;
|
|
432
|
-
}
|
|
433
|
-
case "link": {
|
|
511
|
+
function htmlModeInlineEmitters() {
|
|
512
|
+
return {
|
|
513
|
+
...templateInlineEmitters(),
|
|
514
|
+
// Inherited from the template map, this emitted `title={"…"}` — an
|
|
515
|
+
// EXPRESSION, inside a string that becomes a `set:html` value. Astro never
|
|
516
|
+
// parses it as markup, so the reader saw the braces and quotes.
|
|
517
|
+
link: (node, ctx) => {
|
|
434
518
|
const titleAttr = node.title ? ` title="${escapeAttr(node.title)}"` : "";
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return node.keys.map((k) => `<kbd>${escapeHtml(k)}</kbd>`).join("+");
|
|
445
|
-
case "math":
|
|
446
|
-
return `<code class="math-inline">${escapeHtml(node.latex)}</code>`;
|
|
447
|
-
case "icon": {
|
|
519
|
+
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
520
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}${extraAttrs}>${ctx.recurse(node.children)}</a>`;
|
|
521
|
+
},
|
|
522
|
+
text: (node, ctx) => applyTextFlags(ctx.escape(node.text), node, {
|
|
523
|
+
bold: (s) => `<strong>${s}</strong>`,
|
|
524
|
+
italic: (s) => `<em>${s}</em>`,
|
|
525
|
+
strike: (s) => `<s>${s}</s>`,
|
|
526
|
+
}),
|
|
527
|
+
icon: (node) => {
|
|
448
528
|
// Hybrid-mode HTML rendering — fall back to a textual
|
|
449
529
|
// shortcode reference when we can't inline the SVG. Real
|
|
450
530
|
// resolution happens in template mode via the <Icon>
|
|
@@ -452,14 +532,19 @@ function inlineNodeToHtml(node) {
|
|
|
452
532
|
// production output.
|
|
453
533
|
const fullName = node.library ? `${node.library}:${node.name}` : node.name;
|
|
454
534
|
return `<span class="dogsbay-icon" data-icon="${escapeAttr(fullName)}"></span>`;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
535
|
+
},
|
|
536
|
+
"html-inline": (node) => node.html,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const TEMPLATE_INLINE = templateInlineEmitters();
|
|
540
|
+
const HTML_INLINE = htmlModeInlineEmitters();
|
|
541
|
+
/** Render inline nodes as Astro template markup (clean, editable). */
|
|
542
|
+
function inlineToTemplate(nodes) {
|
|
543
|
+
return walkInline(nodes, { emitters: TEMPLATE_INLINE, escape: escapeTemplate });
|
|
544
|
+
}
|
|
545
|
+
/** Render inline nodes as HTML string (for hybrid mode set:html). */
|
|
546
|
+
function inlineToHtml(nodes) {
|
|
547
|
+
return walkInline(nodes, { emitters: HTML_INLINE, escape: escapeHtml });
|
|
463
548
|
}
|
|
464
549
|
/** Render inline content in the current mode. */
|
|
465
550
|
function renderInline(nodes, ctx) {
|
|
@@ -490,15 +575,21 @@ function headingToAstro(node, ctx) {
|
|
|
490
575
|
? `<a href="#${escapeAttr(id)}" class="ml-2 text-muted-foreground opacity-0 group-hover:opacity-100 no-underline" aria-hidden="true" tabindex="-1">¶</a>`
|
|
491
576
|
: "";
|
|
492
577
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : "";
|
|
493
|
-
const
|
|
494
|
-
const
|
|
495
|
-
"level", "slug", "id", "class", "text",
|
|
496
|
-
]);
|
|
578
|
+
const consumed = ["level", "slug", "id", "class", "text"];
|
|
579
|
+
const userClass = node.props?.class;
|
|
497
580
|
if (ctx.mode === "template" && node.inline) {
|
|
581
|
+
// Real Astro markup: expressions are parsed.
|
|
582
|
+
const classAttr = mergeClassAttr("group scroll-mt-20", userClass);
|
|
583
|
+
const passthrough = renderPassthroughAttrs(node.props, consumed);
|
|
498
584
|
const text = inlineToTemplate(node.inline);
|
|
499
585
|
return `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
500
586
|
}
|
|
501
|
-
// Hybrid or no inline —
|
|
587
|
+
// Hybrid or no inline — the whole heading becomes a set:html STRING, where an
|
|
588
|
+
// expression would be literal text. Every MDX/Mintlify heading takes this
|
|
589
|
+
// branch (that importer emits `props.text` + children, never `inline`), so
|
|
590
|
+
// this is the common path, not an edge case.
|
|
591
|
+
const classAttr = mergeClassAttr("group scroll-mt-20", userClass, "html");
|
|
592
|
+
const passthrough = renderPassthroughAttrs(node.props, consumed, "html");
|
|
502
593
|
const text = node.inline ? inlineToHtml(node.inline) : node.props?.text ?? "";
|
|
503
594
|
const html = `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
504
595
|
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
@@ -515,15 +606,26 @@ function paragraphToAstro(node, ctx) {
|
|
|
515
606
|
if (meaningful.length === 1 && meaningful[0].type === "image") {
|
|
516
607
|
const img = meaningful[0];
|
|
517
608
|
ctx.imports.add("image");
|
|
609
|
+
// imageMap is keyed by the unprefixed publicPath built from
|
|
610
|
+
// `import.meta.glob("/src/assets/**", ...)` at runtime, so
|
|
611
|
+
// strip the URL prefix back off if rewriteTreeImageSrcs has
|
|
612
|
+
// already applied it. Keeps the optimized-image lookup working
|
|
613
|
+
// alongside the prefix-on-emit fix.
|
|
614
|
+
const lookupKey = ctx.combinedPrefix && img.src.startsWith(`${ctx.combinedPrefix}/`)
|
|
615
|
+
? img.src.slice(ctx.combinedPrefix.length)
|
|
616
|
+
: img.src;
|
|
518
617
|
const imgDataAttr = ctx.imageOptimization
|
|
519
|
-
? ` imageData={imageMap[${escapeExpr(
|
|
618
|
+
? ` imageData={imageMap[${escapeExpr(lookupKey)}]}`
|
|
520
619
|
: "";
|
|
521
620
|
// Forward arbitrary `data-*` / `aria-*` attrs that plugins
|
|
522
621
|
// may have attached to the wrapping paragraph (e.g.
|
|
523
622
|
// @dogsbay/plugin-image-zoom tags `data-zoomable="true"` so
|
|
524
623
|
// the runtime can attach handlers without scanning the DOM).
|
|
525
624
|
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
526
|
-
|
|
625
|
+
// Author-supplied {.class #id ...} on the image itself
|
|
626
|
+
// (Phase 1.5 of plans/inline-attrs.md).
|
|
627
|
+
const imageAttrs = renderInlineElementAttrs(img.attrs);
|
|
628
|
+
return `<BlockImage${componentAttr("src", img.src)}${componentAttr("alt", img.alt ?? "")}${imageAttrs}${passthrough}${imgDataAttr} />`;
|
|
527
629
|
}
|
|
528
630
|
}
|
|
529
631
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
@@ -563,28 +665,37 @@ function codeToAstro(node, ctx) {
|
|
|
563
665
|
const title = node.props?.title;
|
|
564
666
|
const highlights = node.props?.highlights;
|
|
565
667
|
const lineNumbers = node.props?.lineNumbers;
|
|
566
|
-
const
|
|
668
|
+
const copyText = node.props?.copyText;
|
|
669
|
+
const diffAdd = node.props?.diffAdd;
|
|
670
|
+
const diffRemove = node.props?.diffRemove;
|
|
671
|
+
const isRich = lineNumbers || highlights || code.includes("[!code") || Boolean(diffAdd || diffRemove);
|
|
567
672
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
568
673
|
const consumed = [
|
|
569
674
|
"lang", "language", "code", "title", "highlights", "lineNumbers",
|
|
570
|
-
"class", "ins", "del", "mark", "collapse",
|
|
675
|
+
"class", "ins", "del", "mark", "collapse", "copyText", "diffAdd", "diffRemove",
|
|
571
676
|
];
|
|
572
677
|
const passthrough = renderPassthroughAttrs(node.props, consumed);
|
|
573
678
|
if (isRich) {
|
|
574
679
|
ctx.imports.add("code-rich");
|
|
575
680
|
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
576
681
|
if (title)
|
|
577
|
-
attrs.push(
|
|
682
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
578
683
|
if (lineNumbers)
|
|
579
684
|
attrs.push("lineNumbers");
|
|
580
685
|
if (highlights)
|
|
581
686
|
attrs.push(`highlights="${escapeAttr(highlights)}"`);
|
|
687
|
+
if (copyText !== undefined)
|
|
688
|
+
attrs.push(`copyText={${escapeExpr(copyText)}}`);
|
|
689
|
+
if (diffAdd)
|
|
690
|
+
attrs.push(`diffAdd="${escapeAttr(diffAdd)}"`);
|
|
691
|
+
if (diffRemove)
|
|
692
|
+
attrs.push(`diffRemove="${escapeAttr(diffRemove)}"`);
|
|
582
693
|
return `<CodeRich ${attrs.join(" ")}${classAttr}${passthrough} />`;
|
|
583
694
|
}
|
|
584
695
|
ctx.imports.add("code");
|
|
585
696
|
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
586
697
|
if (title)
|
|
587
|
-
attrs.push(
|
|
698
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
588
699
|
if (ctx.codeBlockTitle !== true) {
|
|
589
700
|
attrs.push(`showTitle="${ctx.codeBlockTitle}"`);
|
|
590
701
|
}
|
|
@@ -600,6 +711,17 @@ const CALLOUT_VARIANT_MAP = {
|
|
|
600
711
|
hint: "tip",
|
|
601
712
|
attention: "warning",
|
|
602
713
|
};
|
|
714
|
+
// Default title shown when a callout has no explicit one (post-variant-map).
|
|
715
|
+
// Keyed by the mapped variant. Falls back to a capitalised variant otherwise.
|
|
716
|
+
const CALLOUT_LABELS = {
|
|
717
|
+
note: "Note",
|
|
718
|
+
tip: "Tip",
|
|
719
|
+
info: "Important",
|
|
720
|
+
warning: "Warning",
|
|
721
|
+
danger: "Caution",
|
|
722
|
+
failure: "Error",
|
|
723
|
+
success: "Success",
|
|
724
|
+
};
|
|
603
725
|
function calloutToAstro(node, ctx) {
|
|
604
726
|
ctx.imports.add("callout");
|
|
605
727
|
// Callout variant may live on props.variant (Starlight / MkDocs importers)
|
|
@@ -608,7 +730,14 @@ function calloutToAstro(node, ctx) {
|
|
|
608
730
|
?? node.props?.type
|
|
609
731
|
?? "note").toLowerCase();
|
|
610
732
|
const variant = CALLOUT_VARIANT_MAP[rawVariant] ?? rawVariant;
|
|
611
|
-
|
|
733
|
+
// When the source gives no explicit title, fall back to the type label
|
|
734
|
+
// (Note / Warning / …) instead of an empty <AlertTitle> — AsciiDoc
|
|
735
|
+
// admonitions and GitHub-style alerts both show the type next to the icon,
|
|
736
|
+
// and an empty title leaves the icon standing alone. Issue #004.
|
|
737
|
+
const explicitTitle = node.props?.title;
|
|
738
|
+
const title = explicitTitle && explicitTitle.length > 0
|
|
739
|
+
? explicitTitle
|
|
740
|
+
: CALLOUT_LABELS[variant] ?? variant.charAt(0).toUpperCase() + variant.slice(1);
|
|
612
741
|
const icon = node.props?.icon;
|
|
613
742
|
const iconAttr = icon ? ` icon="${escapeAttr(icon)}"` : "";
|
|
614
743
|
// User classes and passthrough attributes (id, data-*, aria-*, style)
|
|
@@ -629,15 +758,19 @@ ${indentStr(inner, 4)}
|
|
|
629
758
|
* Concatenates (user classes appended) so utility frameworks like Tailwind
|
|
630
759
|
* resolve conflicts via component-internal class-variance-authority / tv().
|
|
631
760
|
*/
|
|
632
|
-
function mergeClassAttr(defaults, userClass) {
|
|
761
|
+
function mergeClassAttr(defaults, userClass, target = "astro") {
|
|
633
762
|
const combined = [defaults, userClass].filter(Boolean).join(" ").trim();
|
|
634
|
-
|
|
763
|
+
if (!combined)
|
|
764
|
+
return "";
|
|
765
|
+
return target === "html"
|
|
766
|
+
? ` class="${escapeAttr(combined)}"`
|
|
767
|
+
: componentAttr("class", combined);
|
|
635
768
|
}
|
|
636
769
|
/**
|
|
637
770
|
* Render attributes not consumed as component props — `id`, `data-*`, `aria-*`,
|
|
638
771
|
* and `style`. These pass through to the rendered element.
|
|
639
772
|
*/
|
|
640
|
-
function renderPassthroughAttrs(props, consumed) {
|
|
773
|
+
function renderPassthroughAttrs(props, consumed, target = "astro") {
|
|
641
774
|
if (!props)
|
|
642
775
|
return "";
|
|
643
776
|
const skip = new Set([...consumed, "source", "children"]);
|
|
@@ -652,9 +785,12 @@ function renderPassthroughAttrs(props, consumed) {
|
|
|
652
785
|
if (value === true) {
|
|
653
786
|
parts.push(key);
|
|
654
787
|
}
|
|
655
|
-
else {
|
|
788
|
+
else if (target === "html") {
|
|
656
789
|
parts.push(`${key}="${escapeAttr(String(value))}"`);
|
|
657
790
|
}
|
|
791
|
+
else {
|
|
792
|
+
parts.push(componentAttr(key, String(value)).trimStart());
|
|
793
|
+
}
|
|
658
794
|
}
|
|
659
795
|
return parts.length ? " " + parts.join(" ") : "";
|
|
660
796
|
}
|
|
@@ -724,7 +860,7 @@ function detailsToAstro(node, ctx) {
|
|
|
724
860
|
const open = node.props?.open;
|
|
725
861
|
const icon = node.props?.icon;
|
|
726
862
|
const inner = childrenToAstro(node, ctx);
|
|
727
|
-
const attrs = [`variant="${escapeAttr(variant)}"`,
|
|
863
|
+
const attrs = [`variant="${escapeAttr(variant)}"`, componentAttr("title", title).trimStart()];
|
|
728
864
|
if (open)
|
|
729
865
|
attrs.push("open");
|
|
730
866
|
if (icon)
|
|
@@ -749,18 +885,62 @@ function tabsToAstro(node, ctx) {
|
|
|
749
885
|
return { value, label, node: tab };
|
|
750
886
|
});
|
|
751
887
|
const defaultValue = tabItems[0].value;
|
|
888
|
+
// Diff-trigger indicator: a marked tab panel is invisible until
|
|
889
|
+
// opened, so its TRIGGER carries a data-diff-tab attribute (styled
|
|
890
|
+
// as a colored dot by DIFF_CSS) telling the reader WHICH tab to
|
|
891
|
+
// open. Structural read of @dogsbay/tree-diff marks, same as
|
|
892
|
+
// diff-decoration; unmarked trees are unaffected.
|
|
893
|
+
const tabDiffSignal = (tab) => {
|
|
894
|
+
const own = tab.diff;
|
|
895
|
+
if (own)
|
|
896
|
+
return own;
|
|
897
|
+
const deep = (n) => Boolean(n.diff || n.diffInline) || (n.children ?? []).some(deep);
|
|
898
|
+
return (tab.children ?? []).some(deep) ? "changed" : undefined;
|
|
899
|
+
};
|
|
900
|
+
// The indicator must not live in COLOUR alone (WCAG 1.4.1), and a
|
|
901
|
+
// background-tinted dot disappears in forced-colors mode. So it is a
|
|
902
|
+
// GLYPH (shape carries the meaning) plus visually-hidden text, so the
|
|
903
|
+
// trigger announces "Astro, changed in this comparison" rather than
|
|
904
|
+
// relying on a `title` screen readers may never speak.
|
|
905
|
+
const TAB_GLYPH = {
|
|
906
|
+
added: "+",
|
|
907
|
+
changed: "•",
|
|
908
|
+
removed: "−",
|
|
909
|
+
moved: "→",
|
|
910
|
+
};
|
|
752
911
|
const triggers = tabItems
|
|
753
|
-
.map((t) =>
|
|
912
|
+
.map((t) => {
|
|
913
|
+
const signal = tabDiffSignal(t.node);
|
|
914
|
+
if (!signal) {
|
|
915
|
+
return ` <TabsTrigger${componentAttr("value", t.value)}>${escapeTemplate(t.label)}</TabsTrigger>`;
|
|
916
|
+
}
|
|
917
|
+
const glyph = TAB_GLYPH[signal] ?? "•";
|
|
918
|
+
// The glyph and its announced text are ONE element: "Hide
|
|
919
|
+
// highlights" hides `.db-tab-mark`, and a sibling sr-only span
|
|
920
|
+
// would survive that — leaving screen-reader users still hearing
|
|
921
|
+
// "changed in this comparison" on every tab while the visible
|
|
922
|
+
// marker is gone (code review, 2026-07-13).
|
|
923
|
+
const marker = `<span class="db-tab-mark" data-diff-tab="${escapeAttr(signal)}">` +
|
|
924
|
+
`<span aria-hidden="true">${glyph}</span>` +
|
|
925
|
+
`<span class="sr-only">${escapeTemplate(signal)} in this comparison</span>` +
|
|
926
|
+
`</span>`;
|
|
927
|
+
// `value` MUST match TabsContent's, which is a componentAttr expression:
|
|
928
|
+
// a title containing `&` was emitted `&` here and `&` there, so the
|
|
929
|
+
// trigger and its panel no longer paired and the tab rendered empty.
|
|
930
|
+
// `data-diff-tab` is a plain data attribute — entity escaping is right.
|
|
931
|
+
return ` <TabsTrigger${componentAttr("value", t.value)} data-diff-tab="${escapeAttr(signal)}">${escapeTemplate(t.label)}<Fragment set:html={${escapeExpr(marker)}} /></TabsTrigger>`;
|
|
932
|
+
})
|
|
754
933
|
.join("\n");
|
|
755
934
|
const contents = tabItems
|
|
756
935
|
.map((t) => {
|
|
757
936
|
const inner = childrenToAstro(t.node, ctx);
|
|
758
|
-
return ` <TabsContent
|
|
937
|
+
return ` <TabsContent${componentAttr("value", t.value)}>\n${indentStr(inner, 4)}\n </TabsContent>`;
|
|
759
938
|
})
|
|
760
939
|
.join("\n");
|
|
761
940
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
762
941
|
const passthrough = renderPassthroughAttrs(node.props, ["sync", "default", "class"]);
|
|
763
|
-
|
|
942
|
+
// Same encoding as the trigger/content `value` it selects — see above.
|
|
943
|
+
return `<Tabs${componentAttr("defaultValue", defaultValue)}${classAttr}${passthrough}>
|
|
764
944
|
<TabsList>
|
|
765
945
|
${triggers}
|
|
766
946
|
</TabsList>
|
|
@@ -770,8 +950,10 @@ ${contents}
|
|
|
770
950
|
function tableToAstro(node, ctx) {
|
|
771
951
|
ctx.imports.add("table");
|
|
772
952
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
773
|
-
const
|
|
774
|
-
|
|
953
|
+
const caption = node.props?.caption;
|
|
954
|
+
const captionAttr = caption ? ` caption={${escapeExpr(caption)}}` : "";
|
|
955
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class", "caption"]);
|
|
956
|
+
return `<Table${classAttr}${captionAttr}${passthrough}>\n${childrenToAstro(node, ctx)}\n</Table>`;
|
|
775
957
|
}
|
|
776
958
|
function diagramToAstro(node, ctx) {
|
|
777
959
|
ctx.imports.add("diagram");
|
|
@@ -781,7 +963,7 @@ function diagramToAstro(node, ctx) {
|
|
|
781
963
|
const title = node.props?.title;
|
|
782
964
|
const attrs = [`lang="${escapeAttr(lang)}"`, `code={${escapeExpr(code)}}`, `svg={${escapeExpr(svg)}}`];
|
|
783
965
|
if (title)
|
|
784
|
-
attrs.push(
|
|
966
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
785
967
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
786
968
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
787
969
|
"lang", "code", "svg", "title", "class",
|
|
@@ -794,7 +976,7 @@ function youtubeToAstro(node, ctx) {
|
|
|
794
976
|
const title = node.props?.title ?? "";
|
|
795
977
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
796
978
|
const passthrough = renderPassthroughAttrs(node.props, ["id", "title", "class"]);
|
|
797
|
-
return `<YouTube
|
|
979
|
+
return `<YouTube${componentAttr("id", id)}${componentAttr("title", title)}${classAttr}${passthrough} />`;
|
|
798
980
|
}
|
|
799
981
|
function mathBlockToAstro(node, ctx) {
|
|
800
982
|
ctx.imports.add("math-block");
|
|
@@ -853,7 +1035,11 @@ function apiSymbolToAstro(node, ctx) {
|
|
|
853
1035
|
const signature = node.props?.signature;
|
|
854
1036
|
const bases = node.props?.bases || [];
|
|
855
1037
|
const level = node.type === "api-class" ? 2 : 3;
|
|
856
|
-
const attrs = [
|
|
1038
|
+
const attrs = [
|
|
1039
|
+
componentAttr("name", name).trimStart(),
|
|
1040
|
+
componentAttr("kind", kind).trimStart(),
|
|
1041
|
+
`level={${level}}`,
|
|
1042
|
+
];
|
|
857
1043
|
if (signature)
|
|
858
1044
|
attrs.push(`signature={${escapeExpr(signature)}}`);
|
|
859
1045
|
if (bases.length > 0)
|
|
@@ -975,7 +1161,8 @@ function endpointToAstro(node, ctx) {
|
|
|
975
1161
|
const codeBody = codePanels.length > 0
|
|
976
1162
|
? `\n${indentStr(codePanels.join("\n"), 4)}\n `
|
|
977
1163
|
: "";
|
|
978
|
-
|
|
1164
|
+
const layoutAttrs = ctx.embeddedEndpoints ? ' variant="embedded"' : "";
|
|
1165
|
+
return (`<ApiLayout${layoutAttrs}>\n` +
|
|
979
1166
|
` <EndpointCard ${cardAttrs.join(" ")}>${sectionsBody}</EndpointCard>\n` +
|
|
980
1167
|
` <ApiCodePanel slot="code">${codeBody}</ApiCodePanel>\n` +
|
|
981
1168
|
`</ApiLayout>`);
|
|
@@ -1007,8 +1194,7 @@ function cardsToAstro(node, ctx) {
|
|
|
1007
1194
|
// top of the page.
|
|
1008
1195
|
let iconHtml = "";
|
|
1009
1196
|
if (icon) {
|
|
1010
|
-
|
|
1011
|
-
iconHtml = `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1197
|
+
iconHtml = cardIconHtml(icon, title, ctx);
|
|
1012
1198
|
}
|
|
1013
1199
|
const titleHtml = title ? `<h3 class="text-base font-semibold">${escapeTemplate(title)}</h3>` : "";
|
|
1014
1200
|
const desc = inner
|
|
@@ -1089,6 +1275,45 @@ function standaloneCardToAstro(node, ctx) {
|
|
|
1089
1275
|
* If the parser kept rich body children instead of folding into description,
|
|
1090
1276
|
* we fall back to the standalone-card path which can hold arbitrary content.
|
|
1091
1277
|
*/
|
|
1278
|
+
/**
|
|
1279
|
+
* `link-button` — a call-to-action link styled as a button.
|
|
1280
|
+
*
|
|
1281
|
+
* There was no case for this at all, so it fell through to rendering the
|
|
1282
|
+
* node's CHILDREN: `<p>Get started</p>`. The label survived, the href did not,
|
|
1283
|
+
* and nothing warned — the reader saw a stray sentence where the page's
|
|
1284
|
+
* primary call to action should be. Every importer that produces one was
|
|
1285
|
+
* affected; `Button.astro` has taken an `href` all along.
|
|
1286
|
+
*/
|
|
1287
|
+
/**
|
|
1288
|
+
* A card icon is either a registry NAME or a path to an image file.
|
|
1289
|
+
*
|
|
1290
|
+
* `<Icon name>` resolves names and renders NOTHING when the name does not
|
|
1291
|
+
* resolve — so a path handed to it disappeared silently, which is how every
|
|
1292
|
+
* "Related products" card lost its logo without anyone noticing.
|
|
1293
|
+
*/
|
|
1294
|
+
function cardIconHtml(icon, title, ctx) {
|
|
1295
|
+
if (/\.(svg|png|jpe?g|webp|avif)$/i.test(icon)) {
|
|
1296
|
+
return `<img src="${escapeAttr(icon)}" alt="" class="size-5 mb-2" loading="lazy" />`;
|
|
1297
|
+
}
|
|
1298
|
+
ctx.imports.add("icon");
|
|
1299
|
+
return `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1300
|
+
}
|
|
1301
|
+
function linkButtonToAstro(node, ctx) {
|
|
1302
|
+
ctx.imports.add("link-button");
|
|
1303
|
+
const href = node.props?.href ?? "";
|
|
1304
|
+
const variant = node.props?.variant;
|
|
1305
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1306
|
+
const passthrough = renderPassthroughAttrs(node.props, ["href", "variant", "class"]);
|
|
1307
|
+
// A button's label is phrasing content, so a lone paragraph child is
|
|
1308
|
+
// unwrapped — `<Button><p>Get started</p></Button>` is invalid markup and
|
|
1309
|
+
// renders with the paragraph's block spacing inside the button.
|
|
1310
|
+
const kids = node.children ?? [];
|
|
1311
|
+
const soleParagraph = kids.length === 1 && kids[0].type === "paragraph" ? kids[0] : undefined;
|
|
1312
|
+
const label = soleParagraph
|
|
1313
|
+
? leafContent(soleParagraph, ctx)
|
|
1314
|
+
: leafContent(node, ctx) || childrenToAstro(node, ctx);
|
|
1315
|
+
return `<Button${componentAttr("href", href)}${variant ? componentAttr("variant", variant) : ""}${classAttr}${passthrough}>${label}</Button>`;
|
|
1316
|
+
}
|
|
1092
1317
|
function linkCardToAstro(node, ctx) {
|
|
1093
1318
|
const hasRichChildren = (node.children ?? []).length > 0;
|
|
1094
1319
|
if (hasRichChildren) {
|
|
@@ -1098,9 +1323,9 @@ function linkCardToAstro(node, ctx) {
|
|
|
1098
1323
|
const title = node.props?.title ?? "";
|
|
1099
1324
|
const description = node.props?.description;
|
|
1100
1325
|
const href = node.props?.href ?? "";
|
|
1101
|
-
const titleAttr =
|
|
1102
|
-
const hrefAttr =
|
|
1103
|
-
const descAttr = description ?
|
|
1326
|
+
const titleAttr = componentAttr("title", title);
|
|
1327
|
+
const hrefAttr = componentAttr("href", href);
|
|
1328
|
+
const descAttr = description ? componentAttr("description", description) : "";
|
|
1104
1329
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1105
1330
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1106
1331
|
"title", "description", "href", "icon", "class",
|
|
@@ -1124,9 +1349,9 @@ function accordionToAstro(node, ctx) {
|
|
|
1124
1349
|
const defaultValue = node.props?.defaultValue;
|
|
1125
1350
|
const typeAttr = ` type="${escapeAttr(type)}"`;
|
|
1126
1351
|
const defaultAttr = defaultValue !== undefined
|
|
1127
|
-
?
|
|
1128
|
-
?
|
|
1129
|
-
: `{${JSON.stringify(defaultValue)}}`
|
|
1352
|
+
? typeof defaultValue === "string"
|
|
1353
|
+
? componentAttr("defaultValue", defaultValue)
|
|
1354
|
+
: ` defaultValue={${JSON.stringify(defaultValue)}}`
|
|
1130
1355
|
: "";
|
|
1131
1356
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1132
1357
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
@@ -1142,7 +1367,7 @@ function accordionItemToAstro(node, ctx, standalone) {
|
|
|
1142
1367
|
ctx.imports.add("accordion");
|
|
1143
1368
|
const value = node.props?.value ?? "item-1";
|
|
1144
1369
|
const label = node.props?.label ?? node.props?.title ?? "Item";
|
|
1145
|
-
const valueAttr =
|
|
1370
|
+
const valueAttr = componentAttr("value", value);
|
|
1146
1371
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1147
1372
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1148
1373
|
"value", "label", "title", "class",
|
|
@@ -1169,9 +1394,9 @@ function avatarToAstro(node, ctx) {
|
|
|
1169
1394
|
const src = node.props?.src ?? "";
|
|
1170
1395
|
const alt = node.props?.alt ?? "";
|
|
1171
1396
|
const fallback = node.props?.fallback;
|
|
1172
|
-
const srcAttr = src ?
|
|
1173
|
-
const altAttr =
|
|
1174
|
-
const fbAttr = fallback ?
|
|
1397
|
+
const srcAttr = src ? componentAttr("src", src) : "";
|
|
1398
|
+
const altAttr = componentAttr("alt", alt);
|
|
1399
|
+
const fbAttr = fallback ? componentAttr("fallback", fallback) : "";
|
|
1175
1400
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1176
1401
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1177
1402
|
"src", "alt", "fallback", "class",
|
|
@@ -1192,7 +1417,7 @@ function exampleToAstro(node, ctx) {
|
|
|
1192
1417
|
const source = String(node.props?.source ?? "");
|
|
1193
1418
|
const title = node.props?.title;
|
|
1194
1419
|
const showFallback = node.props?.fallback === true || node.props?.fallback === "true";
|
|
1195
|
-
const titleAttr = title ?
|
|
1420
|
+
const titleAttr = title ? componentAttr("title", title) : "";
|
|
1196
1421
|
const fallbackAttr = showFallback ? " showFallback" : "";
|
|
1197
1422
|
const inner = childrenToAstro(node, ctx);
|
|
1198
1423
|
return `<MarkdownExample source={${escapeExpr(source)}}${titleAttr}${fallbackAttr}>\n${inner}\n</MarkdownExample>`;
|
|
@@ -1212,24 +1437,25 @@ function htmlContainerToAstro(node, ctx) {
|
|
|
1212
1437
|
}
|
|
1213
1438
|
// ─── Utilities ────────────────────────────────────────────────────
|
|
1214
1439
|
/**
|
|
1215
|
-
* Render a node's content
|
|
1216
|
-
* parser
|
|
1217
|
-
*
|
|
1440
|
+
* Render a node's content across all THREE TreeNode shapes: flat `inline`
|
|
1441
|
+
* (dogsbay-md parser), wrapped `children[{prose, inline}]` (Starlight
|
|
1442
|
+
* importer), and a node carrying only `html` (MDX/Starlight/MkDocs). Inline
|
|
1443
|
+
* comes before children when both are present.
|
|
1218
1444
|
*
|
|
1219
1445
|
* Used for list-item, table cells, step, dt, dd — types that commonly
|
|
1220
1446
|
* carry leaf text content directly on the node.
|
|
1447
|
+
*
|
|
1448
|
+
* This function is where serialize-core's `renderLeaf` was extracted FROM, so
|
|
1449
|
+
* delegating to it closes the loop: the wrapper-dedup and html-shape handling
|
|
1450
|
+
* the core grew afterwards (from the Docusaurus dogfooding bug) now flow back
|
|
1451
|
+
* here instead of the two implementations continuing to drift apart.
|
|
1221
1452
|
*/
|
|
1222
1453
|
function leafContent(node, ctx) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const rendered = childrenToAstro(node, ctx);
|
|
1229
|
-
if (rendered)
|
|
1230
|
-
parts.push(rendered);
|
|
1231
|
-
}
|
|
1232
|
-
return parts.join("\n");
|
|
1454
|
+
return renderLeaf(node, {
|
|
1455
|
+
inline: inlineToTemplate,
|
|
1456
|
+
children: () => childrenToAstro(node, ctx),
|
|
1457
|
+
html: (html) => `<Fragment set:html={${escapeExpr(html)}} />`,
|
|
1458
|
+
});
|
|
1233
1459
|
}
|
|
1234
1460
|
function childrenToAstro(node, ctx) {
|
|
1235
1461
|
if (!node.children || node.children.length === 0)
|
|
@@ -1269,13 +1495,8 @@ function inlineHasIcon(node) {
|
|
|
1269
1495
|
}
|
|
1270
1496
|
return false;
|
|
1271
1497
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
return text
|
|
1275
|
-
.split("\n")
|
|
1276
|
-
.map((line) => (line ? `${prefix}${line}` : line))
|
|
1277
|
-
.join("\n");
|
|
1278
|
-
}
|
|
1498
|
+
/** Indent, via the core's shared implementation (was a fourth local copy). */
|
|
1499
|
+
const indentStr = indent;
|
|
1279
1500
|
function escapeHtml(s) {
|
|
1280
1501
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1281
1502
|
}
|