@dogsbay/render-html 0.2.0-beta.117 → 0.2.0-beta.118

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/README.md CHANGED
@@ -25,17 +25,31 @@ const html = renderBlocks(dogsbayMdToTree(markdown).tree, ctx);
25
25
  The output uses Tailwind utility classes and the theme tokens. Styling it needs the Dogsbay
26
26
  theme CSS, compiled for the classes the page uses.
27
27
 
28
+ ## API reference pages
29
+
30
+ Autodoc symbols (`api-class`, `api-member`, `api-doc`, `api-params`,
31
+ `api-source`) and OpenAPI operations (`endpoint`) render with the markup of the
32
+ Astro API components. Set `ctx.embeddedEndpoints` from `apiPageLayout(tree)`
33
+ (`@dogsbay/serialize-core`), which also says when the page should drop its
34
+ reading width and table of contents. Include `css/api.css`.
35
+
36
+ ```ts
37
+ import { apiPageLayout } from "@dogsbay/serialize-core";
38
+ const layout = apiPageLayout(tree); // { wide, embeddedEndpoints }
39
+ const html = renderBlocks(tree, { ...ctx, embeddedEndpoints: layout.embeddedEndpoints });
40
+ ```
41
+
28
42
  ## Stylesheets
29
43
 
30
44
  `css/` holds the stylesheets for this markup: the theme presets (`theme-default.css`,
31
- `theme-mintlify.css`), `docs.css`, `steps.css` and `code-rich.css`. Import them as
45
+ `theme-mintlify.css`), `docs.css`, `steps.css`, `code-rich.css` and `api.css`. Import them as
32
46
  `@dogsbay/render-html/css/<file>`. They are Tailwind 4 sources (`@apply`, `@utility`,
33
47
  tokens), so compile them with the page's classes.
34
48
 
35
49
  ## Copied sources
36
50
 
37
51
  `alert-icons.ts`, `badge-variants.ts`, `kbd-variants.ts`, `file-tree.ts`,
38
- `code-markers.ts`, `tone-classes.ts` and everything in `css/` are copies from `@dogsbay/ui` and
52
+ `code-markers.ts`, `tone-classes.ts`, `api-markdown.ts` and everything in `css/` are copies from `@dogsbay/ui` and
39
53
  `@dogsbay/format-astro`. Copying them keeps this package free of Astro.
40
54
 
41
55
  After changing them upstream, run `pnpm --filter @dogsbay/render-html sync-sources`.
