@waveso/docs 0.1.0

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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +518 -0
  3. package/dist/frontmatter.d.ts +55 -0
  4. package/dist/frontmatter.js +80 -0
  5. package/dist/highlighter.d.ts +99 -0
  6. package/dist/highlighter.js +183 -0
  7. package/dist/meta.d.ts +75 -0
  8. package/dist/meta.js +183 -0
  9. package/dist/next.d.ts +256 -0
  10. package/dist/next.js +365 -0
  11. package/dist/plugins/rehype-capture-toc.d.ts +18 -0
  12. package/dist/plugins/rehype-capture-toc.js +69 -0
  13. package/dist/plugins/remark-doc-links.d.ts +63 -0
  14. package/dist/plugins/remark-doc-links.js +122 -0
  15. package/dist/plugins/remark-unwrap-images.d.ts +11 -0
  16. package/dist/plugins/remark-unwrap-images.js +25 -0
  17. package/dist/plugins/remark-youtube.d.ts +22 -0
  18. package/dist/plugins/remark-youtube.js +84 -0
  19. package/dist/react/callout.d.ts +37 -0
  20. package/dist/react/callout.js +113 -0
  21. package/dist/react/doc-content.d.ts +29 -0
  22. package/dist/react/doc-content.js +30 -0
  23. package/dist/react/markdown-components.d.ts +84 -0
  24. package/dist/react/markdown-components.js +122 -0
  25. package/dist/react/search-dialog.d.ts +41 -0
  26. package/dist/react/search-dialog.js +404 -0
  27. package/dist/react/sidebar.d.ts +29 -0
  28. package/dist/react/sidebar.js +196 -0
  29. package/dist/react/skip-link.d.ts +37 -0
  30. package/dist/react/skip-link.js +37 -0
  31. package/dist/react/toc.d.ts +35 -0
  32. package/dist/react/toc.js +87 -0
  33. package/dist/react/youtube.d.ts +27 -0
  34. package/dist/react/youtube.js +75 -0
  35. package/dist/render.d.ts +72 -0
  36. package/dist/render.js +279 -0
  37. package/dist/search-index.d.ts +51 -0
  38. package/dist/search-index.js +274 -0
  39. package/dist/search-options.d.ts +18 -0
  40. package/dist/search-options.js +40 -0
  41. package/dist/source.d.ts +67 -0
  42. package/dist/source.js +332 -0
  43. package/dist/styles.css +1033 -0
  44. package/dist/types.d.ts +334 -0
  45. package/dist/types.js +0 -0
  46. package/package.json +166 -0
