@dogsbay/render-html 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.
@@ -40,7 +40,7 @@ export const RENDER_HTML_CAPABILITIES = [
40
40
  note: "Deliberately no embed: the page must work offline and make no request to YouTube. The Astro pipeline embeds on click.",
41
41
  },
42
42
  // ── Components ───────────────────────────────────────────
43
- { feature: "callout", support: "full", rendersAs: "Alert markup (12 variants, aliases mapped); collapsible → CollapsibleAlert `<details>`" },
43
+ { feature: "callout", support: "full", rendersAs: "Alert markup (12 variants, aliases mapped, a custom `icon`); collapsible → CollapsibleAlert `<details>`" },
44
44
  { feature: "details", support: "full", rendersAs: "CollapsibleAlert `<details>`, as the Astro serializer" },
45
45
  { feature: "details-sheet", support: "full", rendersAs: "`<db-details-sheet>` around the CollapsibleAlert `<details>` (a right-hand native `<dialog>` when the element script runs; a collapsible block with `js: none`)" },
46
46
  { feature: "theme", support: "full", rendersAs: "a `<div class=\"dark …\" data-theme>` (THEME_SCOPES); an unknown name keeps the content and is reported" },
package/dist/render.js CHANGED
@@ -98,6 +98,10 @@ function inline(n, ctx) {
98
98
  case "code":
99
99
  return decorate(`<code>${esc(n.text)}</code>`, n);
100
100
  case "link": {
101
+ // Inside a whole-card link, a link is its text: a browser ends the outer <a> at an inner one,
102
+ // and the rest of the card falls out of the link
103
+ if (ctx.inLink)
104
+ return renderInline(n.children, ctx);
101
105
  const href = linkUrl(n.href, ctx);
102
106
  const external = /^[a-z][a-z0-9+.-]*:|^\/\//i.test(href) && !href.startsWith("mailto:");
103
107
  // An inline `:button`: Button.astro's classes at its small size, as the Astro pipeline renders it.
@@ -749,23 +753,52 @@ function calloutLabel(type, ctx) {
749
753
  }
750
754
  function callout(n, ctx) {
751
755
  const p = (n.props ?? {});
752
- const type = (str(p.type) ?? "note").toLowerCase();
756
+ // `variant` first, as format-astro reads it (Starlight and MkDocs importers write it)
757
+ const type = (str(p.variant) ?? str(p.type) ?? "note").toLowerCase();
753
758
  const variant = ALERT_VARIANT[VARIANT_ALIAS[type] ?? type] ? VARIANT_ALIAS[type] ?? type : "note";
754
759
  const title = str(p.title);
755
- const titleHtml = title ? renderInline(parseInline(title), ctx) : esc(calloutLabel(type, ctx));
760
+ const titleHtml = title ? titleMarkup(title, ctx) : esc(calloutLabel(type, ctx));
756
761
  const body = renderBlocks(n.children, ctx);
757
- if (p.collapsible === true || p.collapsible === "true" || p.collapsed != null) {
758
- const open = p.collapsed === false || p.collapsed === "false" || p.open != null;
759
- return collapsibleAlert(variant, titleHtml, body, open, p);
762
+ if (flagOn(p.collapsible) || p.collapsed != null) {
763
+ const open = p.collapsed === false || p.collapsed === "false" || flagOn(p.open);
764
+ return collapsibleAlert(variant, titleHtml, body, open, { ...p, "data-type": p["data-type"] ?? type }, ctx);
760
765
  }
761
766
  return (`<div${attrs({ ...passthrough(p, `${ALERT_BASE} ${ALERT_VARIANT[variant]} my-4`), "data-variant": variant, "data-type": type, role: "note" })}>` +
762
- (ALERT_ICONS[variant] ?? "").replace("<svg ", '<svg aria-hidden="true" focusable="false" ') +
767
+ alertIcon(variant, p, ctx) +
763
768
  `<div data-part="title" class="mb-1 text-sm font-semibold leading-none tracking-tight">${titleHtml}</div>` +
764
769
  `<div class="text-sm [&_p]:leading-relaxed">${body}</div></div>\n`);
765
770
  }
766
- function collapsibleAlert(variant, titleHtml, body, open, p) {
771
+ /** Formatting tags with no attributes, the HTML a title keeps (format-astro `TITLE_TAG`). */
772
+ const TITLE_TAG = /^<\/?(?:code|kbd|b|strong|i|em|s|del|mark|sub|sup|br)\s*\/?>$/i;
773
+ /**
774
+ * A callout or details title: inline Markdown, and raw HTML other than bare formatting tags as text
775
+ * (format-astro `titleInline`). `Map<K, V>` from an importer lost words as HTML.
776
+ */
777
+ function titleMarkup(title, ctx) {
778
+ return renderInline(parseInline(title).map((n) => (n.type === "html-inline" && !TITLE_TAG.test(n.html.trim()) ? { type: "text", text: n.html } : n)), ctx);
779
+ }
780
+ /**
781
+ * A presence flag: `open`, `open=""`, `open="true"` and `true` are on; absent, `false` and `"false"` are
782
+ * off. format-astro reads flags with the same rule (`flagOn`); `open="false"` used to open here.
783
+ */
784
+ function flagOn(v) {
785
+ return v !== undefined && v !== null && v !== false && v !== "false";
786
+ }
787
+ /**
788
+ * The icon an alert draws: the author's `icon` when it resolves (Alert.astro's rule, through the same
789
+ * resolver as `:icon`), else the variant's. An `icon` that resolves to nothing is reported, as `:icon` is.
790
+ */
791
+ function alertIcon(variant, p, ctx) {
792
+ const name = str(p.icon);
793
+ const custom = name ? resolveIconMarkup(name, ctx) : undefined;
794
+ if (name && custom === undefined)
795
+ (ctx.missingIcons ??= new Set()).add(name);
796
+ const svg = custom ?? ALERT_ICONS[variant] ?? "";
797
+ return svg.startsWith("<svg") && !/^<svg[^>]*aria-hidden/.test(svg) ? svg.replace("<svg ", '<svg aria-hidden="true" focusable="false" ') : svg;
798
+ }
799
+ function collapsibleAlert(variant, titleHtml, body, open, p, ctx) {
767
800
  const c = COLLAPSIBLE[variant] ?? COLLAPSIBLE.note;
768
- const icon = (ALERT_ICONS[variant] ?? "").replace("<svg ", '<svg aria-hidden="true" focusable="false" ');
801
+ const icon = alertIcon(variant, p, ctx);
769
802
  return (`<details${attrs({ ...passthrough(p, `group/collapsible relative my-4 w-full overflow-hidden rounded-lg border [&>summary]:list-none [&>summary::-webkit-details-marker]:hidden ${c.border}`), "data-variant": variant, open })}>` +
770
803
  `<summary class="flex cursor-pointer select-none items-center gap-2 px-4 py-3 text-sm font-semibold ${c.summary}"><span class="shrink-0 [&>svg]:size-4 ${c.icon}">${icon}</span><span class="flex-1 text-foreground" data-part="title">${titleHtml}</span><span class="shrink-0 text-muted-foreground transition-transform duration-200 group-open/collapsible:rotate-180">${ALERT_CHEVRON.replace("<svg ", '<svg aria-hidden="true" focusable="false" ')}</span></summary>` +
771
804
  `<div class="px-4 py-3 text-sm [&_p]:leading-relaxed ${c.body}">${body}</div></details>\n`);
@@ -895,7 +928,7 @@ function details(n, ctx) {
895
928
  // `variant` as documented, then `type`, as the Astro pipeline reads it.
896
929
  const type = (str(p.variant) ?? str(p.type) ?? "").toLowerCase();
897
930
  const variant = ALERT_VARIANT[VARIANT_ALIAS[type] ?? type] ? VARIANT_ALIAS[type] ?? type : "note";
898
- const html = collapsibleAlert(variant, renderInline(parseInline(title), ctx), renderBlocks(n.children, ctx), p.open != null && p.open !== false, p);
931
+ const html = collapsibleAlert(variant, titleMarkup(title, ctx), renderBlocks(n.children, ctx), flagOn(p.open), p, ctx);
899
932
  // `sheet`: the same details, which <db-details-sheet> opens as a side
900
933
  // sheet when its script runs (and which stays a details when it does not)
901
934
  if (!isDetailsSheet(p))
@@ -930,6 +963,14 @@ function insideLink(yes, ctx, fn) {
930
963
  * gives, so pages match the Astro pipeline's. Without one: the built-in Lucide set. An icon neither knows is
931
964
  * recorded for a build warning and leaves a marked, empty span.
932
965
  */
966
+ /** An icon's markup without a wrapper: the host's resolver (an SVG, or an emoji in Alert.astro's span), then the built-in set. */
967
+ function resolveIconMarkup(name, ctx) {
968
+ const resolved = ctx.icons?.(name);
969
+ if (resolved)
970
+ return resolved.startsWith("<") ? resolved : `<span aria-hidden="true" class="inline-block w-[1em] text-center leading-none">${esc(resolved)}</span>`;
971
+ const builtin = name.replace(/^lucide:/, "");
972
+ return hasIcon(builtin) ? icon(builtin) : undefined;
973
+ }
933
974
  function namedIcon(name, cls, ctx) {
934
975
  const resolved = ctx.icons?.(name);
935
976
  if (resolved)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/render-html",
3
- "version": "0.2.0-beta.127",
3
+ "version": "0.2.0-beta.129",
4
4
  "description": "Dogsbay MD TreeNode → HTML with the Astro components' markup and classes. No highlighter, math or CSS compiler unless you pass one; Worker- and browser-safe",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,9 +30,9 @@
30
30
  "dependencies": {
31
31
  "htmlparser2": "^10.1.0",
32
32
  "markdown-it": "^14.3.0",
33
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.127",
34
- "@dogsbay/serialize-core": "0.2.0-beta.127",
35
- "@dogsbay/types": "0.2.0-beta.127"
33
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.129",
34
+ "@dogsbay/serialize-core": "0.2.0-beta.129",
35
+ "@dogsbay/types": "0.2.0-beta.129"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "katex": "^0.16.0"