package/css/api.css ADDED
@@ -0,0 +1,133 @@
1
+ /* Generated by packages/render-html/scripts/sync-sources.mjs from @dogsbay/ui api-symbol.css, api-source.css, EndpointSection.astro, PropertyItem.astro. Do not edit. */
2
+ /* API Symbol badges — colored by kind */
3
+ .api-badge {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ border-radius: 0.375rem;
7
+ padding: 0.125rem 0.5rem;
8
+ font-size: 0.7rem;
9
+ font-weight: 600;
10
+ letter-spacing: 0.025em;
11
+ text-transform: uppercase;
12
+ vertical-align: middle;
13
+ flex-shrink: 0;
14
+ }
15
+
16
+ .api-badge-class {
17
+ background-color: oklch(0.55 0.18 265 / 0.2);
18
+ color: oklch(0.55 0.18 265);
19
+ }
20
+ .dark .api-badge-class {
21
+ background-color: oklch(0.7 0.15 265 / 0.2);
22
+ color: oklch(0.7 0.15 265);
23
+ }
24
+
25
+ .api-badge-meth {
26
+ background-color: oklch(0.55 0.15 155 / 0.2);
27
+ color: oklch(0.55 0.15 155);
28
+ }
29
+ .dark .api-badge-meth {
30
+ background-color: oklch(0.7 0.15 155 / 0.2);
31
+ color: oklch(0.7 0.15 155);
32
+ }
33
+
34
+ .api-badge-attr {
35
+ background-color: oklch(0.6 0.15 55 / 0.2);
36
+ color: oklch(0.6 0.15 55);
37
+ }
38
+ .dark .api-badge-attr {
39
+ background-color: oklch(0.75 0.15 55 / 0.2);
40
+ color: oklch(0.75 0.15 55);
41
+ }
42
+
43
+ .api-badge-func {
44
+ background-color: oklch(0.55 0.18 330 / 0.2);
45
+ color: oklch(0.55 0.18 330);
46
+ }
47
+ .dark .api-badge-func {
48
+ background-color: oklch(0.7 0.15 330 / 0.2);
49
+ color: oklch(0.7 0.15 330);
50
+ }
51
+
52
+ .api-badge-module {
53
+ background-color: oklch(0.5 0.12 200 / 0.2);
54
+ color: oklch(0.5 0.12 200);
55
+ }
56
+ .dark .api-badge-module {
57
+ background-color: oklch(0.65 0.12 200 / 0.2);
58
+ color: oklch(0.65 0.12 200);
59
+ }
60
+
61
+ /* API Symbol container */
62
+ .api-symbol {
63
+ margin-top: 2rem;
64
+ padding-top: 1.5rem;
65
+ border-top: 1px solid var(--border, oklch(0.5 0 0 / 0.15));
66
+ }
67
+
68
+ .api-symbol:first-child {
69
+ margin-top: 0;
70
+ padding-top: 0;
71
+ border-top: none;
72
+ }
73
+
74
+ .api-symbol-heading {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 0.5rem;
78
+ margin-bottom: 0.75rem;
79
+ }
80
+
81
+ .api-symbol-heading code {
82
+ font-size: 1.25em;
83
+ font-weight: 600;
84
+ }
85
+
86
+ .api-symbol-bases {
87
+ font-size: 0.875rem;
88
+ color: var(--muted-foreground);
89
+ margin-bottom: 0.5rem;
90
+ }
91
+
92
+ .api-source {
93
+ margin-top: 0.75rem;
94
+ margin-bottom: 0.75rem;
95
+ border: 1px solid var(--color-border, oklch(0.85 0 0));
96
+ border-radius: 0.375rem;
97
+ overflow: hidden;
98
+ }
99
+
100
+ .api-source summary {
101
+ cursor: pointer;
102
+ padding: 0.5rem 0.75rem;
103
+ font-size: 0.8125rem;
104
+ font-weight: 500;
105
+ color: var(--color-muted-foreground, oklch(0.55 0 0));
106
+ background: var(--color-muted, oklch(0.96 0 0));
107
+ user-select: none;
108
+ }
109
+
110
+ .api-source summary:hover {
111
+ color: var(--color-foreground, oklch(0.15 0 0));
112
+ }
113
+
114
+ .api-source[open] summary {
115
+ border-bottom: 1px solid var(--color-border, oklch(0.85 0 0));
116
+ }
117
+
118
+ /* Remove extra margin from the code block inside */
119
+ .api-source pre {
120
+ margin: 0 !important;
121
+ border-radius: 0 !important;
122
+ border: none !important;
123
+ }
124
+
125
+
126
+ details[open] > summary [data-section-chevron] {
127
+ transform: rotate(90deg);
128
+ }
129
+
130
+
131
+ details[open] > summary [data-property-chevron] {
132
+ transform: rotate(90deg);
133
+ }
@@ -0,0 +1,7 @@
1
+ /** Block Markdown → HTML (an operation's description). */
2
+ export declare function apiBlockHtml(text: string): string;
3
+ /** One line of Markdown → inline HTML (a parameter or property). */
4
+ export declare function apiInlineHtml(text: string): string;
5
+ /** A schema property list with every `description` rendered inline,
6
+ * through nested `properties` / `items`. A copy; the node is untouched. */
7
+ export declare function apiPropertiesHtml(list: unknown): unknown;
@@ -0,0 +1,41 @@
1
+ // Generated by packages/render-html/scripts/sync-sources.mjs from @dogsbay/format-astro/src/api-markdown.ts. Do not edit.
2
+ /**
3
+ * API description text → HTML, for the API components that render it with
4
+ * `set:html` (EndpointCard, PropertyItem, ParameterTable).
5
+ *
6
+ * OpenAPI defines `description` as CommonMark, and every producer of an
7
+ * `endpoint` node (format-openapi, the MDX adapters) supplies text or
8
+ * Markdown, never HTML. It was passed through as HTML: a spec's markdown
9
+ * showed as `## Authentication` and `**Creating a token**`, and a spec from
10
+ * someone else's repository could put a `<script>` into the built site.
11
+ * Rendered here with raw HTML OFF (`<` is escaped) and markdown-it's link
12
+ * validation (no `javascript:` / `vbscript:` / `file:` / `data:` links).
13
+ */
14
+ import MarkdownIt from "markdown-it";
15
+ const md = new MarkdownIt({ html: false, linkify: false, typographer: false });
16
+ /** Block Markdown → HTML (an operation's description). */
17
+ export function apiBlockHtml(text) {
18
+ return md.render(text).trim();
19
+ }
20
+ /** One line of Markdown → inline HTML (a parameter or property). */
21
+ export function apiInlineHtml(text) {
22
+ return md.renderInline(text.replace(/\s*\n\s*/g, " ")).trim();
23
+ }
24
+ /** A schema property list with every `description` rendered inline,
25
+ * through nested `properties` / `items`. A copy; the node is untouched. */
26
+ export function apiPropertiesHtml(list) {
27
+ if (!Array.isArray(list))
28
+ return list;
29
+ return list.map((p) => {
30
+ if (!p || typeof p !== "object")
31
+ return p;
32
+ const o = { ...p };
33
+ if (typeof o.description === "string")
34
+ o.description = apiInlineHtml(o.description);
35
+ if (o.properties)
36
+ o.properties = apiPropertiesHtml(o.properties);
37
+ if (o.items)
38
+ o.items = apiPropertiesHtml(o.items);
39
+ return o;
40
+ });
41
+ }
package/dist/api.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * API reference nodes → HTML, with the markup and classes the Astro pipeline
3
+ * gets from format-astro's serializer and the @dogsbay/ui components:
4
+ *
5
+ * - code symbols (`api-class`, `api-member`, `api-doc`, `api-params`,
6
+ * `api-source`): ApiSymbol, ApiDoc, ApiParams, ApiSource;
7
+ * - an OpenAPI operation (`endpoint`): ApiLayout with an EndpointCard
8
+ * (ApiUrlBar, EndpointSection, PropertyList, SchemaViewer, ResponseTabs)
9
+ * and an ApiCodePanel (CodeSamples, ExampleBlock).
10
+ *
11
+ * Class lists are the ones Astro writes after tailwind-variants merges them
12
+ * (apps/demo/dist/api/*), not the components' source strings: the merge
13
+ * drops `border-l-4` and `border-l-current` from the URL bar, for one.
14
+ *
15
+ * Content: operation descriptions are CommonMark rendered with raw HTML off
16
+ * (api-markdown.ts, copied from format-astro); autodoc's `html` / `docHtml`
17
+ * are author HTML and go through rewriteHtml, so the host's sanitizer and
18
+ * link rewriting apply to them as to any raw HTML. Behaviour needs no new
19
+ * script: sample and response tabs are db-tabs, sections are <details>,
20
+ * and the URL bar's copy button sits in a db-code-block.
21
+ */
22
+ import type { TreeNode } from "@dogsbay/types";
23
+ import { type RenderContext } from "./render.js";
24
+ /** The node types this module renders. */
25
+ export declare const API_NODE_TYPES: Set<string>;
26
+ export declare function renderApiNode(n: TreeNode, ctx: RenderContext): string;
27
+ /**
28
+ * The code an API node renders as code blocks, with the language each is
29
+ * highlighted in, for highlightInputs: a highlighter that loads grammars per
30
+ * language must see them before rendering.
31
+ */
32
+ export declare function apiCodeInputs(n: TreeNode): Array<{
33
+ lang: string | undefined;
34
+ code: string;
35
+ }>;
package/dist/api.js ADDED
@@ -0,0 +1,346 @@
1
+ import { apiBlockHtml, apiPropertiesHtml } from "./api-markdown.js";
2
+ import { attrs, esc } from "./html.js";
3
+ import { CHECK_ICON, COPY_ICON, codeHtml, renderBlocks, rewriteHtml, tabGroup, uniqueId } from "./render.js";
4
+ const str = (v) => (v == null || v === "" ? undefined : String(v));
5
+ const list = (v) => (Array.isArray(v) ? v.filter((x) => x && typeof x === "object") : []);
6
+ /** The node types this module renders. */
7
+ export const API_NODE_TYPES = new Set(["api-class", "api-member", "api-doc", "api-params", "api-source", "endpoint"]);
8
+ export function renderApiNode(n, ctx) {
9
+ switch (n.type) {
10
+ case "api-class":
11
+ case "api-member":
12
+ return apiSymbol(n, ctx);
13
+ case "api-doc":
14
+ return docSegments(str(n.props?.html) ?? "", "[&>p:first-child]:mt-0 [&>p:last-child]:mb-2", ctx);
15
+ case "api-params":
16
+ return apiParams(n, ctx);
17
+ case "api-source":
18
+ return apiSource(n, ctx);
19
+ case "endpoint":
20
+ return endpoint(n, ctx);
21
+ default:
22
+ return "";
23
+ }
24
+ }
25
+ // ─── Code symbols ────────────────────────────────────────────────────
26
+ /** ApiSymbol.astro: kind badge, the name as a heading, signature, bases, then the body. */
27
+ function apiSymbol(n, ctx) {
28
+ const p = (n.props ?? {});
29
+ const name = str(p.name) ?? "";
30
+ const kind = str(p.kind) ?? "func";
31
+ const signature = str(p.signature);
32
+ const bases = Array.isArray(p.bases) ? p.bases.map(String) : [];
33
+ const level = n.type === "api-class" ? 2 : 3;
34
+ const badgeKind = kind === "method" ? "meth" : kind;
35
+ // ApiSymbol's slug, so a link to #mypkg-client reaches the same symbol on both pipelines.
36
+ const id = uniqueId(ctx, name.toLowerCase().replace(/[^\w]/g, "-").replace(/-+/g, "-"));
37
+ if (ctx.apiSymbolHeadings)
38
+ ctx.headings.push({ depth: level, id, text: name });
39
+ return (`<div class="api-symbol not-prose" data-kind="${esc(kind)}">` +
40
+ `<div class="api-symbol-heading"><span class="api-badge api-badge-${esc(badgeKind)}">${esc(badgeKind)}</span>` +
41
+ `<h${level} id="${esc(id)}" class="!mt-0 !mb-0 !border-0"><code class="text-lg font-semibold">${esc(name)}</code></h${level}></div>` +
42
+ (signature ? codeHtml(signature, "python", ctx) : "") +
43
+ (bases.length ? `<p class="api-symbol-bases">Bases: ${bases.map((b) => `<code>${esc(b)}</code>`).join("")}</p>` : "") +
44
+ `<div class="mt-3">${renderBlocks(n.children, ctx)}</div></div>\n`);
45
+ }
46
+ const CODE_IN_HTML = /<pre><code(?:\s+class="language-(\w+)")?>([\s\S]*?)<\/code><\/pre>/g;
47
+ const decodeCode = (s) => s.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
48
+ /** ApiDoc's and ApiParams' split: the code blocks in autodoc HTML become code blocks, the rest stays prose. */
49
+ function splitCode(html) {
50
+ const out = [];
51
+ let last = 0;
52
+ for (const m of html.matchAll(CODE_IN_HTML)) {
53
+ if (m.index > last)
54
+ out.push({ html: html.slice(last, m.index) });
55
+ out.push({ code: decodeCode(m[2]), lang: m[1] || "python" });
56
+ last = m.index + m[0].length;
57
+ }
58
+ if (last < html.length)
59
+ out.push({ html: html.slice(last) });
60
+ return out;
61
+ }
62
+ function docSegments(html, edges, ctx) {
63
+ return splitCode(html)
64
+ .map((s) => "code" in s
65
+ ? codeHtml(s.code, s.lang, ctx)
66
+ : `<div class="prose prose-sm dark:prose-invert max-w-none ${edges}">${rewriteHtml(s.html, ctx)}</div>`)
67
+ .join("");
68
+ }
69
+ /** ApiParams.astro: a two-column table, the description with its type and default under it. */
70
+ function apiParams(n, ctx) {
71
+ const rows = list(n.props?.params)
72
+ .map((p) => {
73
+ const type = str(p.type);
74
+ const dflt = str(p.default);
75
+ return (`<tr class="border-b border-border/50"><td class="py-3 px-4 align-top"><code class="text-sm font-medium">${esc(str(p.name) ?? "")}</code></td>` +
76
+ `<td class="py-3 px-4 align-top">${docSegments(str(p.docHtml) ?? "", "[&>p:first-child]:mt-0 [&>p:last-child]:mb-0", ctx)}` +
77
+ `<div class="flex items-center gap-4 mt-2 text-xs text-muted-foreground">` +
78
+ (type ? `<span><strong>TYPE:</strong> <code class="text-xs">${esc(type)}</code></span>` : "") +
79
+ (dflt ? `<span class="ml-auto"><strong>DEFAULT:</strong> <code class="text-xs">${esc(dflt)}</code></span>` : "") +
80
+ `</div></td></tr>`);
81
+ })
82
+ .join("");
83
+ const th = "py-3 px-4 text-left font-semibold text-muted-foreground uppercase tracking-wider text-xs";
84
+ return (`<table class="api-params w-full text-sm border-collapse not-prose"><thead><tr class="border-b border-border">` +
85
+ `<th class="${th} w-[140px]">Parameter</th><th class="${th}">Description</th></tr></thead><tbody>${rows}</tbody></table>\n`);
86
+ }
87
+ /** ApiSource.astro: the source in a closed disclosure. */
88
+ function apiSource(n, ctx) {
89
+ const p = (n.props ?? {});
90
+ const file = str(p.file);
91
+ const line = Number(p.line) || undefined;
92
+ const label = file ? `Source code in ${file}${line ? `, line ${line}` : ""}` : "Source code";
93
+ return `<details class="api-source"><summary>${esc(label)}</summary>${codeHtml(str(p.code) ?? "", "python", ctx)}</details>\n`;
94
+ }
95
+ // ─── OpenAPI operation ───────────────────────────────────────────────
96
+ const METHODS = ["get", "post", "put", "patch", "delete", "head"];
97
+ function methodBadge(method) {
98
+ const v = METHODS.includes(method) ? ` bg-api-${method}/20 text-api-${method}` : method === "options" ? " bg-muted/20 text-muted-foreground" : "";
99
+ return `<span class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-bold uppercase tracking-wide font-mono${v}" data-method="${esc(method)}">${esc(method)}</span>`;
100
+ }
101
+ /** ApiUrlBar.astro: method, base URL, the path with {params} tinted, a copy button. */
102
+ function urlBar(method, path, baseUrl) {
103
+ const tint = METHODS.includes(method)
104
+ ? `border px-4 py-3 bg-api-${method}/10 border-l-api-${method}`
105
+ : method === "options"
106
+ ? "border px-4 py-3 bg-muted/40 border-l-muted-foreground"
107
+ : "border border-l-current px-4 py-3";
108
+ const parts = path
109
+ .split(/(\{[^}]+\})/g)
110
+ .map((s) => (s.startsWith("{") ? `<span class="text-api-put">${esc(s)}</span>` : `<span>${esc(s)}</span>`))
111
+ .join("");
112
+ // The copy button sits in a db-code-block, whose behaviour copies data-copy-text: no script of its own.
113
+ const copy = `<db-code-block class="shrink-0" data-copy-text="${esc(baseUrl + path)}"><button type="button" data-part="copy" aria-label="Copy to clipboard" ` +
114
+ `class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-border bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">${COPY_ICON}${CHECK_ICON}</button></db-code-block>`;
115
+ return (`<div class="flex items-center gap-3 rounded-lg ${tint}">${methodBadge(method)}` +
116
+ `<code class="flex-1 truncate text-sm font-medium">${baseUrl ? `<span class="text-muted-foreground">${esc(baseUrl)}</span>` : ""}${parts}</code>${copy}</div>`);
117
+ }
118
+ const SECTION_CHEVRON = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200" data-section-chevron aria-hidden="true"><polyline points="9 18 15 12 9 6"></polyline></svg>';
119
+ const PROPERTY_CHEVRON = '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-3 shrink-0 text-muted-foreground transition-transform duration-200" data-property-chevron aria-hidden="true"><polyline points="9 18 15 12 9 6"></polyline></svg>';
120
+ const DEPRECATED_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>';
121
+ /** EndpointSection.astro: an open <details> with a turning chevron. */
122
+ function section(title, body) {
123
+ return (`<details open class="rounded-lg border bg-card"><summary class="flex cursor-pointer items-center gap-2 px-4 py-3 text-sm font-semibold list-none [&::-webkit-details-marker]:hidden hover:bg-muted/50 select-none">` +
124
+ `${SECTION_CHEVRON}<span>${esc(title)}</span></summary><div class="px-4 pb-4 pt-1">${body}</div></details>`);
125
+ }
126
+ const TYPE_TINT = {
127
+ string: "bg-api-type-string/20 text-api-type-string",
128
+ number: "bg-api-type-number/20 text-api-type-number",
129
+ integer: "bg-api-type-number/20 text-api-type-number",
130
+ boolean: "bg-api-type-boolean/20 text-api-type-boolean",
131
+ object: "bg-api-type-object/20 text-api-type-object",
132
+ array: "bg-api-type-array/20 text-api-type-array",
133
+ null: "bg-muted/20 text-muted-foreground",
134
+ };
135
+ /** PropertyItem.astro: one parameter or schema property; nested properties/items open in a <details>. */
136
+ function propertyItem(p, level, ctx) {
137
+ const name = esc(str(p.name) ?? "");
138
+ const type = str(p.type) ?? "";
139
+ const format = str(p.format);
140
+ const description = str(p.description);
141
+ const children = list(p.properties).length ? list(p.properties) : list(p.items);
142
+ const indent = level * 16;
143
+ const badge = `<span class="rounded px-1.5 py-0.5 text-[10px] font-medium ${Object.hasOwn(TYPE_TINT, type) ? TYPE_TINT[type] : TYPE_TINT.string}">${esc(type)}${format ? esc(` <${format}>`) : ""}</span>` +
144
+ (p.nullable === true ? '<span class="text-muted-foreground text-[10px]">nullable</span>' : "");
145
+ const required = p.required === true ? '<span class="text-[10px] font-semibold uppercase text-api-required">required</span>' : "";
146
+ const outer = `border-b border-border last:border-b-0${p.deprecated === true ? " opacity-60" : ""}`;
147
+ const pad = indent > 0 ? ` style="padding-left: ${16 + indent}px;"` : "";
148
+ // Descriptions are HTML from api-markdown (raw HTML off); rewriteHtml still resolves their links.
149
+ const desc = (cls, style = "") => (description ? `<p class="${cls}"${style}>${rewriteHtml(description, ctx)}</p>` : "");
150
+ if (children.length) {
151
+ return (`<div class="${outer}"><details><summary class="flex cursor-pointer items-start justify-between gap-4 px-4 py-3 list-none [&::-webkit-details-marker]:hidden hover:bg-muted/50 rounded"${pad}>` +
152
+ `<div class="flex items-center gap-2 min-w-0">${PROPERTY_CHEVRON}<code class="text-sm font-semibold">${name}</code>${badge}</div>` +
153
+ `<div class="flex shrink-0 items-center gap-2">${required}</div></summary>` +
154
+ desc("px-4 pb-1 text-sm text-muted-foreground", ` style="padding-left: ${32 + indent}px;"`) +
155
+ `<div class="border-l border-border" style="margin-left: ${24 + indent}px;">${children.map((c) => propertyItem(c, level + 1, ctx)).join("")}</div></details></div>`);
156
+ }
157
+ const sub = level > 0 ? " pl-5" : "";
158
+ const enums = Array.isArray(p.enum) ? p.enum : undefined;
159
+ return (`<div class="${outer}"><div class="flex items-start justify-between gap-4 px-4 py-3"${pad}><div class="min-w-0 flex-1">` +
160
+ `<div class="flex items-center gap-2">${level > 0 ? '<span class="size-3 shrink-0"></span>' : ""}<code class="text-sm font-semibold">${name}</code>${badge}</div>` +
161
+ desc(`mt-1 text-sm text-muted-foreground${sub}`) +
162
+ (enums
163
+ ? `<div class="mt-1.5 flex flex-wrap items-center gap-1 text-[10px]${sub}"><span class="text-muted-foreground">Enum:</span>${enums.map((v) => `<code class="rounded bg-muted px-1.5 py-0.5">${esc(v)}</code>`).join("")}</div>`
164
+ : "") +
165
+ (p.example !== undefined ? `<div class="mt-1 text-xs text-muted-foreground${sub}">Example: <code class="rounded bg-muted px-1 py-0.5">${esc(JSON.stringify(p.example))}</code></div>` : "") +
166
+ (p.default !== undefined ? `<div class="mt-1 text-xs text-muted-foreground${sub}">Default: <code class="rounded bg-muted px-1 py-0.5">${esc(JSON.stringify(p.default))}</code></div>` : "") +
167
+ `</div><div class="flex shrink-0 items-center gap-2 pt-0.5">${required}</div></div></div>`);
168
+ }
169
+ const LOCATIONS = [
170
+ ["path", "Path Parameters"],
171
+ ["query", "Query Parameters"],
172
+ ["header", "Header Parameters"],
173
+ ["cookie", "Cookie Parameters"],
174
+ ];
175
+ /** PropertyList.astro: parameters grouped by location. */
176
+ function propertyList(params, ctx) {
177
+ const groups = LOCATIONS.map(([loc, label]) => ({ label, items: params.filter((p) => p.in === loc) })).filter((g) => g.items.length);
178
+ return (`<div>` +
179
+ groups
180
+ .map((g) => `<div class="mb-4 last:mb-0"><h4 class="mb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">${g.label}</h4>` +
181
+ `<div class="rounded-lg border bg-card">${g.items.map((p) => propertyItem(p, 0, ctx)).join("")}</div></div>`)
182
+ .join("") +
183
+ `</div>`);
184
+ }
185
+ /** SchemaViewer.astro. */
186
+ function schemaViewer(props, ctx) {
187
+ return `<div class="rounded-lg border bg-card"><div>${props.map((p) => propertyItem(p, 0, ctx)).join("")}</div></div>`;
188
+ }
189
+ const TABS_ROOT = { class: "my-4 w-full overflow-hidden rounded-lg border border-border" };
190
+ /** Distinct tab values: db-tabs selects by value, so two Python samples ("Sync", "Async") need two. */
191
+ function distinct(values) {
192
+ const used = new Set();
193
+ return values.map((v, i) => {
194
+ while (used.has(v))
195
+ v += ` (${i + 1})`;
196
+ used.add(v);
197
+ return v;
198
+ });
199
+ }
200
+ function statusTint(status) {
201
+ const c = status[0];
202
+ return c >= "2" && c <= "5" ? `bg-api-status-${c}xx/20 text-api-status-${c}xx` : "bg-muted/20 text-muted-foreground";
203
+ }
204
+ /** ResponseTabs.astro: one tab per status, its description and schema. */
205
+ function responseTabs(responses, ctx) {
206
+ const values = distinct(responses.map((r) => String(r.status ?? "")));
207
+ return tabGroup(responses.map((r, i) => {
208
+ const status = String(r.status ?? "");
209
+ const description = str(r.description);
210
+ return {
211
+ value: values[i],
212
+ label: `<span class="rounded px-1.5 py-0.5 text-xs font-mono font-bold ${statusTint(status)}">${esc(status)}</span>`,
213
+ body: () => (description ? `<p class="mb-3 text-sm text-muted-foreground">${esc(description)}</p>` : "") +
214
+ (Array.isArray(r.schema) ? schemaViewer(list(r.schema), ctx) : ""),
215
+ };
216
+ }), ctx, TABS_ROOT);
217
+ }
218
+ const SAMPLE_LABELS = {
219
+ curl: "cURL",
220
+ python: "Python",
221
+ typescript: "TypeScript",
222
+ javascript: "JavaScript",
223
+ go: "Go",
224
+ ruby: "Ruby",
225
+ java: "Java",
226
+ csharp: "C#",
227
+ php: "PHP",
228
+ rust: "Rust",
229
+ };
230
+ const sampleLang = (language) => (language === "curl" ? "bash" : language);
231
+ /** CodeSamples.astro: one tab per language. */
232
+ function codeSamples(samples, ctx) {
233
+ const values = distinct(samples.map((s) => str(s.language) ?? ""));
234
+ return tabGroup(samples.map((s, i) => {
235
+ const language = str(s.language) ?? "";
236
+ return {
237
+ value: values[i],
238
+ label: esc(str(s.label) ?? SAMPLE_LABELS[language] ?? language),
239
+ body: () => codeHtml(str(s.code) ?? "", sampleLang(language), ctx),
240
+ };
241
+ }), ctx, TABS_ROOT);
242
+ }
243
+ const CONTENT_TYPE_LANG = {
244
+ "application/json": "json",
245
+ "application/xml": "xml",
246
+ "application/x-www-form-urlencoded": "plaintext",
247
+ "text/xml": "xml",
248
+ "text/html": "html",
249
+ "text/plain": "plaintext",
250
+ "text/yaml": "yaml",
251
+ "application/yaml": "yaml",
252
+ "application/x-yaml": "yaml",
253
+ "multipart/form-data": "plaintext",
254
+ };
255
+ /** ExampleBlock.astro's language for a content type. */
256
+ function exampleLang(contentType) {
257
+ if (!contentType)
258
+ return "json";
259
+ const base = contentType.split(";")[0].trim();
260
+ const known = CONTENT_TYPE_LANG[contentType] ?? CONTENT_TYPE_LANG[base];
261
+ if (known)
262
+ return known;
263
+ if (base.includes("json"))
264
+ return "json";
265
+ if (base.includes("xml"))
266
+ return "xml";
267
+ if (base.includes("yaml") || base.includes("yml"))
268
+ return "yaml";
269
+ return "json";
270
+ }
271
+ const PANEL_HEADING = "mb-3 text-xs font-semibold uppercase tracking-wider text-white/60";
272
+ /** The code column's blocks, in format-astro's order: samples, the request body example, response examples. */
273
+ function codePanels(p) {
274
+ const out = [];
275
+ const samples = list(p.codeSamples);
276
+ if (samples.length)
277
+ out.push({ title: "Code samples", samples });
278
+ if (p.requestBodyExample !== undefined)
279
+ out.push({ title: "Request body", code: JSON.stringify(p.requestBodyExample, null, 2), lang: "json" });
280
+ for (const r of list(p.responses)) {
281
+ if (r.example === undefined)
282
+ continue;
283
+ out.push({ title: `Response ${String(r.status ?? "")}`, code: JSON.stringify(r.example, null, 2), lang: exampleLang(str(r.contentType)) });
284
+ }
285
+ return out;
286
+ }
287
+ /** format-astro's endpointToAstro: ApiLayout, the EndpointCard on the left, the code panel on the right. */
288
+ function endpoint(n, ctx) {
289
+ const p = (n.props ?? {});
290
+ const method = (str(p.method) ?? "get").toLowerCase();
291
+ const path = str(p.path) ?? "";
292
+ const baseUrl = str(p.baseUrl) ?? "";
293
+ const summary = str(p.summary);
294
+ // The card hides a description equal to the summary (compared as text: the rendered HTML never equals it).
295
+ const description = typeof p.description === "string" && p.description && p.description !== p.summary ? apiBlockHtml(p.description) : undefined;
296
+ const deprecated = p.deprecated === true;
297
+ const parameters = list(apiPropertiesHtml(p.parameters));
298
+ const requestBody = list(apiPropertiesHtml(p.requestBody));
299
+ const responses = list(p.responses).map((r) => (Array.isArray(r.schema) ? { ...r, schema: apiPropertiesHtml(r.schema) } : r));
300
+ const sections = (parameters.length ? section("Parameters", propertyList(parameters, ctx)) : "") +
301
+ (requestBody.length ? section("Request body", schemaViewer(requestBody, ctx)) : "") +
302
+ (responses.length ? section("Responses", responseTabs(responses, ctx)) : "");
303
+ const card = `<div${attrs({ class: "space-y-4", "data-component": "endpoint-card", "data-deprecated": deprecated })}>` +
304
+ urlBar(method, path, baseUrl) +
305
+ (deprecated
306
+ ? `<div class="flex items-center gap-2 rounded-lg border border-api-deprecated/30 bg-api-deprecated/10 px-4 py-2 text-sm text-api-deprecated">${DEPRECATED_ICON}<span>This endpoint is deprecated.</span></div>`
307
+ : "") +
308
+ (summary ? `<p class="text-sm text-muted-foreground">${esc(summary)}</p>` : "") +
309
+ (description ? `<div class="text-sm text-muted-foreground">${rewriteHtml(description, ctx)}</div>` : "") +
310
+ `<div class="space-y-3">${sections}</div></div>`;
311
+ const panels = codePanels(p)
312
+ .map((c) => c.samples
313
+ ? `<div><h3 class="${PANEL_HEADING}">${esc(c.title)}</h3>${codeSamples(c.samples, ctx)}</div>`
314
+ : `<div><h3 class="${PANEL_HEADING}">${esc(c.title)}</h3>${codeHtml(c.code, c.lang, ctx)}</div>`)
315
+ .join("");
316
+ // Embedded among prose unless the host says the page is an operation page (apiPageLayout in @dogsbay/serialize-core).
317
+ const page = ctx.embeddedEndpoints === false;
318
+ return (`<div class="dba-api grid grid-cols-1 lg:grid-cols-2 ${page ? "lg:min-h-[calc(100vh-3rem)]" : "my-6 rounded-lg border overflow-hidden"}">` +
319
+ `<div class="${page ? "px-6 py-10" : "p-6"} lg:border-r">${card}</div>` +
320
+ `<div class="bg-[var(--api-panel-bg)] text-[var(--api-panel-fg)]"><div class="${page ? "px-6 py-10 lg:sticky lg:top-12 lg:max-h-[calc(100vh-3rem)] lg:overflow-y-auto" : "p-6"}">` +
321
+ `<div class="space-y-8">${panels}</div></div></div></div>\n`);
322
+ }
323
+ /**
324
+ * The code an API node renders as code blocks, with the language each is
325
+ * highlighted in, for highlightInputs: a highlighter that loads grammars per
326
+ * language must see them before rendering.
327
+ */
328
+ export function apiCodeInputs(n) {
329
+ const p = (n.props ?? {});
330
+ const fromHtml = (html) => splitCode(html ?? "").flatMap((s) => ("code" in s ? [{ lang: s.lang, code: s.code }] : []));
331
+ switch (n.type) {
332
+ case "api-class":
333
+ case "api-member":
334
+ return str(p.signature) ? [{ lang: "python", code: String(p.signature) }] : [];
335
+ case "api-source":
336
+ return [{ lang: "python", code: str(p.code) ?? "" }];
337
+ case "api-doc":
338
+ return fromHtml(str(p.html));
339
+ case "api-params":
340
+ return list(p.params).flatMap((x) => fromHtml(str(x.docHtml)));
341
+ case "endpoint":
342
+ return codePanels(p).flatMap((c) => c.samples ? c.samples.map((s) => ({ lang: sampleLang(str(s.language) ?? ""), code: str(s.code) ?? "" })) : [{ lang: c.lang, code: c.code }]);
343
+ default:
344
+ return [];
345
+ }
346
+ }
package/dist/render.d.ts CHANGED
@@ -62,6 +62,19 @@ export interface RenderContext {
62
62
  * sanitizer here; without one, raw HTML is written as authored.
63
63
  */
64
64
  rawHtml?: (html: string) => string;
65
+ /**
66
+ * OpenAPI operations sit inline among prose (true, the default) or fill an
67
+ * operation page (false): `apiPageLayout(tree).embeddedEndpoints` from
68
+ * @dogsbay/serialize-core, the rule format-astro applies.
69
+ */
70
+ embeddedEndpoints?: boolean;
71
+ /**
72
+ * List autodoc symbol names (api-class, api-member) in `headings`, for the
73
+ * table of contents. Off by default, as in the Astro pipeline, which lists
74
+ * them only when a live `:::autodoc` resolved: a 130-method module page
75
+ * would flood the TOC. Their ids are reserved either way.
76
+ */
77
+ apiSymbolHeadings?: boolean;
65
78
  }
