@dogsbay/render-html 0.0.0-stage → 0.2.0-beta.116

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.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Every class name in the page's class attributes, entity-decoded. Markup
3
+ * from other tools (KaTeX, Mermaid), between the foreign markers, is skipped:
4
+ * its class names are not Tailwind utilities.
5
+ */
6
+ export declare function pageClasses(html: string, into?: Set<string>): Set<string>;
7
+ /**
8
+ * Whitespace and comment removal. Tailwind's own minifier (Lightning CSS)
9
+ * is native, so it cannot run in a Worker; this keeps strings intact and
10
+ * only drops what CSS never needs.
11
+ */
12
+ export declare function minifyCss(css: string): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Helpers for compiling the stylesheets in css/ for a page: the class names a
3
+ * page uses (Tailwind compiles only those) and a minifier that runs anywhere.
4
+ * @dogsbay/render uses both; dogsbay-ssg uses minifyCss, and keeps its own
5
+ * collectClasses (the same extraction, with a memo for whole-site builds).
6
+ */
7
+ import { FOREIGN_END, FOREIGN_START, unesc } from "./html.js";
8
+ const FOREIGN_BLOCK = new RegExp(`${FOREIGN_START}[\\s\\S]*?${FOREIGN_END}`, "g");
9
+ /**
10
+ * Every class name in the page's class attributes, entity-decoded. Markup
11
+ * from other tools (KaTeX, Mermaid), between the foreign markers, is skipped:
12
+ * its class names are not Tailwind utilities.
13
+ */
14
+ export function pageClasses(html, into = new Set()) {
15
+ const page = html.includes(FOREIGN_START) ? html.replace(FOREIGN_BLOCK, "") : html;
16
+ // Only inside tags: text that merely looks like class="…" (escaped prose, code) must
17
+ // not become CSS. A class compiled from text once wrote `</style>` into the stylesheet.
18
+ for (const tag of page.matchAll(/<[a-zA-Z][^<>]*>/g))
19
+ for (const m of tag[0].matchAll(/\sclass\s*=\s*(?:"([^"]*)"|'([^']*)')/g))
20
+ for (const tok of (m[1] ?? m[2]).split(/\s+/))
21
+ if (tok)
22
+ into.add(unesc(tok));
23
+ return into;
24
+ }
25
+ /**
26
+ * Whitespace and comment removal. Tailwind's own minifier (Lightning CSS)
27
+ * is native, so it cannot run in a Worker; this keeps strings intact and
28
+ * only drops what CSS never needs.
29
+ */
30
+ export function minifyCss(css) {
31
+ let out = "";
32
+ let i = 0;
33
+ const n = css.length;
34
+ while (i < n) {
35
+ const c = css[i];
36
+ if (c === '"' || c === "'") {
37
+ const end = findStringEnd(css, i);
38
+ out += css.slice(i, end);
39
+ i = end;
40
+ continue;
41
+ }
42
+ if (c === "/" && css[i + 1] === "*") {
43
+ const end = css.indexOf("*/", i + 2);
44
+ i = end < 0 ? n : end + 2;
45
+ // A comment separates tokens: `a/**/b` must not become `ab`.
46
+ if (/[\w-]$/.test(out) && /^[\w-]/.test(css[i] ?? ""))
47
+ out += " ";
48
+ continue;
49
+ }
50
+ if (/\s/.test(c)) {
51
+ while (i < n && /\s/.test(css[i]))
52
+ i++;
53
+ const prev = out[out.length - 1];
54
+ const next = css[i];
55
+ // Keep one space only where it separates tokens (selectors, values, "and", combinators need it).
56
+ if (prev && next && !"{};,>~".includes(prev) && !"{};,>~".includes(next) && !(prev === ":" && out.endsWith("):")))
57
+ out += " ";
58
+ continue;
59
+ }
60
+ if (c === ";" && css[i + 1] === "}") {
61
+ i++;
62
+ continue;
63
+ }
64
+ out += c;
65
+ i++;
66
+ }
67
+ return out;
68
+ }
69
+ function findStringEnd(s, start) {
70
+ const q = s[start];
71
+ let i = start + 1;
72
+ while (i < s.length && s[i] !== q)
73
+ i += s[i] === "\\" ? 2 : 1;
74
+ return i + 1;
75
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The Dogsbay paw, the default favicon for pages Dogsbay renders: dogsbay-ssg
3
+ * writes it as _dogsbay/assets/favicon.svg, and a standalone page inlines it
4
+ * as a data: URI so the browser never requests /favicon.ico.
5
+ *
6
+ * Source, with the design notes: assets/favicon.svg (from the dogsbay.ai
7
+ * site). Filled rather than stroked so it does not read as a face at 16px,
8
+ * and theme-aware (a prefers-color-scheme rule inside the SVG), because the
9
+ * browser composites a favicon against a light or a dark tab strip.
10
+ */
11
+ export declare const FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><style> .paw { fill: #1e6e7d; } @media (prefers-color-scheme: dark) { .paw { fill: #7fd3e0; } } </style><g class=\"paw\"><ellipse cx=\"14.5\" cy=\"29.5\" rx=\"6.3\" ry=\"8.6\" transform=\"rotate(-22 14.5 29.5)\"/><ellipse cx=\"26\" cy=\"17.5\" rx=\"6.3\" ry=\"9.2\" transform=\"rotate(-8 26 17.5)\"/><ellipse cx=\"40\" cy=\"17.5\" rx=\"6.3\" ry=\"9.2\" transform=\"rotate(8 40 17.5)\"/><ellipse cx=\"51.5\" cy=\"29.5\" rx=\"6.3\" ry=\"8.6\" transform=\"rotate(22 51.5 29.5)\"/><path d=\"M32 36c9.8 0 16.8 6.8 16.8 13.8S41.8 59.5 32 59.5 15.2 54.3 15.2 47.3 22.2 36 32 36Z\"/></g></svg>";
12
+ /** The favicon as a data: URI, for <link rel="icon"> in a self-contained page. */
13
+ export declare const FAVICON_DATA_URI: string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The Dogsbay paw, the default favicon for pages Dogsbay renders: dogsbay-ssg
3
+ * writes it as _dogsbay/assets/favicon.svg, and a standalone page inlines it
4
+ * as a data: URI so the browser never requests /favicon.ico.
5
+ *
6
+ * Source, with the design notes: assets/favicon.svg (from the dogsbay.ai
7
+ * site). Filled rather than stroked so it does not read as a face at 16px,
8
+ * and theme-aware (a prefers-color-scheme rule inside the SVG), because the
9
+ * browser composites a favicon against a light or a dark tab strip.
10
+ */
11
+ export const FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><style> .paw { fill: #1e6e7d; } @media (prefers-color-scheme: dark) { .paw { fill: #7fd3e0; } } </style><g class=\"paw\"><ellipse cx=\"14.5\" cy=\"29.5\" rx=\"6.3\" ry=\"8.6\" transform=\"rotate(-22 14.5 29.5)\"/><ellipse cx=\"26\" cy=\"17.5\" rx=\"6.3\" ry=\"9.2\" transform=\"rotate(-8 26 17.5)\"/><ellipse cx=\"40\" cy=\"17.5\" rx=\"6.3\" ry=\"9.2\" transform=\"rotate(8 40 17.5)\"/><ellipse cx=\"51.5\" cy=\"29.5\" rx=\"6.3\" ry=\"8.6\" transform=\"rotate(22 51.5 29.5)\"/><path d=\"M32 36c9.8 0 16.8 6.8 16.8 13.8S41.8 59.5 32 59.5 15.2 54.3 15.2 47.3 22.2 36 32 36Z\"/></g></svg>";
12
+ /** The favicon as a data: URI, for <link rel="icon"> in a self-contained page. */
13
+ export const FAVICON_DATA_URI = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}`;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The `:::files` tree, written once for both pipelines: FileTree.astro renders it,
3
+ * and dogsbay-ssg copies this file (scripts/sync-astro-styles.mjs) and calls it.
4
+ * Each pipeline turns an entry's inline Markdown into `label` HTML itself.
5
+ */
6
+ export interface FileTreeItem {
7
+ /** The entry's label as HTML (already escaped). */
8
+ label: string;
9
+ /** A folder: it has entries, or its name ends in "/". */
10
+ folder: boolean;
11
+ children: FileTreeItem[];
12
+ }
13
+ /** The whole tree, in a bordered, monospaced box. */
14
+ export declare function fileTreeHtml(items: FileTreeItem[], extraClass?: string): string;
@@ -0,0 +1,20 @@
1
+ // Generated by packages/render-html/scripts/sync-sources.mjs from @dogsbay/ui/src/files/markup.ts. Do not edit.
2
+ /**
3
+ * The `:::files` tree, written once for both pipelines: FileTree.astro renders it,
4
+ * and dogsbay-ssg copies this file (scripts/sync-astro-styles.mjs) and calls it.
5
+ * Each pipeline turns an entry's inline Markdown into `label` HTML itself.
6
+ */
7
+ const svg = (body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 shrink-0 text-muted-foreground" aria-hidden="true" focusable="false">${body}</svg>`;
8
+ // Lucide folder and file (ISC licence).
9
+ const FOLDER = svg('<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>');
10
+ const FILE = svg('<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>');
11
+ function list(items, nested) {
12
+ const cls = nested ? "m-0 ml-2 list-none border-l border-border pl-4" : "m-0 list-none p-0";
13
+ return `<ul class="${cls}">${items
14
+ .map((it) => `<li class="my-0.5" data-kind="${it.folder ? "folder" : "file"}"><span class="inline-flex items-center gap-1.5">${it.folder ? FOLDER : FILE}<span>${it.label}</span></span>${it.children.length ? list(it.children, true) : ""}</li>`)
15
+ .join("")}</ul>`;
16
+ }
17
+ /** The whole tree, in a bordered, monospaced box. */
18
+ export function fileTreeHtml(items, extraClass = "") {
19
+ return `<div class="not-prose my-4 overflow-x-auto rounded-lg border bg-card p-3 font-mono text-sm${extraClass ? ` ${extraClass}` : ""}" data-component="files">${list(items, false)}</div>`;
20
+ }
package/dist/html.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /** HTML escaping and attribute helpers shared by the renderer and layout. */
2
+ export declare function esc(s: unknown): string;
3
+ /** Render `{ a: "x", b: true, c: false }` as ` a="x" b`. Null, undefined and false are dropped. */
4
+ export declare function attrs(map: Record<string, unknown>): string;
5
+ /** Keep the attributes an author may set on any element: id, class, style, data-*, aria-*. */
6
+ export declare function passthrough(props: Record<string, unknown> | undefined, extraClass?: string): Record<string, unknown>;
7
+ /** Lowercase, hyphenated, ASCII-safe id. */
8
+ export declare function slugify(s: string): string;
9
+ /** Decode the entities `esc` writes (and `&amp;` first-class), in one pass. */
10
+ export declare function unesc(s: string): string;
11
+ /**
12
+ * Around markup made by other tools (KaTeX, Mermaid SVG), whose class names
13
+ * are their own: the stylesheet build skips it, so a class like "underline"
14
+ * or "hidden" is not taken as a Tailwind utility. See css-build.ts.
15
+ */
16
+ export declare const FOREIGN_START = "<!--db-foreign-->";
17
+ export declare const FOREIGN_END = "<!--/db-foreign-->";
package/dist/html.js ADDED
@@ -0,0 +1,53 @@
1
+ /** HTML escaping and attribute helpers shared by the renderer and layout. */
2
+ const ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
3
+ export function esc(s) {
4
+ return String(s ?? "").replace(/[&<>"']/g, (c) => ESC[c]);
5
+ }
6
+ /** Render `{ a: "x", b: true, c: false }` as ` a="x" b`. Null, undefined and false are dropped. */
7
+ export function attrs(map) {
8
+ let out = "";
9
+ for (const [k, v] of Object.entries(map)) {
10
+ // An empty alt is meaningful (a decorative image) and must be written.
11
+ if (v == null || v === false || (v === "" && k !== "alt"))
12
+ continue;
13
+ out += v === true ? ` ${k}` : ` ${k}="${esc(v)}"`;
14
+ }
15
+ return out;
16
+ }
17
+ /** Keep the attributes an author may set on any element: id, class, style, data-*, aria-*. */
18
+ export function passthrough(props, extraClass) {
19
+ const out = {};
20
+ const cls = [extraClass, props?.class].filter(Boolean).join(" ");
21
+ if (cls)
22
+ out.class = cls;
23
+ if (!props)
24
+ return out;
25
+ for (const [k, v] of Object.entries(props)) {
26
+ if (k === "id" || k === "style" || k.startsWith("data-") || k.startsWith("aria-"))
27
+ out[k] = v;
28
+ }
29
+ return out;
30
+ }
31
+ /** Lowercase, hyphenated, ASCII-safe id. */
32
+ export function slugify(s) {
33
+ return (s
34
+ .toLowerCase()
35
+ .normalize("NFKD")
36
+ .replace(/[̀-ͯ]/g, "")
37
+ .replace(/<[^>]*>/g, "")
38
+ .replace(/[^\p{L}\p{N}\s_-]/gu, "")
39
+ .trim()
40
+ .replace(/[\s_]+/g, "-")
41
+ .replace(/-+/g, "-") || "section");
42
+ }
43
+ /** Decode the entities `esc` writes (and `&amp;` first-class), in one pass. */
44
+ export function unesc(s) {
45
+ return s.replace(/&(amp|lt|gt|quot|#39|#x27|apos);/g, (_, e) => ({ amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'", "#x27": "'", apos: "'" })[e]);
46
+ }
47
+ /**
48
+ * Around markup made by other tools (KaTeX, Mermaid SVG), whose class names
49
+ * are their own: the stylesheet build skips it, so a class like "underline"
50
+ * or "hidden" is not taken as a Tailwind utility. See css-build.ts.
51
+ */
52
+ export const FOREIGN_START = "<!--db-foreign-->";
53
+ export const FOREIGN_END = "<!--/db-foreign-->";
@@ -0,0 +1,8 @@
1
+ export declare function icon(name: string, cls?: string): string;
2
+ export declare function hasIcon(name: string): boolean;
3
+ /** Admonition variants. Aliases share a look but keep their own label. */
4
+ export declare const CALLOUTS: Record<string, {
5
+ variant: string;
6
+ icon: string;
7
+ label: string;
8
+ }>;
package/dist/icons.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Inline SVG icons (Lucide, ISC licence). Inline so pages need no
3
+ * icon font or network, which keeps them working from file://.
4
+ */
5
+ const svg = (body, cls = "db-icon") => `<svg class="${cls}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${body}</svg>`;
6
+ const PATHS = {
7
+ info: '<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>',
8
+ lightbulb: '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
9
+ "alert-triangle": '<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-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
10
+ "alert-octagon": '<path d="M12 16h.01"/><path d="M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/>',
11
+ "message-square-warning": '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M12 7v2"/><path d="M12 13h.01"/>',
12
+ "circle-check": '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
13
+ "circle-x": '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
14
+ "circle-help": '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
15
+ bug: '<path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/>',
16
+ "file-text": '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M16 13H8"/><path d="M16 17H8"/>',
17
+ quote: '<path d="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/><path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/>',
18
+ "flask-conical": '<path d="M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"/><path d="M8.5 2h7"/><path d="M7 16h10"/>',
19
+ flame: '<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>',
20
+ copy: '<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"/>',
21
+ check: '<path d="M20 6 9 17l-5-5"/>',
22
+ "chevron-right": '<path d="m9 18 6-6-6-6"/>',
23
+ "chevron-left": '<path d="m15 18-6-6 6-6"/>',
24
+ "chevron-down": '<path d="m6 9 6 6 6-6"/>',
25
+ search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
26
+ menu: '<line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/>',
27
+ x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
28
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
29
+ moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
30
+ "external-link": '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
31
+ link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
32
+ rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
33
+ book: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/>',
34
+ globe: '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
35
+ "git-branch": '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
36
+ "list-tree": '<path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/>',
37
+ printer: '<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><path d="M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6"/><rect x="6" y="14" width="12" height="8" rx="1"/>',
38
+ };
39
+ export function icon(name, cls) {
40
+ const body = PATHS[name];
41
+ return body ? svg(body, cls) : "";
42
+ }
43
+ export function hasIcon(name) {
44
+ return name in PATHS;
45
+ }
46
+ /** Admonition variants. Aliases share a look but keep their own label. */
47
+ export const CALLOUTS = {
48
+ note: { variant: "note", icon: "info", label: "Note" },
49
+ info: { variant: "info", icon: "info", label: "Info" },
50
+ tip: { variant: "tip", icon: "lightbulb", label: "Tip" },
51
+ hint: { variant: "tip", icon: "lightbulb", label: "Hint" },
52
+ success: { variant: "success", icon: "circle-check", label: "Success" },
53
+ important: { variant: "important", icon: "message-square-warning", label: "Important" },
54
+ warning: { variant: "warning", icon: "alert-triangle", label: "Warning" },
55
+ attention: { variant: "warning", icon: "alert-triangle", label: "Attention" },
56
+ caution: { variant: "caution", icon: "alert-octagon", label: "Caution" },
57
+ danger: { variant: "danger", icon: "flame", label: "Danger" },
58
+ error: { variant: "danger", icon: "circle-x", label: "Error" },
59
+ failure: { variant: "danger", icon: "circle-x", label: "Failure" },
60
+ question: { variant: "question", icon: "circle-help", label: "Question" },
61
+ bug: { variant: "danger", icon: "bug", label: "Bug" },
62
+ example: { variant: "example", icon: "flask-conical", label: "Example" },
63
+ abstract: { variant: "note", icon: "file-text", label: "Abstract" },
64
+ quote: { variant: "quote", icon: "quote", label: "Quote" },
65
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @dogsbay/render-html: Dogsbay MD TreeNode → HTML, with the Astro components'
3
+ * markup and Tailwind classes. Syntax highlighting is a `Highlighter` you pass
4
+ * (`@dogsbay/highlight`, or none for plain text); math is a function you pass
5
+ * (`@dogsbay/render-html/math` for KaTeX, or none to keep LaTeX). This entry
6
+ * imports neither, so it carries no Shiki, KaTeX or Node built-ins.
7
+ */
8
+ export * from "./render.js";
9
+ export * from "./lines.js";
10
+ export { esc, attrs, passthrough, slugify, unesc } from "./html.js";
11
+ export { icon, hasIcon, CALLOUTS } from "./icons.js";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @dogsbay/render-html: Dogsbay MD TreeNode → HTML, with the Astro components'
3
+ * markup and Tailwind classes. Syntax highlighting is a `Highlighter` you pass
4
+ * (`@dogsbay/highlight`, or none for plain text); math is a function you pass
5
+ * (`@dogsbay/render-html/math` for KaTeX, or none to keep LaTeX). This entry
6
+ * imports neither, so it carries no Shiki, KaTeX or Node built-ins.
7
+ */
8
+ export * from "./render.js";
9
+ export * from "./lines.js";
10
+ export { esc, attrs, passthrough, slugify, unesc } from "./html.js";
11
+ export { icon, hasIcon, CALLOUTS } from "./icons.js";
@@ -0,0 +1,2 @@
1
+ /** Kbd.astro's classes, shared with dogsbay-ssg (copied by scripts/sync-astro-styles.mjs). */
2
+ export declare const KBD_CLASS = "pointer-events-none inline-flex h-5 items-center justify-center gap-1 rounded border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground shadow-[0_1px_0_1px] shadow-muted";
@@ -0,0 +1,3 @@
1
+ // Generated by packages/render-html/scripts/sync-sources.mjs from @dogsbay/ui/src/kbd/variants.ts. Do not edit.
2
+ /** Kbd.astro's classes, shared with dogsbay-ssg (copied by scripts/sync-astro-styles.mjs). */
3
+ export const KBD_CLASS = "pointer-events-none inline-flex h-5 items-center justify-center gap-1 rounded border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground shadow-[0_1px_0_1px] shadow-muted";
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Line annotations, as CodeRich.astro takes them: highlighted lines, diff
3
+ * additions and removals, and line numbers. `[!code …]` markers in the code
4
+ * (Shiki's notation) are read too.
5
+ */
6
+ export interface CodeOptions {
7
+ highlights?: number[];
8
+ lineNumbers?: boolean;
9
+ diffAdd?: number[];
10
+ diffRemove?: number[];
11
+ }
12
+ export interface Highlighter {
13
+ /** Uncached rendering; `code` is the memoised entry point. */
14
+ render(code: string, lang: string | undefined, opts?: CodeOptions): string;
15
+ /** Returns a `<pre class="shiki">…</pre>` block. */
16
+ code(code: string, lang: string | undefined, opts?: CodeOptions): string;
17
+ /** Resolved language id actually used ("text" when unknown). */
18
+ resolve(lang: string | undefined): string;
19
+ /** Whether `code` would be served from the cache. */
20
+ has(code: string, lang: string | undefined, opts?: CodeOptions): boolean;
21
+ /** Put a block rendered elsewhere (a worker thread) in the cache, as `code` would have. The caller vouches for it: whoever rendered it had loaded what the block needs. */
22
+ prime(code: string, lang: string | undefined, opts: CodeOptions, html: string): void;
23
+ /** The cache: entries, characters held, and hits and misses so far; `unloaded`, blocks rendered plain because their grammar was not loaded. */
24
+ cacheStats(): {
25
+ entries: number;
26
+ chars: number;
27
+ hits: number;
28
+ misses: number;
29
+ unloaded?: number;
30
+ };
31
+ /**
32
+ * Load what these blocks need (`highlightInputs` lists a tree's), before
33
+ * they render: `code` and `render` are synchronous. A block's code matters
34
+ * as well as its tag: a markdown block reaches the grammars its fences name.
35
+ * A tag alone loads everything that tag could reach. Optional: a highlighter
36
+ * with everything loaded up front has none.
37
+ */
38
+ load?(blocks: Iterable<string | undefined | {
39
+ lang: string | undefined;
40
+ code: string;
41
+ }>): Promise<void>;
42
+ }
43
+ /** Language names that mean "no highlighting". */
44
+ export declare const PLAIN: Set<string>;
45
+ /** Parse "1,3-5" into [1,3,4,5]. */
46
+ export declare function parseLineRanges(spec: unknown): number[];
47
+ /** CodeRich's gutter: the line number and a diff marker (+, −, or a space) in one column. */
48
+ export declare function gutterHtml(n: number, kind: string, lineNumbers: boolean, diffs: boolean): string;
49
+ /** Code as escaped text, with the annotations and gutter a highlighted block has. `src` has had its unprocessable markers removed. */
50
+ export declare function plainBlock(src: string, opts?: CodeOptions): string;
51
+ /**
52
+ * Applies the line notation without a grammar, as Shiki's notation transformers
53
+ * do with one: a marker at the end of a line annotates that line (and the next
54
+ * N-1 with `:N`); a line that is only a marker annotates the lines after it and
55
+ * is removed. Other markers (focus, error, word) are left for stripping.
56
+ */
57
+ export declare function applyLineNotation(code: string, opts?: CodeOptions): {
58
+ code: string;
59
+ opts: CodeOptions;
60
+ };
61
+ export declare function createPlainHighlighter(): Highlighter;
62
+ export declare const plainHighlighter: Highlighter;
package/dist/lines.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The code-block contract without Shiki: line annotations, the gutter, and a
3
+ * plain highlighter that renders code as escaped text with the same markup
4
+ * (`pre.shiki.shiki-plain`, one `span.line` per line). `render.ts` depends only
5
+ * on this file, so a renderer built without `highlight.ts` carries no grammar,
6
+ * theme or TextMate code. `createHighlighter` (highlight.ts) uses the same
7
+ * plain path for unknown languages.
8
+ */
9
+ import { stripUnprocessableMarkers } from "./code-markers.js";
10
+ import { esc } from "./html.js";
11
+ /** Language names that mean "no highlighting". */
12
+ export const PLAIN = new Set(["", "text", "txt", "plain", "plaintext", "none", "output", "log", "code", "codeblock"]);
13
+ /** Parse "1,3-5" into [1,3,4,5]. */
14
+ export function parseLineRanges(spec) {
15
+ if (spec == null)
16
+ return [];
17
+ const out = [];
18
+ for (const part of String(spec).replace(/[{}]/g, "").split(/[,\s]+/)) {
19
+ const m = /^(\d+)(?:-(\d+))?$/.exec(part);
20
+ if (!m)
21
+ continue;
22
+ const a = Number(m[1]);
23
+ const b = m[2] ? Number(m[2]) : a;
24
+ for (let i = a; i <= Math.min(b, a + 5000); i++)
25
+ out.push(i);
26
+ }
27
+ return out;
28
+ }
29
+ /** CodeRich's gutter: the line number and a diff marker (+, −, or a space) in one column. */
30
+ export function gutterHtml(n, kind, lineNumbers, diffs) {
31
+ if (!lineNumbers && !diffs)
32
+ return "";
33
+ const parts = [];
34
+ if (lineNumbers)
35
+ parts.push(String(n).padStart(3));
36
+ if (diffs)
37
+ parts.push(kind === "add" ? "+" : kind === "remove" ? "−" : " ");
38
+ return `<span class="gutter${kind ? ` ${kind}` : ""}" data-pagefind-ignore>${parts.join(" ")}</span>`;
39
+ }
40
+ /** Code as escaped text, with the annotations and gutter a highlighted block has. `src` has had its unprocessable markers removed. */
41
+ export function plainBlock(src, opts = {}) {
42
+ const marks = new Set(opts.highlights ?? []);
43
+ const adds = new Set(opts.diffAdd ?? []);
44
+ const removes = new Set(opts.diffRemove ?? []);
45
+ const cls = opts.lineNumbers ? " line-numbers" : "";
46
+ const diffs = adds.size > 0 || removes.size > 0;
47
+ const out = src.split("\n").map((l, i) => {
48
+ const n = i + 1;
49
+ const kind = adds.has(n) ? "add" : removes.has(n) ? "remove" : "";
50
+ const classes = ["line", marks.has(n) && "highlighted", kind && `diff ${kind}`].filter(Boolean).join(" ");
51
+ return `<span class="${classes}">${gutterHtml(n, kind, !!opts.lineNumbers, diffs)}${esc(l)}</span>`;
52
+ });
53
+ return `<pre class="shiki shiki-plain${cls}" tabindex="0"><code>${out.join("\n")}</code></pre>`;
54
+ }
55
+ /**
56
+ * A highlighter that does not highlight: every block is plain text, and the
57
+ * language is kept for the block's label. Line notation (`[!code ++]`,
58
+ * `--`, `highlight`) is applied; other markers are removed. The default when a caller passes
59
+ * none, so syntax highlighting is opt-in for library users.
60
+ */
61
+ /** Shiki's line notation: `[!code ++]`, `[!code --]`, `[!code highlight]` / `[!code hl]`, with an optional `:N` line count. */
62
+ const NOTATION = /[ \t]*(?:(?:\/\/|#|--|<!--|\/\*|;)[ \t]*)?\[!code (\+\+|--|highlight|hl)(?::(\d+))?\](?:[ \t]*(?:-->|\*\/))?/;
63
+ /**
64
+ * Applies the line notation without a grammar, as Shiki's notation transformers
65
+ * do with one: a marker at the end of a line annotates that line (and the next
66
+ * N-1 with `:N`); a line that is only a marker annotates the lines after it and
67
+ * is removed. Other markers (focus, error, word) are left for stripping.
68
+ */
69
+ export function applyLineNotation(code, opts = {}) {
70
+ if (!code.includes("[!code"))
71
+ return { code, opts };
72
+ const marks = new Set(opts.highlights ?? []);
73
+ const adds = new Set(opts.diffAdd ?? []);
74
+ const removes = new Set(opts.diffRemove ?? []);
75
+ const out = [];
76
+ let pending;
77
+ const mark = (n, kind) => (kind === "++" ? adds : kind === "--" ? removes : marks).add(n);
78
+ for (const line of code.split("\n")) {
79
+ const m = NOTATION.exec(line);
80
+ const rest = m ? line.replace(NOTATION, "") : line;
81
+ if (m && rest.trim() === "") {
82
+ pending = { kind: m[1], left: Number(m[2] ?? 1) };
83
+ continue;
84
+ }
85
+ out.push(rest);
86
+ const n = out.length;
87
+ if (pending && pending.left > 0) {
88
+ mark(n, pending.kind);
89
+ if (--pending.left === 0)
90
+ pending = undefined;
91
+ }
92
+ if (m) {
93
+ const count = Number(m[2] ?? 1);
94
+ mark(n, m[1]);
95
+ if (count > 1)
96
+ pending = { kind: m[1], left: count - 1 };
97
+ }
98
+ }
99
+ return { code: out.join("\n"), opts: { ...opts, highlights: [...marks].sort((a, b) => a - b), diffAdd: [...adds].sort((a, b) => a - b), diffRemove: [...removes].sort((a, b) => a - b) } };
100
+ }
101
+ export function createPlainHighlighter() {
102
+ const resolve = (lang) => {
103
+ const l = (lang ?? "").toLowerCase().trim();
104
+ return PLAIN.has(l) ? "text" : l;
105
+ };
106
+ const render = (code, _lang, opts = {}) => {
107
+ const applied = applyLineNotation(code.replace(/\n$/, ""), opts);
108
+ return plainBlock(stripUnprocessableMarkers(applied.code, true), applied.opts);
109
+ };
110
+ return {
111
+ resolve,
112
+ render,
113
+ code: render,
114
+ has: () => false,
115
+ prime: () => { },
116
+ cacheStats: () => ({ entries: 0, chars: 0, hits: 0, misses: 0 }),
117
+ };
118
+ }
119
+ export const plainHighlighter = createPlainHighlighter();
package/dist/math.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function katexMath(latex: string, display: boolean): string;
package/dist/math.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * LaTeX → HTML with MathML for screen readers, at build time: no script on
3
+ * the page. Passed to the renderer as `RenderContext.math` when ssg.math is
4
+ * on, so a renderer built without it carries no KaTeX.
5
+ */
6
+ import katex from "katex";
7
+ export function katexMath(latex, display) {
8
+ // The visual copy is aria-hidden and repeats the MathML, so Pagefind skips it. The FOREIGN
9
+ // markers render.ts adds keep KaTeX's own class names (underline, overline…) out of the Tailwind candidates.
10
+ return katex
11
+ .renderToString(latex, { displayMode: display, throwOnError: false, output: "htmlAndMathml" })
12
+ .replace(/(<span class="katex-html" aria-hidden="true")/g, "$1 data-pagefind-ignore");
13
+ }