@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 +16 -2
- package/css/api.css +133 -0
- package/dist/api-markdown.d.ts +7 -0
- package/dist/api-markdown.js +41 -0
- package/dist/api.d.ts +35 -0
- package/dist/api.js +346 -0
- package/dist/render.d.ts +27 -0
- package/dist/render.js +50 -9
- package/package.json +6 -4
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
|
|
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(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/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
|
|
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
|
-
|
|
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(
|
|
711
|
-
panels += `<div data-part="content" data-value="${esc(
|
|
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({ ...
|
|
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.
|
|
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
|
-
"
|
|
33
|
-
"@dogsbay/
|
|
34
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
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",
|