66
79
  export { FOREIGN_END, FOREIGN_START };
67
80
  export { scriptUrl };
@@ -87,6 +100,8 @@ export declare function inlineText(nodes: InlineNode[] | undefined): string;
87
100
  */
88
101
  export declare function rewriteHtml(raw: string, ctx: RenderContext): string;
89
102
  export declare function renderBlocks(nodes: TreeNode[] | undefined, ctx: RenderContext): string;
103
+ export declare const COPY_ICON = "<svg data-part=\"copy-icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg>";
104
+ export declare const CHECK_ICON = "<svg data-part=\"check-icon\" hidden xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M20 6 9 17l-5-5\"/></svg>";
90
105
  /** What highlighting a code node asks of the highlighter; the look-ahead in `build` uses it to highlight blocks ahead of their page. */
91
106
  export interface CodeRequest {
92
107
  code: string;
@@ -112,3 +127,15 @@ export declare function highlightInputs(tree: unknown): Array<{
112
127
  lang: string | undefined;
113
128
  code: string;
114
129
  }>;
130
+ /** A code block for code a component holds in a prop (an API signature, a sample), as CodeBlock.astro renders it. */
131
+ export declare function codeHtml(code: string, lang: string | undefined, ctx: RenderContext): string;
132
+ export interface TabItem {
133
+ /** The tab's value (db-tabs matches triggers, panels and synced groups by it). */
134
+ value: string;
135
+ /** The trigger's content, as HTML. */
136
+ label: string;
137
+ /** The panel's content, as HTML; rendered in order, after the earlier panels. */
138
+ body: () => string;
139
+ }
140
+ /** A `db-tabs` group with Tabs.astro's markup, the first tab selected. */
141
+ export declare function tabGroup(items: TabItem[], ctx: RenderContext, rootAttrs: Record<string, unknown>, syncKey?: string): string;
package/dist/render.js CHANGED
@@ -9,6 +9,7 @@ import { stripMarkersForCopy } from "./code-markers.js";
9
9
  import { icons as ALERT_ICONS, chevronSvg as ALERT_CHEVRON } from "./alert-icons.js";
10
10
  import { parseLineRanges, plainHighlighter } from "./lines.js";
11
11
  import { scriptUrl } from "./url.js";
12
+ import { API_NODE_TYPES, apiCodeInputs, renderApiNode } from "./api.js";
12
13
  export { FOREIGN_END, FOREIGN_START };
13
14
  export { scriptUrl };
14
15
  /** ctx.link, unless the target is a script link: then "#", and it is reported. */
@@ -226,6 +227,24 @@ export function rewriteHtml(raw, ctx) {
226
227
  return `${sp}${attr}${eq}"${esc(out)}"`;
227
228
  }));