@@ -0,0 +1,196 @@
1
+ "use client";
2
+ import { useId, useRef, useState } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/react/sidebar.tsx
5
+ /** Trailing slashes are a routing detail, not a difference in identity. */
6
+ function normalizeHref(href) {
7
+ return href.length > 1 ? href.replace(/\/+$/, "") : href;
8
+ }
9
+ function isActiveHref(pathname, href) {
10
+ return normalizeHref(pathname) === normalizeHref(href);
11
+ }
12
+ /** Whether the active page lives anywhere under this node. */
13
+ function containsActive(node, pathname) {
14
+ switch (node.type) {
15
+ case "page": return isActiveHref(pathname, node.href);
16
+ case "group": return node.href !== void 0 && isActiveHref(pathname, node.href) || node.children.some((child) => containsActive(child, pathname));
17
+ case "link": return !node.external && isActiveHref(pathname, node.href);
18
+ default: return false;
19
+ }
20
+ }
21
+ /**
22
+ * The docs navigation tree.
23
+ *
24
+ * Prefetch is off by default on the injected link. A full-tree sidebar on a
25
+ * few hundred pages otherwise asks Next to prefetch every route in it —
26
+ * ~1.8 KB brotli each, all of it wasted on the routes nobody clicks.
27
+ */
28
+ function DocsSidebar({ nav, pathname, Link, label = "Docs", className }) {
29
+ const baseId = useId();
30
+ const [toggled, setToggled] = useState({});
31
+ const lastPathname = useRef(pathname);
32
+ if (lastPathname.current !== pathname) {
33
+ lastPathname.current = pathname;
34
+ setToggled({});
35
+ }
36
+ const handleToggle = (key, isOpen) => {
37
+ setToggled((previous) => ({
38
+ ...previous,
39
+ [key]: isOpen
40
+ }));
41
+ };
42
+ return /* @__PURE__ */ jsx("nav", {
43
+ "aria-label": label,
44
+ className: ["wave-docs-sidebar", className].filter(Boolean).join(" "),
45
+ children: /* @__PURE__ */ jsx(NavList, {
46
+ nodes: nav,
47
+ depth: 0,
48
+ keyPrefix: baseId,
49
+ pathname,
50
+ Link,
51
+ toggled,
52
+ onToggle: handleToggle
53
+ })
54
+ });
55
+ }
56
+ function NavList({ nodes, depth, keyPrefix, pathname, Link, toggled, onToggle, id }) {
57
+ return /* @__PURE__ */ jsx("ul", {
58
+ id,
59
+ className: "wave-docs-sidebar__list",
60
+ "data-depth": depth,
61
+ children: nodes.map((node, index) => {
62
+ const key = `${keyPrefix}-${index}`;
63
+ switch (node.type) {
64
+ case "separator": return /* @__PURE__ */ jsx("li", {
65
+ className: "wave-docs-sidebar__separator-item",
66
+ children: /* @__PURE__ */ jsx("span", {
67
+ className: "wave-docs-sidebar__separator",
68
+ children: node.title
69
+ })
70
+ }, key);
71
+ case "link": return /* @__PURE__ */ jsx("li", {
72
+ className: "wave-docs-sidebar__item",
73
+ children: /* @__PURE__ */ jsx(NavLink, {
74
+ href: node.href,
75
+ isExternal: node.external,
76
+ isActive: !node.external && isActiveHref(pathname, node.href),
77
+ Link,
78
+ children: node.title
79
+ })
80
+ }, key);
81
+ case "page": return /* @__PURE__ */ jsx("li", {
82
+ className: "wave-docs-sidebar__item",
83
+ children: /* @__PURE__ */ jsx(NavLink, {
84
+ href: node.href,
85
+ isExternal: false,
86
+ isActive: isActiveHref(pathname, node.href),
87
+ Link,
88
+ children: node.title
89
+ })
90
+ }, key);
91
+ case "group": return /* @__PURE__ */ jsx(NavGroup, {
92
+ node,
93
+ itemKey: key,
94
+ depth,
95
+ pathname,
96
+ Link,
97
+ toggled,
98
+ onToggle
99
+ }, key);
100
+ default: return null;
101
+ }
102
+ })
103
+ });
104
+ }
105
+ function NavGroup({ node, itemKey, depth, pathname, Link, toggled, onToggle }) {
106
+ const listId = `${itemKey}-list`;
107
+ const hasActive = containsActive(node, pathname);
108
+ const isOpen = toggled[itemKey] ?? hasActive;
109
+ const isGroupActive = node.href !== void 0 && isActiveHref(pathname, node.href);
110
+ return /* @__PURE__ */ jsxs("li", {
111
+ className: "wave-docs-sidebar__item",
112
+ "data-open": isOpen ? "" : void 0,
113
+ children: [/* @__PURE__ */ jsx("div", {
114
+ className: "wave-docs-sidebar__group-header",
115
+ children: node.href === void 0 ? /* @__PURE__ */ jsxs("button", {
116
+ type: "button",
117
+ className: "wave-docs-sidebar__group-button",
118
+ "aria-expanded": isOpen,
119
+ "aria-controls": isOpen ? listId : void 0,
120
+ onClick: () => onToggle(itemKey, !isOpen),
121
+ children: [/* @__PURE__ */ jsx("span", {
122
+ className: "wave-docs-sidebar__group-title",
123
+ children: node.title
124
+ }), /* @__PURE__ */ jsx(Chevron, { isOpen })]
125
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(NavLink, {
126
+ href: node.href,
127
+ isExternal: false,
128
+ isActive: isGroupActive,
129
+ Link,
130
+ children: node.title
131
+ }), /* @__PURE__ */ jsx("button", {
132
+ type: "button",
133
+ className: "wave-docs-sidebar__group-toggle",
134
+ "aria-expanded": isOpen,
135
+ "aria-controls": isOpen ? listId : void 0,
136
+ "aria-label": `${isOpen ? "Collapse" : "Expand"} ${node.title}`,
137
+ onClick: () => onToggle(itemKey, !isOpen),
138
+ children: /* @__PURE__ */ jsx(Chevron, { isOpen })
139
+ })] })
140
+ }), isOpen ? /* @__PURE__ */ jsx(NavList, {
141
+ id: listId,
142
+ nodes: node.children,
143
+ depth: depth + 1,
144
+ keyPrefix: itemKey,
145
+ pathname,
146
+ Link,
147
+ toggled,
148
+ onToggle
149
+ }) : null]
150
+ });
151
+ }
152
+ function NavLink({ href, isExternal, isActive, Link, children }) {
153
+ const className = "wave-docs-sidebar__link";
154
+ if (isExternal) return /* @__PURE__ */ jsxs("a", {
155
+ className,
156
+ href,
157
+ target: "_blank",
158
+ rel: "noopener noreferrer",
159
+ children: [children, /* @__PURE__ */ jsx("span", {
160
+ className: "wave-docs-sr-only",
161
+ children: " (opens in a new tab)"
162
+ })]
163
+ });
164
+ if (Link === void 0) return /* @__PURE__ */ jsx("a", {
165
+ className,
166
+ href,
167
+ "aria-current": isActive ? "page" : void 0,
168
+ children
169
+ });
170
+ return /* @__PURE__ */ jsx(Link, {
171
+ className,
172
+ href,
173
+ prefetch: false,
174
+ "aria-current": isActive ? "page" : void 0,
175
+ children
176
+ });
177
+ }
178
+ function Chevron({ isOpen }) {
179
+ return /* @__PURE__ */ jsx("svg", {
180
+ className: "wave-docs-sidebar__chevron",
181
+ "data-open": isOpen ? "" : void 0,
182
+ viewBox: "0 0 24 24",
183
+ width: "16",
184
+ height: "16",
185
+ fill: "none",
186
+ stroke: "currentColor",
187
+ strokeWidth: "2",
188
+ strokeLinecap: "round",
189
+ strokeLinejoin: "round",
190
+ "aria-hidden": "true",
191
+ focusable: "false",
192
+ children: /* @__PURE__ */ jsx("path", { d: "m9 18 6-6-6-6" })
193
+ });
194
+ }
195
+ //#endregion
196
+ export { DocsSidebar };
@@ -0,0 +1,37 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/react/skip-link.d.ts
3
+ /**
4
+ * The `id` this link targets, and the one `createDocsRoute` puts on its
5
+ * `<article>`.
6
+ *
7
+ * One constant for both halves: the two files spelled the string independently,
8
+ * and a skip link pointing at an id nothing carries scrolls nowhere and focuses
9
+ * nothing — a failure with no symptom until a keyboard user hits it.
10
+ */
11
+ declare const DOCS_CONTENT_ID = "docs-content";
12
+ interface SkipLinkProps {
13
+ /** Fragment id of the main content region. */
14
+ href?: string | undefined;
15
+ className?: string | undefined;
16
+ children?: ReactNode;
17
+ }
18
+ /**
19
+ * Skip-to-content link: invisible until focused, first in the tab order.
20
+ *
21
+ * A docs sidebar can be a hundred links deep, and without this every keyboard
22
+ * and switch user tabs through all of them on every page to reach the prose.
23
+ * It is three lines of markup and most docs sites still do not have it.
24
+ *
25
+ * Put it first inside `<body>`, and give the target `tabIndex={-1}` — browsers
26
+ * move the *scroll* position on a fragment link but not always the focus, so
27
+ * an unfocusable target leaves focus stranded at the top of the document.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <SkipLink href="#docs-content" />
32
+ * <main id="docs-content" tabIndex={-1}>{children}</main>
33
+ * ```
34
+ */
35
+ declare function SkipLink({ href, className, children }: SkipLinkProps): ReactNode;
36
+ //#endregion
37
+ export { DOCS_CONTENT_ID, SkipLink, SkipLinkProps };
@@ -0,0 +1,37 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ //#region src/react/skip-link.tsx
3
+ /**
4
+ * The `id` this link targets, and the one `createDocsRoute` puts on its
5
+ * `<article>`.
6
+ *
7
+ * One constant for both halves: the two files spelled the string independently,
8
+ * and a skip link pointing at an id nothing carries scrolls nowhere and focuses
9
+ * nothing — a failure with no symptom until a keyboard user hits it.
10
+ */
11
+ const DOCS_CONTENT_ID = "docs-content";
12
+ /**
13
+ * Skip-to-content link: invisible until focused, first in the tab order.
14
+ *
15
+ * A docs sidebar can be a hundred links deep, and without this every keyboard
16
+ * and switch user tabs through all of them on every page to reach the prose.
17
+ * It is three lines of markup and most docs sites still do not have it.
18
+ *
19
+ * Put it first inside `<body>`, and give the target `tabIndex={-1}` — browsers
20
+ * move the *scroll* position on a fragment link but not always the focus, so
21
+ * an unfocusable target leaves focus stranded at the top of the document.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <SkipLink href="#docs-content" />
26
+ * <main id="docs-content" tabIndex={-1}>{children}</main>
27
+ * ```
28
+ */
29
+ function SkipLink({ href = `#${DOCS_CONTENT_ID}`, className, children = "Skip to content" }) {
30
+ return /* @__PURE__ */ jsx("a", {
31
+ href,
32
+ className: ["wave-docs-skip-link", className].filter(Boolean).join(" "),
33
+ children
34
+ });
35
+ }
36
+ //#endregion
37
+ export { DOCS_CONTENT_ID, SkipLink };
@@ -0,0 +1,35 @@
1
+ import { TocEntry } from "../types.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/react/toc.d.ts
4
+ interface DocsTocProps {
5
+ /** Headings from `@waveso/docs/render`, already nested by depth. */
6
+ entries: TocEntry[];
7
+ /** Accessible name for the landmark. */
8
+ label?: string | undefined;
9
+ /**
10
+ * Region of the viewport that counts as "current", as an
11
+ * `IntersectionObserver` root margin. The default reserves 80px for a sticky
12
+ * header and ignores the bottom 60% of the screen, so the active entry
13
+ * tracks what you are reading rather than what has scrolled into view.
14
+ *
15
+ * Only `px` and `%` are legal here — `IntersectionObserver` throws on any
16
+ * other unit, `rem` included.
17
+ */
18
+ rootMargin?: string | undefined;
19
+ className?: string | undefined;
20
+ }
21
+ /**
22
+ * On-this-page navigation with scrollspy.
23
+ *
24
+ * The ids come from the same `rehype-slug` pass that annotated the document, so
25
+ * `getElementById` matches by construction — no second slugging pass to drift
26
+ * out of sync on duplicate headings.
27
+ *
28
+ * Scrolling itself is left to the browser: the links are real anchors, and
29
+ * smooth scrolling is applied in CSS under
30
+ * `@media (prefers-reduced-motion: no-preference)`. Doing it in JavaScript
31
+ * means reimplementing that check, and getting it wrong makes people ill.
32
+ */
33
+ declare function DocsToc({ entries, label, rootMargin, className }: DocsTocProps): ReactNode;
34
+ //#endregion
35
+ export { DocsToc, DocsTocProps };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/react/toc.tsx
5
+ const DEFAULT_ROOT_MARGIN = "-80px 0px -60% 0px";
6
+ function flattenTocIds(entries) {
7
+ const ids = [];
8
+ const walk = (list) => {
9
+ for (const entry of list) {
10
+ ids.push(entry.id);
11
+ walk(entry.children);
12
+ }
13
+ };
14
+ walk(entries);
15
+ return ids;
16
+ }
17
+ /**
18
+ * On-this-page navigation with scrollspy.
19
+ *
20
+ * The ids come from the same `rehype-slug` pass that annotated the document, so
21
+ * `getElementById` matches by construction — no second slugging pass to drift
22
+ * out of sync on duplicate headings.
23
+ *
24
+ * Scrolling itself is left to the browser: the links are real anchors, and
25
+ * smooth scrolling is applied in CSS under
26
+ * `@media (prefers-reduced-motion: no-preference)`. Doing it in JavaScript
27
+ * means reimplementing that check, and getting it wrong makes people ill.
28
+ */
29
+ function DocsToc({ entries, label = "On this page", rootMargin = DEFAULT_ROOT_MARGIN, className }) {
30
+ const ids = useMemo(() => flattenTocIds(entries), [entries]);
31
+ const [activeId, setActiveId] = useState(void 0);
32
+ const lastIds = useRef(ids);
33
+ if (lastIds.current !== ids) {
34
+ lastIds.current = ids;
35
+ setActiveId(void 0);
36
+ }
37
+ useEffect(() => {
38
+ if (ids.length === 0 || typeof IntersectionObserver === "undefined") return;
39
+ const visible = /* @__PURE__ */ new Set();
40
+ const observer = new IntersectionObserver((records) => {
41
+ for (const record of records) if (record.isIntersecting) visible.add(record.target.id);
42
+ else visible.delete(record.target.id);
43
+ const next = ids.find((id) => visible.has(id));
44
+ if (next !== void 0) setActiveId(next);
45
+ }, {
46
+ rootMargin,
47
+ threshold: 0
48
+ });
49
+ for (const id of ids) {
50
+ const element = document.getElementById(id);
51
+ if (element !== null) observer.observe(element);
52
+ }
53
+ return () => observer.disconnect();
54
+ }, [ids, rootMargin]);
55
+ if (entries.length === 0) return null;
56
+ return /* @__PURE__ */ jsx("nav", {
57
+ "aria-label": label,
58
+ className: ["wave-docs-toc", className].filter(Boolean).join(" "),
59
+ children: /* @__PURE__ */ jsx(TocList, {
60
+ entries,
61
+ activeId,
62
+ onSelect: setActiveId
63
+ })
64
+ });
65
+ }
66
+ function TocList({ entries, activeId, onSelect }) {
67
+ return /* @__PURE__ */ jsx("ul", {
68
+ className: "wave-docs-toc__list",
69
+ children: entries.map((entry) => /* @__PURE__ */ jsxs("li", {
70
+ className: "wave-docs-toc__item",
71
+ children: [/* @__PURE__ */ jsx("a", {
72
+ className: "wave-docs-toc__link",
73
+ href: `#${entry.id}`,
74
+ "data-depth": entry.depth,
75
+ "aria-current": entry.id === activeId ? "location" : void 0,
76
+ onClick: () => onSelect(entry.id),
77
+ children: entry.text
78
+ }), entry.children.length > 0 ? /* @__PURE__ */ jsx(TocList, {
79
+ entries: entry.children,
80
+ activeId,
81
+ onSelect
82
+ }) : null]
83
+ }, entry.id))
84
+ });
85
+ }
86
+ //#endregion
87
+ export { DocsToc };
@@ -0,0 +1,27 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/react/youtube.d.ts
3
+ interface YouTubeProps {
4
+ /** The 11-character video id, e.g. `dQw4w9WgXcQ`. */
5
+ id?: string | undefined;
6
+ /**
7
+ * Accessible name for the player. Markdown carries no video title, so the
8
+ * fallback is generic — pass a real one where you have it.
9
+ */
10
+ title?: string | undefined;
11
+ className?: string | undefined;
12
+ }
13
+ /**
14
+ * Click-to-load YouTube embed.
15
+ *
16
+ * An eager `<iframe>` costs ~137 KB of embed document plus ~580 KB gzipped of
17
+ * player JavaScript, on every page view, whether or not anyone presses play.
18
+ * A facade costs one ~15 KB JPEG and loads the rest on demand. On a docs page
19
+ * with three videos that is the difference between a good Lighthouse score and
20
+ * a bad one.
21
+ *
22
+ * `hqdefault.jpg` rather than `maxresdefault.jpg` deliberately: maxres does not
23
+ * exist for uploads below 1280×720 and 404s to a broken image with no fallback.
24
+ */
25
+ declare function YouTube({ id, title, className }: YouTubeProps): ReactNode;
26
+ //#endregion
27
+ export { YouTube, YouTubeProps };
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/react/youtube.tsx
5
+ const DEFAULT_TITLE = "YouTube video player";
6
+ /**
7
+ * Click-to-load YouTube embed.
8
+ *
9
+ * An eager `<iframe>` costs ~137 KB of embed document plus ~580 KB gzipped of
10
+ * player JavaScript, on every page view, whether or not anyone presses play.
11
+ * A facade costs one ~15 KB JPEG and loads the rest on demand. On a docs page
12
+ * with three videos that is the difference between a good Lighthouse score and
13
+ * a bad one.
14
+ *
15
+ * `hqdefault.jpg` rather than `maxresdefault.jpg` deliberately: maxres does not
16
+ * exist for uploads below 1280×720 and 404s to a broken image with no fallback.
17
+ */
18
+ function YouTube({ id, title, className }) {
19
+ const [isPlaying, setIsPlaying] = useState(false);
20
+ if (!id) return null;
21
+ const safeId = encodeURIComponent(id);
22
+ const label = title?.trim() || DEFAULT_TITLE;
23
+ const rootClassName = ["wave-docs-youtube", className].filter(Boolean).join(" ");
24
+ if (isPlaying) return /* @__PURE__ */ jsx("div", {
25
+ className: rootClassName,
26
+ children: /* @__PURE__ */ jsx("iframe", {
27
+ className: "wave-docs-youtube__frame",
28
+ src: `https://www.youtube-nocookie.com/embed/${safeId}?autoplay=1&rel=0`,
29
+ title: label,
30
+ loading: "lazy",
31
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
32
+ allowFullScreen: true,
33
+ ref: (node) => {
34
+ node?.focus();
35
+ }
36
+ })
37
+ });
38
+ return /* @__PURE__ */ jsx("div", {
39
+ className: rootClassName,
40
+ children: /* @__PURE__ */ jsxs("button", {
41
+ type: "button",
42
+ className: "wave-docs-youtube__facade",
43
+ onClick: () => setIsPlaying(true),
44
+ "aria-label": `Play video: ${label}`,
45
+ children: [/* @__PURE__ */ jsx("img", {
46
+ className: "wave-docs-youtube__thumbnail",
47
+ src: `https://i.ytimg.com/vi/${safeId}/hqdefault.jpg`,
48
+ alt: "",
49
+ width: 480,
50
+ height: 360,
51
+ loading: "lazy",
52
+ decoding: "async"
53
+ }), /* @__PURE__ */ jsx("span", {
54
+ className: "wave-docs-youtube__play",
55
+ "aria-hidden": "true",
56
+ children: /* @__PURE__ */ jsxs("svg", {
57
+ viewBox: "0 0 68 48",
58
+ width: "68",
59
+ height: "48",
60
+ "aria-hidden": "true",
61
+ focusable: "false",
62
+ children: [/* @__PURE__ */ jsx("path", {
63
+ className: "wave-docs-youtube__play-bg",
64
+ d: "M66.52 7.74a8 8 0 0 0-5.65-5.66C56.1.99 34 .99 34 .99s-22.1 0-26.87 1.09a8 8 0 0 0-5.65 5.66C.39 12.51.39 24 .39 24s0 11.49 1.09 16.26a8 8 0 0 0 5.65 5.66C11.9 47 34 47 34 47s22.1 0 26.87-1.08a8 8 0 0 0 5.65-5.66C67.61 35.49 67.61 24 67.61 24s0-11.49-1.09-16.26"
65
+ }), /* @__PURE__ */ jsx("path", {
66
+ className: "wave-docs-youtube__play-arrow",
67
+ d: "M27 34V14l17 10z"
68
+ })]
69
+ })
70
+ })]
71
+ })
72
+ });
73
+ }
74
+ //#endregion
75
+ export { YouTube };
@@ -0,0 +1,72 @@
1
+ import { DocFile, DocFrontmatter, ImageResolver, LinkResolver, RenderedDoc, ResolvedDocsConfig } from "./types.js";
2
+ import { DocsHighlighter, DocsLang, DocsThemes } from "./highlighter.js";
3
+ //#region src/render.d.ts
4
+ /**
5
+ * The parts of {@link ResolvedDocsConfig} rendering actually depends on.
6
+ *
7
+ * Declared as a `Pick` so a full resolved config passes straight through: the
8
+ * host resolves configuration once, with `resolveDocsConfig`, and hands the
9
+ * same object to the source walk and to the renderer. Nothing here re-applies
10
+ * defaults, so the two cannot drift.
11
+ */
12
+ type DocsRendererConfig = Pick<ResolvedDocsConfig, 'basePath' | 'assertLinks'>;
13
+ interface DocsRendererOptions {
14
+ config: DocsRendererConfig;
15
+ /**
16
+ * Reuse an existing highlighter — the escape hatch for grammars and themes
17
+ * outside the curated set. Defaults to {@link createDocsHighlighter}.
18
+ */
19
+ highlighter?: DocsHighlighter | Promise<DocsHighlighter>;
20
+ /** Grammars to load, when building the default highlighter. */
21
+ langs?: readonly DocsLang[];
22
+ /** Theme pair. Defaults to {@link DEFAULT_DOCS_THEMES}. */
23
+ themes?: DocsThemes;
24
+ /**
25
+ * Prepend an `<h1>` built from `frontmatter.title` when the markdown body
26
+ * has none. Defaults to `true`.
27
+ *
28
+ * Turn it off only if your layout renders the page title itself: a document
29
+ * with no `h1` fails `page-has-heading-one` and leaves the heading outline
30
+ * starting at `h2`, and markdown that repeats the frontmatter title as `# `
31
+ * is a duplication authors forget to keep in step.
32
+ */
33
+ titleHeading?: boolean;
34
+ /** Replaces the built-in markdown-link resolution. */
35
+ linkResolver?: LinkResolver;
36
+ /**
37
+ * Resolves image `src` to a public URL and intrinsic dimensions, so
38
+ * `next/image` can render without `fill`. Images are left untouched when
39
+ * omitted, or when the resolver returns `undefined`.
40
+ */
41
+ imageResolver?: ImageResolver;
42
+ /**
43
+ * Every route the site publishes, used by `assertLinks`. Read at render
44
+ * time, so a host may pass a set it populates during the source walk.
45
+ *
46
+ * Without it only unresolvable links can be caught; with it, links to pages
47
+ * that simply do not exist are caught too.
48
+ */
49
+ knownRoutes?: ReadonlySet<string>;
50
+ }
51
+ /**
52
+ * Renders {@link DocFile}s. Build one per process and reuse it.
53
+ *
54
+ * The frontmatter type parameter sits on `render`, not on the interface: the
55
+ * renderer reads only `frontmatter.title` and passes the rest through, so one
56
+ * renderer serves files parsed by any schema — which is what lets a host build
57
+ * the processor and the highlighter once. A renderer-level parameter would
58
+ * force a second highlighter per frontmatter shape and buy nothing.
59
+ */
60
+ interface DocsRenderer {
61
+ render<TFrontmatter extends DocFrontmatter>(file: DocFile<TFrontmatter>): Promise<RenderedDoc<TFrontmatter>>;
62
+ }
63
+ /**
64
+ * Create a renderer.
65
+ *
66
+ * The processor and the highlighter are built once, eagerly, and shared by
67
+ * every call to `render`. Constructing them per file is the difference between
68
+ * a docs build that takes a second and one that takes a minute.
69
+ */
70
+ declare function createDocsRenderer(options: DocsRendererOptions): DocsRenderer;
71
+ //#endregion
72
+ export { DocsRenderer, DocsRendererConfig, DocsRendererOptions, createDocsRenderer };