@dogsbay/format-astro 0.2.0-beta.93 → 0.2.0-beta.99
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/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/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/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/lead.d.ts.map +1 -1
- package/dist/lead.js +30 -6
- package/dist/lead.js.map +1 -1
- package/dist/llms-txt.d.ts +35 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +52 -2
- package/dist/llms-txt.js.map +1 -1
- package/dist/project.d.ts +121 -8
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +1121 -142
- package/dist/project.js.map +1 -1
- package/dist/serialize.d.ts +1 -0
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +283 -137
- package/dist/serialize.js.map +1 -1
- package/dist/sitemap.d.ts +24 -0
- package/dist/sitemap.d.ts.map +1 -1
- package/dist/sitemap.js +73 -9
- package/dist/sitemap.js.map +1 -1
- package/dist/taxonomy.d.ts +24 -1
- package/dist/taxonomy.d.ts.map +1 -1
- package/dist/taxonomy.js +23 -2
- package/dist/taxonomy.js.map +1 -1
- package/package.json +8 -7
package/dist/serialize.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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
|
|
@@ -103,6 +105,9 @@ const COMPONENT_IMPORTS = {
|
|
|
103
105
|
"link-card": [
|
|
104
106
|
'import LinkCard from "@ui/link-card/LinkCard.astro";',
|
|
105
107
|
],
|
|
108
|
+
"link-button": [
|
|
109
|
+
'import Button from "@ui/button/Button.astro";',
|
|
110
|
+
],
|
|
106
111
|
avatar: [
|
|
107
112
|
'import Avatar from "@ui/avatar/Avatar.astro";',
|
|
108
113
|
],
|
|
@@ -122,6 +127,11 @@ export function escapeExpr(s) {
|
|
|
122
127
|
export function escapeAttr(s) {
|
|
123
128
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
124
129
|
}
|
|
130
|
+
export function componentAttr(name, value) {
|
|
131
|
+
return /[&"<>]/.test(value)
|
|
132
|
+
? ` ${name}={${escapeExpr(value)}}`
|
|
133
|
+
: ` ${name}="${value}"`;
|
|
134
|
+
}
|
|
125
135
|
/**
|
|
126
136
|
* Render extra attributes from an inline element's `attrs` field
|
|
127
137
|
* (links and images today; spans in a future phase) as ` key="val"`
|
|
@@ -176,6 +186,11 @@ export function treeToAstro(nodes, options) {
|
|
|
176
186
|
imageOptimization: options?.imageOptimization ?? false,
|
|
177
187
|
codeBlockTitle: options?.codeBlockTitle ?? true,
|
|
178
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"),
|
|
179
194
|
};
|
|
180
195
|
// Pre-scan for inline `icon` nodes anywhere in the tree.
|
|
181
196
|
// `inlineNodeToTemplate` is intentionally pure (no ctx) — it
|
|
@@ -208,7 +223,18 @@ export function treeToAstro(nodes, options) {
|
|
|
208
223
|
};
|
|
209
224
|
}
|
|
210
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
|
+
*/
|
|
211
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) {
|
|
212
238
|
switch (node.type) {
|
|
213
239
|
case "prose":
|
|
214
240
|
return proseToAstro(node, ctx);
|
|
@@ -239,6 +265,13 @@ function nodeToAstro(node, ctx) {
|
|
|
239
265
|
return wrapBlock("li", "", node, leafContent(node, ctx));
|
|
240
266
|
case "blockquote":
|
|
241
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));
|
|
242
275
|
case "hr": {
|
|
243
276
|
const classAttr = mergeClassAttr("my-8 border-border", node.props?.class);
|
|
244
277
|
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
@@ -324,6 +357,8 @@ function nodeToAstro(node, ctx) {
|
|
|
324
357
|
return standaloneCardToAstro(node, ctx);
|
|
325
358
|
case "link-card":
|
|
326
359
|
return linkCardToAstro(node, ctx);
|
|
360
|
+
case "link-button":
|
|
361
|
+
return linkButtonToAstro(node, ctx);
|
|
327
362
|
case "accordion":
|
|
328
363
|
return accordionToAstro(node, ctx);
|
|
329
364
|
case "accordion-item":
|
|
@@ -391,6 +426,15 @@ ${childrenToAstro(node, ctx)}
|
|
|
391
426
|
: "";
|
|
392
427
|
return `<section${classAttr}${passthrough}>\n${titleHtml}${childrenToAstro(node, ctx)}\n</section>`;
|
|
393
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
|
+
}
|
|
394
438
|
default:
|
|
395
439
|
if (node.html)
|
|
396
440
|
return `<Fragment set:html={${escapeExpr(node.html)}} />`;
|
|
@@ -400,40 +444,51 @@ ${childrenToAstro(node, ctx)}
|
|
|
400
444
|
}
|
|
401
445
|
}
|
|
402
446
|
// ─── Inline rendering ─────────────────────────────────────────────
|
|
403
|
-
/**
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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) : "";
|
|
421
472
|
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
422
|
-
return `<a href="${escapeAttr(node.href)}"${titleAttr}${extraAttrs}>${
|
|
423
|
-
}
|
|
424
|
-
|
|
473
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}${extraAttrs}>${ctx.recurse(node.children)}</a>`;
|
|
474
|
+
},
|
|
475
|
+
image: (node) => {
|
|
425
476
|
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
426
477
|
return `<img src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt ?? "")}" loading="lazy"${extraAttrs} />`;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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) => {
|
|
437
492
|
// Inline icon — emits the platform `<Icon>` component which
|
|
438
493
|
// resolves at build time against Lucide (default) plus any
|
|
439
494
|
// other Iconify pack via the `pack:name` shorthand. Caller
|
|
@@ -447,50 +502,29 @@ function inlineNodeToTemplate(node) {
|
|
|
447
502
|
// to Lucide for everything.
|
|
448
503
|
const fullName = node.library ? `${node.library}:${node.name}` : node.name;
|
|
449
504
|
return `<Icon name="${escapeAttr(fullName)}" class="inline-block size-[1em] align-[-0.125em]" />`;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return "<br />";
|
|
456
|
-
default:
|
|
457
|
-
return "";
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/** Render inline nodes as HTML string (for hybrid mode set:html). */
|
|
461
|
-
function inlineToHtml(nodes) {
|
|
462
|
-
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
|
+
};
|
|
463
510
|
}
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
text = `<em>${text}</em>`;
|
|
472
|
-
if (node.strikethrough)
|
|
473
|
-
text = `<s>${text}</s>`;
|
|
474
|
-
return text;
|
|
475
|
-
}
|
|
476
|
-
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) => {
|
|
477
518
|
const titleAttr = node.title ? ` title="${escapeAttr(node.title)}"` : "";
|
|
478
519
|
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
479
|
-
return `<a href="${escapeAttr(node.href)}"${titleAttr}${extraAttrs}>${
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
case "footnote-ref":
|
|
488
|
-
return `<sup><a href="#fn-${escapeAttr(node.label)}" id="fnref-${escapeAttr(node.label)}" class="text-primary hover:underline">[${escapeHtml(node.label)}]</a></sup>`;
|
|
489
|
-
case "kbd":
|
|
490
|
-
return node.keys.map((k) => `<kbd>${escapeHtml(k)}</kbd>`).join("+");
|
|
491
|
-
case "math":
|
|
492
|
-
return `<code class="math-inline">${escapeHtml(node.latex)}</code>`;
|
|
493
|
-
case "icon": {
|
|
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) => {
|
|
494
528
|
// Hybrid-mode HTML rendering — fall back to a textual
|
|
495
529
|
// shortcode reference when we can't inline the SVG. Real
|
|
496
530
|
// resolution happens in template mode via the <Icon>
|
|
@@ -498,14 +532,19 @@ function inlineNodeToHtml(node) {
|
|
|
498
532
|
// production output.
|
|
499
533
|
const fullName = node.library ? `${node.library}:${node.name}` : node.name;
|
|
500
534
|
return `<span class="dogsbay-icon" data-icon="${escapeAttr(fullName)}"></span>`;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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 });
|
|
509
548
|
}
|
|
510
549
|
/** Render inline content in the current mode. */
|
|
511
550
|
function renderInline(nodes, ctx) {
|
|
@@ -536,15 +575,21 @@ function headingToAstro(node, ctx) {
|
|
|
536
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>`
|
|
537
576
|
: "";
|
|
538
577
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : "";
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
"level", "slug", "id", "class", "text",
|
|
542
|
-
]);
|
|
578
|
+
const consumed = ["level", "slug", "id", "class", "text"];
|
|
579
|
+
const userClass = node.props?.class;
|
|
543
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);
|
|
544
584
|
const text = inlineToTemplate(node.inline);
|
|
545
585
|
return `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
546
586
|
}
|
|
547
|
-
// 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");
|
|
548
593
|
const text = node.inline ? inlineToHtml(node.inline) : node.props?.text ?? "";
|
|
549
594
|
const html = `<h${level}${idAttr}${classAttr}${passthrough}>${text}${anchor}</h${level}>`;
|
|
550
595
|
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
@@ -580,7 +625,7 @@ function paragraphToAstro(node, ctx) {
|
|
|
580
625
|
// Author-supplied {.class #id ...} on the image itself
|
|
581
626
|
// (Phase 1.5 of plans/inline-attrs.md).
|
|
582
627
|
const imageAttrs = renderInlineElementAttrs(img.attrs);
|
|
583
|
-
return `<BlockImage
|
|
628
|
+
return `<BlockImage${componentAttr("src", img.src)}${componentAttr("alt", img.alt ?? "")}${imageAttrs}${passthrough}${imgDataAttr} />`;
|
|
584
629
|
}
|
|
585
630
|
}
|
|
586
631
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
@@ -620,28 +665,37 @@ function codeToAstro(node, ctx) {
|
|
|
620
665
|
const title = node.props?.title;
|
|
621
666
|
const highlights = node.props?.highlights;
|
|
622
667
|
const lineNumbers = node.props?.lineNumbers;
|
|
623
|
-
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);
|
|
624
672
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
625
673
|
const consumed = [
|
|
626
674
|
"lang", "language", "code", "title", "highlights", "lineNumbers",
|
|
627
|
-
"class", "ins", "del", "mark", "collapse",
|
|
675
|
+
"class", "ins", "del", "mark", "collapse", "copyText", "diffAdd", "diffRemove",
|
|
628
676
|
];
|
|
629
677
|
const passthrough = renderPassthroughAttrs(node.props, consumed);
|
|
630
678
|
if (isRich) {
|
|
631
679
|
ctx.imports.add("code-rich");
|
|
632
680
|
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
633
681
|
if (title)
|
|
634
|
-
attrs.push(
|
|
682
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
635
683
|
if (lineNumbers)
|
|
636
684
|
attrs.push("lineNumbers");
|
|
637
685
|
if (highlights)
|
|
638
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)}"`);
|
|
639
693
|
return `<CodeRich ${attrs.join(" ")}${classAttr}${passthrough} />`;
|
|
640
694
|
}
|
|
641
695
|
ctx.imports.add("code");
|
|
642
696
|
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
643
697
|
if (title)
|
|
644
|
-
attrs.push(
|
|
698
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
645
699
|
if (ctx.codeBlockTitle !== true) {
|
|
646
700
|
attrs.push(`showTitle="${ctx.codeBlockTitle}"`);
|
|
647
701
|
}
|
|
@@ -704,15 +758,19 @@ ${indentStr(inner, 4)}
|
|
|
704
758
|
* Concatenates (user classes appended) so utility frameworks like Tailwind
|
|
705
759
|
* resolve conflicts via component-internal class-variance-authority / tv().
|
|
706
760
|
*/
|
|
707
|
-
function mergeClassAttr(defaults, userClass) {
|
|
761
|
+
function mergeClassAttr(defaults, userClass, target = "astro") {
|
|
708
762
|
const combined = [defaults, userClass].filter(Boolean).join(" ").trim();
|
|
709
|
-
|
|
763
|
+
if (!combined)
|
|
764
|
+
return "";
|
|
765
|
+
return target === "html"
|
|
766
|
+
? ` class="${escapeAttr(combined)}"`
|
|
767
|
+
: componentAttr("class", combined);
|
|
710
768
|
}
|
|
711
769
|
/**
|
|
712
770
|
* Render attributes not consumed as component props — `id`, `data-*`, `aria-*`,
|
|
713
771
|
* and `style`. These pass through to the rendered element.
|
|
714
772
|
*/
|
|
715
|
-
function renderPassthroughAttrs(props, consumed) {
|
|
773
|
+
function renderPassthroughAttrs(props, consumed, target = "astro") {
|
|
716
774
|
if (!props)
|
|
717
775
|
return "";
|
|
718
776
|
const skip = new Set([...consumed, "source", "children"]);
|
|
@@ -727,9 +785,12 @@ function renderPassthroughAttrs(props, consumed) {
|
|
|
727
785
|
if (value === true) {
|
|
728
786
|
parts.push(key);
|
|
729
787
|
}
|
|
730
|
-
else {
|
|
788
|
+
else if (target === "html") {
|
|
731
789
|
parts.push(`${key}="${escapeAttr(String(value))}"`);
|
|
732
790
|
}
|
|
791
|
+
else {
|
|
792
|
+
parts.push(componentAttr(key, String(value)).trimStart());
|
|
793
|
+
}
|
|
733
794
|
}
|
|
734
795
|
return parts.length ? " " + parts.join(" ") : "";
|
|
735
796
|
}
|
|
@@ -799,7 +860,7 @@ function detailsToAstro(node, ctx) {
|
|
|
799
860
|
const open = node.props?.open;
|
|
800
861
|
const icon = node.props?.icon;
|
|
801
862
|
const inner = childrenToAstro(node, ctx);
|
|
802
|
-
const attrs = [`variant="${escapeAttr(variant)}"`,
|
|
863
|
+
const attrs = [`variant="${escapeAttr(variant)}"`, componentAttr("title", title).trimStart()];
|
|
803
864
|
if (open)
|
|
804
865
|
attrs.push("open");
|
|
805
866
|
if (icon)
|
|
@@ -824,18 +885,62 @@ function tabsToAstro(node, ctx) {
|
|
|
824
885
|
return { value, label, node: tab };
|
|
825
886
|
});
|
|
826
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
|
+
};
|
|
827
911
|
const triggers = tabItems
|
|
828
|
-
.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
|
+
})
|
|
829
933
|
.join("\n");
|
|
830
934
|
const contents = tabItems
|
|
831
935
|
.map((t) => {
|
|
832
936
|
const inner = childrenToAstro(t.node, ctx);
|
|
833
|
-
return ` <TabsContent
|
|
937
|
+
return ` <TabsContent${componentAttr("value", t.value)}>\n${indentStr(inner, 4)}\n </TabsContent>`;
|
|
834
938
|
})
|
|
835
939
|
.join("\n");
|
|
836
940
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
837
941
|
const passthrough = renderPassthroughAttrs(node.props, ["sync", "default", "class"]);
|
|
838
|
-
|
|
942
|
+
// Same encoding as the trigger/content `value` it selects — see above.
|
|
943
|
+
return `<Tabs${componentAttr("defaultValue", defaultValue)}${classAttr}${passthrough}>
|
|
839
944
|
<TabsList>
|
|
840
945
|
${triggers}
|
|
841
946
|
</TabsList>
|
|
@@ -845,8 +950,10 @@ ${contents}
|
|
|
845
950
|
function tableToAstro(node, ctx) {
|
|
846
951
|
ctx.imports.add("table");
|
|
847
952
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
848
|
-
const
|
|
849
|
-
|
|
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>`;
|
|
850
957
|
}
|
|
851
958
|
function diagramToAstro(node, ctx) {
|
|
852
959
|
ctx.imports.add("diagram");
|
|
@@ -856,7 +963,7 @@ function diagramToAstro(node, ctx) {
|
|
|
856
963
|
const title = node.props?.title;
|
|
857
964
|
const attrs = [`lang="${escapeAttr(lang)}"`, `code={${escapeExpr(code)}}`, `svg={${escapeExpr(svg)}}`];
|
|
858
965
|
if (title)
|
|
859
|
-
attrs.push(
|
|
966
|
+
attrs.push(componentAttr("title", title).trimStart());
|
|
860
967
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
861
968
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
862
969
|
"lang", "code", "svg", "title", "class",
|
|
@@ -869,7 +976,7 @@ function youtubeToAstro(node, ctx) {
|
|
|
869
976
|
const title = node.props?.title ?? "";
|
|
870
977
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
871
978
|
const passthrough = renderPassthroughAttrs(node.props, ["id", "title", "class"]);
|
|
872
|
-
return `<YouTube
|
|
979
|
+
return `<YouTube${componentAttr("id", id)}${componentAttr("title", title)}${classAttr}${passthrough} />`;
|
|
873
980
|
}
|
|
874
981
|
function mathBlockToAstro(node, ctx) {
|
|
875
982
|
ctx.imports.add("math-block");
|
|
@@ -928,7 +1035,11 @@ function apiSymbolToAstro(node, ctx) {
|
|
|
928
1035
|
const signature = node.props?.signature;
|
|
929
1036
|
const bases = node.props?.bases || [];
|
|
930
1037
|
const level = node.type === "api-class" ? 2 : 3;
|
|
931
|
-
const attrs = [
|
|
1038
|
+
const attrs = [
|
|
1039
|
+
componentAttr("name", name).trimStart(),
|
|
1040
|
+
componentAttr("kind", kind).trimStart(),
|
|
1041
|
+
`level={${level}}`,
|
|
1042
|
+
];
|
|
932
1043
|
if (signature)
|
|
933
1044
|
attrs.push(`signature={${escapeExpr(signature)}}`);
|
|
934
1045
|
if (bases.length > 0)
|
|
@@ -1050,7 +1161,8 @@ function endpointToAstro(node, ctx) {
|
|
|
1050
1161
|
const codeBody = codePanels.length > 0
|
|
1051
1162
|
? `\n${indentStr(codePanels.join("\n"), 4)}\n `
|
|
1052
1163
|
: "";
|
|
1053
|
-
|
|
1164
|
+
const layoutAttrs = ctx.embeddedEndpoints ? ' variant="embedded"' : "";
|
|
1165
|
+
return (`<ApiLayout${layoutAttrs}>\n` +
|
|
1054
1166
|
` <EndpointCard ${cardAttrs.join(" ")}>${sectionsBody}</EndpointCard>\n` +
|
|
1055
1167
|
` <ApiCodePanel slot="code">${codeBody}</ApiCodePanel>\n` +
|
|
1056
1168
|
`</ApiLayout>`);
|
|
@@ -1082,8 +1194,7 @@ function cardsToAstro(node, ctx) {
|
|
|
1082
1194
|
// top of the page.
|
|
1083
1195
|
let iconHtml = "";
|
|
1084
1196
|
if (icon) {
|
|
1085
|
-
|
|
1086
|
-
iconHtml = `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1197
|
+
iconHtml = cardIconHtml(icon, title, ctx);
|
|
1087
1198
|
}
|
|
1088
1199
|
const titleHtml = title ? `<h3 class="text-base font-semibold">${escapeTemplate(title)}</h3>` : "";
|
|
1089
1200
|
const desc = inner
|
|
@@ -1164,6 +1275,45 @@ function standaloneCardToAstro(node, ctx) {
|
|
|
1164
1275
|
* If the parser kept rich body children instead of folding into description,
|
|
1165
1276
|
* we fall back to the standalone-card path which can hold arbitrary content.
|
|
1166
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
|
+
}
|
|
1167
1317
|
function linkCardToAstro(node, ctx) {
|
|
1168
1318
|
const hasRichChildren = (node.children ?? []).length > 0;
|
|
1169
1319
|
if (hasRichChildren) {
|
|
@@ -1173,9 +1323,9 @@ function linkCardToAstro(node, ctx) {
|
|
|
1173
1323
|
const title = node.props?.title ?? "";
|
|
1174
1324
|
const description = node.props?.description;
|
|
1175
1325
|
const href = node.props?.href ?? "";
|
|
1176
|
-
const titleAttr =
|
|
1177
|
-
const hrefAttr =
|
|
1178
|
-
const descAttr = description ?
|
|
1326
|
+
const titleAttr = componentAttr("title", title);
|
|
1327
|
+
const hrefAttr = componentAttr("href", href);
|
|
1328
|
+
const descAttr = description ? componentAttr("description", description) : "";
|
|
1179
1329
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1180
1330
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1181
1331
|
"title", "description", "href", "icon", "class",
|
|
@@ -1199,9 +1349,9 @@ function accordionToAstro(node, ctx) {
|
|
|
1199
1349
|
const defaultValue = node.props?.defaultValue;
|
|
1200
1350
|
const typeAttr = ` type="${escapeAttr(type)}"`;
|
|
1201
1351
|
const defaultAttr = defaultValue !== undefined
|
|
1202
|
-
?
|
|
1203
|
-
?
|
|
1204
|
-
: `{${JSON.stringify(defaultValue)}}`
|
|
1352
|
+
? typeof defaultValue === "string"
|
|
1353
|
+
? componentAttr("defaultValue", defaultValue)
|
|
1354
|
+
: ` defaultValue={${JSON.stringify(defaultValue)}}`
|
|
1205
1355
|
: "";
|
|
1206
1356
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1207
1357
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
@@ -1217,7 +1367,7 @@ function accordionItemToAstro(node, ctx, standalone) {
|
|
|
1217
1367
|
ctx.imports.add("accordion");
|
|
1218
1368
|
const value = node.props?.value ?? "item-1";
|
|
1219
1369
|
const label = node.props?.label ?? node.props?.title ?? "Item";
|
|
1220
|
-
const valueAttr =
|
|
1370
|
+
const valueAttr = componentAttr("value", value);
|
|
1221
1371
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1222
1372
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1223
1373
|
"value", "label", "title", "class",
|
|
@@ -1244,9 +1394,9 @@ function avatarToAstro(node, ctx) {
|
|
|
1244
1394
|
const src = node.props?.src ?? "";
|
|
1245
1395
|
const alt = node.props?.alt ?? "";
|
|
1246
1396
|
const fallback = node.props?.fallback;
|
|
1247
|
-
const srcAttr = src ?
|
|
1248
|
-
const altAttr =
|
|
1249
|
-
const fbAttr = fallback ?
|
|
1397
|
+
const srcAttr = src ? componentAttr("src", src) : "";
|
|
1398
|
+
const altAttr = componentAttr("alt", alt);
|
|
1399
|
+
const fbAttr = fallback ? componentAttr("fallback", fallback) : "";
|
|
1250
1400
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1251
1401
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1252
1402
|
"src", "alt", "fallback", "class",
|
|
@@ -1267,7 +1417,7 @@ function exampleToAstro(node, ctx) {
|
|
|
1267
1417
|
const source = String(node.props?.source ?? "");
|
|
1268
1418
|
const title = node.props?.title;
|
|
1269
1419
|
const showFallback = node.props?.fallback === true || node.props?.fallback === "true";
|
|
1270
|
-
const titleAttr = title ?
|
|
1420
|
+
const titleAttr = title ? componentAttr("title", title) : "";
|
|
1271
1421
|
const fallbackAttr = showFallback ? " showFallback" : "";
|
|
1272
1422
|
const inner = childrenToAstro(node, ctx);
|
|
1273
1423
|
return `<MarkdownExample source={${escapeExpr(source)}}${titleAttr}${fallbackAttr}>\n${inner}\n</MarkdownExample>`;
|
|
@@ -1287,24 +1437,25 @@ function htmlContainerToAstro(node, ctx) {
|
|
|
1287
1437
|
}
|
|
1288
1438
|
// ─── Utilities ────────────────────────────────────────────────────
|
|
1289
1439
|
/**
|
|
1290
|
-
* Render a node's content
|
|
1291
|
-
* parser
|
|
1292
|
-
*
|
|
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.
|
|
1293
1444
|
*
|
|
1294
1445
|
* Used for list-item, table cells, step, dt, dd — types that commonly
|
|
1295
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.
|
|
1296
1452
|
*/
|
|
1297
1453
|
function leafContent(node, ctx) {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
const rendered = childrenToAstro(node, ctx);
|
|
1304
|
-
if (rendered)
|
|
1305
|
-
parts.push(rendered);
|
|
1306
|
-
}
|
|
1307
|
-
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
|
+
});
|
|
1308
1459
|
}
|
|
1309
1460
|
function childrenToAstro(node, ctx) {
|
|
1310
1461
|
if (!node.children || node.children.length === 0)
|
|
@@ -1344,13 +1495,8 @@ function inlineHasIcon(node) {
|
|
|
1344
1495
|
}
|
|
1345
1496
|
return false;
|
|
1346
1497
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
return text
|
|
1350
|
-
.split("\n")
|
|
1351
|
-
.map((line) => (line ? `${prefix}${line}` : line))
|
|
1352
|
-
.join("\n");
|
|
1353
|
-
}
|
|
1498
|
+
/** Indent, via the core's shared implementation (was a fourth local copy). */
|
|
1499
|
+
const indentStr = indent;
|
|
1354
1500
|
function escapeHtml(s) {
|
|
1355
1501
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1356
1502
|
}
|