@waveso/docs 0.4.0 → 0.6.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.
- package/CHANGELOG.md +270 -0
- package/README.md +221 -79
- package/dist/anchors.d.ts +44 -0
- package/dist/anchors.js +76 -0
- package/dist/errors.d.ts +4 -0
- package/dist/highlighter.js +2 -1
- package/dist/link-suggestion.d.ts +31 -0
- package/dist/link-suggestion.js +94 -0
- package/dist/meta.js +6 -9
- package/dist/next.d.ts +54 -14
- package/dist/next.js +135 -20
- package/dist/plugins/rehype-code-frame.d.ts +13 -1
- package/dist/plugins/rehype-code-frame.js +2 -1
- package/dist/plugins/rehype-fallback-heading-ids.js +1 -1
- package/dist/plugins/remark-doc-links.d.ts +83 -1
- package/dist/plugins/remark-doc-links.js +50 -23
- package/dist/plugins/remark-youtube.d.ts +18 -3
- package/dist/plugins/remark-youtube.js +57 -9
- package/dist/react/callout.d.ts +13 -1
- package/dist/react/callout.js +2 -2
- package/dist/react/code-runtime.d.ts +12 -2
- package/dist/react/code-runtime.js +28 -4
- package/dist/react/doc-content.d.ts +12 -1
- package/dist/react/doc-content.js +2 -2
- package/dist/react/layout.d.ts +27 -10
- package/dist/react/layout.js +6 -3
- package/dist/react/link-adapter.d.ts +34 -0
- package/dist/react/link-adapter.js +30 -0
- package/dist/react/markdown-components.d.ts +29 -1
- package/dist/react/markdown-components.js +69 -67
- package/dist/react/nav.d.ts +5 -1
- package/dist/react/nav.js +5 -2
- package/dist/react/next-link.d.ts +6 -28
- package/dist/react/next-link.js +45 -24
- package/dist/react/next-nav.d.ts +5 -1
- package/dist/react/next-nav.js +6 -3
- package/dist/react/next-search.js +1 -1
- package/dist/react/search-dialog.d.ts +59 -3
- package/dist/react/search-dialog.js +53 -9
- package/dist/react/shell-labels.d.ts +135 -21
- package/dist/react/shell-labels.js +47 -6
- package/dist/react/sidebar.d.ts +18 -1
- package/dist/react/sidebar.js +59 -23
- package/dist/react/youtube.d.ts +22 -1
- package/dist/react/youtube.js +22 -4
- package/dist/render.d.ts +12 -1
- package/dist/render.js +107 -21
- package/dist/route-path.js +7 -2
- package/dist/safe-href.d.ts +47 -0
- package/dist/safe-href.js +73 -0
- package/dist/search-index.js +1 -1
- package/dist/search-options.d.ts +64 -2
- package/dist/search-options.js +25 -1
- package/dist/semaphore.d.ts +46 -0
- package/dist/semaphore.js +60 -0
- package/dist/source.js +86 -12
- package/dist/types.d.ts +102 -6
- package/package.json +6 -3
|
@@ -20,6 +20,10 @@ const FILE_EXTENSION = /\.[^./]+$/;
|
|
|
20
20
|
function isRelativeLink(href) {
|
|
21
21
|
return href !== "" && !href.startsWith("#") && !href.startsWith("?") && !href.startsWith("/") && !HAS_SCHEME.test(href);
|
|
22
22
|
}
|
|
23
|
+
/** No prefix at all — the docs own the whole origin. */
|
|
24
|
+
function isRootMount(basePath) {
|
|
25
|
+
return basePath.replace(/\/+$/, "") === "";
|
|
26
|
+
}
|
|
23
27
|
/**
|
|
24
28
|
* Is this already-absolute href one of OUR routes?
|
|
25
29
|
*
|
|
@@ -28,9 +32,11 @@ function isRelativeLink(href) {
|
|
|
28
32
|
* A typo in a hand-written absolute link is exactly as likely as one in a
|
|
29
33
|
* relative link; only the rewriting differs.
|
|
30
34
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
35
|
+
* Answers `false` at a root mount, where there is no prefix to test against.
|
|
36
|
+
* That is not the end of the matter: the caller records those links anyway,
|
|
37
|
+
* marked, because at a root mount the *common* case is an origin that serves
|
|
38
|
+
* documentation and nothing else — so they are checked, and a site with other
|
|
39
|
+
* routes names them through `externalRoutes`.
|
|
34
40
|
*/
|
|
35
41
|
function isInternalAbsoluteLink(href, basePath) {
|
|
36
42
|
const base = basePath.replace(/\/+$/, "");
|
|
@@ -107,11 +113,31 @@ function decodeSegment(segment, href) {
|
|
|
107
113
|
* Decode BEFORE folding, never after: `%2E%2E%2F` is `../` in disguise, and
|
|
108
114
|
* `foldSegments` is the only thing that refuses a chain climbing out of the
|
|
109
115
|
* content root.
|
|
116
|
+
*
|
|
117
|
+
* Exported for `route-path.ts`, which needs the decode without the split:
|
|
118
|
+
* an alias may carry a literal `#` or `?` — `c# guide` is a page name — and
|
|
119
|
+
* {@link splitHref} would cut the string there and throw the rest away.
|
|
110
120
|
*/
|
|
111
121
|
function decodePath(path, href) {
|
|
112
122
|
return path.split("/").map((segment) => decodeSegment(segment, href)).join("/");
|
|
113
123
|
}
|
|
114
124
|
/**
|
|
125
|
+
* Split an href into its path, query and fragment; decode only the path.
|
|
126
|
+
*
|
|
127
|
+
* Throws a {@link URIError} if the path is not valid percent-encoding, with the
|
|
128
|
+
* whole href in the message — see {@link decodeSegment}.
|
|
129
|
+
*/
|
|
130
|
+
function splitHref(href) {
|
|
131
|
+
const parts = HREF_PARTS.exec(href);
|
|
132
|
+
const rawPath = parts?.[1] ?? "";
|
|
133
|
+
return {
|
|
134
|
+
path: decodePath(rawPath, href),
|
|
135
|
+
rawPath,
|
|
136
|
+
query: parts?.[2] ?? "",
|
|
137
|
+
hash: parts?.[3] ?? ""
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
115
141
|
* The built-in {@link LinkResolver}: markdown file path in, route out.
|
|
116
142
|
*
|
|
117
143
|
* Exported for reuse by hosts that want to wrap rather than replace it. Throws
|
|
@@ -119,12 +145,9 @@ function decodePath(path, href) {
|
|
|
119
145
|
* reported as `undefined`.
|
|
120
146
|
*/
|
|
121
147
|
function resolveMarkdownLink(href, fromDir, basePath) {
|
|
122
|
-
const
|
|
123
|
-
const path = parts?.[1] ?? "";
|
|
124
|
-
const query = parts?.[2] ?? "";
|
|
125
|
-
const hash = parts?.[3] ?? "";
|
|
148
|
+
const { path, query, hash } = splitHref(href);
|
|
126
149
|
if (path === "") return;
|
|
127
|
-
const segments = foldSegments(fromDir,
|
|
150
|
+
const segments = foldSegments(fromDir, path);
|
|
128
151
|
if (segments === void 0) return;
|
|
129
152
|
const last = segments.at(-1);
|
|
130
153
|
if (last !== void 0) {
|
|
@@ -149,12 +172,9 @@ function resolveMarkdownLink(href, fromDir, basePath) {
|
|
|
149
172
|
* browser resolves happily and `knownRoutes` has never heard of.
|
|
150
173
|
*/
|
|
151
174
|
function normalizeInternalRoute(href, basePath) {
|
|
152
|
-
const
|
|
153
|
-
const path = parts?.[1] ?? "";
|
|
154
|
-
const query = parts?.[2] ?? "";
|
|
155
|
-
const hash = parts?.[3] ?? "";
|
|
175
|
+
const { rawPath, query, hash } = splitHref(href);
|
|
156
176
|
const base = basePath.replace(/\/+$/, "");
|
|
157
|
-
const segments = foldSegments([], decodePath(
|
|
177
|
+
const segments = foldSegments([], decodePath(rawPath.slice(base.length), href));
|
|
158
178
|
if (segments === void 0) return;
|
|
159
179
|
return `${toRoute(basePath, segments)}${query}${hash}`;
|
|
160
180
|
}
|
|
@@ -183,11 +203,8 @@ function isAssetLink(href) {
|
|
|
183
203
|
* the exception.
|
|
184
204
|
*/
|
|
185
205
|
function resolveAssetLink(href, fromDir, basePath) {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const query = parts?.[2] ?? "";
|
|
189
|
-
const hash = parts?.[3] ?? "";
|
|
190
|
-
const segments = foldSegments(fromDir, decodePath(path, href));
|
|
206
|
+
const { path, query, hash } = splitHref(href);
|
|
207
|
+
const segments = foldSegments(fromDir, path);
|
|
191
208
|
if (segments === void 0) return;
|
|
192
209
|
return `${toRoute(basePath, segments)}${query}${hash}`;
|
|
193
210
|
}
|
|
@@ -213,23 +230,33 @@ const remarkDocLinks = (options) => {
|
|
|
213
230
|
const raw = node.url;
|
|
214
231
|
if (node.type === "definition" && imageIdentifiers.has(node.identifier)) continue;
|
|
215
232
|
const line = node.position?.start.line;
|
|
216
|
-
const record = (href,
|
|
233
|
+
const record = (href, flags = {}) => {
|
|
217
234
|
const ref = {
|
|
218
235
|
raw,
|
|
219
236
|
href
|
|
220
237
|
};
|
|
221
238
|
if (line !== void 0) ref.line = line;
|
|
222
|
-
if (asset !== void 0) ref.asset = asset;
|
|
239
|
+
if (flags.asset !== void 0) ref.asset = flags.asset;
|
|
240
|
+
if (flags.unverifiable !== void 0) ref.unverifiable = flags.unverifiable;
|
|
241
|
+
if (flags.anchorOnly !== void 0) ref.anchorOnly = flags.anchorOnly;
|
|
223
242
|
refs.push(ref);
|
|
224
243
|
if (href !== void 0) node.url = href;
|
|
225
244
|
};
|
|
226
245
|
try {
|
|
246
|
+
if (raw.startsWith("#")) {
|
|
247
|
+
record(raw, { anchorOnly: true });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
227
250
|
if (!isRelativeLink(raw)) {
|
|
228
|
-
if (isInternalAbsoluteLink(raw, basePath) && !isAssetLink(raw))
|
|
251
|
+
if (isInternalAbsoluteLink(raw, basePath) && !isAssetLink(raw)) {
|
|
252
|
+
record(normalizeInternalRoute(raw, basePath));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (isRootMount(basePath) && raw.startsWith("/") && !raw.startsWith("//") && !isAssetLink(raw)) record(normalizeInternalRoute(raw, basePath), { unverifiable: true });
|
|
229
256
|
continue;
|
|
230
257
|
}
|
|
231
258
|
if (resolve === void 0 && isAssetLink(raw)) {
|
|
232
|
-
record(resolveAssetLink(raw, context.dirSegments, basePath), true);
|
|
259
|
+
record(resolveAssetLink(raw, context.dirSegments, basePath), { asset: true });
|
|
233
260
|
continue;
|
|
234
261
|
}
|
|
235
262
|
if (resolve) {
|
|
@@ -246,4 +273,4 @@ const remarkDocLinks = (options) => {
|
|
|
246
273
|
};
|
|
247
274
|
};
|
|
248
275
|
//#endregion
|
|
249
|
-
export { foldSegments, remarkDocLinks, resolveMarkdownLink };
|
|
276
|
+
export { decodePath, foldSegments, isRootMount, remarkDocLinks, resolveMarkdownLink, splitHref };
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { Plugin } from "unified";
|
|
2
2
|
import { Root } from "mdast";
|
|
3
3
|
//#region src/plugins/remark-youtube.d.ts
|
|
4
|
+
/** What a YouTube URL says, beyond which video it is. */
|
|
5
|
+
interface YouTubeRef {
|
|
6
|
+
id: string;
|
|
7
|
+
/** Seconds to start at, from `t` or `start`. */
|
|
8
|
+
start?: number | undefined;
|
|
9
|
+
/** Playlist the video was linked inside, from `list`. */
|
|
10
|
+
list?: string | undefined;
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
|
-
* Extract a video
|
|
13
|
+
* Extract a video reference from a YouTube watch/short/embed URL.
|
|
6
14
|
*
|
|
7
15
|
* Returns `undefined` for anything else, including YouTube URLs that are not a
|
|
8
16
|
* single video (channels, playlists) — those stay ordinary links.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ THE TIMESTAMP AND THE PLAYLIST ARE PART OF THE LINK, AND WERE DROPPED. Only
|
|
19
|
+
* the id survived, so `https://youtu.be/x?t=754` — a link to one specific moment
|
|
20
|
+
* in a two-hour talk, which is most of why anyone deep-links a video — opened at
|
|
21
|
+
* zero. And because the facade passes `autoplay=1`, it did not merely start in
|
|
22
|
+
* the wrong place: it started *playing* in the wrong place, so the reader had to
|
|
23
|
+
* work out that the author had meant somewhere else.
|
|
9
24
|
*/
|
|
10
|
-
declare function
|
|
25
|
+
declare function parseYouTubeRef(href: string): YouTubeRef | undefined;
|
|
11
26
|
/**
|
|
12
27
|
* remark plugin. Replaces the whole PARAGRAPH, not the link inside it — which
|
|
13
28
|
* is the point: leaving the paragraph is what nested a block element in
|
|
@@ -19,4 +34,4 @@ declare function parseYouTubeId(href: string): string | undefined;
|
|
|
19
34
|
*/
|
|
20
35
|
declare const remarkYouTube: Plugin<[], Root>;
|
|
21
36
|
//#endregion
|
|
22
|
-
export {
|
|
37
|
+
export { YouTubeRef, parseYouTubeRef, remarkYouTube };
|
|
@@ -4,13 +4,40 @@ import { SKIP, visit } from "unist-util-visit";
|
|
|
4
4
|
const VIDEO_ID = /^[A-Za-z0-9_-]{11}$/;
|
|
5
5
|
/** `https://` or `http://`, for comparing a label against its own href. */
|
|
6
6
|
const HTTP_SCHEME = /^https?:\/\//i;
|
|
7
|
+
/** A playlist id, conservatively: what YouTube uses and nothing else. */
|
|
8
|
+
const PLAYLIST_ID = /^[A-Za-z0-9_-]{2,64}$/;
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
10
|
+
* `1h2m3s`, `90s`, `90` — every spelling YouTube's own `t` parameter takes.
|
|
11
|
+
*/
|
|
12
|
+
const TIMESTAMP = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/;
|
|
13
|
+
/**
|
|
14
|
+
* Seconds from a `t`/`start` value, or `undefined` if it is not one.
|
|
15
|
+
*
|
|
16
|
+
* YouTube accepts `t=90`, `t=90s` and `t=1m30s`, and an author linking to a
|
|
17
|
+
* moment in a talk pastes whichever the share dialog gave them.
|
|
18
|
+
*/
|
|
19
|
+
function parseTimestamp(value) {
|
|
20
|
+
if (value === null || value === "") return void 0;
|
|
21
|
+
const match = TIMESTAMP.exec(value);
|
|
22
|
+
if (match === null) return void 0;
|
|
23
|
+
const [, hours, minutes, seconds] = match;
|
|
24
|
+
const total = Number(hours ?? 0) * 3600 + Number(minutes ?? 0) * 60 + Number(seconds ?? 0);
|
|
25
|
+
return Number.isFinite(total) && total > 0 ? total : void 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract a video reference from a YouTube watch/short/embed URL.
|
|
9
29
|
*
|
|
10
30
|
* Returns `undefined` for anything else, including YouTube URLs that are not a
|
|
11
31
|
* single video (channels, playlists) — those stay ordinary links.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ THE TIMESTAMP AND THE PLAYLIST ARE PART OF THE LINK, AND WERE DROPPED. Only
|
|
34
|
+
* the id survived, so `https://youtu.be/x?t=754` — a link to one specific moment
|
|
35
|
+
* in a two-hour talk, which is most of why anyone deep-links a video — opened at
|
|
36
|
+
* zero. And because the facade passes `autoplay=1`, it did not merely start in
|
|
37
|
+
* the wrong place: it started *playing* in the wrong place, so the reader had to
|
|
38
|
+
* work out that the author had meant somewhere else.
|
|
12
39
|
*/
|
|
13
|
-
function
|
|
40
|
+
function parseYouTubeRef(href) {
|
|
14
41
|
let url;
|
|
15
42
|
try {
|
|
16
43
|
url = new URL(href, "https://example.invalid");
|
|
@@ -19,17 +46,34 @@ function parseYouTubeId(href) {
|
|
|
19
46
|
}
|
|
20
47
|
const host = url.hostname.replace(/^(www|m)\./, "");
|
|
21
48
|
const segments = url.pathname.split("/").filter(Boolean);
|
|
49
|
+
const extras = () => {
|
|
50
|
+
const start = parseTimestamp(url.searchParams.get("t")) ?? parseTimestamp(url.searchParams.get("start")) ?? parseTimestamp(url.hash.replace(/^#t=/, "") || null);
|
|
51
|
+
const list = url.searchParams.get("list");
|
|
52
|
+
return {
|
|
53
|
+
...start === void 0 ? {} : { start },
|
|
54
|
+
...list !== null && PLAYLIST_ID.test(list) ? { list } : {}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
22
57
|
if (host === "youtu.be") {
|
|
23
58
|
const [id] = segments;
|
|
24
|
-
return id !== void 0 && VIDEO_ID.test(id) ?
|
|
59
|
+
return id !== void 0 && VIDEO_ID.test(id) ? {
|
|
60
|
+
id,
|
|
61
|
+
...extras()
|
|
62
|
+
} : void 0;
|
|
25
63
|
}
|
|
26
64
|
if (host !== "youtube.com" && host !== "youtube-nocookie.com") return;
|
|
27
65
|
if (url.pathname === "/watch") {
|
|
28
66
|
const id = url.searchParams.get("v");
|
|
29
|
-
return id !== null && VIDEO_ID.test(id) ?
|
|
67
|
+
return id !== null && VIDEO_ID.test(id) ? {
|
|
68
|
+
id,
|
|
69
|
+
...extras()
|
|
70
|
+
} : void 0;
|
|
30
71
|
}
|
|
31
72
|
const [prefix, id] = segments;
|
|
32
|
-
if ((prefix === "embed" || prefix === "shorts") && id !== void 0) return VIDEO_ID.test(id) ?
|
|
73
|
+
if ((prefix === "embed" || prefix === "shorts") && id !== void 0) return VIDEO_ID.test(id) ? {
|
|
74
|
+
id,
|
|
75
|
+
...extras()
|
|
76
|
+
} : void 0;
|
|
33
77
|
}
|
|
34
78
|
/** Whitespace-only text is what separates two links on consecutive lines. */
|
|
35
79
|
function isIgnorable(node) {
|
|
@@ -66,14 +110,18 @@ const remarkYouTube = () => {
|
|
|
66
110
|
const meaningful = node.children.filter((child) => !isIgnorable(child));
|
|
67
111
|
const only = meaningful[0];
|
|
68
112
|
if (meaningful.length !== 1 || only === void 0 || !isBareUrl(only)) return;
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
113
|
+
const ref = only.type === "link" ? parseYouTubeRef(only.url) : void 0;
|
|
114
|
+
if (ref === void 0) return;
|
|
71
115
|
parent.children[index] = {
|
|
72
116
|
type: "paragraph",
|
|
73
117
|
children: [],
|
|
74
118
|
data: {
|
|
75
119
|
hName: "youtube",
|
|
76
|
-
hProperties: {
|
|
120
|
+
hProperties: {
|
|
121
|
+
id: ref.id,
|
|
122
|
+
...ref.start === void 0 ? {} : { start: ref.start },
|
|
123
|
+
...ref.list === void 0 ? {} : { list: ref.list }
|
|
124
|
+
}
|
|
77
125
|
}
|
|
78
126
|
};
|
|
79
127
|
return [SKIP, index + 1];
|
|
@@ -81,4 +129,4 @@ const remarkYouTube = () => {
|
|
|
81
129
|
};
|
|
82
130
|
};
|
|
83
131
|
//#endregion
|
|
84
|
-
export {
|
|
132
|
+
export { parseYouTubeRef, remarkYouTube };
|
package/dist/react/callout.d.ts
CHANGED
|
@@ -16,6 +16,18 @@ interface CalloutProps {
|
|
|
16
16
|
type?: string | undefined;
|
|
17
17
|
/** Overrides the default label ("Note", "Warning", …). */
|
|
18
18
|
title?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Default headings per type, for a site that is not in English.
|
|
21
|
+
*
|
|
22
|
+
* `title` still wins — it is what a single callout in the markdown asked for,
|
|
23
|
+
* and this is what every callout on the site is called otherwise.
|
|
24
|
+
*
|
|
25
|
+
* Here rather than resolved by the caller so that {@link normalizeCalloutType}
|
|
26
|
+
* stays the one place that decides what an unrecognised type falls back to. A
|
|
27
|
+
* caller picking the heading itself would need that rule too, and a second
|
|
28
|
+
* copy of it is a second thing to keep in step.
|
|
29
|
+
*/
|
|
30
|
+
labels?: Partial<Record<CalloutType, string>> | undefined;
|
|
19
31
|
className?: string | undefined;
|
|
20
32
|
children?: ReactNode;
|
|
21
33
|
}
|
|
@@ -32,6 +44,6 @@ interface CalloutProps {
|
|
|
32
44
|
* All styling lives in `@waveso/docs/styles.css` under `.wave-docs-callout`,
|
|
33
45
|
* so consumers can restyle it without forking the component.
|
|
34
46
|
*/
|
|
35
|
-
declare function Callout({ type, title, className, children }: CalloutProps): ReactNode;
|
|
47
|
+
declare function Callout({ type, title, labels, className, children }: CalloutProps): ReactNode;
|
|
36
48
|
//#endregion
|
|
37
49
|
export { CALLOUT_TYPES, Callout, CalloutProps, CalloutType };
|
package/dist/react/callout.js
CHANGED
|
@@ -69,9 +69,9 @@ function normalizeCalloutType(value) {
|
|
|
69
69
|
* All styling lives in `@waveso/docs/styles.css` under `.wave-docs-callout`,
|
|
70
70
|
* so consumers can restyle it without forking the component.
|
|
71
71
|
*/
|
|
72
|
-
function Callout({ type, title, className, children }) {
|
|
72
|
+
function Callout({ type, title, labels, className, children }) {
|
|
73
73
|
const kind = normalizeCalloutType(type);
|
|
74
|
-
const label = title?.trim() || CALLOUT_LABELS[kind];
|
|
74
|
+
const label = title?.trim() || labels?.[kind] || CALLOUT_LABELS[kind];
|
|
75
75
|
return /* @__PURE__ */ jsxs("aside", {
|
|
76
76
|
role: "note",
|
|
77
77
|
"aria-label": label,
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { ReactNode } from "react";
|
|
2
2
|
//#region src/react/code-runtime.d.ts
|
|
3
|
+
/** The two announcements, for a site that is not in English. */
|
|
4
|
+
interface CodeRuntimeLabels {
|
|
5
|
+
/** Announced after a successful copy. Default `'Copied to the clipboard.'` */
|
|
6
|
+
copied?: string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Announced after a failed one. Default
|
|
9
|
+
* `'Copy failed. Select the code and press Control or Command + C.'`
|
|
10
|
+
*/
|
|
11
|
+
copyFailed?: string | undefined;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Mount the copy runtime. Renders nothing.
|
|
5
15
|
*
|
|
@@ -9,6 +19,6 @@ import { ReactNode } from "react";
|
|
|
9
19
|
* re-renders and there is no state to get out of step with a page that was
|
|
10
20
|
* server-rendered.
|
|
11
21
|
*/
|
|
12
|
-
declare function DocsCodeRuntime(): ReactNode;
|
|
22
|
+
declare function DocsCodeRuntime({ copied, copyFailed }?: CodeRuntimeLabels): ReactNode;
|
|
13
23
|
//#endregion
|
|
14
|
-
export { DocsCodeRuntime };
|
|
24
|
+
export { CodeRuntimeLabels, DocsCodeRuntime };
|
|
@@ -40,6 +40,24 @@ const SKIP_LINE_CLASSES = [];
|
|
|
40
40
|
const TRIMMED_LINE_CLASSES = [];
|
|
41
41
|
let refCount = 0;
|
|
42
42
|
let detach;
|
|
43
|
+
const DEFAULT_COPIED = "Copied to the clipboard.";
|
|
44
|
+
const DEFAULT_COPY_FAILED = "Copy failed. Select the code and press Control or Command + C.";
|
|
45
|
+
/**
|
|
46
|
+
* Module scope, beside `refCount`, because the listener is a singleton too.
|
|
47
|
+
*
|
|
48
|
+
* The runtime installs once per page however many `DocContent`s mount it, so
|
|
49
|
+
* the messages belong to the installation rather than to a component — and two
|
|
50
|
+
* mounts with different labels would be a page with two languages in it, which
|
|
51
|
+
* is not a case worth code. First one in wins, and `refCount` says which.
|
|
52
|
+
*
|
|
53
|
+
* Spelled out rather than `Required<CodeRuntimeLabels>`: the props are declared
|
|
54
|
+
* `string | undefined` for `exactOptionalPropertyTypes`, and `Required` strips
|
|
55
|
+
* the `?` while leaving the `undefined` in the value type.
|
|
56
|
+
*/
|
|
57
|
+
let messages = {
|
|
58
|
+
copied: DEFAULT_COPIED,
|
|
59
|
+
copyFailed: DEFAULT_COPY_FAILED
|
|
60
|
+
};
|
|
43
61
|
/**
|
|
44
62
|
* Mount the copy runtime. Renders nothing.
|
|
45
63
|
*
|
|
@@ -49,10 +67,16 @@ let detach;
|
|
|
49
67
|
* re-renders and there is no state to get out of step with a page that was
|
|
50
68
|
* server-rendered.
|
|
51
69
|
*/
|
|
52
|
-
function DocsCodeRuntime() {
|
|
70
|
+
function DocsCodeRuntime({ copied, copyFailed } = {}) {
|
|
53
71
|
useEffect(() => {
|
|
54
72
|
refCount += 1;
|
|
55
|
-
if (refCount === 1)
|
|
73
|
+
if (refCount === 1) {
|
|
74
|
+
messages = {
|
|
75
|
+
copied: copied ?? DEFAULT_COPIED,
|
|
76
|
+
copyFailed: copyFailed ?? DEFAULT_COPY_FAILED
|
|
77
|
+
};
|
|
78
|
+
detach = install();
|
|
79
|
+
}
|
|
56
80
|
return () => {
|
|
57
81
|
refCount -= 1;
|
|
58
82
|
if (refCount === 0) {
|
|
@@ -60,7 +84,7 @@ function DocsCodeRuntime() {
|
|
|
60
84
|
detach = void 0;
|
|
61
85
|
}
|
|
62
86
|
};
|
|
63
|
-
}, []);
|
|
87
|
+
}, [copied, copyFailed]);
|
|
64
88
|
return null;
|
|
65
89
|
}
|
|
66
90
|
function install() {
|
|
@@ -110,7 +134,7 @@ function readCode(pre) {
|
|
|
110
134
|
async function copy(text, button, status) {
|
|
111
135
|
const copied = await writeClipboard(text) ? "true" : "false";
|
|
112
136
|
button.dataset.copied = copied;
|
|
113
|
-
status.textContent = copied === "true" ?
|
|
137
|
+
status.textContent = copied === "true" ? messages.copied : messages.copyFailed;
|
|
114
138
|
const existing = timers.get(button);
|
|
115
139
|
if (existing !== void 0) window.clearTimeout(existing);
|
|
116
140
|
timers.set(button, window.setTimeout(() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MarkdownComponents } from "./markdown-components.js";
|
|
2
|
+
import { CodeRuntimeLabels } from "./code-runtime.js";
|
|
2
3
|
import { ReactNode } from "react";
|
|
3
4
|
import { Root } from "hast";
|
|
4
5
|
//#region src/react/doc-content.d.ts
|
|
@@ -20,6 +21,16 @@ interface DocContentProps {
|
|
|
20
21
|
* choice rather than as a mistake.
|
|
21
22
|
*/
|
|
22
23
|
className?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* The two things the copy runtime announces, for a site that is not in
|
|
26
|
+
* English.
|
|
27
|
+
*
|
|
28
|
+
* Here rather than on `docs.Layout` because this is the component that mounts
|
|
29
|
+
* the runtime, and it is the one no consumer can avoid — the hand-rolled route
|
|
30
|
+
* in the README renders it directly. Forwarded only when set, so a site that
|
|
31
|
+
* overrides nothing sends no extra props across the boundary.
|
|
32
|
+
*/
|
|
33
|
+
labels?: CodeRuntimeLabels | undefined;
|
|
23
34
|
}
|
|
24
35
|
/**
|
|
25
36
|
* Render a hast tree as React elements, inside the prose wrapper.
|
|
@@ -61,6 +72,6 @@ interface DocContentProps {
|
|
|
61
72
|
* you, because `node` is a legal prop on the component and an unknown attribute
|
|
62
73
|
* on the element.
|
|
63
74
|
*/
|
|
64
|
-
declare function DocContent({ hast, components, className }: DocContentProps): ReactNode;
|
|
75
|
+
declare function DocContent({ hast, components, className, labels }: DocContentProps): ReactNode;
|
|
65
76
|
//#endregion
|
|
66
77
|
export { DocContent, DocContentProps };
|
|
@@ -44,10 +44,10 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
44
44
|
* you, because `node` is a legal prop on the component and an unknown attribute
|
|
45
45
|
* on the element.
|
|
46
46
|
*/
|
|
47
|
-
function DocContent({ hast, components, className }) {
|
|
47
|
+
function DocContent({ hast, components, className, labels }) {
|
|
48
48
|
return /* @__PURE__ */ jsxs("div", {
|
|
49
49
|
className: className === void 0 || className === "" ? "wave-docs-prose" : `wave-docs-prose ${className}`,
|
|
50
|
-
children: [hasCodeFrame(hast) ? /* @__PURE__ */ jsx(DocsCodeRuntime, {}) : null, toJsxRuntime(hast, {
|
|
50
|
+
children: [hasCodeFrame(hast) ? /* @__PURE__ */ jsx(DocsCodeRuntime, { ...labels }) : null, toJsxRuntime(hast, {
|
|
51
51
|
Fragment,
|
|
52
52
|
jsx,
|
|
53
53
|
jsxs,
|
package/dist/react/layout.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DocNavNode } from "../types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { SerializableSearchOptions } from "../search-options.js";
|
|
3
3
|
import { DocsLabels } from "./shell-labels.js";
|
|
4
|
+
import { DocsSearchProps } from "./next-search.js";
|
|
4
5
|
import { ReactNode } from "react";
|
|
5
6
|
//#region src/react/layout.d.ts
|
|
6
7
|
/**
|
|
@@ -9,8 +10,19 @@ import { ReactNode } from "react";
|
|
|
9
10
|
* `indexUrl` is derived from `basePath` and is not negotiable here: the whole
|
|
10
11
|
* reason `docs.Layout` exists is that nobody should have to know the index's
|
|
11
12
|
* address, and a hand-passed one is wrong under every non-root `basePath`.
|
|
13
|
+
*
|
|
14
|
+
* `miniSearchOptions` is narrower than the one `DocsSearch` itself takes, and
|
|
15
|
+
* has to be. `docs.Layout` is a Server Component and `DocsSearch` is a Client
|
|
16
|
+
* Component, so everything here is serialised on its way across — a `tokenize`
|
|
17
|
+
* or a `processTerm` passed at this seam fails `next build` with *"Functions
|
|
18
|
+
* cannot be passed directly to Client Components"*. {@link
|
|
19
|
+
* SerializableSearchOptions} documents the escape hatch: a `'use client'`
|
|
20
|
+
* wrapper of your own, where the function is a module import on both sides
|
|
21
|
+
* rather than a prop between them.
|
|
12
22
|
*/
|
|
13
|
-
type DocsLayoutSearchProps = Omit<DocsSearchProps, 'indexUrl'
|
|
23
|
+
type DocsLayoutSearchProps = Omit<DocsSearchProps, 'indexUrl' | 'miniSearchOptions'> & {
|
|
24
|
+
miniSearchOptions?: SerializableSearchOptions | undefined;
|
|
25
|
+
};
|
|
14
26
|
interface DocsLayoutShellProps {
|
|
15
27
|
children: ReactNode;
|
|
16
28
|
nav: DocNavNode[];
|
|
@@ -20,14 +32,19 @@ interface DocsLayoutShellProps {
|
|
|
20
32
|
/**
|
|
21
33
|
* `false` to omit the trigger; an object to configure it.
|
|
22
34
|
*
|
|
23
|
-
* ⚠️ AN OBJECT IS WHAT MAKES `miniSearchOptions` REACHABLE
|
|
24
|
-
* `tokenize` and `processTerm` both
|
|
25
|
-
*
|
|
26
|
-
* dialog queries it with — and while this
|
|
27
|
-
* channel for it at all. Configuring the
|
|
28
|
-
* produced an index whose terms no query
|
|
29
|
-
*
|
|
30
|
-
*
|
|
35
|
+
* ⚠️ AN OBJECT IS WHAT MAKES `miniSearchOptions` REACHABLE, AND ONLY THE
|
|
36
|
+
* SERIALISABLE PART OF IT. MiniSearch reads `tokenize` and `processTerm` both
|
|
37
|
+
* when indexing and when querying, so the object `createDocsRoute` built the
|
|
38
|
+
* index with has to be the object the dialog queries it with — and while this
|
|
39
|
+
* was a bare boolean there was no channel for it at all. Configuring the
|
|
40
|
+
* route and rendering `docs.Layout` produced an index whose terms no query
|
|
41
|
+
* could spell: zero results, no error, nothing in the console.
|
|
42
|
+
*
|
|
43
|
+
* Widening it to a boolean-or-object fixed that for data overrides and broke
|
|
44
|
+
* the function ones, which is the harder half: this prop is serialised on its
|
|
45
|
+
* way from a Server Component to a Client one, so a function in it is a build
|
|
46
|
+
* failure rather than a silent miss. `createDocsRoute` refuses to forward one
|
|
47
|
+
* and says so; {@link DocsLayoutSearchProps} carries the remedy.
|
|
31
48
|
*/
|
|
32
49
|
search?: boolean | DocsLayoutSearchProps | undefined;
|
|
33
50
|
/**
|
package/dist/react/layout.js
CHANGED
|
@@ -38,8 +38,8 @@ function DocsLayoutShell({ children, nav, searchIndexUrl, title, actions, search
|
|
|
38
38
|
}),
|
|
39
39
|
search === false ? null : /* @__PURE__ */ jsx(DocsSearch, {
|
|
40
40
|
indexUrl: searchIndexUrl,
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
...search === true ? {} : search,
|
|
42
|
+
className: ["wave-docs-layout__search", search === true ? void 0 : search?.className].filter(Boolean).join(" ")
|
|
43
43
|
}),
|
|
44
44
|
actions === void 0 ? null : /* @__PURE__ */ jsx("div", {
|
|
45
45
|
className: "wave-docs-layout__actions",
|
|
@@ -55,7 +55,10 @@ function DocsLayoutShell({ children, nav, searchIndexUrl, title, actions, search
|
|
|
55
55
|
children: /* @__PURE__ */ jsx(DocsNextNav, {
|
|
56
56
|
nav,
|
|
57
57
|
label: text.nav,
|
|
58
|
-
closeLabel: text.closeNav
|
|
58
|
+
closeLabel: text.closeNav,
|
|
59
|
+
...labels?.expandGroup === void 0 ? {} : { expandGroup: labels.expandGroup },
|
|
60
|
+
...labels?.collapseGroup === void 0 ? {} : { collapseGroup: labels.collapseGroup },
|
|
61
|
+
...labels?.externalLink === void 0 ? {} : { externalLink: labels.externalLink }
|
|
59
62
|
})
|
|
60
63
|
}), children]
|
|
61
64
|
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DocsLinkComponent } from "./markdown-components.js";
|
|
2
|
+
import { ComponentProps, ComponentType } from "react";
|
|
3
|
+
//#region src/react/link-adapter.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The part of `next/link` this package uses.
|
|
6
|
+
*
|
|
7
|
+
* Declared structurally rather than imported: `next` is an optional peer, and
|
|
8
|
+
* a type-only import of it would still be a hard resolution requirement for
|
|
9
|
+
* anyone type-checking against our `.d.ts`.
|
|
10
|
+
*/
|
|
11
|
+
type NextLinkComponent = ComponentType<Omit<ComponentProps<'a'>, 'href' | 'ref'> & {
|
|
12
|
+
href: string;
|
|
13
|
+
prefetch?: boolean | null;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Adapt `next/link` to {@link DocsLinkProps}.
|
|
17
|
+
*
|
|
18
|
+
* `next/link` widens `href` to `string | UrlObject` and `prefetch` to
|
|
19
|
+
* `boolean | 'auto' | null`; the React layer promises neither, because it must
|
|
20
|
+
* also run with a plain `<a>`. One wrapper keeps that mismatch in a single
|
|
21
|
+
* place instead of at every call site.
|
|
22
|
+
*
|
|
23
|
+
* `prefetch` is omitted rather than passed as `undefined`, which is not
|
|
24
|
+
* pedantry: under `exactOptionalPropertyTypes` — which this package compiles
|
|
25
|
+
* with, and which any consumer may turn on — `undefined` is not assignable to
|
|
26
|
+
* `boolean | 'auto' | null`, and `<SearchDialog Link={Link} />` written by
|
|
27
|
+
* hand fails to compile for a reason that reads as our bug.
|
|
28
|
+
*
|
|
29
|
+
* Call it once at module scope, never during a render: a fresh component
|
|
30
|
+
* identity for `a` remounts every link in the document on every render.
|
|
31
|
+
*/
|
|
32
|
+
declare function wrapNextLink(NextLink: NextLinkComponent): DocsLinkComponent;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { NextLinkComponent, wrapNextLink };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
//#region src/react/link-adapter.ts
|
|
3
|
+
/**
|
|
4
|
+
* Adapt `next/link` to {@link DocsLinkProps}.
|
|
5
|
+
*
|
|
6
|
+
* `next/link` widens `href` to `string | UrlObject` and `prefetch` to
|
|
7
|
+
* `boolean | 'auto' | null`; the React layer promises neither, because it must
|
|
8
|
+
* also run with a plain `<a>`. One wrapper keeps that mismatch in a single
|
|
9
|
+
* place instead of at every call site.
|
|
10
|
+
*
|
|
11
|
+
* `prefetch` is omitted rather than passed as `undefined`, which is not
|
|
12
|
+
* pedantry: under `exactOptionalPropertyTypes` — which this package compiles
|
|
13
|
+
* with, and which any consumer may turn on — `undefined` is not assignable to
|
|
14
|
+
* `boolean | 'auto' | null`, and `<SearchDialog Link={Link} />` written by
|
|
15
|
+
* hand fails to compile for a reason that reads as our bug.
|
|
16
|
+
*
|
|
17
|
+
* Call it once at module scope, never during a render: a fresh component
|
|
18
|
+
* identity for `a` remounts every link in the document on every render.
|
|
19
|
+
*/
|
|
20
|
+
function wrapNextLink(NextLink) {
|
|
21
|
+
return function DocsNextLink({ href, prefetch, children, ...rest }) {
|
|
22
|
+
return createElement(NextLink, {
|
|
23
|
+
...rest,
|
|
24
|
+
href,
|
|
25
|
+
...prefetch === void 0 ? {} : { prefetch }
|
|
26
|
+
}, children);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { wrapNextLink };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CalloutProps } from "./callout.js";
|
|
2
|
+
import { DocsLabels } from "./shell-labels.js";
|
|
2
3
|
import { YouTubeProps } from "./youtube.js";
|
|
3
4
|
import { ComponentProps, ComponentType, JSX, ReactNode } from "react";
|
|
4
5
|
//#region src/react/markdown-components.d.ts
|
|
@@ -53,6 +54,20 @@ interface DocsImageProps {
|
|
|
53
54
|
className?: string | undefined;
|
|
54
55
|
sizes?: string | undefined;
|
|
55
56
|
loading?: 'eager' | 'lazy' | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Passed through to the image. Defaults to `'async'` on the plain `<img>`.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ DECLARED BECAUSE THE COMMENT BELOW PROMISED IT AND THE CODE DROPPED IT.
|
|
61
|
+
* `createImage` spreads the tree's own attributes into whichever component it
|
|
62
|
+
* was given, with a comment saying `decoding` and `fetchPriority` survive into
|
|
63
|
+
* the optimising branch — and `wrapNextImage` destructures a fixed list, so
|
|
64
|
+
* they did not. A closed props interface is the right shape for this seam, so
|
|
65
|
+
* the two the comment named are members of it now rather than a promise it
|
|
66
|
+
* could not keep.
|
|
67
|
+
*/
|
|
68
|
+
decoding?: 'async' | 'auto' | 'sync' | undefined;
|
|
69
|
+
/** Passed through to the image. `'high'` on a hero image is the usual reason. */
|
|
70
|
+
fetchPriority?: 'high' | 'low' | 'auto' | undefined;
|
|
56
71
|
}
|
|
57
72
|
/** A `next/image`-compatible component. */
|
|
58
73
|
type DocsImageComponent = ComponentType<DocsImageProps>;
|
|
@@ -61,7 +76,20 @@ interface MarkdownComponentsOptions {
|
|
|
61
76
|
Link?: DocsLinkComponent | undefined;
|
|
62
77
|
/** Optimising image component, e.g. `next/image`. Falls back to `<img>`. */
|
|
63
78
|
Image?: DocsImageComponent | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Overrides for the strings this map renders itself.
|
|
81
|
+
*
|
|
82
|
+
* Five callout headings, the external-link suffix, a wide table's region
|
|
83
|
+
* name and the YouTube facade's three — every one of them hardcoded English
|
|
84
|
+
* until this existed, on a shell whose `labels` prop claimed to be the whole
|
|
85
|
+
* of a site's translatable chrome.
|
|
86
|
+
*
|
|
87
|
+
* All server-rendered, so overriding them costs no client bytes.
|
|
88
|
+
*/
|
|
89
|
+
labels?: MarkdownLabels | undefined;
|
|
64
90
|
}
|
|
91
|
+
/** The subset of `DocsLabels` this map is responsible for. */
|
|
92
|
+
type MarkdownLabels = Pick<DocsLabels, 'externalLink' | 'table' | 'calloutNote' | 'calloutTip' | 'calloutImportant' | 'calloutWarning' | 'calloutCaution' | 'youtubeTitle' | 'youtubePlay' | 'youtubeHide'>;
|
|
65
93
|
/**
|
|
66
94
|
* Build the default component map, optionally injecting host-specific link and
|
|
67
95
|
* image components.
|
|
@@ -81,4 +109,4 @@ declare function createMarkdownComponents(options?: MarkdownComponentsOptions):
|
|
|
81
109
|
/** The map used when a caller supplies none. Plain `<a>` and `<img>`. */
|
|
82
110
|
declare const defaultMarkdownComponents: MarkdownComponents;
|
|
83
111
|
//#endregion
|
|
84
|
-
export { DocsImageComponent, DocsImageProps, DocsLinkComponent, DocsLinkProps, MarkdownComponents, MarkdownComponentsOptions, createMarkdownComponents, defaultMarkdownComponents };
|
|
112
|
+
export { DocsImageComponent, DocsImageProps, DocsLinkComponent, DocsLinkProps, MarkdownComponents, MarkdownComponentsOptions, MarkdownLabels, createMarkdownComponents, defaultMarkdownComponents };
|