@dogsbay/format-astro 0.2.0-beta.114 → 0.2.0-beta.116
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 +8 -0
- package/dist/api-markdown.d.ts.map +1 -0
- package/dist/api-markdown.js +41 -0
- package/dist/api-markdown.js.map +1 -0
- package/dist/blog.d.ts.map +1 -1
- package/dist/blog.js +5 -1
- package/dist/blog.js.map +1 -1
- package/dist/browser.d.ts +14 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +14 -0
- package/dist/browser.js.map +1 -0
- package/dist/canonical.d.ts +34 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +107 -0
- package/dist/canonical.js.map +1 -0
- package/dist/head-metadata.d.ts +26 -0
- package/dist/head-metadata.d.ts.map +1 -0
- package/dist/head-metadata.js +56 -0
- package/dist/head-metadata.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/llms-txt.d.ts +3 -3
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +48 -242
- package/dist/llms-txt.js.map +1 -1
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +3 -1
- package/dist/plugins.js.map +1 -1
- package/dist/project.d.ts +36 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +292 -840
- package/dist/project.js.map +1 -1
- package/dist/redirects.d.ts +22 -0
- package/dist/redirects.d.ts.map +1 -1
- package/dist/redirects.js +70 -123
- package/dist/redirects.js.map +1 -1
- package/dist/serialize.d.ts +4 -13
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +305 -103
- package/dist/serialize.js.map +1 -1
- package/dist/sitemap.d.ts +44 -17
- package/dist/sitemap.d.ts.map +1 -1
- package/dist/sitemap.js +31 -104
- package/dist/sitemap.js.map +1 -1
- package/dist/taxonomy.d.ts.map +1 -1
- package/dist/taxonomy.js +7 -1
- package/dist/taxonomy.js.map +1 -1
- package/dist/theme-css.d.ts +26 -0
- package/dist/theme-css.d.ts.map +1 -0
- package/dist/theme-css.js +642 -0
- package/dist/theme-css.js.map +1 -0
- package/dist/tone-classes.d.ts +14 -0
- package/dist/tone-classes.d.ts.map +1 -0
- package/dist/tone-classes.js +30 -0
- package/dist/tone-classes.js.map +1 -0
- package/dist/write-if-changed.d.ts +21 -0
- package/dist/write-if-changed.d.ts.map +1 -0
- package/dist/write-if-changed.js +58 -0
- package/dist/write-if-changed.js.map +1 -0
- package/package.json +13 -5
package/dist/serialize.js
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
import { walkInline, applyTextFlags, renderLeaf, indent, } from "@dogsbay/serialize-core";
|
|
1
|
+
import { walkInline, applyTextFlags, renderLeaf, indent, isSafeHref, } from "@dogsbay/serialize-core";
|
|
2
|
+
import { parseInline } from "@dogsbay/format-dogsbay-md";
|
|
2
3
|
import { decorateDiff, hasDiffMarks } from "./diff-decoration.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
14
|
-
*/
|
|
15
|
-
export const TONE_CLASSES = {
|
|
16
|
-
// Primary scale (intensity)
|
|
17
|
-
"primary": "bg-primary text-primary-foreground",
|
|
18
|
-
"primary-strong": "bg-primary/80 text-primary-foreground",
|
|
19
|
-
"primary-medium": "bg-primary/60 text-primary-foreground",
|
|
20
|
-
"primary-half": "bg-primary/50 text-primary-foreground",
|
|
21
|
-
"primary-light": "bg-primary/20 text-primary",
|
|
22
|
-
"primary-soft": "bg-primary/10 text-primary",
|
|
23
|
-
// Neutral
|
|
24
|
-
"muted": "bg-muted text-muted-foreground",
|
|
25
|
-
// Named colors (for layout demos with distinct cells)
|
|
26
|
-
"blue": "bg-blue-500/15 text-blue-700 dark:text-blue-300",
|
|
27
|
-
"amber": "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
|
28
|
-
"green": "bg-green-500/15 text-green-700 dark:text-green-300",
|
|
29
|
-
"purple": "bg-purple-500/15 text-purple-700 dark:text-purple-300",
|
|
30
|
-
"red": "bg-red-500/15 text-red-700 dark:text-red-300",
|
|
31
|
-
};
|
|
4
|
+
import { TONE_CLASSES } from "./tone-classes.js";
|
|
5
|
+
import { apiBlockHtml, apiPropertiesHtml } from "./api-markdown.js";
|
|
6
|
+
export { TONE_CLASSES };
|
|
32
7
|
const COMPONENT_IMPORTS = {
|
|
33
8
|
"markdown-example": [
|
|
34
9
|
'import MarkdownExample from "@ui/markdown-example/MarkdownExample.astro";',
|
|
@@ -64,6 +39,9 @@ const COMPONENT_IMPORTS = {
|
|
|
64
39
|
],
|
|
65
40
|
diagram: ['import Diagram from "@ui/diagram/Diagram.astro";'],
|
|
66
41
|
youtube: ['import YouTube from "@ui/youtube/YouTube.astro";'],
|
|
42
|
+
badge: ['import Badge from "@ui/badge/Badge.astro";'],
|
|
43
|
+
kbd: ['import Kbd from "@ui/kbd/Kbd.astro";'],
|
|
44
|
+
files: ['import FileTree from "@ui/files/FileTree.astro";'],
|
|
67
45
|
"math-block": ['import MathKatex from "@ui/math/MathKatex.astro";'],
|
|
68
46
|
image: ['import BlockImage from "@ui/image/Image.astro";'],
|
|
69
47
|
"api-class": ['import ApiSymbol from "@ui/api-symbol/ApiSymbol.astro";'],
|
|
@@ -121,7 +99,12 @@ export function escapeExpr(s) {
|
|
|
121
99
|
// JSON.stringify handles all special characters (quotes, backslashes, newlines, etc.)
|
|
122
100
|
// and produces a valid JS string literal. This avoids template literal edge cases
|
|
123
101
|
// where backtick + brace combinations create false expression boundaries.
|
|
124
|
-
|
|
102
|
+
//
|
|
103
|
+
// `<` becomes \u003c: Astro parses the template as HTML before the expression is
|
|
104
|
+
// JavaScript, so a `<script>` inside a string (a code block showing one, say) would
|
|
105
|
+
// be taken for a real script tag and its text compiled as JavaScript. Vite's
|
|
106
|
+
// dependency scan then fails to parse it and a production build breaks.
|
|
107
|
+
return JSON.stringify(s).replace(/</g, "\\u003c");
|
|
125
108
|
}
|
|
126
109
|
/** Escape for use in an HTML attribute value (double-quoted). */
|
|
127
110
|
export function escapeAttr(s) {
|
|
@@ -140,6 +123,15 @@ export function componentAttr(name, value) {
|
|
|
140
123
|
* src, etc.). `id`, `class`, and `style` are first-class; any other
|
|
141
124
|
* key passes through verbatim.
|
|
142
125
|
*/
|
|
126
|
+
/** `{…}` attributes on a COMPONENT (Badge, Button): `componentAttr`, which Astro does not entity-decode. */
|
|
127
|
+
function renderInlineComponentAttrs(attrs) {
|
|
128
|
+
if (!attrs)
|
|
129
|
+
return "";
|
|
130
|
+
return Object.entries(attrs)
|
|
131
|
+
.filter(([, value]) => value !== undefined)
|
|
132
|
+
.map(([key, value]) => componentAttr(key, String(value)))
|
|
133
|
+
.join("");
|
|
134
|
+
}
|
|
143
135
|
function renderInlineElementAttrs(attrs) {
|
|
144
136
|
if (!attrs)
|
|
145
137
|
return "";
|
|
@@ -185,6 +177,7 @@ export function treeToAstro(nodes, options) {
|
|
|
185
177
|
mode: options?.mode ?? "template",
|
|
186
178
|
imageOptimization: options?.imageOptimization ?? false,
|
|
187
179
|
codeBlockTitle: options?.codeBlockTitle ?? true,
|
|
180
|
+
imageCaptions: options?.imageCaptions ?? false,
|
|
188
181
|
combinedPrefix: options?.combinedPrefix ?? "",
|
|
189
182
|
// Mixed prose+endpoint pages render endpoints as embedded blocks;
|
|
190
183
|
// pages that are nothing but endpoints/headings keep the
|
|
@@ -199,9 +192,9 @@ export function treeToAstro(nodes, options) {
|
|
|
199
192
|
// (card.props.icon) registers `icon` directly at the call
|
|
200
193
|
// site; this pre-scan picks up the inline `:icon[name]`
|
|
201
194
|
// directives the writer can drop into prose.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
195
|
+
// Badges and inline buttons are components too, registered the same way.
|
|
196
|
+
for (const key of inlineComponentsIn(nodes))
|
|
197
|
+
ctx.imports.add(key);
|
|
205
198
|
const body = nodes.map((n) => nodeToAstro(n, ctx)).filter(Boolean).join("\n");
|
|
206
199
|
const imports = [];
|
|
207
200
|
const seen = new Set();
|
|
@@ -246,6 +239,10 @@ function nodeToAstroBase(node, ctx) {
|
|
|
246
239
|
return codeToAstro(node, ctx);
|
|
247
240
|
case "callout":
|
|
248
241
|
return calloutToAstro(node, ctx);
|
|
242
|
+
case "files":
|
|
243
|
+
return filesToAstro(node, ctx);
|
|
244
|
+
case "container":
|
|
245
|
+
return genericContainerToAstro(node, ctx);
|
|
249
246
|
case "details":
|
|
250
247
|
return detailsToAstro(node, ctx);
|
|
251
248
|
case "tabs":
|
|
@@ -278,8 +275,10 @@ function nodeToAstroBase(node, ctx) {
|
|
|
278
275
|
return `<hr${classAttr}${passthrough} />`;
|
|
279
276
|
}
|
|
280
277
|
case "html":
|
|
281
|
-
// Raw HTML always uses set:html — it may contain anything
|
|
282
|
-
|
|
278
|
+
// Raw HTML always uses set:html — it may contain anything. Callouts inside it
|
|
279
|
+
// (dogsbay-md's HTML table fallback writes `<div role="note" data-callout>`)
|
|
280
|
+
// render as real callouts
|
|
281
|
+
return htmlToAstro(node.html ?? "", ctx);
|
|
283
282
|
case "table":
|
|
284
283
|
return tableToAstro(node, ctx);
|
|
285
284
|
case "thead":
|
|
@@ -330,9 +329,20 @@ function nodeToAstroBase(node, ctx) {
|
|
|
330
329
|
return exampleToAstro(node, ctx);
|
|
331
330
|
case "figure": {
|
|
332
331
|
const caption = node.props?.caption;
|
|
332
|
+
// A figure of one code block: the caption goes in the code block's header bar,
|
|
333
|
+
// which then is the figure's caption
|
|
334
|
+
const only = node.children?.length === 1 ? node.children[0] : undefined;
|
|
335
|
+
if (caption && only?.type === "code") {
|
|
336
|
+
return codeToAstro(only, ctx, {
|
|
337
|
+
caption: captionToAstro(caption, ctx),
|
|
338
|
+
// The figure's attributes, except those the code block has itself (an id twice is invalid)
|
|
339
|
+
attrs: renderPassthroughAttrs(node.props, ["caption", "class", ...Object.keys(only.props ?? {})]),
|
|
340
|
+
classes: ["my-4", node.props?.class].filter(Boolean).join(" "),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
333
343
|
const inner = childrenToAstro(node, ctx);
|
|
334
344
|
const body = caption
|
|
335
|
-
? `${inner}\n<figcaption class="text-center text-sm text-muted-foreground mt-2">${
|
|
345
|
+
? `${inner}\n<figcaption class="text-center text-sm text-muted-foreground mt-2">${captionToAstro(caption, ctx)}</figcaption>`
|
|
336
346
|
: inner;
|
|
337
347
|
return wrapBlock("figure", "my-4", node, body, ["caption"]);
|
|
338
348
|
}
|
|
@@ -468,15 +478,37 @@ function templateInlineEmitters() {
|
|
|
468
478
|
strike: (s) => `<s>${s}</s>`,
|
|
469
479
|
}),
|
|
470
480
|
link: (node, ctx) => {
|
|
481
|
+
// A link node an importer built directly never passed markdown-it's
|
|
482
|
+
// scheme check: a `javascript:` href keeps its text and loses the link.
|
|
483
|
+
if (!isSafeHref(node.href)) {
|
|
484
|
+
ctx.report("link", `unsafe link target dropped (${node.href.slice(0, 40)})`);
|
|
485
|
+
return ctx.recurse(node.children);
|
|
486
|
+
}
|
|
471
487
|
const titleAttr = node.title ? componentAttr("title", node.title) : "";
|
|
472
488
|
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
489
|
+
// An inline `:button`: the Button component, small, keeping its variant ("primary" is the filled default).
|
|
490
|
+
if (node.button) {
|
|
491
|
+
const variant = buttonVariant(node.button.variant);
|
|
492
|
+
return `<Button${componentAttr("href", node.href)}${componentAttr("variant", variant)} size="sm"${titleAttr}${renderInlineComponentAttrs(node.attrs)}>${ctx.recurse(node.children)}</Button>`;
|
|
493
|
+
}
|
|
473
494
|
return `<a href="${escapeAttr(node.href)}"${titleAttr}${extraAttrs}>${ctx.recurse(node.children)}</a>`;
|
|
474
495
|
},
|
|
475
|
-
|
|
496
|
+
badge: (node, ctx) => `<Badge${componentAttr("variant", node.variant ?? "default")}${renderInlineComponentAttrs(node.attrs)}>${ctx.escape(node.text)}</Badge>`,
|
|
497
|
+
image: (node, ctx) => {
|
|
498
|
+
if (!isSafeHref(node.src, "image")) {
|
|
499
|
+
ctx.report("image", `unsafe image source dropped (${node.src.slice(0, 40)})`);
|
|
500
|
+
return ctx.escape(node.alt ?? "");
|
|
501
|
+
}
|
|
476
502
|
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
477
503
|
return `<img src="${escapeAttr(node.src)}" alt="${escapeAttr(node.alt ?? "")}" loading="lazy"${extraAttrs} />`;
|
|
478
504
|
},
|
|
479
|
-
|
|
505
|
+
// Code inherits the emphasis around it (`*using the \`dita\` command*`): the parser
|
|
506
|
+
// flags the code span, and dropping the flags split the italics around the code
|
|
507
|
+
code: (node, ctx) => applyTextFlags(`<code>${ctx.escape(node.text)}</code>`, node, {
|
|
508
|
+
bold: (s) => `<strong>${s}</strong>`,
|
|
509
|
+
italic: (s) => `<em>${s}</em>`,
|
|
510
|
+
strike: (s) => `<s>${s}</s>`,
|
|
511
|
+
}),
|
|
480
512
|
// The arm that did not exist — `:highlight[…]` / `:mark[…]` rendered as
|
|
481
513
|
// nothing at all. The class list mirrors the canonical rendering in
|
|
482
514
|
// `packages/ui/src/content-renderer/InlineRenderer.astro`, so a highlight
|
|
@@ -486,7 +518,8 @@ function templateInlineEmitters() {
|
|
|
486
518
|
// pages, where nothing declares `color-scheme`.
|
|
487
519
|
highlight: (node, ctx) => `<mark class="${HIGHLIGHT_CLASS}">${ctx.recurse(node.children)}</mark>`,
|
|
488
520
|
"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
|
-
|
|
521
|
+
// The Kbd component (keys styled as keycaps); plain <kbd> had no style beyond a monospace font.
|
|
522
|
+
kbd: (node, ctx) => node.keys.map((k) => `<Kbd>${ctx.escape(k)}</Kbd>`).join("+"),
|
|
490
523
|
math: (node, ctx) => `<code class="math-inline">${ctx.escape(node.latex)}</code>`,
|
|
491
524
|
icon: (node) => {
|
|
492
525
|
// Inline icon — emits the platform `<Icon>` component which
|
|
@@ -515,10 +548,18 @@ function htmlModeInlineEmitters() {
|
|
|
515
548
|
// EXPRESSION, inside a string that becomes a `set:html` value. Astro never
|
|
516
549
|
// parses it as markup, so the reader saw the braces and quotes.
|
|
517
550
|
link: (node, ctx) => {
|
|
551
|
+
if (!isSafeHref(node.href)) {
|
|
552
|
+
ctx.report("link", `unsafe link target dropped (${node.href.slice(0, 40)})`);
|
|
553
|
+
return ctx.recurse(node.children);
|
|
554
|
+
}
|
|
518
555
|
const titleAttr = node.title ? ` title="${escapeAttr(node.title)}"` : "";
|
|
519
556
|
const extraAttrs = renderInlineElementAttrs(node.attrs);
|
|
520
|
-
|
|
557
|
+
// No components in HTML mode: a marked link (and a marked span for a badge below).
|
|
558
|
+
const button = node.button ? ` data-button data-variant="${escapeAttr(buttonVariant(node.button.variant))}"` : "";
|
|
559
|
+
return `<a href="${escapeAttr(node.href)}"${titleAttr}${button}${extraAttrs}>${ctx.recurse(node.children)}</a>`;
|
|
521
560
|
},
|
|
561
|
+
kbd: (node, ctx) => node.keys.map((k) => `<kbd>${ctx.escape(k)}</kbd>`).join("+"),
|
|
562
|
+
badge: (node, ctx) => `<span data-badge data-variant="${escapeAttr(node.variant ?? "default")}"${renderInlineElementAttrs(node.attrs)}>${ctx.escape(node.text)}</span>`,
|
|
522
563
|
text: (node, ctx) => applyTextFlags(ctx.escape(node.text), node, {
|
|
523
564
|
bold: (s) => `<strong>${s}</strong>`,
|
|
524
565
|
italic: (s) => `<em>${s}</em>`,
|
|
@@ -552,6 +593,8 @@ function renderInline(nodes, ctx) {
|
|
|
552
593
|
const html = inlineToHtml(nodes);
|
|
553
594
|
return `<Fragment set:html={${escapeExpr(html)}} />`;
|
|
554
595
|
}
|
|
596
|
+
// Inline parsed from a prop (a caption) is not in the page-level pre-scan; its components are imported here.
|
|
597
|
+
inlineComponentsIn([{ type: "paragraph", inline: nodes }], ctx.imports);
|
|
555
598
|
return inlineToTemplate(nodes);
|
|
556
599
|
}
|
|
557
600
|
/** Render an HTML string in the current mode. */
|
|
@@ -572,7 +615,7 @@ function headingToAstro(node, ctx) {
|
|
|
572
615
|
// `id` takes precedence over `slug` (slug is auto-generated, id is explicit user override)
|
|
573
616
|
const id = node.props?.id ?? node.props?.slug ?? "";
|
|
574
617
|
const anchor = id
|
|
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>`
|
|
618
|
+
? `<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>`
|
|
576
619
|
: "";
|
|
577
620
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : "";
|
|
578
621
|
const consumed = ["level", "slug", "id", "class", "text"];
|
|
@@ -625,7 +668,9 @@ function paragraphToAstro(node, ctx) {
|
|
|
625
668
|
// Author-supplied {.class #id ...} on the image itself
|
|
626
669
|
// (Phase 1.5 of plans/inline-attrs.md).
|
|
627
670
|
const imageAttrs = renderInlineElementAttrs(img.attrs);
|
|
628
|
-
|
|
671
|
+
// `content.imageCaptions: title`: the figure form of BlockImage, captioned by the title (as dogsbay-ssg).
|
|
672
|
+
const figure = ctx.imageCaptions && img.title ? ` block${componentAttr("title", img.title)}` : "";
|
|
673
|
+
return `<BlockImage${componentAttr("src", img.src)}${componentAttr("alt", img.alt ?? "")}${figure}${imageAttrs}${passthrough}${imgDataAttr} />`;
|
|
629
674
|
}
|
|
630
675
|
}
|
|
631
676
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
@@ -652,7 +697,7 @@ function paragraphToAstro(node, ctx) {
|
|
|
652
697
|
}).join("");
|
|
653
698
|
return `<p${classAttr}${passthrough} set:html={${escapeExpr(html)}} />`;
|
|
654
699
|
}
|
|
655
|
-
function codeToAstro(node, ctx) {
|
|
700
|
+
function codeToAstro(node, ctx, figure) {
|
|
656
701
|
// Code content may live on props.code (Starlight convention) or node.html
|
|
657
702
|
// (dogsbay-md convention). Same for lang — Starlight uses props.lang,
|
|
658
703
|
// dogsbay-md uses props.language.
|
|
@@ -663,13 +708,18 @@ function codeToAstro(node, ctx) {
|
|
|
663
708
|
?? node.html
|
|
664
709
|
?? "";
|
|
665
710
|
const title = node.props?.title;
|
|
666
|
-
|
|
711
|
+
// The fence's documented `mark`, `ins` and `del` are CodeRich's highlights, diffAdd and diffRemove.
|
|
712
|
+
const lineSpec = (...values) => values.filter((v) => typeof v === "string" || typeof v === "number").map(String).filter(Boolean).join(",") || undefined;
|
|
713
|
+
const highlights = lineSpec(node.props?.highlights, node.props?.mark);
|
|
667
714
|
const lineNumbers = node.props?.lineNumbers;
|
|
668
715
|
const copyText = node.props?.copyText;
|
|
669
|
-
const diffAdd = node.props?.diffAdd;
|
|
670
|
-
const diffRemove = node.props?.diffRemove;
|
|
716
|
+
const diffAdd = lineSpec(node.props?.diffAdd, node.props?.ins);
|
|
717
|
+
const diffRemove = lineSpec(node.props?.diffRemove, node.props?.del);
|
|
671
718
|
const isRich = lineNumbers || highlights || code.includes("[!code") || Boolean(diffAdd || diffRemove);
|
|
672
|
-
const classAttr = mergeClassAttr("", node.props?.class);
|
|
719
|
+
const classAttr = mergeClassAttr(figure?.classes ?? "", node.props?.class);
|
|
720
|
+
const figureAttrs = figure?.attrs ?? "";
|
|
721
|
+
// The component with its caption slot filled, or self-closing
|
|
722
|
+
const close = (tag, open) => figure ? `${open}>\n<Fragment slot="caption">${figure.caption}</Fragment>\n</${tag}>` : `${open} />`;
|
|
673
723
|
const consumed = [
|
|
674
724
|
"lang", "language", "code", "title", "highlights", "lineNumbers",
|
|
675
725
|
"class", "ins", "del", "mark", "collapse", "copyText", "diffAdd", "diffRemove",
|
|
@@ -690,7 +740,7 @@ function codeToAstro(node, ctx) {
|
|
|
690
740
|
attrs.push(`diffAdd="${escapeAttr(diffAdd)}"`);
|
|
691
741
|
if (diffRemove)
|
|
692
742
|
attrs.push(`diffRemove="${escapeAttr(diffRemove)}"`);
|
|
693
|
-
return `<CodeRich ${attrs.join(" ")}${classAttr}${passthrough}
|
|
743
|
+
return close("CodeRich", `<CodeRich ${attrs.join(" ")}${classAttr}${passthrough}${figureAttrs}`);
|
|
694
744
|
}
|
|
695
745
|
ctx.imports.add("code");
|
|
696
746
|
const attrs = [`code={${escapeExpr(code)}}`, `lang="${escapeAttr(lang)}"`];
|
|
@@ -699,7 +749,7 @@ function codeToAstro(node, ctx) {
|
|
|
699
749
|
if (ctx.codeBlockTitle !== true) {
|
|
700
750
|
attrs.push(`showTitle="${ctx.codeBlockTitle}"`);
|
|
701
751
|
}
|
|
702
|
-
return `<CodeBlock ${attrs.join(" ")}${classAttr}${passthrough}
|
|
752
|
+
return close("CodeBlock", `<CodeBlock ${attrs.join(" ")}${classAttr}${passthrough}${figureAttrs}`);
|
|
703
753
|
}
|
|
704
754
|
// Map GitHub alert names (and other aliases) to Alert component variants.
|
|
705
755
|
// The Alert variant palette follows MkDocs Material; not every GitHub name
|
|
@@ -711,17 +761,71 @@ const CALLOUT_VARIANT_MAP = {
|
|
|
711
761
|
hint: "tip",
|
|
712
762
|
attention: "warning",
|
|
713
763
|
};
|
|
714
|
-
// Default title shown when a callout has no explicit one
|
|
715
|
-
//
|
|
764
|
+
// Default title shown when a callout has no explicit one, keyed by the type
|
|
765
|
+
// the author wrote: aliases share a look but keep their own name (`:::info` is
|
|
766
|
+
// "Info", not the "Important" of the variant it shares). The same labels as
|
|
767
|
+
// dogsbay-ssg (src/render/icons.ts CALLOUTS). Anything else: the type, capitalised.
|
|
716
768
|
const CALLOUT_LABELS = {
|
|
717
769
|
note: "Note",
|
|
770
|
+
info: "Info",
|
|
718
771
|
tip: "Tip",
|
|
719
|
-
|
|
720
|
-
warning: "Warning",
|
|
721
|
-
danger: "Caution",
|
|
722
|
-
failure: "Error",
|
|
772
|
+
hint: "Hint",
|
|
723
773
|
success: "Success",
|
|
774
|
+
important: "Important",
|
|
775
|
+
warning: "Warning",
|
|
776
|
+
attention: "Attention",
|
|
777
|
+
caution: "Caution",
|
|
778
|
+
danger: "Danger",
|
|
779
|
+
error: "Error",
|
|
780
|
+
failure: "Failure",
|
|
781
|
+
question: "Question",
|
|
782
|
+
bug: "Bug",
|
|
783
|
+
example: "Example",
|
|
784
|
+
abstract: "Abstract",
|
|
785
|
+
quote: "Quote",
|
|
724
786
|
};
|
|
787
|
+
const CALLOUT_OPEN = /<div role="note" data-callout="([^"]*)"(?: data-title="([^"]*)")?>/g;
|
|
788
|
+
/**
|
|
789
|
+
* Raw HTML as set:html fragments, with each marked callout div rendered by
|
|
790
|
+
* calloutToAstro. The fragments around a callout may hold unbalanced tags
|
|
791
|
+
* (`<table><tr><td>` before, `</td></tr></table>` after); set:html writes
|
|
792
|
+
* them verbatim, so the page's HTML is whole again.
|
|
793
|
+
*/
|
|
794
|
+
function htmlToAstro(html, ctx) {
|
|
795
|
+
const raw = (s) => (s ? `<Fragment set:html={${escapeExpr(s)}} />` : "");
|
|
796
|
+
const out = [];
|
|
797
|
+
let rest = html;
|
|
798
|
+
for (;;) {
|
|
799
|
+
CALLOUT_OPEN.lastIndex = 0;
|
|
800
|
+
const open = CALLOUT_OPEN.exec(rest);
|
|
801
|
+
if (!open)
|
|
802
|
+
break;
|
|
803
|
+
// The callout ends at its matching </div>
|
|
804
|
+
const divs = /<div\b|<\/div>/g;
|
|
805
|
+
divs.lastIndex = open.index + open[0].length;
|
|
806
|
+
let depth = 1;
|
|
807
|
+
let end = -1;
|
|
808
|
+
for (let m = divs.exec(rest); m; m = divs.exec(rest)) {
|
|
809
|
+
depth += m[0] === "</div>" ? -1 : 1;
|
|
810
|
+
if (depth === 0) {
|
|
811
|
+
end = m.index;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (end < 0)
|
|
816
|
+
break;
|
|
817
|
+
const unescape = (s) => s.replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
818
|
+
out.push(raw(rest.slice(0, open.index)));
|
|
819
|
+
out.push(calloutToAstro({
|
|
820
|
+
type: "callout",
|
|
821
|
+
props: { type: unescape(open[1]), ...(open[2] !== undefined ? { title: unescape(open[2]) } : {}) },
|
|
822
|
+
children: [{ type: "html", html: rest.slice(open.index + open[0].length, end) }],
|
|
823
|
+
}, ctx));
|
|
824
|
+
rest = rest.slice(end + "</div>".length);
|
|
825
|
+
}
|
|
826
|
+
out.push(raw(rest));
|
|
827
|
+
return out.filter(Boolean).join("\n") || raw(html);
|
|
828
|
+
}
|
|
725
829
|
function calloutToAstro(node, ctx) {
|
|
726
830
|
ctx.imports.add("callout");
|
|
727
831
|
// Callout variant may live on props.variant (Starlight / MkDocs importers)
|
|
@@ -735,11 +839,13 @@ function calloutToAstro(node, ctx) {
|
|
|
735
839
|
// admonitions and GitHub-style alerts both show the type next to the icon,
|
|
736
840
|
// and an empty title leaves the icon standing alone. Issue #004.
|
|
737
841
|
const explicitTitle = node.props?.title;
|
|
842
|
+
// The default title names the authored type, not the variant it maps to.
|
|
738
843
|
const title = explicitTitle && explicitTitle.length > 0
|
|
739
844
|
? explicitTitle
|
|
740
|
-
: CALLOUT_LABELS[
|
|
845
|
+
: CALLOUT_LABELS[rawVariant] ?? rawVariant.charAt(0).toUpperCase() + rawVariant.slice(1);
|
|
741
846
|
const icon = node.props?.icon;
|
|
742
|
-
|
|
847
|
+
// The authored type (`hint`, `caution`) beside the variant it shares, as dogsbay-ssg writes it.
|
|
848
|
+
const iconAttr = (icon ? ` icon="${escapeAttr(icon)}"` : "") + (node.props?.["data-type"] === undefined ? ` data-type="${escapeAttr(rawVariant)}"` : "");
|
|
743
849
|
// User classes and passthrough attributes (id, data-*, aria-*, style)
|
|
744
850
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
745
851
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
@@ -853,9 +959,47 @@ function wrapBlock(tag, defaultClass, node, content, consumed = []) {
|
|
|
853
959
|
return `<${tag}${classAttr}${passthrough}>${sep}${content}${sep}</${tag}>`;
|
|
854
960
|
void needsNewline;
|
|
855
961
|
}
|
|
962
|
+
/**
|
|
963
|
+
* `:::files` around a nested bullet list: FileTree, whose markup (files/markup.ts) dogsbay-ssg
|
|
964
|
+
* shares. An entry is a folder when it has entries or its name ends in "/".
|
|
965
|
+
*/
|
|
966
|
+
function filesToAstro(node, ctx) {
|
|
967
|
+
ctx.imports.add("files");
|
|
968
|
+
const isList = (n) => n.type === "unordered-list" || n.type === "ordered-list";
|
|
969
|
+
const items = (list) => (list?.children ?? [])
|
|
970
|
+
.filter((li) => li.type === "list-item")
|
|
971
|
+
.map((li) => {
|
|
972
|
+
const inline = li.inline ?? (li.children ?? []).find((c) => c.type === "paragraph")?.inline ?? [];
|
|
973
|
+
const sub = (li.children ?? []).find(isList);
|
|
974
|
+
const text = inline.map((n) => ("text" in n && typeof n.text === "string" ? n.text : "")).join("");
|
|
975
|
+
return { label: inlineToHtml(inline), folder: !!sub || /\/\s*$/.test(text), children: items(sub) };
|
|
976
|
+
});
|
|
977
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
978
|
+
// Each list is a tree; anything else in the block (an intro, a note) renders as it is.
|
|
979
|
+
// `<` is escaped in the JSON: a label may hold inline HTML, and a literal `</script>` in an
|
|
980
|
+
// expression breaks Vite's dependency scan (see escapeExpr).
|
|
981
|
+
return (node.children ?? [])
|
|
982
|
+
.map((child) => isList(child)
|
|
983
|
+
? `<FileTree items={${JSON.stringify(items(child)).replace(/</g, "\\u003c")}}${classAttr} />`
|
|
984
|
+
: nodeToAstro(child, ctx))
|
|
985
|
+
.filter(Boolean)
|
|
986
|
+
.join("\n");
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* A `:::name` block with a name no renderer knows (a generic container): its content in a
|
|
990
|
+
* marked <div>, the title (text after the name) first.
|
|
991
|
+
*/
|
|
992
|
+
function genericContainerToAstro(node, ctx) {
|
|
993
|
+
const name = typeof node.meta?.name === "string" ? node.meta.name : "container";
|
|
994
|
+
const title = typeof node.meta?.title === "string" ? `<p class="font-semibold">${escapeTemplate(node.meta.title)}</p>\n` : "";
|
|
995
|
+
const classAttr = mergeClassAttr("", node.props?.class);
|
|
996
|
+
const passthrough = renderPassthroughAttrs(node.props, ["class"]);
|
|
997
|
+
return `<div data-directive="${escapeAttr(name)}"${classAttr}${passthrough}>\n${title}${childrenToAstro(node, ctx)}\n</div>`;
|
|
998
|
+
}
|
|
856
999
|
function detailsToAstro(node, ctx) {
|
|
857
1000
|
ctx.imports.add("details");
|
|
858
|
-
|
|
1001
|
+
// `variant` as documented; `type` as dogsbay-ssg (and callouts) read it.
|
|
1002
|
+
const variant = node.props?.variant ?? node.props?.type ?? "note";
|
|
859
1003
|
const title = node.props?.title ?? "Details";
|
|
860
1004
|
const open = node.props?.open;
|
|
861
1005
|
const icon = node.props?.icon;
|
|
@@ -867,7 +1011,7 @@ function detailsToAstro(node, ctx) {
|
|
|
867
1011
|
attrs.push(`icon="${escapeAttr(icon)}"`);
|
|
868
1012
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
869
1013
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
870
|
-
"variant", "title", "open", "icon", "class", "summary",
|
|
1014
|
+
"variant", "type", "title", "open", "icon", "class", "summary",
|
|
871
1015
|
]);
|
|
872
1016
|
return `<CollapsibleAlert ${attrs.join(" ")}${classAttr}${passthrough}>
|
|
873
1017
|
${indentStr(inner, 2)}
|
|
@@ -939,21 +1083,44 @@ function tabsToAstro(node, ctx) {
|
|
|
939
1083
|
.join("\n");
|
|
940
1084
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
941
1085
|
const passthrough = renderPassthroughAttrs(node.props, ["sync", "default", "class"]);
|
|
1086
|
+
// Groups with the same key switch together (db-tabs). Written `syncKey`, `sync-key` or `group`, as dogsbay-ssg reads it.
|
|
1087
|
+
const syncKey = [node.props?.syncKey, node.props?.["sync-key"], node.props?.group].find((v) => typeof v === "string" && v !== "");
|
|
1088
|
+
const syncAttr = syncKey ? componentAttr("syncKey", syncKey) : "";
|
|
942
1089
|
// Same encoding as the trigger/content `value` it selects — see above.
|
|
943
|
-
return `<Tabs${componentAttr("defaultValue", defaultValue)}${classAttr}${passthrough}>
|
|
1090
|
+
return `<Tabs${componentAttr("defaultValue", defaultValue)}${syncAttr}${classAttr}${passthrough}>
|
|
944
1091
|
<TabsList>
|
|
945
1092
|
${triggers}
|
|
946
1093
|
</TabsList>
|
|
947
1094
|
${contents}
|
|
948
1095
|
</Tabs>`;
|
|
949
1096
|
}
|
|
1097
|
+
/**
|
|
1098
|
+
* A caption is inline Markdown. Plain text renders exactly as before;
|
|
1099
|
+
* formatting (`Sample \`<plugin>\` file`) renders as inline markup.
|
|
1100
|
+
*/
|
|
1101
|
+
function captionInline(caption) {
|
|
1102
|
+
return parseInline(caption);
|
|
1103
|
+
}
|
|
1104
|
+
const isPlainCaption = (nodes) => nodes.every((n) => n.type === "text" && !n.bold && !n.italic && !n.strikethrough);
|
|
1105
|
+
function captionToAstro(caption, ctx) {
|
|
1106
|
+
const nodes = captionInline(caption);
|
|
1107
|
+
return isPlainCaption(nodes)
|
|
1108
|
+
? escapeTemplate(nodes.map((n) => (n.type === "text" ? n.text : "")).join(""))
|
|
1109
|
+
: renderInline(nodes, ctx);
|
|
1110
|
+
}
|
|
950
1111
|
function tableToAstro(node, ctx) {
|
|
951
1112
|
ctx.imports.add("table");
|
|
952
1113
|
const classAttr = mergeClassAttr("my-4", node.props?.class);
|
|
953
1114
|
const caption = node.props?.caption;
|
|
954
|
-
const
|
|
1115
|
+
const nodes = caption ? captionInline(caption) : [];
|
|
1116
|
+
const plain = isPlainCaption(nodes);
|
|
1117
|
+
// A plain caption stays the `caption` prop; a formatted one fills the table's caption slot
|
|
1118
|
+
const captionAttr = caption && plain
|
|
1119
|
+
? ` caption={${escapeExpr(nodes.map((n) => (n.type === "text" ? n.text : "")).join(""))}}`
|
|
1120
|
+
: "";
|
|
1121
|
+
const captionSlot = caption && !plain ? `<Fragment slot="caption">${renderInline(nodes, ctx)}</Fragment>\n` : "";
|
|
955
1122
|
const passthrough = renderPassthroughAttrs(node.props, ["class", "caption"]);
|
|
956
|
-
return `<Table${classAttr}${captionAttr}${passthrough}>\n${childrenToAstro(node, ctx)}\n</Table>`;
|
|
1123
|
+
return `<Table${classAttr}${captionAttr}${passthrough}>\n${captionSlot}${childrenToAstro(node, ctx)}\n</Table>`;
|
|
957
1124
|
}
|
|
958
1125
|
function diagramToAstro(node, ctx) {
|
|
959
1126
|
ctx.imports.add("diagram");
|
|
@@ -1019,6 +1186,11 @@ function footnoteListToAstro(node, ctx) {
|
|
|
1019
1186
|
function footnoteDefToAstro(node, ctx) {
|
|
1020
1187
|
const id = node.props?.id ?? "";
|
|
1021
1188
|
const backrefId = node.props?.backrefId ?? "";
|
|
1189
|
+
// Block content (a list, code): the blocks, with the backref after them
|
|
1190
|
+
if (node.children?.length) {
|
|
1191
|
+
const backref = backrefId ? `\n<a href="#${escapeAttr(backrefId)}" class="text-primary hover:underline">↩</a>` : "";
|
|
1192
|
+
return `<li id="${escapeAttr(id)}" class="text-sm text-muted-foreground">\n${childrenToAstro(node, ctx)}${backref}\n</li>`;
|
|
1193
|
+
}
|
|
1022
1194
|
if (ctx.mode === "template" && node.inline) {
|
|
1023
1195
|
const content = inlineToTemplate(node.inline);
|
|
1024
1196
|
const backref = backrefId ? ` <a href="#${escapeAttr(backrefId)}" class="text-primary hover:underline">↩</a>` : "";
|
|
@@ -1088,11 +1260,17 @@ function endpointToAstro(node, ctx) {
|
|
|
1088
1260
|
const path = props.path ?? "";
|
|
1089
1261
|
const baseUrl = props.baseUrl ?? "";
|
|
1090
1262
|
const summary = props.summary;
|
|
1091
|
-
|
|
1263
|
+
// Descriptions are CommonMark, rendered with raw HTML off: the components
|
|
1264
|
+
// put them in the page with set:html (api-markdown.ts).
|
|
1265
|
+
// (Compared as text first: the card hides a description equal to the
|
|
1266
|
+
// summary, and the rendered HTML never equals it.)
|
|
1267
|
+
const description = typeof props.description === "string" && props.description !== props.summary
|
|
1268
|
+
? apiBlockHtml(props.description)
|
|
1269
|
+
: undefined;
|
|
1092
1270
|
const deprecated = props.deprecated === true;
|
|
1093
|
-
const parameters = props.parameters ?? [];
|
|
1094
|
-
const requestBody = props.requestBody ?? [];
|
|
1095
|
-
const responses = props.responses ?? [];
|
|
1271
|
+
const parameters = apiPropertiesHtml(props.parameters) ?? [];
|
|
1272
|
+
const requestBody = apiPropertiesHtml(props.requestBody) ?? [];
|
|
1273
|
+
const responses = (props.responses ?? []).map((r) => r && Array.isArray(r.schema) ? { ...r, schema: apiPropertiesHtml(r.schema) } : r);
|
|
1096
1274
|
const codeSamples = props.codeSamples ?? [];
|
|
1097
1275
|
const requestBodyExample = props.requestBodyExample;
|
|
1098
1276
|
const cardAttrs = [
|
|
@@ -1180,13 +1358,16 @@ function cardsToAstro(node, ctx) {
|
|
|
1180
1358
|
if (cardNodes.length > 0) {
|
|
1181
1359
|
const cards = cardNodes.map((card) => {
|
|
1182
1360
|
const title = card.props?.title ?? "";
|
|
1183
|
-
const href = card.props?.href
|
|
1361
|
+
const href = safeHref(card.props?.href);
|
|
1184
1362
|
const description = card.props?.description;
|
|
1185
1363
|
const icon = card.props?.icon;
|
|
1186
1364
|
// Description sources: inline content, children, or props.description.
|
|
1187
|
-
|
|
1365
|
+
// Child blocks carry their own <p>, so they get a <div>: <p><p>…</p></p> is invalid HTML.
|
|
1366
|
+
const blocks = childrenToAstro(card, ctx);
|
|
1367
|
+
const inner = blocks
|
|
1188
1368
|
|| (card.inline ? inlineToTemplate(card.inline) : "")
|
|
1189
1369
|
|| (description ? escapeTemplate(description) : "");
|
|
1370
|
+
const descTag = blocks ? "div" : "p";
|
|
1190
1371
|
// Icon renders inline before the title — picks up Lucide
|
|
1191
1372
|
// SVG via the `icon` import. Resolved at build time; nothing
|
|
1192
1373
|
// emitted when the name doesn't resolve (silent fallback).
|
|
@@ -1197,9 +1378,13 @@ function cardsToAstro(node, ctx) {
|
|
|
1197
1378
|
iconHtml = cardIconHtml(icon, title, ctx);
|
|
1198
1379
|
}
|
|
1199
1380
|
const titleHtml = title ? `<h3 class="text-base font-semibold">${escapeTemplate(title)}</h3>` : "";
|
|
1200
|
-
|
|
1201
|
-
|
|
1381
|
+
// A description and a body are both shown (the description used to be dropped when a body existed).
|
|
1382
|
+
const lead = blocks && description
|
|
1383
|
+
? `<p class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${escapeTemplate(description)}</p>`
|
|
1202
1384
|
: "";
|
|
1385
|
+
const desc = lead + (inner
|
|
1386
|
+
? `<${descTag} class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${inner}</${descTag}>`
|
|
1387
|
+
: "");
|
|
1203
1388
|
const cardDefaultClass = href
|
|
1204
1389
|
? "p-4 hover:border-foreground/20 transition-colors"
|
|
1205
1390
|
: "p-4";
|
|
@@ -1237,12 +1422,15 @@ function cardsToAstro(node, ctx) {
|
|
|
1237
1422
|
function standaloneCardToAstro(node, ctx) {
|
|
1238
1423
|
ctx.imports.add("card");
|
|
1239
1424
|
const title = node.props?.title ?? "";
|
|
1240
|
-
const href = node.props?.href
|
|
1425
|
+
const href = safeHref(node.props?.href);
|
|
1241
1426
|
const description = node.props?.description;
|
|
1242
1427
|
const icon = node.props?.icon;
|
|
1243
|
-
|
|
1428
|
+
// Child blocks carry their own <p>, so they get a <div>: <p><p>…</p></p> is invalid HTML.
|
|
1429
|
+
const blocks = childrenToAstro(node, ctx);
|
|
1430
|
+
const inner = blocks
|
|
1244
1431
|
|| (node.inline ? inlineToTemplate(node.inline) : "")
|
|
1245
1432
|
|| (description ? escapeTemplate(description) : "");
|
|
1433
|
+
const descTag = blocks ? "div" : "p";
|
|
1246
1434
|
// Icon renders inline before the title (same shape as the
|
|
1247
1435
|
// cards-list path). Resolved at build time via @dogsbay/icons.
|
|
1248
1436
|
let iconHtml = "";
|
|
@@ -1253,9 +1441,13 @@ function standaloneCardToAstro(node, ctx) {
|
|
|
1253
1441
|
const titleHtml = title
|
|
1254
1442
|
? `<h3 class="text-base font-semibold">${escapeTemplate(title)}</h3>`
|
|
1255
1443
|
: "";
|
|
1256
|
-
|
|
1257
|
-
|
|
1444
|
+
// A description and a body are both shown (the description used to be dropped when a body existed).
|
|
1445
|
+
const lead = blocks && description
|
|
1446
|
+
? `<p class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${escapeTemplate(description)}</p>`
|
|
1258
1447
|
: "";
|
|
1448
|
+
const desc = lead + (inner
|
|
1449
|
+
? `<${descTag} class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${inner}</${descTag}>`
|
|
1450
|
+
: "");
|
|
1259
1451
|
const cardDefaultClass = href
|
|
1260
1452
|
? "p-4 hover:border-foreground/20 transition-colors"
|
|
1261
1453
|
: "p-4";
|
|
@@ -1298,10 +1490,19 @@ function cardIconHtml(icon, title, ctx) {
|
|
|
1298
1490
|
ctx.imports.add("icon");
|
|
1299
1491
|
return `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1300
1492
|
}
|
|
1493
|
+
/**
|
|
1494
|
+
* A button's variant as `Button.astro` names it. The spec's `primary` is its
|
|
1495
|
+
* filled `default` style; `Button.astro` has no `primary`, so it rendered
|
|
1496
|
+
* with no fill at all.
|
|
1497
|
+
*/
|
|
1498
|
+
function buttonVariant(variant) {
|
|
1499
|
+
return !variant || variant === "primary" ? "default" : variant;
|
|
1500
|
+
}
|
|
1301
1501
|
function linkButtonToAstro(node, ctx) {
|
|
1302
1502
|
ctx.imports.add("link-button");
|
|
1303
|
-
const href = node.props?.href
|
|
1304
|
-
const
|
|
1503
|
+
const href = safeHref(node.props?.href);
|
|
1504
|
+
const written = node.props?.variant;
|
|
1505
|
+
const variant = written === undefined ? undefined : buttonVariant(written);
|
|
1305
1506
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1306
1507
|
const passthrough = renderPassthroughAttrs(node.props, ["href", "variant", "class"]);
|
|
1307
1508
|
// A button's label is phrasing content, so a lone paragraph child is
|
|
@@ -1314,6 +1515,11 @@ function linkButtonToAstro(node, ctx) {
|
|
|
1314
1515
|
: leafContent(node, ctx) || childrenToAstro(node, ctx);
|
|
1315
1516
|
return `<Button${componentAttr("href", href)}${variant ? componentAttr("variant", variant) : ""}${classAttr}${passthrough}>${label}</Button>`;
|
|
1316
1517
|
}
|
|
1518
|
+
/** A card or button `href` from an importer, or "" (no link) when its
|
|
1519
|
+
* scheme could run script: these never passed markdown-it's link check. */
|
|
1520
|
+
function safeHref(href) {
|
|
1521
|
+
return typeof href === "string" && isSafeHref(href) ? href : "";
|
|
1522
|
+
}
|
|
1317
1523
|
function linkCardToAstro(node, ctx) {
|
|
1318
1524
|
const hasRichChildren = (node.children ?? []).length > 0;
|
|
1319
1525
|
if (hasRichChildren) {
|
|
@@ -1322,15 +1528,17 @@ function linkCardToAstro(node, ctx) {
|
|
|
1322
1528
|
ctx.imports.add("link-card");
|
|
1323
1529
|
const title = node.props?.title ?? "";
|
|
1324
1530
|
const description = node.props?.description;
|
|
1325
|
-
const href = node.props?.href
|
|
1531
|
+
const href = safeHref(node.props?.href);
|
|
1326
1532
|
const titleAttr = componentAttr("title", title);
|
|
1327
1533
|
const hrefAttr = componentAttr("href", href);
|
|
1328
1534
|
const descAttr = description ? componentAttr("description", description) : "";
|
|
1535
|
+
const icon = node.props?.icon;
|
|
1536
|
+
const iconAttr = icon ? componentAttr("icon", icon) : "";
|
|
1329
1537
|
const classAttr = mergeClassAttr("", node.props?.class);
|
|
1330
1538
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1331
1539
|
"title", "description", "href", "icon", "class",
|
|
1332
1540
|
]);
|
|
1333
|
-
return `<LinkCard${titleAttr}${descAttr}${hrefAttr}${classAttr}${passthrough} />`;
|
|
1541
|
+
return `<LinkCard${titleAttr}${descAttr}${hrefAttr}${iconAttr}${classAttr}${passthrough} />`;
|
|
1334
1542
|
}
|
|
1335
1543
|
/**
|
|
1336
1544
|
* Render an `accordion` TreeNode and its `accordion-item` children.
|
|
@@ -1469,31 +1677,25 @@ function childrenToAstro(node, ctx) {
|
|
|
1469
1677
|
* icon usage on cards registers `icon` at the renderer call site;
|
|
1470
1678
|
* this is the inline-only path. Returns true on first hit.
|
|
1471
1679
|
*/
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
return true;
|
|
1490
|
-
if ("children" in node && Array.isArray(node.children)) {
|
|
1491
|
-
for (const child of node.children) {
|
|
1492
|
-
if (inlineHasIcon(child))
|
|
1493
|
-
return true;
|
|
1494
|
-
}
|
|
1680
|
+
/** The component imports inline nodes need: `icon`, `badge`, and `link-button` for an inline `:button`. */
|
|
1681
|
+
function inlineComponentsIn(nodes, out = new Set()) {
|
|
1682
|
+
const visit = (node) => {
|
|
1683
|
+
if (node.type === "icon")
|
|
1684
|
+
out.add("icon");
|
|
1685
|
+
else if (node.type === "badge")
|
|
1686
|
+
out.add("badge");
|
|
1687
|
+
else if (node.type === "kbd")
|
|
1688
|
+
out.add("kbd");
|
|
1689
|
+
else if (node.type === "link" && node.button)
|
|
1690
|
+
out.add("link-button");
|
|
1691
|
+
if ("children" in node && Array.isArray(node.children))
|
|
1692
|
+
node.children.forEach(visit);
|
|
1693
|
+
};
|
|
1694
|
+
for (const node of nodes ?? []) {
|
|
1695
|
+
node.inline?.forEach(visit);
|
|
1696
|
+
inlineComponentsIn(node.children, out);
|
|
1495
1697
|
}
|
|
1496
|
-
return
|
|
1698
|
+
return out;
|
|
1497
1699
|
}
|
|
1498
1700
|
/** Indent, via the core's shared implementation (was a fourth local copy). */
|
|
1499
1701
|
const indentStr = indent;
|