@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.
Files changed (107) hide show
  1. package/lib/components/Collapsible/index.d.ts.map +1 -1
  2. package/lib/components/Collapsible/index.js +9 -5
  3. package/lib/components/Collapsible/index.js.map +1 -1
  4. package/lib/components/Details/index.d.ts +1 -2
  5. package/lib/components/Details/index.d.ts.map +1 -1
  6. package/lib/components/Details/index.js +5 -4
  7. package/lib/components/Details/index.js.map +1 -1
  8. package/lib/hooks/useKeyboardNavigation.d.ts +1 -0
  9. package/lib/hooks/useKeyboardNavigation.d.ts.map +1 -1
  10. package/lib/hooks/useKeyboardNavigation.js +1 -1
  11. package/lib/hooks/useKeyboardNavigation.js.map +1 -1
  12. package/lib/hooks/useWindowSize.d.ts.map +1 -1
  13. package/lib/hooks/useWindowSize.js +4 -2
  14. package/lib/hooks/useWindowSize.js.map +1 -1
  15. package/lib/index.d.ts +4 -3
  16. package/lib/index.d.ts.map +1 -1
  17. package/lib/index.js +4 -3
  18. package/lib/index.js.map +1 -1
  19. package/lib/utils/ThemeClassNames.d.ts +1 -0
  20. package/lib/utils/ThemeClassNames.d.ts.map +1 -1
  21. package/lib/utils/ThemeClassNames.js +3 -1
  22. package/lib/utils/ThemeClassNames.js.map +1 -1
  23. package/lib/utils/codeBlockUtils.d.ts.map +1 -1
  24. package/lib/utils/codeBlockUtils.js +5 -4
  25. package/lib/utils/codeBlockUtils.js.map +1 -1
  26. package/lib/utils/colorModeUtils.d.ts.map +1 -1
  27. package/lib/utils/colorModeUtils.js +38 -16
  28. package/lib/utils/colorModeUtils.js.map +1 -1
  29. package/lib/utils/docsPreferredVersion/DocsPreferredVersionProvider.d.ts.map +1 -1
  30. package/lib/utils/docsPreferredVersion/DocsPreferredVersionProvider.js +3 -7
  31. package/lib/utils/docsPreferredVersion/DocsPreferredVersionProvider.js.map +1 -1
  32. package/lib/utils/docsUtils.d.ts +6 -1
  33. package/lib/utils/docsUtils.d.ts.map +1 -1
  34. package/lib/utils/docsUtils.js +38 -10
  35. package/lib/utils/docsUtils.js.map +1 -1
  36. package/lib/utils/historyUtils.d.ts +12 -1
  37. package/lib/utils/historyUtils.d.ts.map +1 -1
  38. package/lib/utils/historyUtils.js +9 -8
  39. package/lib/utils/historyUtils.js.map +1 -1
  40. package/lib/utils/jsUtils.d.ts +5 -2
  41. package/lib/utils/jsUtils.d.ts.map +1 -1
  42. package/lib/utils/jsUtils.js +5 -2
  43. package/lib/utils/jsUtils.js.map +1 -1
  44. package/lib/utils/mobileSecondaryMenu.d.ts.map +1 -1
  45. package/lib/utils/mobileSecondaryMenu.js.map +1 -1
  46. package/lib/utils/pathUtils.d.ts.map +1 -1
  47. package/lib/utils/pathUtils.js +7 -2
  48. package/lib/utils/pathUtils.js.map +1 -1
  49. package/lib/utils/reactUtils.d.ts +18 -0
  50. package/lib/utils/reactUtils.d.ts.map +1 -1
  51. package/lib/utils/reactUtils.js +20 -11
  52. package/lib/utils/reactUtils.js.map +1 -1
  53. package/lib/utils/regexpUtils.d.ts +1 -1
  54. package/lib/utils/regexpUtils.js +1 -1
  55. package/lib/utils/routesUtils.d.ts +11 -0
  56. package/lib/utils/routesUtils.d.ts.map +1 -0
  57. package/lib/utils/routesUtils.js +33 -0
  58. package/lib/utils/routesUtils.js.map +1 -0
  59. package/lib/utils/storageUtils.d.ts +4 -4
  60. package/lib/utils/storageUtils.d.ts.map +1 -1
  61. package/lib/utils/storageUtils.js +18 -20
  62. package/lib/utils/storageUtils.js.map +1 -1
  63. package/lib/utils/tocUtils.d.ts +9 -5
  64. package/lib/utils/tocUtils.d.ts.map +1 -1
  65. package/lib/utils/tocUtils.js +45 -7
  66. package/lib/utils/tocUtils.js.map +1 -1
  67. package/lib/utils/useAlternatePageUtils.d.ts.map +1 -1
  68. package/lib/utils/useAlternatePageUtils.js +2 -1
  69. package/lib/utils/useAlternatePageUtils.js.map +1 -1
  70. package/lib/utils/useContextualSearchFilters.js +2 -2
  71. package/lib/utils/useContextualSearchFilters.js.map +1 -1
  72. package/lib/utils/useLocationChange.d.ts +1 -1
  73. package/lib/utils/useLocationChange.d.ts.map +1 -1
  74. package/lib/utils/useLocationChange.js +3 -0
  75. package/lib/utils/useLocationChange.js.map +1 -1
  76. package/lib/utils/usePluralForm.d.ts.map +1 -1
  77. package/lib/utils/usePluralForm.js +26 -21
  78. package/lib/utils/usePluralForm.js.map +1 -1
  79. package/lib/utils/useTOCHighlight.d.ts +1 -2
  80. package/lib/utils/useTOCHighlight.d.ts.map +1 -1
  81. package/lib/utils/useTOCHighlight.js +22 -20
  82. package/lib/utils/useTOCHighlight.js.map +1 -1
  83. package/package.json +11 -11
  84. package/src/components/Collapsible/index.tsx +14 -9
  85. package/src/components/Details/index.tsx +7 -4
  86. package/src/hooks/useKeyboardNavigation.ts +2 -2
  87. package/src/hooks/useWindowSize.ts +4 -2
  88. package/src/index.ts +12 -2
  89. package/src/utils/ThemeClassNames.ts +3 -1
  90. package/src/utils/codeBlockUtils.ts +5 -4
  91. package/src/utils/colorModeUtils.tsx +39 -18
  92. package/src/utils/docsPreferredVersion/DocsPreferredVersionProvider.tsx +3 -5
  93. package/src/utils/docsUtils.tsx +56 -11
  94. package/src/utils/historyUtils.ts +10 -9
  95. package/src/utils/jsUtils.ts +5 -2
  96. package/src/utils/mobileSecondaryMenu.tsx +6 -5
  97. package/src/utils/pathUtils.ts +5 -2
  98. package/src/utils/reactUtils.tsx +20 -11
  99. package/src/utils/regexpUtils.ts +1 -1
  100. package/src/utils/routesUtils.ts +39 -0
  101. package/src/utils/storageUtils.ts +21 -19
  102. package/src/utils/tocUtils.ts +66 -13
  103. package/src/utils/useAlternatePageUtils.ts +4 -3
  104. package/src/utils/useContextualSearchFilters.ts +2 -2
  105. package/src/utils/useLocationChange.ts +5 -1
  106. package/src/utils/usePluralForm.ts +29 -23
  107. 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=(["'])(.*?)\1/;
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)?.[2] ?? '';
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)![1];
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 ThemeStorage = createStorageSlot('theme');
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
- createStorageSlot('theme').set(coerceToTheme(newTheme));
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
- try {
76
- const storedTheme = ThemeStorage.get();
77
- if (storedTheme !== null) {
78
- setTheme(coerceToTheme(storedTheme));
75
+ const onChange = (e: StorageEvent) => {
76
+ if (e.key !== ThemeStorageKey) {
77
+ return;
79
78
  }
80
- } catch (err) {
81
- console.error(err);
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
- window
91
- .matchMedia('(prefers-color-scheme: dark)')
92
- .addListener(({matches}) => {
93
- setTheme(matches ? themes.dark : themes.light);
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({
@@ -6,13 +6,17 @@
6
6
  */
7
7
 
8
8
  import React, {createContext, type ReactNode, useContext} from 'react';
9
- import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
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 can have no sidebar)
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
- } else {
105
- const subItem = findSidebarCategory(item.items, predicate);
106
- if (subItem) {
107
- return subItem;
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 '@docusaurus/history';
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,push,replace)
16
- If the handler returns false, the navigation transition will be blocked/cancelled
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 (backward/forward)
37
- If the handler returns false, the backward/forward transition will be blocked
38
-
39
- Unfortunately there's no good way to detect the "direction" (backward/forward) of the POP event.
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) => {
@@ -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 (duplicated).
14
- * @returns Value of the elements `v` that have a preceding element `u` where `comparator(u, v) === true`. Values within the returned array are not guaranteed to be unique.
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 placeholder in the generic layout
20
- The doc page should be able to fill the secondary menu of the main mobile navbar.
21
- This permits to reduce coupling between the main layout and the specific page.
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 unmaintained React libs exist
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
 
@@ -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('/') ? pathname : `${pathname}/`;
14
+ (!pathname || pathname?.endsWith('/')
15
+ ? pathname
16
+ : `${pathname}/`
17
+ )?.toLowerCase();
15
18
  return normalize(path1) === normalize(path2);
16
19
  };
@@ -7,19 +7,27 @@
7
7
 
8
8
  import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
9
9
 
10
- // This hook is like useLayoutEffect, but without the SSR warning
11
- // It seems hacky but it's used in many React libs (Redux, Formik...)
12
- // Also mentioned here: https://github.com/facebook/react/issues/16956
13
- // It is useful when you need to update a ref as soon as possible after a React render (before useEffect)
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
- // Permits to transform an unstable callback (like an arrow function provided as props)
18
- // to a "stable" callback that is safe to use in a useEffect dependency array
19
- // Useful to avoid React stale closure problems + avoid useless effect re-executions
20
- //
21
- // Workaround until the React team recommends a good solution, see https://github.com/facebook/react/issues/16956
22
- // This generally works has some potential drawbacks, such as https://github.com/facebook/react/issues/16956#issuecomment-536636418
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: TODO, not sure how to fix this TS 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
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  /**
9
- * Utility to convert an optional string into a Regex case insensitive and global
9
+ * Converts an optional string into a Regex case insensitive and global
10
10
  */
11
11
  export function isRegexpStringMatch(
12
12
  regexAsString?: string,
@@ -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 iframe)
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
- } else {
27
- try {
28
- return window[storageType];
29
- } catch (e) {
30
- logOnceBrowserStorageNotAvailableWarning(e as Error);
31
- return null;
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 unreliable
83
- * Local storage might simply be unavailable (iframe + browser security) or operations might fail individually
84
- * Please assume that using this API can be a NO-OP
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 (e) {
103
- console.error(`Docusaurus storage error, can't get key=${key}`, e);
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 (e) {
111
- console.error(`Docusaurus storage error, can't set ${key}=${value}`, e);
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 (e) {
118
- console.error(`Docusaurus storage error, can't delete key=${key}`, e);
119
+ } catch (err) {
120
+ console.error(`Docusaurus storage error, can't delete key=${key}`, err);
119
121
  }
120
122
  },
121
123
  };
@@ -8,18 +8,64 @@
8
8
  import {useMemo} from 'react';
9
9
  import type {TOCItem} from '@docusaurus/types';
10
10
 
11
- type FilterTOCParam = {
12
- toc: readonly TOCItem[];
13
- minHeadingLevel: number;
14
- maxHeadingLevel: number;
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
- export function filterTOC({
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
- }: FilterTOCParam): TOCItem[] {
22
- function isValid(item: TOCItem) {
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
- // Memoize potentially expensive filtering logic
46
- export function useTOCFilter({
90
+ export function useFilteredAndTreeifiedTOC({
47
91
  toc,
48
92
  minHeadingLevel,
49
93
  maxHeadingLevel,
50
- }: FilterTOCParam): readonly TOCItem[] {
94
+ }: {
95
+ toc: readonly TOCItem[];
96
+ minHeadingLevel: number;
97
+ maxHeadingLevel: number;
98
+ }): readonly TOCTreeNode[] {
51
99
  return useMemo(
52
- () => filterTOC({toc, minHeadingLevel, maxHeadingLevel}),
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 another domain
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 protocol/domain/path...)
49
- // For locale dropdown, using a path is good enough
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 Algolia/DocSearch
22
- // Maybe users will want to use its own search engine solution
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();