228
229
  }
230
+ /**
231
+ * Headings with ids inside a block of HTML (an OpenAPI embed's descriptions,
232
+ * which format-openapi renders with ids): each id is made unique on the page
233
+ * and h2/h3 join the outline, as the CLI splices `embed.headings` into the
234
+ * Astro page's.
235
+ */
236
+ function proseHeadings(html, ctx) {
237
+ return html.replace(/<h([1-6])\b([^>]*)>([\s\S]*?)<\/h\1>/gi, (m, lvl, at, inner) => {
238
+ const idm = /\sid="([^"]*)"/.exec(at);
239
+ if (!idm)
240
+ return m;
241
+ const id = uniqueId(ctx, unesc(idm[1]));
242
+ const depth = Number(lvl);
243
+ if (depth === 2 || depth === 3)
244
+ ctx.headings.push({ depth, id, text: unesc(inner.replace(/<[^>]*>/g, "")).trim() });
245
+ return `<h${lvl}${at.replace(idm[0], ` id="${esc(id)}"`)}>${inner}</h${lvl}>`;
246
+ });
247
+ }
229
248
  // ─── Blocks ──────────────────────────────────────────────────────────
230
249
  export function renderBlocks(nodes, ctx) {
231
250
  if (!nodes)
@@ -436,6 +455,17 @@ function block(n, ctx) {
436
455
  return `<pre class="my-4 overflow-x-auto rounded-md border bg-muted p-4 text-sm"><code class="math-display">${esc(n.html ?? inlineText(n.inline))}</code></pre>\n`;
437
456
  case "related":
438
457
  return `<nav class="related-resources" aria-label="${esc(ctx.t("related"))}">${renderInline(n.inline, ctx)}${renderBlocks(n.children, ctx)}</nav>\n`;
458
+ case "prose":
459
+ // Inline content or a block of HTML with no wrapper (format-astro's proseToAstro): an OpenAPI
460
+ // embed's descriptions arrive this way, rendered by format-openapi with raw HTML off.
461
+ return n.inline ? renderInline(n.inline, ctx) : typeof n.html === "string" ? proseHeadings(rewriteHtml(n.html, ctx), ctx) + "\n" : "";
462
+ case "api-class":
463
+ case "api-member":
464
+ case "api-doc":
465
+ case "api-params":
466
+ case "api-source":
467
+ case "endpoint":
468
+ return renderApiNode(n, ctx);
439
469
  case "include": {
440
470
  const target = str(p.path) ?? str(p.src) ?? str(p.file) ?? "";
441
471
  const remote = /https?:\/\/\S+/.exec(target)?.[0];
@@ -495,8 +525,8 @@ const BUTTON_VARIANTS = {
495
525
  destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
496
526
  };
497
527
  // ─── Code (CodeBlock.astro, with db-code-block for copy) ────────────
498
- const COPY_ICON = '<svg data-part="copy-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
499
- const CHECK_ICON = '<svg data-part="check-icon" hidden xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg>';
528
+ export const COPY_ICON = '<svg data-part="copy-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>';
529
+ export const CHECK_ICON = '<svg data-part="check-icon" hidden xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M20 6 9 17l-5-5"/></svg>';
500
530
  /** A rendered diagram, as `@dogsbay/ui`'s Diagram.astro: dark mode inverts it and turns the hues back. */
501
531
  function diagramFigure(lang, svg, title, p) {
502
532
  return (`<figure${attrs({ ...passthrough(p, "diagram not-prose my-4"), "data-lang": lang })}>` +
@@ -542,6 +572,9 @@ export function highlightInputs(tree) {
542
572
  const { lang, code } = codeRequest({ type: "code", props: { ...p, language: str(p.lang) ?? "mermaid" }, html: str(p.code) ?? "" });
543
573
  out.push({ lang, code });
544
574
  }
575
+ else if (API_NODE_TYPES.has(n.type)) {
576
+ out.push(...apiCodeInputs(n));
577
+ }
545
578
  if (n.children)
546
579
  walk(n.children);
547
580
  if (n.props)
@@ -552,6 +585,10 @@ export function highlightInputs(tree) {
552
585
  walk(tree);
553
586
  return out;
554
587
  }
588
+ /** A code block for code a component holds in a prop (an API signature, a sample), as CodeBlock.astro renders it. */
589
+ export function codeHtml(code, lang, ctx) {
590
+ return codeBlock({ type: "code", props: lang ? { language: lang } : {}, html: code }, ctx);
591
+ }
555
592
  function codeBlock(n, ctx) {
556
593
  const p = (n.props ?? {});
557
594
  const rawLang = str(p.language) ?? str(p.lang);
@@ -690,27 +727,31 @@ function tabs(n, ctx) {
690
727
  const kids = (n.children ?? []).filter((c) => c.type === "tab");
691
728
  if (!kids.length)
692
729
  return renderBlocks(n.children, ctx);
693
- const group = ++ctx.seq;
694
730
  const used = new Set();
695
731
  // The label itself: synced groups match by value, and a slug would merge C, C++ and C#.
696
- const values = kids.map((k, i) => {
732
+ const items = kids.map((k, i) => {
697
733
  let v = (str(k.props?.label) ?? `Tab ${i + 1}`).trim();
698
734
  while (used.has(v))
699
735
  v += ` (${i + 1})`;
700
736
  used.add(v);
701
- return v;
737
+ return { value: v, label: esc(str(k.props?.label) ?? `Tab ${i + 1}`), body: () => renderBlocks(k.children, ctx) };
702
738
  });
703
739
  const syncKey = str(p.syncKey) ?? str(p["sync-key"]) ?? str(p.group);
740
+ return tabGroup(items, ctx, passthrough(p, "my-4 w-full overflow-hidden rounded-lg border border-border"), syncKey);
741
+ }
742
+ /** A `db-tabs` group with Tabs.astro's markup, the first tab selected. */
743
+ export function tabGroup(items, ctx, rootAttrs, syncKey) {
744
+ const group = ++ctx.seq;
704
745
  let list = "";
705
746
  let panels = "";
706
- kids.forEach((k, i) => {
747
+ items.forEach((it, i) => {
707
748
  const tid = `tab-${group}-${i + 1}`;
708
749
  const pid = `tabpanel-${group}-${i + 1}`;
709
750
  const sel = i === 0;
710
- list += `<button data-part="trigger" data-value="${esc(values[i])}" role="tab" type="button" id="${tid}" aria-controls="${pid}" aria-selected="${sel}" tabindex="${sel ? 0 : -1}"${sel ? ' data-state="active"' : ""} class="${TAB_TRIGGER}">${esc(str(k.props?.label) ?? `Tab ${i + 1}`)}</button>`;
711
- panels += `<div data-part="content" data-value="${esc(values[i])}" role="tabpanel" id="${pid}" aria-labelledby="${tid}" tabindex="0"${sel ? ' data-state="active"' : " hidden"} class="${TAB_CONTENT}">${renderBlocks(k.children, ctx)}</div>`;
751
+ list += `<button data-part="trigger" data-value="${esc(it.value)}" role="tab" type="button" id="${tid}" aria-controls="${pid}" aria-selected="${sel}" tabindex="${sel ? 0 : -1}"${sel ? ' data-state="active"' : ""} class="${TAB_TRIGGER}">${it.label}</button>`;
752
+ panels += `<div data-part="content" data-value="${esc(it.value)}" role="tabpanel" id="${pid}" aria-labelledby="${tid}" tabindex="0"${sel ? ' data-state="active"' : " hidden"} class="${TAB_CONTENT}">${it.body()}</div>`;
712
753
  });
713
- return `<db-tabs${attrs({ ...passthrough(p, "my-4 w-full overflow-hidden rounded-lg border border-border"), "default-value": values[0], "sync-key": syncKey })}><div data-part="list" role="tablist" aria-orientation="horizontal" class="flex items-center border-b border-border bg-muted p-1 text-muted-foreground">${list}</div>${panels}</db-tabs>\n`;
754
+ return `<db-tabs${attrs({ ...rootAttrs, "default-value": items[0]?.value, "sync-key": syncKey })}><div data-part="list" role="tablist" aria-orientation="horizontal" class="flex items-center border-b border-border bg-muted p-1 text-muted-foreground">${list}</div>${panels}</db-tabs>\n`;
714
755
  }
715
756
  // ─── Accordion (Accordion*.astro + db-accordion) ────────────────────
716
757
  function accordion(n, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/render-html",
3
- "version": "0.2.0-beta.117",
3
+ "version": "0.2.0-beta.118",
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",
@@ -29,9 +29,10 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "htmlparser2": "^10.1.0",
32
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.117",
33
- "@dogsbay/serialize-core": "0.2.0-beta.117",
34
- "@dogsbay/types": "0.2.0-beta.117"
32
+ "markdown-it": "^14.3.0",
33
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.118",
34
+ "@dogsbay/types": "0.2.0-beta.118",
35
+ "@dogsbay/serialize-core": "0.2.0-beta.118"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "katex": "^0.16.0"
@@ -42,6 +43,7 @@
42
43
  }
43
44
  },
44
45
  "devDependencies": {
46
+ "@types/markdown-it": "^14.1.2",
45
47
  "@types/node": "^22.20.1",
46
48
  "esbuild": "^0.25.0",
47
49
  "katex": "^0.16.47",