@dogsbay/render-html 0.2.0-beta.128 → 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
@@ -753,23 +753,52 @@ function calloutLabel(type, ctx) {
753
753
  }
754
754
  function callout(n, ctx) {
755
755
  const p = (n.props ?? {});
756
- 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();
757
758
  const variant = ALERT_VARIANT[VARIANT_ALIAS[type] ?? type] ? VARIANT_ALIAS[type] ?? type : "note";
758
759
  const title = str(p.title);
759
- const titleHtml = title ? renderInline(parseInline(title), ctx) : esc(calloutLabel(type, ctx));
760
+ const titleHtml = title ? titleMarkup(title, ctx) : esc(calloutLabel(type, ctx));
760
761
  const body = renderBlocks(n.children, ctx);
761
- if (p.collapsible === true || p.collapsible === "true" || p.collapsed != null) {
762
- const open = p.collapsed === false || p.collapsed === "false" || p.open != null;
763
- 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);
764
765
  }
765
766
  return (`<div${attrs({ ...passthrough(p, `${ALERT_BASE} ${ALERT_VARIANT[variant]} my-4`), "data-variant": variant, "data-type": type, role: "note" })}>` +
766
- (ALERT_ICONS[variant] ?? "").replace("<svg ", '<svg aria-hidden="true" focusable="false" ') +
767
+ alertIcon(variant, p, ctx) +
767
768
  `<div data-part="title" class="mb-1 text-sm font-semibold leading-none tracking-tight">${titleHtml}</div>` +
768
769
  `<div class="text-sm [&_p]:leading-relaxed">${body}</div></div>\n`);
769
770
  }
770
- 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) {
771
800
  const c = COLLAPSIBLE[variant] ?? COLLAPSIBLE.note;
772
- const icon = (ALERT_ICONS[variant] ?? "").replace("<svg ", '<svg aria-hidden="true" focusable="false" ');
801
+ const icon = alertIcon(variant, p, ctx);
773
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 })}>` +
774
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>` +
775
804
  `<div class="px-4 py-3 text-sm [&_p]:leading-relaxed ${c.body}">${body}</div></details>\n`);
@@ -899,7 +928,7 @@ function details(n, ctx) {
899
928
  // `variant` as documented, then `type`, as the Astro pipeline reads it.
900
929
  const type = (str(p.variant) ?? str(p.type) ?? "").toLowerCase();
901
930
  const variant = ALERT_VARIANT[VARIANT_ALIAS[type] ?? type] ? VARIANT_ALIAS[type] ?? type : "note";
902
- 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);
903
932
  // `sheet`: the same details, which <db-details-sheet> opens as a side
904
933
  // sheet when its script runs (and which stays a details when it does not)
905
934
  if (!isDetailsSheet(p))
@@ -934,6 +963,14 @@ function insideLink(yes, ctx, fn) {
934
963
  * gives, so pages match the Astro pipeline's. Without one: the built-in Lucide set. An icon neither knows is
935
964
  * recorded for a build warning and leaves a marked, empty span.
936
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
+ }
937
974
  function namedIcon(name, cls, ctx) {
938
975
  const resolved = ctx.icons?.(name);
939
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.128",
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.128",
34
- "@dogsbay/serialize-core": "0.2.0-beta.128",
35
- "@dogsbay/types": "0.2.0-beta.128"
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"