@dogsbay/format-astro 0.2.0-beta.127 → 0.2.0-beta.129
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/capabilities.js +2 -2
- package/dist/serialize.js +111 -15
- package/package.json +7 -7
package/dist/capabilities.js
CHANGED
|
@@ -27,8 +27,8 @@ export const ASTRO_CAPABILITIES = [
|
|
|
27
27
|
{ feature: "diagram", support: "full", rendersAs: "`<Diagram>` (source + pre-rendered SVG)" },
|
|
28
28
|
{ feature: "youtube", support: "full", rendersAs: "`<YouTube>`" },
|
|
29
29
|
// ── Components ───────────────────────────────────────────
|
|
30
|
-
{ feature: "callout", support: "full", rendersAs: "`<Alert variant>` with the authored type kept as `data-type`" },
|
|
31
|
-
{ feature: "details", support: "full", rendersAs: "`<CollapsibleAlert variant title open icon>` (`title`, or its alias `summary`)" },
|
|
30
|
+
{ feature: "callout", support: "full", rendersAs: "`<Alert variant icon>` with the authored type kept as `data-type`; `collapsed`/`collapsible` → `<CollapsibleAlert>` (a `<details>`), as render-html; the title is inline Markdown" },
|
|
31
|
+
{ feature: "details", support: "full", rendersAs: "`<CollapsibleAlert variant title open icon>` (`title`, or its alias `summary`, inline Markdown in a `title` slot)" },
|
|
32
32
|
{ feature: "details-sheet", support: "full", rendersAs: "`<DetailsSheet>` around the CollapsibleAlert: `<db-details-sheet>` opens it as a right-hand native `<dialog>`; without the script it stays a collapsible block" },
|
|
33
33
|
{ feature: "theme", support: "full", rendersAs: "a `<div class=\"dark …\" data-theme>`: the theme re-declares its tokens on `.dark`, so the subtree turns dark" },
|
|
34
34
|
{ feature: "tabs-variant", support: "full", rendersAs: "TabsList/TabsTrigger given the variant's classes (TAB_VARIANTS), merged over their base" },
|
package/dist/serialize.js
CHANGED
|
@@ -176,6 +176,7 @@ export function escapeTemplate(s) {
|
|
|
176
176
|
export function treeToAstro(nodes, options) {
|
|
177
177
|
const ctx = {
|
|
178
178
|
imports: new Set(),
|
|
179
|
+
diagramIds: new Map(),
|
|
179
180
|
indent: 0,
|
|
180
181
|
scripts: new Set(),
|
|
181
182
|
mode: options?.mode ?? "template",
|
|
@@ -495,6 +496,9 @@ function templateInlineEmitters() {
|
|
|
495
496
|
strike: (s) => `<s>${s}</s>`,
|
|
496
497
|
}),
|
|
497
498
|
link: (node, ctx) => {
|
|
499
|
+
// Inside a whole-card link, a link is its text (wholeCardLink)
|
|
500
|
+
if (cardLinkDepth > 0)
|
|
501
|
+
return ctx.recurse(node.children);
|
|
498
502
|
// A link node an importer built directly never passed markdown-it's
|
|
499
503
|
// scheme check: a `javascript:` href keeps its text and loses the link.
|
|
500
504
|
if (!isSafeHref(node.href)) {
|
|
@@ -565,6 +569,8 @@ function htmlModeInlineEmitters() {
|
|
|
565
569
|
// EXPRESSION, inside a string that becomes a `set:html` value. Astro never
|
|
566
570
|
// parses it as markup, so the reader saw the braces and quotes.
|
|
567
571
|
link: (node, ctx) => {
|
|
572
|
+
if (cardLinkDepth > 0)
|
|
573
|
+
return ctx.recurse(node.children);
|
|
568
574
|
if (!isSafeHref(node.href)) {
|
|
569
575
|
ctx.report("link", `unsafe link target dropped (${node.href.slice(0, 40)})`);
|
|
570
576
|
return ctx.recurse(node.children);
|
|
@@ -869,13 +875,54 @@ function calloutToAstro(node, ctx) {
|
|
|
869
875
|
"type", "variant", "title", "icon", "class",
|
|
870
876
|
]);
|
|
871
877
|
const inner = childrenToAstro(node, ctx);
|
|
878
|
+
// An authored title is inline Markdown (`Before \`harbor init\``), as render-html reads it (MD-002);
|
|
879
|
+
// the default label is text
|
|
880
|
+
const titleNodes = explicitTitle && explicitTitle.length > 0 ? titleInline(title) : [{ type: "text", text: title }];
|
|
881
|
+
const plainTitle = isPlainCaption(titleNodes);
|
|
882
|
+
// `collapsed` / `collapsible`: a disclosure, as render-html draws it (MD-001)
|
|
883
|
+
if (isCollapsibleCallout(node.props)) {
|
|
884
|
+
ctx.imports.add("details");
|
|
885
|
+
const attrs = [`variant="${escapeAttr(collapsibleVariant(variant))}"`, componentAttr("title", plainTitle ? captionText(titleNodes) : title).trimStart()];
|
|
886
|
+
if (calloutStartsOpen(node.props))
|
|
887
|
+
attrs.push("open");
|
|
888
|
+
const slot = plainTitle ? "" : `\n <Fragment slot="title">${renderInline(titleNodes, ctx)}</Fragment>`;
|
|
889
|
+
const collapsiblePassthrough = renderPassthroughAttrs(node.props, ["type", "variant", "title", "icon", "class", "collapsed", "collapsible", "open"]);
|
|
890
|
+
return `<CollapsibleAlert ${attrs.join(" ")}${iconAttr}${classAttr}${collapsiblePassthrough}>${slot}
|
|
891
|
+
${indentStr(inner, 2)}
|
|
892
|
+
</CollapsibleAlert>`;
|
|
893
|
+
}
|
|
872
894
|
return `<Alert variant="${escapeAttr(variant)}"${iconAttr}${classAttr}${passthrough}>
|
|
873
|
-
<AlertTitle>${escapeTemplate(
|
|
895
|
+
<AlertTitle>${plainTitle ? escapeTemplate(captionText(titleNodes)) : renderInline(titleNodes, ctx)}</AlertTitle>
|
|
874
896
|
<AlertDescription>
|
|
875
897
|
${indentStr(inner, 4)}
|
|
876
898
|
</AlertDescription>
|
|
877
899
|
</Alert>`;
|
|
878
900
|
}
|
|
901
|
+
/**
|
|
902
|
+
* A presence flag: `open`, `open=""`, `open="true"` and `true` are on; absent, `false` and `"false"` are
|
|
903
|
+
* off. A bare `{open}` parses as "" (falsy), and "false" is truthy, so a plain truth test got both wrong
|
|
904
|
+
* (MD-003). render-html reads flags with the same rule (`flagOn`).
|
|
905
|
+
*/
|
|
906
|
+
function flagOn(v) {
|
|
907
|
+
return v !== undefined && v !== null && v !== false && v !== "false";
|
|
908
|
+
}
|
|
909
|
+
/** CollapsibleAlert's variants; `plain` is chrome (no tint, icon or role). */
|
|
910
|
+
const COLLAPSIBLE_VARIANTS = new Set(["plain", "destructive", "note", "abstract", "info", "tip", "success", "question", "warning", "failure", "danger", "bug", "example", "quote"]);
|
|
911
|
+
/** A disclosure's variant: lower-cased, aliases mapped, anything CollapsibleAlert does not draw → note. */
|
|
912
|
+
function collapsibleVariant(raw) {
|
|
913
|
+
const v = raw.toLowerCase();
|
|
914
|
+
const mapped = CALLOUT_VARIANT_MAP[v] ?? v;
|
|
915
|
+
return COLLAPSIBLE_VARIANTS.has(mapped) ? mapped : "note";
|
|
916
|
+
}
|
|
917
|
+
/** A callout with `collapsible` on or any `collapsed` is a disclosure (render-html's `callout`). */
|
|
918
|
+
function isCollapsibleCallout(props) {
|
|
919
|
+
return flagOn(props?.collapsible) || (props?.collapsed !== undefined && props?.collapsed !== null);
|
|
920
|
+
}
|
|
921
|
+
/** It starts open when `collapsed` is off (`collapsed="false"`) or `open` is on. */
|
|
922
|
+
function calloutStartsOpen(props) {
|
|
923
|
+
const c = props?.collapsed;
|
|
924
|
+
return c === false || c === "false" || flagOn(props?.open);
|
|
925
|
+
}
|
|
879
926
|
/**
|
|
880
927
|
* Merge a default class string with an optional user-provided class string.
|
|
881
928
|
* Concatenates (user classes appended) so utility frameworks like Tailwind
|
|
@@ -1016,13 +1063,17 @@ function genericContainerToAstro(node, ctx) {
|
|
|
1016
1063
|
function detailsToAstro(node, ctx) {
|
|
1017
1064
|
ctx.imports.add("details");
|
|
1018
1065
|
// `variant` as documented; `type` as dogsbay-ssg (and callouts) read it.
|
|
1019
|
-
|
|
1066
|
+
// Aliases (`caution` → danger) and unknown names (→ note) as render-html maps them
|
|
1067
|
+
const variant = collapsibleVariant(node.props?.variant ?? node.props?.type ?? "note");
|
|
1020
1068
|
// `summary` is dogsbay-md's alias for the title (`:::details{summary="…"}`)
|
|
1021
1069
|
const title = node.props?.title ?? node.props?.summary ?? "Details";
|
|
1022
|
-
const open = node.props?.open;
|
|
1070
|
+
const open = flagOn(node.props?.open);
|
|
1023
1071
|
const icon = node.props?.icon;
|
|
1024
1072
|
const inner = childrenToAstro(node, ctx);
|
|
1025
|
-
|
|
1073
|
+
// The title is inline Markdown, as in render-html: plain text stays the `title` prop
|
|
1074
|
+
const titleNodes = titleInline(title);
|
|
1075
|
+
const plainTitle = isPlainCaption(titleNodes);
|
|
1076
|
+
const attrs = [`variant="${escapeAttr(variant)}"`, componentAttr("title", plainTitle ? captionText(titleNodes) : title).trimStart()];
|
|
1026
1077
|
if (open)
|
|
1027
1078
|
attrs.push("open");
|
|
1028
1079
|
if (icon)
|
|
@@ -1031,7 +1082,8 @@ function detailsToAstro(node, ctx) {
|
|
|
1031
1082
|
const passthrough = renderPassthroughAttrs(node.props, [
|
|
1032
1083
|
"variant", "type", "title", "open", "icon", "class", "summary", "sheet",
|
|
1033
1084
|
]);
|
|
1034
|
-
const
|
|
1085
|
+
const slot = plainTitle ? "" : `\n <Fragment slot="title">${renderInline(titleNodes, ctx)}</Fragment>`;
|
|
1086
|
+
const alert = `<CollapsibleAlert ${attrs.join(" ")}${classAttr}${passthrough}>${slot}
|
|
1035
1087
|
${indentStr(inner, 2)}
|
|
1036
1088
|
</CollapsibleAlert>`;
|
|
1037
1089
|
// `sheet`: the same details, opened as a side sheet by <db-details-sheet>
|
|
@@ -1117,10 +1169,25 @@ function captionInline(caption) {
|
|
|
1117
1169
|
return parseInline(caption);
|
|
1118
1170
|
}
|
|
1119
1171
|
const isPlainCaption = (nodes) => nodes.every((n) => n.type === "text" && !n.bold && !n.italic && !n.strikethrough);
|
|
1172
|
+
/**
|
|
1173
|
+
* Formatting tags with no attributes, the HTML a title keeps (a raw `<details><summary><code>x</code>`
|
|
1174
|
+
* becomes the title `<code>x</code>`). render-html has the same list.
|
|
1175
|
+
*/
|
|
1176
|
+
const TITLE_TAG = /^<\/?(?:code|kbd|b|strong|i|em|s|del|mark|sub|sup|br)\s*\/?>$/i;
|
|
1177
|
+
/**
|
|
1178
|
+
* A callout or details title: inline Markdown, and any other raw HTML is TEXT. Importers write
|
|
1179
|
+
* plain-text titles with angle brackets (`Map<K, V>`, `List<String>`), which as HTML dropped words,
|
|
1180
|
+
* and a title with `<img onerror=…>` ran a script in the summary. render-html reads titles the same way.
|
|
1181
|
+
*/
|
|
1182
|
+
function titleInline(title) {
|
|
1183
|
+
return captionInline(title).map((n) => (n.type === "html-inline" && !TITLE_TAG.test(n.html.trim()) ? { type: "text", text: n.html } : n));
|
|
1184
|
+
}
|
|
1185
|
+
/** A plain caption's text (escapes undone by the parse). */
|
|
1186
|
+
const captionText = (nodes) => nodes.map((n) => (n.type === "text" ? n.text : "")).join("");
|
|
1120
1187
|
function captionToAstro(caption, ctx) {
|
|
1121
1188
|
const nodes = captionInline(caption);
|
|
1122
1189
|
return isPlainCaption(nodes)
|
|
1123
|
-
? escapeTemplate(nodes
|
|
1190
|
+
? escapeTemplate(captionText(nodes))
|
|
1124
1191
|
: renderInline(nodes, ctx);
|
|
1125
1192
|
}
|
|
1126
1193
|
function tableToAstro(node, ctx) {
|
|
@@ -1141,8 +1208,18 @@ function diagramToAstro(node, ctx) {
|
|
|
1141
1208
|
ctx.imports.add("diagram");
|
|
1142
1209
|
const lang = node.props?.lang || "mermaid";
|
|
1143
1210
|
const code = node.props?.code ?? "";
|
|
1144
|
-
|
|
1211
|
+
let svg = node.props?.svg ?? "";
|
|
1145
1212
|
const title = node.props?.title;
|
|
1213
|
+
// A Pele SVG's ids (`<prefix>-title`, `<prefix>-desc`, for aria-labelledby) are unique within
|
|
1214
|
+
// its source page. A book view or an include can show the same diagram twice on one page, so
|
|
1215
|
+
// the later copies are renamed; the prefix appears nowhere else in the SVG.
|
|
1216
|
+
const prefix = /^<svg[^>]*\bclass="pele\b/.test(svg) ? /\sid="(dg-[0-9a-f]{12}(?:-\d+)?)-/.exec(svg)?.[1] : undefined;
|
|
1217
|
+
if (prefix) {
|
|
1218
|
+
const n = (ctx.diagramIds.get(prefix) ?? 0) + 1;
|
|
1219
|
+
ctx.diagramIds.set(prefix, n);
|
|
1220
|
+
if (n > 1)
|
|
1221
|
+
svg = svg.split(prefix).join(`${prefix}-p${n}`);
|
|
1222
|
+
}
|
|
1146
1223
|
const attrs = [`lang="${escapeAttr(lang)}"`, `code={${escapeExpr(code)}}`, `svg={${escapeExpr(svg)}}`];
|
|
1147
1224
|
if (title)
|
|
1148
1225
|
attrs.push(componentAttr("title", title).trimStart());
|
|
@@ -1364,6 +1441,25 @@ function endpointToAstro(node, ctx) {
|
|
|
1364
1441
|
` <ApiCodePanel slot="code">${codeBody}</ApiCodePanel>\n` +
|
|
1365
1442
|
`</ApiLayout>`);
|
|
1366
1443
|
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Whole-card links being emitted. Inside one, a link in the card's body is its text: a browser
|
|
1446
|
+
* ends the outer <a> at an inner one, so the rest of the card fell out of the link (and lost
|
|
1447
|
+
* not-prose). The serializer is synchronous, so a depth count is enough.
|
|
1448
|
+
*/
|
|
1449
|
+
let cardLinkDepth = 0;
|
|
1450
|
+
function wholeCardLink(yes, fn) {
|
|
1451
|
+
if (!yes)
|
|
1452
|
+
return fn();
|
|
1453
|
+
cardLinkDepth++;
|
|
1454
|
+
try {
|
|
1455
|
+
return fn();
|
|
1456
|
+
}
|
|
1457
|
+
finally {
|
|
1458
|
+
cardLinkDepth--;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
/** A card's title: not-prose, or docs.css's h3 rule (1.25rem, 2rem above) applies inside the card. */
|
|
1462
|
+
const CARD_TITLE_CLASS = "not-prose m-0 border-0 p-0 text-base font-semibold";
|
|
1367
1463
|
function cardsToAstro(node, ctx) {
|
|
1368
1464
|
ctx.imports.add("grid");
|
|
1369
1465
|
ctx.imports.add("card");
|
|
@@ -1382,9 +1478,9 @@ function cardsToAstro(node, ctx) {
|
|
|
1382
1478
|
const icon = card.props?.icon;
|
|
1383
1479
|
// Description sources: inline content, children, or props.description.
|
|
1384
1480
|
// Child blocks carry their own <p>, so they get a <div>: <p><p>…</p></p> is invalid HTML.
|
|
1385
|
-
const blocks = childrenToAstro(card, ctx);
|
|
1481
|
+
const blocks = wholeCardLink(!!href, () => childrenToAstro(card, ctx));
|
|
1386
1482
|
const inner = blocks
|
|
1387
|
-
|| (card.inline ? inlineToTemplate(card.inline) : "")
|
|
1483
|
+
|| (card.inline ? wholeCardLink(!!href, () => inlineToTemplate(card.inline)) : "")
|
|
1388
1484
|
|| (description ? escapeTemplate(description) : "");
|
|
1389
1485
|
const descTag = blocks ? "div" : "p";
|
|
1390
1486
|
// Icon renders inline before the title — picks up Lucide
|
|
@@ -1396,7 +1492,7 @@ function cardsToAstro(node, ctx) {
|
|
|
1396
1492
|
if (icon) {
|
|
1397
1493
|
iconHtml = cardIconHtml(icon, title, ctx);
|
|
1398
1494
|
}
|
|
1399
|
-
const titleHtml = title ? `<h3 class="
|
|
1495
|
+
const titleHtml = title ? `<h3 class="${CARD_TITLE_CLASS}">${escapeTemplate(title)}</h3>` : "";
|
|
1400
1496
|
// A description and a body are both shown (the description used to be dropped when a body existed).
|
|
1401
1497
|
const lead = blocks && description
|
|
1402
1498
|
? `<p class="text-sm text-muted-foreground mt-1 [text-decoration:none]">${escapeTemplate(description)}</p>`
|
|
@@ -1415,7 +1511,7 @@ function cardsToAstro(node, ctx) {
|
|
|
1415
1511
|
"title", "href", "description", "icon", "variant", "class", "tone",
|
|
1416
1512
|
]);
|
|
1417
1513
|
if (href) {
|
|
1418
|
-
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="[text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1514
|
+
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="not-prose [text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1419
1515
|
}
|
|
1420
1516
|
return `<Card${cardClassAttr}${cardPass}>\n<CardContent class="p-0">\n${iconHtml}\n${titleHtml}\n${desc}\n</CardContent>\n</Card>`;
|
|
1421
1517
|
}).join("\n");
|
|
@@ -1448,9 +1544,9 @@ function standaloneCardToAstro(node, ctx) {
|
|
|
1448
1544
|
const description = node.props?.description;
|
|
1449
1545
|
const icon = node.props?.icon;
|
|
1450
1546
|
// Child blocks carry their own <p>, so they get a <div>: <p><p>…</p></p> is invalid HTML.
|
|
1451
|
-
const blocks = childrenToAstro(node, ctx);
|
|
1547
|
+
const blocks = wholeCardLink(!!href, () => childrenToAstro(node, ctx));
|
|
1452
1548
|
const inner = blocks
|
|
1453
|
-
|| (node.inline ? inlineToTemplate(node.inline) : "")
|
|
1549
|
+
|| (node.inline ? wholeCardLink(!!href, () => inlineToTemplate(node.inline)) : "")
|
|
1454
1550
|
|| (description ? escapeTemplate(description) : "");
|
|
1455
1551
|
const descTag = blocks ? "div" : "p";
|
|
1456
1552
|
// Icon renders inline before the title (same shape as the
|
|
@@ -1461,7 +1557,7 @@ function standaloneCardToAstro(node, ctx) {
|
|
|
1461
1557
|
iconHtml = `<Icon name="${escapeAttr(icon)}" class="size-5 mb-2 text-muted-foreground" />`;
|
|
1462
1558
|
}
|
|
1463
1559
|
const titleHtml = title
|
|
1464
|
-
? `<h3 class="
|
|
1560
|
+
? `<h3 class="${CARD_TITLE_CLASS}">${escapeTemplate(title)}</h3>`
|
|
1465
1561
|
: "";
|
|
1466
1562
|
// A description and a body are both shown (the description used to be dropped when a body existed).
|
|
1467
1563
|
const lead = blocks && description
|
|
@@ -1480,7 +1576,7 @@ function standaloneCardToAstro(node, ctx) {
|
|
|
1480
1576
|
"title", "href", "description", "icon", "variant", "class", "tone",
|
|
1481
1577
|
]);
|
|
1482
1578
|
if (href) {
|
|
1483
|
-
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="[text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1579
|
+
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n<a href="${escapeAttr(href)}" class="not-prose [text-decoration:none] text-foreground">\n${iconHtml}\n${titleHtml}\n${desc}\n</a>\n</CardContent>\n</Card>`;
|
|
1484
1580
|
}
|
|
1485
1581
|
return `<Card${classAttr}${passthrough}>\n<CardContent class="p-0">\n${iconHtml}\n${titleHtml}\n${desc}\n</CardContent>\n</Card>`;
|
|
1486
1582
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-astro",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.129",
|
|
4
4
|
"description": "Astro format plugin for Dogsbay — export TreeNode[] to Astro component markup",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"markdown-it": "^14.3.0",
|
|
35
|
-
"@dogsbay/format-dogsbay-md": "0.2.0-beta.
|
|
36
|
-
"@dogsbay/serialize-core": "0.2.0-beta.
|
|
37
|
-
"@dogsbay/
|
|
38
|
-
"@dogsbay/
|
|
39
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
40
|
-
"@dogsbay/ui": "0.2.0-beta.
|
|
35
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.129",
|
|
36
|
+
"@dogsbay/serialize-core": "0.2.0-beta.129",
|
|
37
|
+
"@dogsbay/render-html": "0.2.0-beta.129",
|
|
38
|
+
"@dogsbay/site-files": "0.2.0-beta.129",
|
|
39
|
+
"@dogsbay/types": "0.2.0-beta.129",
|
|
40
|
+
"@dogsbay/ui": "0.2.0-beta.129"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|