@docusaurus/theme-common 2.0.0-beta.15 → 2.0.0-beta.16
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/lib/components/Collapsible/index.d.ts.map +1 -1
- package/lib/components/Collapsible/index.js +9 -5
- package/lib/components/Collapsible/index.js.map +1 -1
- package/lib/components/Details/index.d.ts +1 -2
- package/lib/components/Details/index.d.ts.map +1 -1
- package/lib/components/Details/index.js +5 -4
- package/lib/components/Details/index.js.map +1 -1
- package/lib/hooks/useKeyboardNavigation.d.ts +1 -0
- package/lib/hooks/useKeyboardNavigation.d.ts.map +1 -1
- package/lib/hooks/useKeyboardNavigation.js +1 -1
- package/lib/hooks/useKeyboardNavigation.js.map +1 -1
- package/lib/hooks/useWindowSize.d.ts.map +1 -1
- package/lib/hooks/useWindowSize.js +4 -2
- package/lib/hooks/useWindowSize.js.map +1 -1
- package/lib/index.d.ts +4 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -3
- package/lib/index.js.map +1 -1
- package/lib/utils/ThemeClassNames.d.ts +1 -0
- package/lib/utils/ThemeClassNames.d.ts.map +1 -1
- package/lib/utils/ThemeClassNames.js +3 -1
- package/lib/utils/ThemeClassNames.js.map +1 -1
- package/lib/utils/codeBlockUtils.d.ts.map +1 -1
- package/lib/utils/codeBlockUtils.js +5 -4
- package/lib/utils/codeBlockUtils.js.map +1 -1
- package/lib/utils/colorModeUtils.d.ts.map +1 -1
- package/lib/utils/colorModeUtils.js +38 -16
- package/lib/utils/colorModeUtils.js.map +1 -1
- package/lib/utils/docsPreferredVersion/DocsPreferredVersionProvider.d.ts.map +1 -1
- package/lib/utils/docsPreferredVersion/DocsPreferredVersionProvider.js +3 -7
- package/lib/utils/docsPreferredVersion/DocsPreferredVersionProvider.js.map +1 -1
- package/lib/utils/docsUtils.d.ts +6 -1
- package/lib/utils/docsUtils.d.ts.map +1 -1
- package/lib/utils/docsUtils.js +38 -10
- package/lib/utils/docsUtils.js.map +1 -1
- package/lib/utils/historyUtils.d.ts +12 -1
- package/lib/utils/historyUtils.d.ts.map +1 -1
- package/lib/utils/historyUtils.js +9 -8
- package/lib/utils/historyUtils.js.map +1 -1
- package/lib/utils/jsUtils.d.ts +5 -2
- package/lib/utils/jsUtils.d.ts.map +1 -1
- package/lib/utils/jsUtils.js +5 -2
- package/lib/utils/jsUtils.js.map +1 -1
- package/lib/utils/mobileSecondaryMenu.d.ts.map +1 -1
- package/lib/utils/mobileSecondaryMenu.js.map +1 -1
- package/lib/utils/pathUtils.d.ts.map +1 -1
- package/lib/utils/pathUtils.js +7 -2
- package/lib/utils/pathUtils.js.map +1 -1
- package/lib/utils/reactUtils.d.ts +18 -0
- package/lib/utils/reactUtils.d.ts.map +1 -1
- package/lib/utils/reactUtils.js +20 -11
- package/lib/utils/reactUtils.js.map +1 -1
- package/lib/utils/regexpUtils.d.ts +1 -1
- package/lib/utils/regexpUtils.js +1 -1
- package/lib/utils/routesUtils.d.ts +11 -0
- package/lib/utils/routesUtils.d.ts.map +1 -0
- package/lib/utils/routesUtils.js +33 -0
- package/lib/utils/routesUtils.js.map +1 -0
- package/lib/utils/storageUtils.d.ts +4 -4
- package/lib/utils/storageUtils.d.ts.map +1 -1
- package/lib/utils/storageUtils.js +18 -20
- package/lib/utils/storageUtils.js.map +1 -1
- package/lib/utils/tocUtils.d.ts +9 -5
- package/lib/utils/tocUtils.d.ts.map +1 -1
- package/lib/utils/tocUtils.js +45 -7
- package/lib/utils/tocUtils.js.map +1 -1
- package/lib/utils/useAlternatePageUtils.d.ts.map +1 -1
- package/lib/utils/useAlternatePageUtils.js +2 -1
- package/lib/utils/useAlternatePageUtils.js.map +1 -1
- package/lib/utils/useContextualSearchFilters.js +2 -2
- package/lib/utils/useContextualSearchFilters.js.map +1 -1
- package/lib/utils/useLocationChange.d.ts +1 -1
- package/lib/utils/useLocationChange.d.ts.map +1 -1
- package/lib/utils/useLocationChange.js +3 -0
- package/lib/utils/useLocationChange.js.map +1 -1
- package/lib/utils/usePluralForm.d.ts.map +1 -1
- package/lib/utils/usePluralForm.js +26 -21
- package/lib/utils/usePluralForm.js.map +1 -1
- package/lib/utils/useTOCHighlight.d.ts +1 -2
- package/lib/utils/useTOCHighlight.d.ts.map +1 -1
- package/lib/utils/useTOCHighlight.js +22 -20
- package/lib/utils/useTOCHighlight.js.map +1 -1
- package/package.json +11 -11
- package/src/components/Collapsible/index.tsx +14 -9
- package/src/components/Details/index.tsx +7 -4
- package/src/hooks/useKeyboardNavigation.ts +2 -2
- package/src/hooks/useWindowSize.ts +4 -2
- package/src/index.ts +12 -2
- package/src/utils/ThemeClassNames.ts +3 -1
- package/src/utils/codeBlockUtils.ts +5 -4
- package/src/utils/colorModeUtils.tsx +39 -18
- package/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx +3 -5
- package/src/utils/docsUtils.tsx +56 -11
- package/src/utils/historyUtils.ts +10 -9
- package/src/utils/jsUtils.ts +5 -2
- package/src/utils/mobileSecondaryMenu.tsx +6 -5
- package/src/utils/pathUtils.ts +5 -2
- package/src/utils/reactUtils.tsx +20 -11
- package/src/utils/regexpUtils.ts +1 -1
- package/src/utils/routesUtils.ts +39 -0
- package/src/utils/storageUtils.ts +21 -19
- package/src/utils/tocUtils.ts +66 -13
- package/src/utils/useAlternatePageUtils.ts +4 -3
- package/src/utils/useContextualSearchFilters.ts +2 -2
- package/src/utils/useLocationChange.ts +5 -1
- package/src/utils/usePluralForm.ts +29 -23
- package/src/utils/useTOCHighlight.ts +24 -21
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import rangeParser from 'parse-numeric-range';
|
|
9
9
|
|
|
10
|
-
const codeBlockTitleRegex = /title=(["'])(
|
|
11
|
-
const highlightLinesRangeRegex = /{([\d,-]+)}/;
|
|
10
|
+
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
|
|
11
|
+
const highlightLinesRangeRegex = /{(?<range>[\d,-]+)}/;
|
|
12
12
|
|
|
13
13
|
const commentTypes = ['js', 'jsBlock', 'jsx', 'python', 'html'] as const;
|
|
14
14
|
type CommentType = typeof commentTypes[number];
|
|
@@ -89,7 +89,7 @@ const magicCommentDirectiveRegex = (lang: string) => {
|
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
export function parseCodeBlockTitle(metastring?: string): string {
|
|
92
|
-
return metastring?.match(codeBlockTitleRegex)?.
|
|
92
|
+
return metastring?.match(codeBlockTitleRegex)?.groups!.title ?? '';
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export function parseLanguage(className: string): string | undefined {
|
|
@@ -114,7 +114,8 @@ export function parseLines(
|
|
|
114
114
|
let code = content.replace(/\n$/, '');
|
|
115
115
|
// Highlighted lines specified in props: don't parse the content
|
|
116
116
|
if (metastring && highlightLinesRangeRegex.test(metastring)) {
|
|
117
|
-
const highlightLinesRange = metastring.match(highlightLinesRangeRegex)!
|
|
117
|
+
const highlightLinesRange = metastring.match(highlightLinesRangeRegex)!
|
|
118
|
+
.groups!.range;
|
|
118
119
|
const highlightLines = rangeParser(highlightLinesRange)
|
|
119
120
|
.filter((n) => n > 0)
|
|
120
121
|
.map((n) => n - 1);
|
|
@@ -24,7 +24,8 @@ type ColorModeContextValue = {
|
|
|
24
24
|
readonly setDarkTheme: () => void;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
const
|
|
27
|
+
const ThemeStorageKey = 'theme';
|
|
28
|
+
const ThemeStorage = createStorageSlot(ThemeStorageKey);
|
|
28
29
|
|
|
29
30
|
const themes = {
|
|
30
31
|
light: 'light',
|
|
@@ -45,7 +46,7 @@ const getInitialTheme = (defaultMode: Themes | undefined): Themes => {
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
const storeTheme = (newTheme: Themes) => {
|
|
48
|
-
|
|
49
|
+
ThemeStorage.set(coerceToTheme(newTheme));
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
function useColorModeContextValue(): ColorModeContextValue {
|
|
@@ -69,29 +70,49 @@ function useColorModeContextValue(): ColorModeContextValue {
|
|
|
69
70
|
|
|
70
71
|
useEffect(() => {
|
|
71
72
|
if (disableSwitch) {
|
|
72
|
-
return;
|
|
73
|
+
return undefined;
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (storedTheme !== null) {
|
|
78
|
-
setTheme(coerceToTheme(storedTheme));
|
|
75
|
+
const onChange = (e: StorageEvent) => {
|
|
76
|
+
if (e.key !== ThemeStorageKey) {
|
|
77
|
+
return;
|
|
79
78
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
try {
|
|
80
|
+
const storedTheme = ThemeStorage.get();
|
|
81
|
+
if (storedTheme !== null) {
|
|
82
|
+
setTheme(coerceToTheme(storedTheme));
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(err);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
window.addEventListener('storage', onChange);
|
|
89
|
+
return () => {
|
|
90
|
+
window.removeEventListener('storage', onChange);
|
|
91
|
+
};
|
|
83
92
|
}, [disableSwitch, setTheme]);
|
|
84
93
|
|
|
94
|
+
// PCS is coerced to light mode when printing, which causes the color mode to
|
|
95
|
+
// be reset to dark when exiting print mode, disregarding user settings. When
|
|
96
|
+
// the listener fires only because of a print/screen switch, we don't change
|
|
97
|
+
// color mode. See https://github.com/facebook/docusaurus/pull/6490
|
|
98
|
+
const previousMediaIsPrint = React.useRef(false);
|
|
99
|
+
|
|
85
100
|
useEffect(() => {
|
|
86
101
|
if (disableSwitch && !respectPrefersColorScheme) {
|
|
87
|
-
return;
|
|
102
|
+
return undefined;
|
|
88
103
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.matchMedia('
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
104
|
+
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
105
|
+
const onChange = ({matches}: MediaQueryListEvent) => {
|
|
106
|
+
if (window.matchMedia('print').matches || previousMediaIsPrint.current) {
|
|
107
|
+
previousMediaIsPrint.current = window.matchMedia('print').matches;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setTheme(matches ? themes.dark : themes.light);
|
|
111
|
+
};
|
|
112
|
+
mql.addListener(onChange);
|
|
113
|
+
return () => {
|
|
114
|
+
mql.removeListener(onChange);
|
|
115
|
+
};
|
|
95
116
|
}, [disableSwitch, respectPrefersColorScheme]);
|
|
96
117
|
|
|
97
118
|
return {
|
|
@@ -76,10 +76,9 @@ function readStorageState({
|
|
|
76
76
|
);
|
|
77
77
|
if (versionExists) {
|
|
78
78
|
return {preferredVersionName: preferredVersionNameUnsafe};
|
|
79
|
-
} else {
|
|
80
|
-
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
|
|
81
|
-
return {preferredVersionName: null};
|
|
82
79
|
}
|
|
80
|
+
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
|
|
81
|
+
return {preferredVersionName: null};
|
|
83
82
|
}
|
|
84
83
|
|
|
85
84
|
const initialState: DocsPreferredVersionState = {};
|
|
@@ -144,9 +143,8 @@ export function DocsPreferredVersionContextProvider({
|
|
|
144
143
|
{children}
|
|
145
144
|
</DocsPreferredVersionContextProviderUnsafe>
|
|
146
145
|
);
|
|
147
|
-
} else {
|
|
148
|
-
return children;
|
|
149
146
|
}
|
|
147
|
+
return children;
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
function DocsPreferredVersionContextProviderUnsafe({
|
package/src/utils/docsUtils.tsx
CHANGED
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React, {createContext, type ReactNode, useContext} from 'react';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useActivePlugin,
|
|
11
|
+
useAllDocsData,
|
|
12
|
+
} from '@docusaurus/plugin-content-docs/client';
|
|
10
13
|
import type {
|
|
11
14
|
PropSidebar,
|
|
12
15
|
PropSidebarItem,
|
|
13
16
|
PropSidebarItemCategory,
|
|
14
17
|
PropVersionDoc,
|
|
15
18
|
PropVersionMetadata,
|
|
19
|
+
PropSidebarBreadcrumbsItem,
|
|
16
20
|
} from '@docusaurus/plugin-content-docs';
|
|
17
21
|
import {isSamePath} from './pathUtils';
|
|
18
22
|
import {useLocation} from '@docusaurus/router';
|
|
@@ -20,7 +24,7 @@ import {useLocation} from '@docusaurus/router';
|
|
|
20
24
|
// TODO not ideal, see also "useDocs"
|
|
21
25
|
export const isDocsPluginEnabled: boolean = !!useAllDocsData;
|
|
22
26
|
|
|
23
|
-
// Using a Symbol because null is a valid context value (a doc
|
|
27
|
+
// Using a Symbol because null is a valid context value (a doc with no sidebar)
|
|
24
28
|
// Inspired by https://github.com/jamiebuilds/unstated-next/blob/master/src/unstated-next.tsx
|
|
25
29
|
const EmptyContextValue: unique symbol = Symbol('EmptyContext');
|
|
26
30
|
|
|
@@ -96,16 +100,14 @@ export function findSidebarCategory(
|
|
|
96
100
|
sidebar: PropSidebar,
|
|
97
101
|
predicate: (category: PropSidebarItemCategory) => boolean,
|
|
98
102
|
): PropSidebarItemCategory | undefined {
|
|
99
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
100
103
|
for (const item of sidebar) {
|
|
101
104
|
if (item.type === 'category') {
|
|
102
105
|
if (predicate(item)) {
|
|
103
106
|
return item;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
107
|
+
}
|
|
108
|
+
const subItem = findSidebarCategory(item.items, predicate);
|
|
109
|
+
if (subItem) {
|
|
110
|
+
return subItem;
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
}
|
|
@@ -120,16 +122,16 @@ export function findFirstCategoryLink(
|
|
|
120
122
|
return item.href;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
124
125
|
for (const subItem of item.items) {
|
|
125
126
|
if (subItem.type === 'link') {
|
|
126
127
|
return subItem.href;
|
|
127
|
-
}
|
|
128
|
-
if (subItem.type === 'category') {
|
|
128
|
+
} else if (subItem.type === 'category') {
|
|
129
129
|
const categoryLink = findFirstCategoryLink(subItem);
|
|
130
130
|
if (categoryLink) {
|
|
131
131
|
return categoryLink;
|
|
132
132
|
}
|
|
133
|
+
} else if (subItem.type === 'html') {
|
|
134
|
+
// skip
|
|
133
135
|
} else {
|
|
134
136
|
throw new Error(
|
|
135
137
|
`Unexpected category item type for ${JSON.stringify(subItem)}`,
|
|
@@ -183,3 +185,46 @@ export function isActiveSidebarItem(
|
|
|
183
185
|
|
|
184
186
|
return false;
|
|
185
187
|
}
|
|
188
|
+
|
|
189
|
+
export function getBreadcrumbs({
|
|
190
|
+
sidebar,
|
|
191
|
+
pathname,
|
|
192
|
+
}: {
|
|
193
|
+
sidebar: PropSidebar;
|
|
194
|
+
pathname: string;
|
|
195
|
+
}): PropSidebarBreadcrumbsItem[] {
|
|
196
|
+
const breadcrumbs: PropSidebarBreadcrumbsItem[] = [];
|
|
197
|
+
|
|
198
|
+
function extract(items: PropSidebar) {
|
|
199
|
+
for (const item of items) {
|
|
200
|
+
if (
|
|
201
|
+
item.type === 'category' &&
|
|
202
|
+
(isSamePath(item.href, pathname) || extract(item.items))
|
|
203
|
+
) {
|
|
204
|
+
breadcrumbs.push(item);
|
|
205
|
+
return true;
|
|
206
|
+
} else if (item.type === 'link' && isSamePath(item.href, pathname)) {
|
|
207
|
+
breadcrumbs.push(item);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
extract(sidebar);
|
|
216
|
+
|
|
217
|
+
return breadcrumbs.reverse();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function useSidebarBreadcrumbs(): PropSidebarBreadcrumbsItem[] | null {
|
|
221
|
+
const sidebar = useDocsSidebar();
|
|
222
|
+
const {pathname} = useLocation();
|
|
223
|
+
const breadcrumbsOption = useActivePlugin()?.pluginData.breadcrumbs;
|
|
224
|
+
|
|
225
|
+
if (breadcrumbsOption === false || !sidebar) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return getBreadcrumbs({sidebar, pathname});
|
|
230
|
+
}
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import {useEffect, useRef} from 'react';
|
|
9
9
|
import {useHistory} from '@docusaurus/router';
|
|
10
|
-
import type {Location, Action} from '
|
|
10
|
+
import type {Location, Action} from 'history';
|
|
11
11
|
|
|
12
12
|
type HistoryBlockHandler = (location: Location, action: Action) => void | false;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
Permits to register a handler that will be called on history actions (pop,
|
|
16
|
-
If the handler returns false, the navigation transition will
|
|
14
|
+
/**
|
|
15
|
+
* Permits to register a handler that will be called on history actions (pop,
|
|
16
|
+
* push, replace) If the handler returns false, the navigation transition will
|
|
17
|
+
* be blocked/cancelled
|
|
17
18
|
*/
|
|
18
19
|
export function useHistoryActionHandler(handler: HistoryBlockHandler): void {
|
|
19
20
|
const {block} = useHistory();
|
|
@@ -32,11 +33,11 @@ export function useHistoryActionHandler(handler: HistoryBlockHandler): void {
|
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
Permits to register a handler that will be called on history pop navigation
|
|
37
|
-
If the handler returns false, the backward/forward
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Permits to register a handler that will be called on history pop navigation
|
|
38
|
+
* (backward/forward) If the handler returns false, the backward/forward
|
|
39
|
+
* transition will be blocked. Unfortunately there's no good way to detect the
|
|
40
|
+
* "direction" (backward/forward) of the POP event.
|
|
40
41
|
*/
|
|
41
42
|
export function useHistoryPopHandler(handler: HistoryBlockHandler): void {
|
|
42
43
|
useHistoryActionHandler((location, action) => {
|
package/src/utils/jsUtils.ts
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* Gets the duplicate values in an array.
|
|
12
12
|
* @param arr The array.
|
|
13
|
-
* @param comparator Compares two values and returns `true` if they are equal
|
|
14
|
-
*
|
|
13
|
+
* @param comparator Compares two values and returns `true` if they are equal
|
|
14
|
+
* (duplicated).
|
|
15
|
+
* @returns Value of the elements `v` that have a preceding element `u` where
|
|
16
|
+
* `comparator(u, v) === true`. Values within the returned array are not
|
|
17
|
+
* guaranteed to be unique.
|
|
15
18
|
*/
|
|
16
19
|
export function duplicates<T>(
|
|
17
20
|
arr: readonly T[],
|
|
@@ -16,12 +16,13 @@ import React, {
|
|
|
16
16
|
} from 'react';
|
|
17
17
|
|
|
18
18
|
/*
|
|
19
|
-
The idea behind all this is that a specific component must be able to fill a
|
|
20
|
-
The doc page should be able to fill the
|
|
21
|
-
|
|
19
|
+
The idea behind all this is that a specific component must be able to fill a
|
|
20
|
+
placeholder in the generic layout. The doc page should be able to fill the
|
|
21
|
+
secondary menu of the main mobile navbar. This permits to reduce coupling
|
|
22
|
+
between the main layout and the specific page.
|
|
22
23
|
|
|
23
|
-
This kind of feature is often called portal/teleport/gateway... various
|
|
24
|
-
Most up-to-date one: https://github.com/gregberge/react-teleporter
|
|
24
|
+
This kind of feature is often called portal/teleport/gateway... various
|
|
25
|
+
unmaintained React libs exist. Most up-to-date one: https://github.com/gregberge/react-teleporter
|
|
25
26
|
Not sure any of those is safe regarding concurrent mode.
|
|
26
27
|
*/
|
|
27
28
|
|
package/src/utils/pathUtils.ts
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// Compare the 2 paths, ignoring trailing
|
|
8
|
+
// Compare the 2 paths, case insensitive and ignoring trailing slash
|
|
9
9
|
export const isSamePath = (
|
|
10
10
|
path1: string | undefined,
|
|
11
11
|
path2: string | undefined,
|
|
12
12
|
): boolean => {
|
|
13
13
|
const normalize = (pathname: string | undefined) =>
|
|
14
|
-
!pathname || pathname?.endsWith('/')
|
|
14
|
+
(!pathname || pathname?.endsWith('/')
|
|
15
|
+
? pathname
|
|
16
|
+
: `${pathname}/`
|
|
17
|
+
)?.toLowerCase();
|
|
15
18
|
return normalize(path1) === normalize(path2);
|
|
16
19
|
};
|
package/src/utils/reactUtils.tsx
CHANGED
|
@@ -7,19 +7,27 @@
|
|
|
7
7
|
|
|
8
8
|
import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* This hook is like useLayoutEffect, but without the SSR warning
|
|
12
|
+
* It seems hacky but it's used in many React libs (Redux, Formik...)
|
|
13
|
+
* Also mentioned here: https://github.com/facebook/react/issues/16956
|
|
14
|
+
* It is useful when you need to update a ref as soon as possible after a React
|
|
15
|
+
* render (before `useEffect`)
|
|
16
|
+
*/
|
|
14
17
|
export const useIsomorphicLayoutEffect =
|
|
15
18
|
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Permits to transform an unstable callback (like an arrow function provided as
|
|
22
|
+
* props) to a "stable" callback that is safe to use in a useEffect dependency
|
|
23
|
+
* array. Useful to avoid React stale closure problems + avoid useless effect
|
|
24
|
+
* re-executions
|
|
25
|
+
*
|
|
26
|
+
* Workaround until the React team recommends a good solution, see
|
|
27
|
+
* https://github.com/facebook/react/issues/16956
|
|
28
|
+
* This generally works but has some potential drawbacks, such as
|
|
29
|
+
* https://github.com/facebook/react/issues/16956#issuecomment-536636418
|
|
30
|
+
*/
|
|
23
31
|
export function useDynamicCallback<T extends (...args: never[]) => unknown>(
|
|
24
32
|
callback: T,
|
|
25
33
|
): T {
|
|
@@ -29,6 +37,7 @@ export function useDynamicCallback<T extends (...args: never[]) => unknown>(
|
|
|
29
37
|
ref.current = callback;
|
|
30
38
|
}, [callback]);
|
|
31
39
|
|
|
32
|
-
// @ts-expect-error:
|
|
40
|
+
// @ts-expect-error: TS is right that this callback may be a supertype of T,
|
|
41
|
+
// but good enough for our use
|
|
33
42
|
return useCallback<T>((...args) => ref.current(...args), []);
|
|
34
43
|
}
|
package/src/utils/regexpUtils.ts
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import GeneratedRoutes, {type Route} from '@generated/routes';
|
|
9
|
+
import {useMemo} from 'react';
|
|
10
|
+
|
|
11
|
+
function isHomePageRoute(route: Route): boolean {
|
|
12
|
+
return route.path === '/' && route.exact === true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isHomeParentRoute(route: Route): boolean {
|
|
16
|
+
return route.path === '/' && route.exact === false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Note that all sites don't always have a homepage in practice
|
|
20
|
+
// See https://github.com/facebook/docusaurus/pull/6517#issuecomment-1048709116
|
|
21
|
+
export function findHomePageRoute(
|
|
22
|
+
routes: Route[] = GeneratedRoutes,
|
|
23
|
+
): Route | undefined {
|
|
24
|
+
if (routes.length === 0) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const homePage = routes.find(isHomePageRoute);
|
|
28
|
+
if (homePage) {
|
|
29
|
+
return homePage;
|
|
30
|
+
}
|
|
31
|
+
const indexSubRoutes = routes
|
|
32
|
+
.filter(isHomeParentRoute)
|
|
33
|
+
.flatMap((route) => route.routes ?? []);
|
|
34
|
+
return findHomePageRoute(indexSubRoutes);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useHomePageRoute(): Route | undefined {
|
|
38
|
+
return useMemo(() => findHomePageRoute(), []);
|
|
39
|
+
}
|
|
@@ -11,8 +11,8 @@ export type StorageType = typeof StorageTypes[number];
|
|
|
11
11
|
|
|
12
12
|
const DefaultStorageType: StorageType = 'localStorage';
|
|
13
13
|
|
|
14
|
-
// Will return null browser storage is unavailable (like running Docusaurus in
|
|
15
|
-
// See https://github.com/facebook/docusaurus/pull/4501
|
|
14
|
+
// Will return null browser storage is unavailable (like running Docusaurus in
|
|
15
|
+
// iframe) See https://github.com/facebook/docusaurus/pull/4501
|
|
16
16
|
function getBrowserStorage(
|
|
17
17
|
storageType: StorageType = DefaultStorageType,
|
|
18
18
|
): Storage | null {
|
|
@@ -23,13 +23,12 @@ function getBrowserStorage(
|
|
|
23
23
|
}
|
|
24
24
|
if (storageType === 'none') {
|
|
25
25
|
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return window[storageType];
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logOnceBrowserStorageNotAvailableWarning(err as Error);
|
|
31
|
+
return null;
|
|
33
32
|
}
|
|
34
33
|
}
|
|
35
34
|
|
|
@@ -79,10 +78,10 @@ Please only call storage APIs in effects and event handlers.`);
|
|
|
79
78
|
|
|
80
79
|
/**
|
|
81
80
|
* Creates an object for accessing a particular key in localStorage.
|
|
82
|
-
* The API is fail-safe, and usage of browser storage should be considered
|
|
83
|
-
* Local storage might simply be unavailable (iframe + browser
|
|
84
|
-
*
|
|
85
|
-
* See also https://github.com/facebook/docusaurus/issues/6036
|
|
81
|
+
* The API is fail-safe, and usage of browser storage should be considered
|
|
82
|
+
* unreliable. Local storage might simply be unavailable (iframe + browser
|
|
83
|
+
* security) or operations might fail individually. Please assume that using
|
|
84
|
+
* this API can be a NO-OP. See also https://github.com/facebook/docusaurus/issues/6036
|
|
86
85
|
*/
|
|
87
86
|
export const createStorageSlot = (
|
|
88
87
|
key: string,
|
|
@@ -99,23 +98,26 @@ export const createStorageSlot = (
|
|
|
99
98
|
get: () => {
|
|
100
99
|
try {
|
|
101
100
|
return browserStorage.getItem(key);
|
|
102
|
-
} catch (
|
|
103
|
-
console.error(`Docusaurus storage error, can't get key=${key}`,
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`Docusaurus storage error, can't get key=${key}`, err);
|
|
104
103
|
return null;
|
|
105
104
|
}
|
|
106
105
|
},
|
|
107
106
|
set: (value) => {
|
|
108
107
|
try {
|
|
109
108
|
browserStorage.setItem(key, value);
|
|
110
|
-
} catch (
|
|
111
|
-
console.error(
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(
|
|
111
|
+
`Docusaurus storage error, can't set ${key}=${value}`,
|
|
112
|
+
err,
|
|
113
|
+
);
|
|
112
114
|
}
|
|
113
115
|
},
|
|
114
116
|
del: () => {
|
|
115
117
|
try {
|
|
116
118
|
browserStorage.removeItem(key);
|
|
117
|
-
} catch (
|
|
118
|
-
console.error(`Docusaurus storage error, can't delete key=${key}`,
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`Docusaurus storage error, can't delete key=${key}`, err);
|
|
119
121
|
}
|
|
120
122
|
},
|
|
121
123
|
};
|
package/src/utils/tocUtils.ts
CHANGED
|
@@ -8,18 +8,64 @@
|
|
|
8
8
|
import {useMemo} from 'react';
|
|
9
9
|
import type {TOCItem} from '@docusaurus/types';
|
|
10
10
|
|
|
11
|
-
type
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export type TOCTreeNode = {
|
|
12
|
+
readonly value: string;
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly level: number;
|
|
15
|
+
readonly children: readonly TOCTreeNode[];
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
function treeifyTOC(flatTOC: readonly TOCItem[]): TOCTreeNode[] {
|
|
19
|
+
const headings = flatTOC.map((heading) => ({
|
|
20
|
+
...heading,
|
|
21
|
+
parentIndex: -1,
|
|
22
|
+
children: [] as TOCTreeNode[],
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// Keep track of which previous index would be the current heading's direct
|
|
26
|
+
// parent. Each entry <i> is the last index of the `headings` array at heading
|
|
27
|
+
// level <i>. We will modify these indices as we iterate through all headings.
|
|
28
|
+
// e.g. if an ### H3 was last seen at index 2, then prevIndexForLevel[3] === 2
|
|
29
|
+
// indices 0 and 1 will remain unused.
|
|
30
|
+
const prevIndexForLevel = Array(7).fill(-1);
|
|
31
|
+
|
|
32
|
+
headings.forEach((curr, currIndex) => {
|
|
33
|
+
// take the last seen index for each ancestor level. the highest
|
|
34
|
+
// index will be the direct ancestor of the current heading.
|
|
35
|
+
const ancestorLevelIndexes = prevIndexForLevel.slice(2, curr.level);
|
|
36
|
+
curr.parentIndex = Math.max(...ancestorLevelIndexes);
|
|
37
|
+
// mark that curr.level was last seen at the current index
|
|
38
|
+
prevIndexForLevel[curr.level] = currIndex;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const rootNodes: TOCTreeNode[] = [];
|
|
42
|
+
|
|
43
|
+
// For a given parentIndex, add each Node into that parent's `children` array
|
|
44
|
+
headings.forEach((heading) => {
|
|
45
|
+
const {parentIndex, ...rest} = heading;
|
|
46
|
+
if (parentIndex >= 0) {
|
|
47
|
+
headings[parentIndex].children.push(rest);
|
|
48
|
+
} else {
|
|
49
|
+
rootNodes.push(rest);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return rootNodes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useTreeifiedTOC(toc: TOCItem[]): readonly TOCTreeNode[] {
|
|
56
|
+
return useMemo(() => treeifyTOC(toc), [toc]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function filterTOC({
|
|
18
60
|
toc,
|
|
19
61
|
minHeadingLevel,
|
|
20
62
|
maxHeadingLevel,
|
|
21
|
-
}:
|
|
22
|
-
|
|
63
|
+
}: {
|
|
64
|
+
toc: readonly TOCTreeNode[];
|
|
65
|
+
minHeadingLevel: number;
|
|
66
|
+
maxHeadingLevel: number;
|
|
67
|
+
}): TOCTreeNode[] {
|
|
68
|
+
function isValid(item: TOCTreeNode) {
|
|
23
69
|
return item.level >= minHeadingLevel && item.level <= maxHeadingLevel;
|
|
24
70
|
}
|
|
25
71
|
|
|
@@ -36,20 +82,27 @@ export function filterTOC({
|
|
|
36
82
|
children: filteredChildren,
|
|
37
83
|
},
|
|
38
84
|
];
|
|
39
|
-
} else {
|
|
40
|
-
return filteredChildren;
|
|
41
85
|
}
|
|
86
|
+
return filteredChildren;
|
|
42
87
|
});
|
|
43
88
|
}
|
|
44
89
|
|
|
45
|
-
|
|
46
|
-
export function useTOCFilter({
|
|
90
|
+
export function useFilteredAndTreeifiedTOC({
|
|
47
91
|
toc,
|
|
48
92
|
minHeadingLevel,
|
|
49
93
|
maxHeadingLevel,
|
|
50
|
-
}:
|
|
94
|
+
}: {
|
|
95
|
+
toc: readonly TOCItem[];
|
|
96
|
+
minHeadingLevel: number;
|
|
97
|
+
maxHeadingLevel: number;
|
|
98
|
+
}): readonly TOCTreeNode[] {
|
|
51
99
|
return useMemo(
|
|
52
|
-
() =>
|
|
100
|
+
() =>
|
|
101
|
+
// Note: we have to filter the TOC after it has been treeified. This is
|
|
102
|
+
// mostly to ensure that weird TOC structures preserve their semantics.
|
|
103
|
+
// For example, an h3-h2-h4 sequence should not be treeified as an h3 > h4
|
|
104
|
+
// hierarchy with min=3, max=4, but should rather be [h3, h4]
|
|
105
|
+
filterTOC({toc: treeifyTOC(toc), minHeadingLevel, maxHeadingLevel}),
|
|
53
106
|
[toc, minHeadingLevel, maxHeadingLevel],
|
|
54
107
|
);
|
|
55
108
|
}
|
|
@@ -39,14 +39,15 @@ export function useAlternatePageUtils(): {
|
|
|
39
39
|
: `${baseUrlUnlocalized}${locale}/`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// TODO support correct alternate url when localized site is deployed on
|
|
42
|
+
// TODO support correct alternate url when localized site is deployed on
|
|
43
|
+
// another domain
|
|
43
44
|
function createUrl({
|
|
44
45
|
locale,
|
|
45
46
|
fullyQualified,
|
|
46
47
|
}: {
|
|
47
48
|
locale: string;
|
|
48
|
-
// For hreflang SEO headers, we need it to be fully qualified (full
|
|
49
|
-
//
|
|
49
|
+
// For hreflang SEO headers, we need it to be fully qualified (full
|
|
50
|
+
// protocol/domain/path...) or locale dropdown, using a path is good enough
|
|
50
51
|
fullyQualified: boolean;
|
|
51
52
|
}) {
|
|
52
53
|
return `${fullyQualified ? url : ''}${getLocalizedBaseUrl(
|
|
@@ -18,8 +18,8 @@ export type useContextualSearchFiltersReturns = {
|
|
|
18
18
|
tags: string[];
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
// We may want to support multiple search engines, don't couple that to
|
|
22
|
-
// Maybe users
|
|
21
|
+
// We may want to support multiple search engines, don't couple that to
|
|
22
|
+
// Algolia/DocSearch. Maybe users want to use their own search engine solution
|
|
23
23
|
export function useContextualSearchFilters(): useContextualSearchFiltersReturns {
|
|
24
24
|
const {i18n} = useDocusaurusContext();
|
|
25
25
|
const allDocsData = useAllDocsData();
|