@waveso/docs 0.4.0 → 0.5.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 +173 -0
- package/README.md +160 -39
- package/dist/errors.d.ts +2 -0
- package/dist/highlighter.js +2 -1
- package/dist/meta.js +6 -9
- package/dist/next.d.ts +54 -14
- package/dist/next.js +115 -18
- package/dist/plugins/rehype-code-frame.d.ts +13 -1
- package/dist/plugins/rehype-code-frame.js +2 -1
- package/dist/plugins/remark-doc-links.d.ts +51 -1
- package/dist/plugins/remark-doc-links.js +27 -16
- 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/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-nav.d.ts +5 -1
- package/dist/react/next-nav.js +5 -2
- 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 +11 -0
- package/dist/render.js +38 -11
- 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 +80 -10
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
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
|
})
|
|
@@ -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 };
|
|
@@ -1,24 +1,14 @@
|
|
|
1
|
+
import { isSafeHref, opensInNewTab } from "../safe-href.js";
|
|
1
2
|
import { Callout } from "./callout.js";
|
|
2
3
|
import { YouTube } from "./youtube.js";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/react/markdown-components.tsx
|
|
5
|
-
/**
|
|
6
|
-
const
|
|
7
|
-
/**
|
|
6
|
+
/** Default, and the only string here a reader sees without a screen reader. */
|
|
7
|
+
const DEFAULT_EXTERNAL_LINK = "(opens in a new tab)";
|
|
8
|
+
/** Default name for a wide table's scroll region. */
|
|
9
|
+
const DEFAULT_TABLE = "Table";
|
|
10
|
+
/** Any URL with a scheme, or protocol-relative. Decides router vs plain `<a>`. */
|
|
8
11
|
const ABSOLUTE_URL = /^([a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
9
|
-
/**
|
|
10
|
-
* The schemes a markdown link may carry.
|
|
11
|
-
*
|
|
12
|
-
* GitHub's own allowlist, which is the bar to match: documentation links to
|
|
13
|
-
* `sms:`, `ftp:` and `irc:` are ordinary, and an allowlist of three silently
|
|
14
|
-
* deleted them. The point of the check is to stop `javascript:`, `data:` and
|
|
15
|
-
* `vbscript:` reaching an `href`, not to have an opinion about protocols.
|
|
16
|
-
*
|
|
17
|
-
* A scheme not listed here — `vscode:`, `obsidian:`, `slack:` — is dropped
|
|
18
|
-
* rather than rendered. That is deliberate: an allowlist that grows on request
|
|
19
|
-
* is safe, one that guesses is not. {@link warnDroppedHref} makes it visible.
|
|
20
|
-
*/
|
|
21
|
-
const SAFE_SCHEME = /^(https?|mailto|tel|sms|ftp|ftps|irc|ircs|xmpp|news|nntp|feed|git|matrix):/i;
|
|
22
12
|
/** Hrefs already reported, so a re-render does not repeat the warning. */
|
|
23
13
|
const warnedHrefs = /* @__PURE__ */ new Set();
|
|
24
14
|
/**
|
|
@@ -32,35 +22,7 @@ const warnedHrefs = /* @__PURE__ */ new Set();
|
|
|
32
22
|
function warnDroppedHref(href) {
|
|
33
23
|
if (process.env.NODE_ENV === "production" || warnedHrefs.has(href)) return;
|
|
34
24
|
warnedHrefs.add(href);
|
|
35
|
-
console.warn(`@waveso/docs: dropped a link to '${href}' — its
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* A copy of `href` as a browser will parse it.
|
|
39
|
-
*
|
|
40
|
-
* ASCII control characters and spaces are stripped before parsing, so
|
|
41
|
-
* ` javascript:` and `java<TAB>script:` both navigate where the raw string
|
|
42
|
-
* matches no scheme at all — which is how a scheme check gets walked around.
|
|
43
|
-
*/
|
|
44
|
-
function normaliseUrl(href) {
|
|
45
|
-
return [...href].filter((char) => (char.codePointAt(0) ?? 0) > 32).join("");
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Would this href navigate somewhere we are willing to send a reader?
|
|
49
|
-
*
|
|
50
|
-
* Nothing upstream filters it: `remarkDocLinks` skips every href with a scheme
|
|
51
|
-
* (`isRelativeLink` is false for it), so `assertLinks` never sees one either,
|
|
52
|
-
* and `remarkRehype` runs with `allowDangerousHtml` off but passes a link's own
|
|
53
|
-
* url through untouched. Verified against React 19: it neutralises
|
|
54
|
-
* `javascript:` in every obfuscated form, silently — but it lets `vbscript:`
|
|
55
|
-
* and `data:text/html;base64,…` reach the DOM verbatim. So the allowlist is
|
|
56
|
-
* ours to keep.
|
|
57
|
-
*
|
|
58
|
-
* Tested against {@link normaliseUrl}, not the raw string.
|
|
59
|
-
*/
|
|
60
|
-
function isSafeHref(href) {
|
|
61
|
-
const normalised = normaliseUrl(href);
|
|
62
|
-
if (!ABSOLUTE_URL.test(normalised)) return true;
|
|
63
|
-
return normalised.startsWith("//") || SAFE_SCHEME.test(normalised);
|
|
25
|
+
console.warn(`@waveso/docs: dropped a link to '${href}' — its scheme is not in the allowlist. Use http(s), mailto, tel or another documented scheme.`);
|
|
64
26
|
}
|
|
65
27
|
function joinClassNames(...values) {
|
|
66
28
|
const joined = values.filter(Boolean).join(" ");
|
|
@@ -73,7 +35,7 @@ function toDimension(value) {
|
|
|
73
35
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
74
36
|
}
|
|
75
37
|
}
|
|
76
|
-
function createAnchor(Link) {
|
|
38
|
+
function createAnchor(Link, externalLink) {
|
|
77
39
|
return function MarkdownAnchor({ href, children, ...rest }) {
|
|
78
40
|
if (href === void 0) return /* @__PURE__ */ jsx("a", {
|
|
79
41
|
...rest,
|
|
@@ -83,14 +45,14 @@ function createAnchor(Link) {
|
|
|
83
45
|
warnDroppedHref(href);
|
|
84
46
|
return /* @__PURE__ */ jsx("span", { children });
|
|
85
47
|
}
|
|
86
|
-
if (
|
|
48
|
+
if (opensInNewTab(href)) return /* @__PURE__ */ jsxs("a", {
|
|
87
49
|
...rest,
|
|
88
50
|
href,
|
|
89
51
|
target: "_blank",
|
|
90
52
|
rel: "noopener noreferrer",
|
|
91
|
-
children: [children, /* @__PURE__ */
|
|
53
|
+
children: [children, /* @__PURE__ */ jsxs("span", {
|
|
92
54
|
className: "wave-docs-sr-only",
|
|
93
|
-
children: "
|
|
55
|
+
children: [" ", externalLink]
|
|
94
56
|
})]
|
|
95
57
|
});
|
|
96
58
|
if (href.startsWith("#") || ABSOLUTE_URL.test(href) || Link === void 0) return /* @__PURE__ */ jsx("a", {
|
|
@@ -106,7 +68,7 @@ function createAnchor(Link) {
|
|
|
106
68
|
};
|
|
107
69
|
}
|
|
108
70
|
function createImage(Image) {
|
|
109
|
-
return function MarkdownImage({ src, alt, width, height, title, className, sizes, loading, ...rest }) {
|
|
71
|
+
return function MarkdownImage({ src, alt, width, height, title, className, sizes, loading, decoding, fetchPriority, ...rest }) {
|
|
110
72
|
const resolvedWidth = toDimension(width);
|
|
111
73
|
const resolvedHeight = toDimension(height);
|
|
112
74
|
const resolvedLoading = loading ?? "lazy";
|
|
@@ -120,10 +82,11 @@ function createImage(Image) {
|
|
|
120
82
|
title,
|
|
121
83
|
className: resolvedClassName,
|
|
122
84
|
sizes,
|
|
123
|
-
loading: resolvedLoading
|
|
85
|
+
loading: resolvedLoading,
|
|
86
|
+
decoding: decoding ?? "async",
|
|
87
|
+
fetchPriority
|
|
124
88
|
});
|
|
125
89
|
return /* @__PURE__ */ jsx("img", {
|
|
126
|
-
decoding: "async",
|
|
127
90
|
...rest,
|
|
128
91
|
src,
|
|
129
92
|
alt: alt ?? "",
|
|
@@ -132,7 +95,9 @@ function createImage(Image) {
|
|
|
132
95
|
title,
|
|
133
96
|
className: resolvedClassName,
|
|
134
97
|
sizes,
|
|
135
|
-
loading: resolvedLoading
|
|
98
|
+
loading: resolvedLoading,
|
|
99
|
+
decoding: decoding ?? "async",
|
|
100
|
+
fetchPriority
|
|
136
101
|
});
|
|
137
102
|
};
|
|
138
103
|
}
|
|
@@ -146,16 +111,18 @@ function createImage(Image) {
|
|
|
146
111
|
* `<section>` is a `region` landmark, so the tab stop announces itself instead
|
|
147
112
|
* of being a mystery stop in the tab order.
|
|
148
113
|
*/
|
|
149
|
-
function
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
114
|
+
function createTable(label) {
|
|
115
|
+
return function MarkdownTable({ className, ...rest }) {
|
|
116
|
+
return /* @__PURE__ */ jsx("section", {
|
|
117
|
+
className: "wave-docs-table-scroll",
|
|
118
|
+
"aria-label": label,
|
|
119
|
+
tabIndex: 0,
|
|
120
|
+
children: /* @__PURE__ */ jsx("table", {
|
|
121
|
+
...rest,
|
|
122
|
+
className: joinClassNames("wave-docs-table", className)
|
|
123
|
+
})
|
|
124
|
+
});
|
|
125
|
+
};
|
|
159
126
|
}
|
|
160
127
|
/**
|
|
161
128
|
* Build the default component map, optionally injecting host-specific link and
|
|
@@ -173,12 +140,47 @@ function MarkdownTable({ className, ...rest }) {
|
|
|
173
140
|
* ```
|
|
174
141
|
*/
|
|
175
142
|
function createMarkdownComponents(options = {}) {
|
|
143
|
+
const labels = options.labels ?? {};
|
|
144
|
+
const calloutTitles = calloutTitleMap(labels);
|
|
176
145
|
return {
|
|
177
|
-
a: createAnchor(options.Link),
|
|
146
|
+
a: createAnchor(options.Link, labels.externalLink ?? DEFAULT_EXTERNAL_LINK),
|
|
178
147
|
img: createImage(options.Image),
|
|
179
|
-
table:
|
|
180
|
-
callout: Callout,
|
|
181
|
-
|
|
148
|
+
table: createTable(labels.table ?? DEFAULT_TABLE),
|
|
149
|
+
callout: (props) => /* @__PURE__ */ jsx(Callout, {
|
|
150
|
+
...props,
|
|
151
|
+
...calloutTitles === void 0 ? {} : { labels: calloutTitles }
|
|
152
|
+
}),
|
|
153
|
+
youtube: (props) => /* @__PURE__ */ jsx(YouTube, { ...youtubeDefaults(labels, props) })
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** The five headings as `Callout` wants them, or `undefined` if none are set. */
|
|
157
|
+
function calloutTitleMap(labels) {
|
|
158
|
+
const titles = {};
|
|
159
|
+
let found = false;
|
|
160
|
+
for (const [type, key] of Object.entries(CALLOUT_LABEL_KEYS)) {
|
|
161
|
+
const value = labels[key];
|
|
162
|
+
if (value !== void 0) {
|
|
163
|
+
titles[type] = value;
|
|
164
|
+
found = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return found ? titles : void 0;
|
|
168
|
+
}
|
|
169
|
+
/** Which `DocsLabels` key names a given callout type's heading. */
|
|
170
|
+
const CALLOUT_LABEL_KEYS = {
|
|
171
|
+
note: "calloutNote",
|
|
172
|
+
tip: "calloutTip",
|
|
173
|
+
important: "calloutImportant",
|
|
174
|
+
warning: "calloutWarning",
|
|
175
|
+
caution: "calloutCaution"
|
|
176
|
+
};
|
|
177
|
+
/** `props`, with the site's YouTube strings filled in. */
|
|
178
|
+
function youtubeDefaults(labels, props) {
|
|
179
|
+
return {
|
|
180
|
+
...props,
|
|
181
|
+
...props.title === void 0 && labels.youtubeTitle !== void 0 ? { title: labels.youtubeTitle } : {},
|
|
182
|
+
...labels.youtubePlay === void 0 ? {} : { playLabel: labels.youtubePlay },
|
|
183
|
+
...labels.youtubeHide === void 0 ? {} : { hideLabel: labels.youtubeHide }
|
|
182
184
|
};
|
|
183
185
|
}
|
|
184
186
|
/** The map used when a caller supplies none. Plain `<a>` and `<img>`. */
|
package/dist/react/nav.d.ts
CHANGED
|
@@ -22,7 +22,11 @@ interface DocsNavProps {
|
|
|
22
22
|
label?: string | undefined;
|
|
23
23
|
/** Accessible name for the close button. */
|
|
24
24
|
closeLabel?: string | undefined;
|
|
25
|
+
/** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
|
|
26
|
+
expandGroup?: string | undefined;
|
|
27
|
+
collapseGroup?: string | undefined;
|
|
28
|
+
externalLink?: string | undefined;
|
|
25
29
|
}
|
|
26
|
-
declare function DocsNav({ nav, pathname, Link, label, closeLabel }: DocsNavProps): ReactNode;
|
|
30
|
+
declare function DocsNav({ nav, pathname, Link, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNavProps): ReactNode;
|
|
27
31
|
//#endregion
|
|
28
32
|
export { DOCS_NAV_ID, DocsNav, DocsNavProps };
|
package/dist/react/nav.js
CHANGED
|
@@ -12,7 +12,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* the button does nothing on the first tap.
|
|
13
13
|
*/
|
|
14
14
|
const DOCS_NAV_ID = "wave-docs-nav";
|
|
15
|
-
function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "Close navigation" }) {
|
|
15
|
+
function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "Close navigation", expandGroup, collapseGroup, externalLink }) {
|
|
16
16
|
const ref = useRef(null);
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
ref.current?.close?.();
|
|
@@ -62,7 +62,10 @@ function DocsNav({ nav, pathname, Link, label = "Documentation", closeLabel = "C
|
|
|
62
62
|
nav,
|
|
63
63
|
pathname,
|
|
64
64
|
label,
|
|
65
|
-
Link
|
|
65
|
+
Link,
|
|
66
|
+
...expandGroup === void 0 ? {} : { expandGroup },
|
|
67
|
+
...collapseGroup === void 0 ? {} : { collapseGroup },
|
|
68
|
+
...externalLink === void 0 ? {} : { externalLink }
|
|
66
69
|
})]
|
|
67
70
|
});
|
|
68
71
|
}
|
package/dist/react/next-nav.d.ts
CHANGED
|
@@ -5,7 +5,11 @@ interface DocsNextNavProps {
|
|
|
5
5
|
nav: DocNavNode[];
|
|
6
6
|
label?: string | undefined;
|
|
7
7
|
closeLabel?: string | undefined;
|
|
8
|
+
/** Passed through to the tree. See `DocsSidebarProps.expandGroup`. */
|
|
9
|
+
expandGroup?: string | undefined;
|
|
10
|
+
collapseGroup?: string | undefined;
|
|
11
|
+
externalLink?: string | undefined;
|
|
8
12
|
}
|
|
9
|
-
declare function DocsNextNav({ nav, label, closeLabel }: DocsNextNavProps): ReactNode;
|
|
13
|
+
declare function DocsNextNav({ nav, label, closeLabel, expandGroup, collapseGroup, externalLink }: DocsNextNavProps): ReactNode;
|
|
10
14
|
//#endregion
|
|
11
15
|
export { DocsNextNav, DocsNextNavProps };
|
package/dist/react/next-nav.js
CHANGED
|
@@ -19,13 +19,16 @@ import { usePathname } from "next/navigation";
|
|
|
19
19
|
*/
|
|
20
20
|
/** Module scope: a fresh identity here remounts every nav link on every render. */
|
|
21
21
|
const Link = wrapNextLink(NextLink);
|
|
22
|
-
function DocsNextNav({ nav, label, closeLabel }) {
|
|
22
|
+
function DocsNextNav({ nav, label, closeLabel, expandGroup, collapseGroup, externalLink }) {
|
|
23
23
|
return /* @__PURE__ */ jsx(DocsNav, {
|
|
24
24
|
nav,
|
|
25
25
|
pathname: usePathname(),
|
|
26
26
|
Link,
|
|
27
27
|
...label === void 0 ? {} : { label },
|
|
28
|
-
...closeLabel === void 0 ? {} : { closeLabel }
|
|
28
|
+
...closeLabel === void 0 ? {} : { closeLabel },
|
|
29
|
+
...expandGroup === void 0 ? {} : { expandGroup },
|
|
30
|
+
...collapseGroup === void 0 ? {} : { collapseGroup },
|
|
31
|
+
...externalLink === void 0 ? {} : { externalLink }
|
|
29
32
|
});
|
|
30
33
|
}
|
|
31
34
|
//#endregion
|