@brillout/docpress 0.16.43 → 0.16.45
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/code-blocks/components/ChoiceGroup.tsx +19 -10
- package/code-blocks/components/Tabs.tsx +6 -3
- package/code-blocks/hooks/useCurrentSelection.ts +7 -3
- package/code-blocks/remarkChoiceGroup.ts +1 -0
- package/code-blocks/types.ts +4 -3
- package/code-blocks/utils/generateChoiceGroupCode.ts +6 -5
- package/code-blocks/utils/getAvailableChoice.ts +20 -0
- package/code-blocks/utils/resolveChoices.ts +18 -0
- package/dist/code-blocks/components/Tabs.js +6 -3
- package/dist/code-blocks/hooks/useCurrentSelection.js +7 -3
- package/dist/code-blocks/remarkChoiceGroup.js +1 -0
- package/dist/code-blocks/types.d.ts +4 -2
- package/dist/code-blocks/utils/generateChoiceGroupCode.js +6 -3
- package/dist/code-blocks/utils/getAvailableChoice.d.ts +3 -0
- package/dist/code-blocks/utils/getAvailableChoice.js +14 -0
- package/dist/code-blocks/utils/resolveChoices.d.ts +8 -0
- package/dist/code-blocks/utils/resolveChoices.js +10 -0
- package/dist/renderer/usePageContext.d.ts +1 -0
- package/dist/resolvePageContext.d.ts +1 -0
- package/dist/resolvePageContext.js +3 -0
- package/dist/types/Config.d.ts +8 -6
- package/package.json +1 -1
- package/resolvePageContext.ts +4 -0
- package/types/Config.ts +4 -2
|
@@ -5,6 +5,7 @@ import React, { useId, useState } from 'react'
|
|
|
5
5
|
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
6
6
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
7
7
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
8
|
+
import { getAvailableChoice } from '../utils/getAvailableChoice.js'
|
|
8
9
|
import { cls } from '../../utils/cls.js'
|
|
9
10
|
import './ChoiceGroup.css'
|
|
10
11
|
|
|
@@ -12,7 +13,7 @@ function ChoiceGroupContainer({
|
|
|
12
13
|
children,
|
|
13
14
|
choiceGroupAll,
|
|
14
15
|
}: { children: React.ReactNode; choiceGroupAll: ChoiceGroupWithParent[] }) {
|
|
15
|
-
const renderCustomSelect = choiceGroupAll.some((choiceGroup) => choiceGroup.lvl === 0 && !choiceGroup.hidden)
|
|
16
|
+
const renderCustomSelect = (choiceGroupAll ?? []).some((choiceGroup) => choiceGroup.lvl === 0 && !choiceGroup.hidden)
|
|
16
17
|
return (
|
|
17
18
|
<div className="choice-group-container">
|
|
18
19
|
{children}
|
|
@@ -28,15 +29,17 @@ function ChoiceGroupContainer({
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function ChoiceGroup({ children, choiceGroup }: { children: React.ReactNode; choiceGroup: TChoiceGroup }) {
|
|
31
|
-
const { name: groupName, choices, default: defaultChoice } = choiceGroup
|
|
32
|
-
const [
|
|
32
|
+
const { name: groupName, choices, default: defaultChoice, emptyChoices } = choiceGroup
|
|
33
|
+
const [selectedChoiceStored] = useCurrentSelection(groupName, defaultChoice)
|
|
34
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, emptyChoices, defaultChoice)
|
|
33
35
|
|
|
34
36
|
return (
|
|
35
37
|
<div className="choice-group">
|
|
36
38
|
{/* Hidden select used to control choice visibility via CSS */}
|
|
37
39
|
<select data-choice-group={groupName} name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
|
|
38
40
|
{choices.map(({ name: choice }) => (
|
|
39
|
-
|
|
41
|
+
// data-empty is read by the initializeChoiceGroup SSR script (useCurrentSelection.ts)
|
|
42
|
+
<option key={choice} value={choice} data-empty={emptyChoices.includes(choice) ? '' : undefined}>
|
|
40
43
|
{choice}
|
|
41
44
|
</option>
|
|
42
45
|
))}
|
|
@@ -49,16 +52,22 @@ function ChoiceGroup({ children, choiceGroup }: { children: React.ReactNode; cho
|
|
|
49
52
|
const OPTION_HEIGHT = 25
|
|
50
53
|
function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
51
54
|
const radioId = useId()
|
|
52
|
-
const choicesAll = usePageContext().
|
|
55
|
+
const choicesAll = usePageContext().resolved.choices
|
|
53
56
|
const { name: groupName, emptyChoices, default: defaultChoice, hidden, parentChoiceGroup, isBuiltIn } = choiceGroup
|
|
54
|
-
const
|
|
57
|
+
const choices = (isBuiltIn ? choiceGroup : choicesAll![groupName]!).choices
|
|
58
|
+
const [selectedChoiceStored, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
59
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, emptyChoices, defaultChoice)
|
|
55
60
|
const [expanded, setExpanded] = useState(false)
|
|
56
61
|
const [isHovered, setIsHovered] = useState(false)
|
|
57
|
-
|
|
62
|
+
let [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup?.name || '', parentChoiceGroup?.default || '')
|
|
63
|
+
let isHidden = hidden
|
|
58
64
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
if (parentChoiceGroup) {
|
|
67
|
+
const { choices, emptyChoices, default: defaultChoice } = parentChoiceGroup
|
|
68
|
+
parentSelectedChoice = getAvailableChoice(parentSelectedChoice, choices, emptyChoices, defaultChoice)
|
|
69
|
+
isHidden = !parentChoiceGroup.choices.includes(parentSelectedChoice)
|
|
70
|
+
}
|
|
62
71
|
const isEmptyChoice = (choice: string) => emptyChoices.includes(choice)
|
|
63
72
|
const filteredChoices = choices.filter((choice) => !isEmptyChoice(choice.name))
|
|
64
73
|
const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice)
|
|
@@ -104,7 +113,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
104
113
|
readOnly
|
|
105
114
|
/>
|
|
106
115
|
<span className="choice-select__option-content">
|
|
107
|
-
<img src={icon} alt="" aria-hidden="true" style={iconStyle} />
|
|
116
|
+
{icon && <img src={icon} alt="" aria-hidden="true" style={iconStyle} />}
|
|
108
117
|
{choice}
|
|
109
118
|
</span>
|
|
110
119
|
</label>
|
|
@@ -3,6 +3,7 @@ export { Tabs }
|
|
|
3
3
|
import React, { useId } from 'react'
|
|
4
4
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
5
5
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
6
|
+
import { getAvailableChoice } from '../utils/getAvailableChoice.js'
|
|
6
7
|
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
7
8
|
import { assertUsage } from '../../utils/assert.js'
|
|
8
9
|
import './Tabs.css'
|
|
@@ -11,10 +12,12 @@ function Tabs({ choice, hide = [] }: { choice: string; hide: string[] }) {
|
|
|
11
12
|
const radioId = useId()
|
|
12
13
|
const groupName = choice
|
|
13
14
|
const pageContext = usePageContext()
|
|
14
|
-
const choicesAll = pageContext.
|
|
15
|
+
const choicesAll = pageContext.resolved.choices
|
|
15
16
|
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`)
|
|
16
17
|
const { choices, default: defaultChoice } = choicesAll[groupName]
|
|
17
|
-
const [
|
|
18
|
+
const [selectedChoiceStored, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
19
|
+
// A hidden tab can't be shown, so treat `hide` as the unavailable choices (#169)
|
|
20
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, hide, defaultChoice)
|
|
18
21
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
19
22
|
const isHidden = (choice: string) => hide.includes(choice)
|
|
20
23
|
|
|
@@ -40,7 +43,7 @@ function Tabs({ choice, hide = [] }: { choice: string; hide: string[] }) {
|
|
|
40
43
|
}}
|
|
41
44
|
/>
|
|
42
45
|
<span className="choice-tabs__tab-content">
|
|
43
|
-
<img src={icon} alt="" aria-hidden="true" style={iconStyle} />
|
|
46
|
+
{icon && <img src={icon} alt="" aria-hidden="true" style={iconStyle} />}
|
|
44
47
|
{choice}
|
|
45
48
|
</span>
|
|
46
49
|
</label>
|
|
@@ -32,13 +32,17 @@ function initializeChoiceGroup() {
|
|
|
32
32
|
switch (groupEl.tagName) {
|
|
33
33
|
case 'SELECT':
|
|
34
34
|
const selectEl = groupEl as HTMLSelectElement
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const option = [...selectEl.options].find((opt) => opt.value === selectedChoice)
|
|
36
|
+
// No option: stale choice removed from the config → forget it.
|
|
37
|
+
// data-empty: choice exists in the group but has no content on this page → keep the
|
|
38
|
+
// server-rendered fallback rather than selecting a blank choice (#169).
|
|
39
|
+
if (!option) localStorage.removeItem(storageKey)
|
|
40
|
+
else if (!option.hasAttribute('data-empty')) selectEl.value = selectedChoice
|
|
38
41
|
break
|
|
39
42
|
case 'DIV':
|
|
40
43
|
const radioEl = groupEl.querySelector<HTMLInputElement>(`input[type="radio"][value="${selectedChoice}"]`)
|
|
41
44
|
if (radioEl) radioEl.checked = true
|
|
45
|
+
break
|
|
42
46
|
default:
|
|
43
47
|
break
|
|
44
48
|
}
|
|
@@ -109,6 +109,7 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
|
|
|
109
109
|
name: parentChoiceGroup.name,
|
|
110
110
|
default: parentChoiceGroup.default,
|
|
111
111
|
choices: !choiceGroup.hidden ? [parentChoiceGroup.choice] : [],
|
|
112
|
+
emptyChoices: parentChoiceGroup.emptyChoices,
|
|
112
113
|
},
|
|
113
114
|
}),
|
|
114
115
|
})
|
package/code-blocks/types.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
export type { ChoiceGroup, ChoiceGroupWithParent, ParentChoiceGroup }
|
|
2
2
|
|
|
3
|
-
import type { Config } from '../types/Config.js'
|
|
3
|
+
import type { Config, ChoiceItem } from '../types/Config.js'
|
|
4
4
|
|
|
5
|
-
type ChoiceGroup = NonNullable<Config['choices']>[string] & {
|
|
5
|
+
type ChoiceGroup = Omit<NonNullable<Config['choices']>[string], 'choices'> & {
|
|
6
6
|
name: string
|
|
7
|
+
choices: ChoiceItem[]
|
|
7
8
|
emptyChoices: string[]
|
|
8
9
|
hidden: boolean
|
|
9
10
|
lvl: number
|
|
10
11
|
isBuiltIn?: boolean
|
|
11
12
|
}
|
|
12
|
-
type ParentChoiceGroup = { name: string; default: string }
|
|
13
|
+
type ParentChoiceGroup = { name: string; default: string; emptyChoices: string[] }
|
|
13
14
|
type ChoiceGroupWithParent = ChoiceGroup & { parentChoiceGroup?: ParentChoiceGroup & { choices: string[] } }
|
|
@@ -8,6 +8,7 @@ import type { MdxJsxAttribute, MdxJsxFlowElement, MdxJsxFlowElementData } from '
|
|
|
8
8
|
import { getVikeConfig } from 'vike/plugin'
|
|
9
9
|
import { assertUsage } from '../../utils/assert.js'
|
|
10
10
|
import { valueToEstree } from 'estree-util-value-to-estree'
|
|
11
|
+
import { resolveChoices } from './resolveChoices.js'
|
|
11
12
|
|
|
12
13
|
type ChoiceNode = {
|
|
13
14
|
choiceValue: string
|
|
@@ -100,6 +101,7 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
100
101
|
name: choiceGroup.name,
|
|
101
102
|
choice: choiceNode.choiceValue,
|
|
102
103
|
default: choiceGroup.default,
|
|
104
|
+
emptyChoices: choiceGroup.emptyChoices,
|
|
103
105
|
lvl,
|
|
104
106
|
},
|
|
105
107
|
}),
|
|
@@ -143,7 +145,7 @@ function resolveChoiceGroupNodes(choiceNodes: ChoiceNode[]) {
|
|
|
143
145
|
const vikeConfig = getVikeConfig()
|
|
144
146
|
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue)
|
|
145
147
|
const { choices: choicesConfig } = vikeConfig.config.docpress
|
|
146
|
-
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig }
|
|
148
|
+
const choicesAll = resolveChoices({ ...CHOICES_BUILT_IN, ...choicesConfig })
|
|
147
149
|
|
|
148
150
|
// Resolve to the group that defines ALL of the block's values. Matching a group that merely
|
|
149
151
|
// shares ANY value would mis-resolve a custom group that collides with a built-in on a single
|
|
@@ -153,13 +155,12 @@ function resolveChoiceGroupNodes(choiceNodes: ChoiceNode[]) {
|
|
|
153
155
|
)
|
|
154
156
|
assertUsage(groupName, `Missing group name for [${choices}]. Define it in +docpress.choices.`)
|
|
155
157
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
)
|
|
158
|
+
const group = choicesAll[groupName]!
|
|
159
|
+
const emptyChoices = group.choices.filter((choice) => !choices.includes(choice.name)).map((choice) => choice.name)
|
|
159
160
|
|
|
160
161
|
const choiceGroup = {
|
|
161
162
|
name: groupName,
|
|
162
|
-
...
|
|
163
|
+
...group,
|
|
163
164
|
emptyChoices,
|
|
164
165
|
}
|
|
165
166
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { getAvailableChoice }
|
|
2
|
+
|
|
3
|
+
import type { ChoiceItem } from '../../types/Config.js'
|
|
4
|
+
import { resolveChoice } from './resolveChoices.js'
|
|
5
|
+
|
|
6
|
+
// A page may include only some of a group's choices, so a choice selected elsewhere (and persisted in
|
|
7
|
+
// localStorage) can be absent here — which would otherwise render nothing (#169). Resolve to a choice
|
|
8
|
+
// that exists on the current page.
|
|
9
|
+
function getAvailableChoice(
|
|
10
|
+
selectedChoice: string,
|
|
11
|
+
choices: (string | ChoiceItem)[],
|
|
12
|
+
emptyChoices: string[],
|
|
13
|
+
defaultChoice: string,
|
|
14
|
+
): string {
|
|
15
|
+
const isAvailable = (choiceName: string) => !emptyChoices.includes(choiceName)
|
|
16
|
+
if (isAvailable(selectedChoice)) return selectedChoice
|
|
17
|
+
if (isAvailable(defaultChoice)) return defaultChoice
|
|
18
|
+
const choicesResolved = choices.map(resolveChoice)
|
|
19
|
+
return choicesResolved.find((choice) => isAvailable(choice.name))?.name ?? selectedChoice
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { resolveChoices, resolveChoice }
|
|
2
|
+
export type { ResolvedChoices }
|
|
3
|
+
|
|
4
|
+
import type { Choice, ChoiceItem } from '../../types/Config.js'
|
|
5
|
+
|
|
6
|
+
type ResolvedChoices = Record<string, Omit<Choice, 'choices'> & { choices: ChoiceItem[] }>
|
|
7
|
+
|
|
8
|
+
function resolveChoices(choicesConfig: Record<string, Choice>): ResolvedChoices {
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(choicesConfig).map(([name, group]) => [
|
|
11
|
+
name,
|
|
12
|
+
{ ...group, choices: group.choices.map(resolveChoice) },
|
|
13
|
+
]),
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
function resolveChoice(choice: string | ChoiceItem): ChoiceItem {
|
|
17
|
+
return typeof choice === 'string' ? { name: choice } : choice
|
|
18
|
+
}
|
|
@@ -2,6 +2,7 @@ export { Tabs };
|
|
|
2
2
|
import React, { useId } from 'react';
|
|
3
3
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js';
|
|
4
4
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js';
|
|
5
|
+
import { getAvailableChoice } from '../utils/getAvailableChoice.js';
|
|
5
6
|
import { usePageContext } from '../../renderer/usePageContext.js';
|
|
6
7
|
import { assertUsage } from '../../utils/assert.js';
|
|
7
8
|
import './Tabs.css';
|
|
@@ -9,10 +10,12 @@ function Tabs({ choice, hide = [] }) {
|
|
|
9
10
|
const radioId = useId();
|
|
10
11
|
const groupName = choice;
|
|
11
12
|
const pageContext = usePageContext();
|
|
12
|
-
const choicesAll = pageContext.
|
|
13
|
+
const choicesAll = pageContext.resolved.choices;
|
|
13
14
|
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`);
|
|
14
15
|
const { choices, default: defaultChoice } = choicesAll[groupName];
|
|
15
|
-
const [
|
|
16
|
+
const [selectedChoiceStored, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
|
|
17
|
+
// A hidden tab can't be shown, so treat `hide` as the unavailable choices (#169)
|
|
18
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, hide, defaultChoice);
|
|
16
19
|
const setPrevPosition = useRestoreScroll([selectedChoice]);
|
|
17
20
|
const isHidden = (choice) => hide.includes(choice);
|
|
18
21
|
return (React.createElement("div", { className: "choice-tabs" },
|
|
@@ -22,6 +25,6 @@ function Tabs({ choice, hide = [] }) {
|
|
|
22
25
|
setSelectedChoice(choice);
|
|
23
26
|
} }),
|
|
24
27
|
React.createElement("span", { className: "choice-tabs__tab-content" },
|
|
25
|
-
React.createElement("img", { src: icon, alt: "", "aria-hidden": "true", style: iconStyle }),
|
|
28
|
+
icon && React.createElement("img", { src: icon, alt: "", "aria-hidden": "true", style: iconStyle }),
|
|
26
29
|
choice)))))));
|
|
27
30
|
}
|
|
@@ -30,16 +30,20 @@ function initializeChoiceGroup() {
|
|
|
30
30
|
switch (groupEl.tagName) {
|
|
31
31
|
case 'SELECT':
|
|
32
32
|
const selectEl = groupEl;
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const option = [...selectEl.options].find((opt) => opt.value === selectedChoice);
|
|
34
|
+
// No option: stale choice removed from the config → forget it.
|
|
35
|
+
// data-empty: choice exists in the group but has no content on this page → keep the
|
|
36
|
+
// server-rendered fallback rather than selecting a blank choice (#169).
|
|
37
|
+
if (!option)
|
|
35
38
|
localStorage.removeItem(storageKey);
|
|
36
|
-
else
|
|
39
|
+
else if (!option.hasAttribute('data-empty'))
|
|
37
40
|
selectEl.value = selectedChoice;
|
|
38
41
|
break;
|
|
39
42
|
case 'DIV':
|
|
40
43
|
const radioEl = groupEl.querySelector(`input[type="radio"][value="${selectedChoice}"]`);
|
|
41
44
|
if (radioEl)
|
|
42
45
|
radioEl.checked = true;
|
|
46
|
+
break;
|
|
43
47
|
default:
|
|
44
48
|
break;
|
|
45
49
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type { ChoiceGroup, ChoiceGroupWithParent, ParentChoiceGroup };
|
|
2
|
-
import type { Config } from '../types/Config.js';
|
|
3
|
-
type ChoiceGroup = NonNullable<Config['choices']>[string] & {
|
|
2
|
+
import type { Config, ChoiceItem } from '../types/Config.js';
|
|
3
|
+
type ChoiceGroup = Omit<NonNullable<Config['choices']>[string], 'choices'> & {
|
|
4
4
|
name: string;
|
|
5
|
+
choices: ChoiceItem[];
|
|
5
6
|
emptyChoices: string[];
|
|
6
7
|
hidden: boolean;
|
|
7
8
|
lvl: number;
|
|
@@ -10,6 +11,7 @@ type ChoiceGroup = NonNullable<Config['choices']>[string] & {
|
|
|
10
11
|
type ParentChoiceGroup = {
|
|
11
12
|
name: string;
|
|
12
13
|
default: string;
|
|
14
|
+
emptyChoices: string[];
|
|
13
15
|
};
|
|
14
16
|
type ChoiceGroupWithParent = ChoiceGroup & {
|
|
15
17
|
parentChoiceGroup?: ParentChoiceGroup & {
|
|
@@ -2,6 +2,7 @@ export { generateChoiceGroupCode, expressionToAttribute };
|
|
|
2
2
|
import { getVikeConfig } from 'vike/plugin';
|
|
3
3
|
import { assertUsage } from '../../utils/assert.js';
|
|
4
4
|
import { valueToEstree } from 'estree-util-value-to-estree';
|
|
5
|
+
import { resolveChoices } from './resolveChoices.js';
|
|
5
6
|
// TODO: determine icon representation for CHOICES_BUILT_IN given lack of SVG/file import support
|
|
6
7
|
// use SVG URLs for now
|
|
7
8
|
const CHOICES_BUILT_IN = {
|
|
@@ -81,6 +82,7 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
81
82
|
name: choiceGroup.name,
|
|
82
83
|
choice: choiceNode.choiceValue,
|
|
83
84
|
default: choiceGroup.default,
|
|
85
|
+
emptyChoices: choiceGroup.emptyChoices,
|
|
84
86
|
lvl,
|
|
85
87
|
},
|
|
86
88
|
}),
|
|
@@ -118,16 +120,17 @@ function resolveChoiceGroupNodes(choiceNodes) {
|
|
|
118
120
|
const vikeConfig = getVikeConfig();
|
|
119
121
|
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue);
|
|
120
122
|
const { choices: choicesConfig } = vikeConfig.config.docpress;
|
|
121
|
-
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig };
|
|
123
|
+
const choicesAll = resolveChoices({ ...CHOICES_BUILT_IN, ...choicesConfig });
|
|
122
124
|
// Resolve to the group that defines ALL of the block's values. Matching a group that merely
|
|
123
125
|
// shares ANY value would mis-resolve a custom group that collides with a built-in on a single
|
|
124
126
|
// value — e.g. a `runtime` group [Node, Bun, Deno, Cloudflare] sharing `Bun` with `pkgManager`.
|
|
125
127
|
const groupName = Object.keys(choicesAll).find((key) => choices.every((choice) => choicesAll[key].choices.some(({ name }) => name === choice)));
|
|
126
128
|
assertUsage(groupName, `Missing group name for [${choices}]. Define it in +docpress.choices.`);
|
|
127
|
-
const
|
|
129
|
+
const group = choicesAll[groupName];
|
|
130
|
+
const emptyChoices = group.choices.filter((choice) => !choices.includes(choice.name)).map((choice) => choice.name);
|
|
128
131
|
const choiceGroup = {
|
|
129
132
|
name: groupName,
|
|
130
|
-
...
|
|
133
|
+
...group,
|
|
131
134
|
emptyChoices,
|
|
132
135
|
};
|
|
133
136
|
const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { getAvailableChoice };
|
|
2
|
+
import { resolveChoice } from './resolveChoices.js';
|
|
3
|
+
// A page may include only some of a group's choices, so a choice selected elsewhere (and persisted in
|
|
4
|
+
// localStorage) can be absent here — which would otherwise render nothing (#169). Resolve to a choice
|
|
5
|
+
// that exists on the current page.
|
|
6
|
+
function getAvailableChoice(selectedChoice, choices, emptyChoices, defaultChoice) {
|
|
7
|
+
const isAvailable = (choiceName) => !emptyChoices.includes(choiceName);
|
|
8
|
+
if (isAvailable(selectedChoice))
|
|
9
|
+
return selectedChoice;
|
|
10
|
+
if (isAvailable(defaultChoice))
|
|
11
|
+
return defaultChoice;
|
|
12
|
+
const choicesResolved = choices.map(resolveChoice);
|
|
13
|
+
return choicesResolved.find((choice) => isAvailable(choice.name))?.name ?? selectedChoice;
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { resolveChoices, resolveChoice };
|
|
2
|
+
export type { ResolvedChoices };
|
|
3
|
+
import type { Choice, ChoiceItem } from '../../types/Config.js';
|
|
4
|
+
type ResolvedChoices = Record<string, Omit<Choice, 'choices'> & {
|
|
5
|
+
choices: ChoiceItem[];
|
|
6
|
+
}>;
|
|
7
|
+
declare function resolveChoices(choicesConfig: Record<string, Choice>): ResolvedChoices;
|
|
8
|
+
declare function resolveChoice(choice: string | ChoiceItem): ChoiceItem;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { resolveChoices, resolveChoice };
|
|
2
|
+
function resolveChoices(choicesConfig) {
|
|
3
|
+
return Object.fromEntries(Object.entries(choicesConfig).map(([name, group]) => [
|
|
4
|
+
name,
|
|
5
|
+
{ ...group, choices: group.choices.map(resolveChoice) },
|
|
6
|
+
]));
|
|
7
|
+
}
|
|
8
|
+
function resolveChoice(choice) {
|
|
9
|
+
return typeof choice === 'string' ? { name: choice } : choice;
|
|
10
|
+
}
|
|
@@ -16,6 +16,7 @@ declare function usePageContextLegacy(): {
|
|
|
16
16
|
pageTitle: string | null;
|
|
17
17
|
documentTitle: string;
|
|
18
18
|
activeCategoryName: string;
|
|
19
|
+
choices: import("../code-blocks/utils/resolveChoices.js").ResolvedChoices | undefined;
|
|
19
20
|
};
|
|
20
21
|
declare function usePageContext(): PageContext;
|
|
21
22
|
declare function PageContextProvider({ pageContext, children, }: {
|
|
@@ -4,6 +4,7 @@ import { jsxToTextContent } from './utils/jsxToTextContent.js';
|
|
|
4
4
|
import pc from '@brillout/picocolors';
|
|
5
5
|
import { parseMarkdownMini } from './parseMarkdownMini.js';
|
|
6
6
|
import { determineNavItemsColumnLayout } from './determineNavItemsColumnLayout.js';
|
|
7
|
+
import { resolveChoices } from './code-blocks/utils/resolveChoices.js';
|
|
7
8
|
function resolvePageContext(pageContext) {
|
|
8
9
|
const config = pageContext.globalContext.config.docpress;
|
|
9
10
|
const { urlPathname } = pageContext;
|
|
@@ -45,6 +46,7 @@ function resolvePageContext(pageContext) {
|
|
|
45
46
|
}
|
|
46
47
|
// Don't show landing page in navigation
|
|
47
48
|
navItemsAll = navItemsAll.filter((navItem) => navItem.url !== '/');
|
|
49
|
+
const choices = config.choices && resolveChoices(config.choices);
|
|
48
50
|
const resolved = {
|
|
49
51
|
navItemsAll,
|
|
50
52
|
navItemsDetached,
|
|
@@ -54,6 +56,7 @@ function resolvePageContext(pageContext) {
|
|
|
54
56
|
pageTitle,
|
|
55
57
|
documentTitle,
|
|
56
58
|
activeCategoryName,
|
|
59
|
+
choices,
|
|
57
60
|
};
|
|
58
61
|
return resolved;
|
|
59
62
|
}
|
package/dist/types/Config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Config };
|
|
1
|
+
export type { Config, Choice, ChoiceItem };
|
|
2
2
|
import type { HeadingDefinition, HeadingDetachedDefinition } from './Heading.js';
|
|
3
3
|
import type React from 'react';
|
|
4
4
|
type Config = {
|
|
@@ -47,11 +47,13 @@ type Category = string | {
|
|
|
47
47
|
/** Hide from Algolia search */
|
|
48
48
|
hide?: boolean;
|
|
49
49
|
};
|
|
50
|
+
/** A choice. A plain `string` is shorthand for `{ name: string }` (no icon). */
|
|
51
|
+
type ChoiceItem = {
|
|
52
|
+
name: string;
|
|
53
|
+
icon?: string;
|
|
54
|
+
iconStyle?: React.CSSProperties;
|
|
55
|
+
};
|
|
50
56
|
type Choice = {
|
|
51
|
-
choices:
|
|
52
|
-
name: string;
|
|
53
|
-
icon: string;
|
|
54
|
-
iconStyle?: React.CSSProperties;
|
|
55
|
-
}[];
|
|
57
|
+
choices: (string | ChoiceItem)[];
|
|
56
58
|
default: string;
|
|
57
59
|
};
|
package/package.json
CHANGED
package/resolvePageContext.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { jsxToTextContent } from './utils/jsxToTextContent.js'
|
|
|
18
18
|
import pc from '@brillout/picocolors'
|
|
19
19
|
import { parseMarkdownMini } from './parseMarkdownMini.js'
|
|
20
20
|
import { determineNavItemsColumnLayout } from './determineNavItemsColumnLayout.js'
|
|
21
|
+
import { resolveChoices } from './code-blocks/utils/resolveChoices.js'
|
|
21
22
|
|
|
22
23
|
type PageSectionResolved = {
|
|
23
24
|
url: string | null
|
|
@@ -80,6 +81,8 @@ function resolvePageContext(pageContext: PageContextServer) {
|
|
|
80
81
|
// Don't show landing page in navigation
|
|
81
82
|
navItemsAll = navItemsAll.filter((navItem) => navItem.url !== '/')
|
|
82
83
|
|
|
84
|
+
const choices = config.choices && resolveChoices(config.choices)
|
|
85
|
+
|
|
83
86
|
const resolved = {
|
|
84
87
|
navItemsAll,
|
|
85
88
|
navItemsDetached,
|
|
@@ -89,6 +92,7 @@ function resolvePageContext(pageContext: PageContextServer) {
|
|
|
89
92
|
pageTitle,
|
|
90
93
|
documentTitle,
|
|
91
94
|
activeCategoryName,
|
|
95
|
+
choices,
|
|
92
96
|
}
|
|
93
97
|
return resolved
|
|
94
98
|
}
|
package/types/Config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Config }
|
|
1
|
+
export type { Config, Choice, ChoiceItem }
|
|
2
2
|
|
|
3
3
|
import type { HeadingDefinition, HeadingDetachedDefinition } from './Heading.js'
|
|
4
4
|
import type React from 'react'
|
|
@@ -62,7 +62,9 @@ type Category =
|
|
|
62
62
|
hide?: boolean
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** A choice. A plain `string` is shorthand for `{ name: string }` (no icon). */
|
|
66
|
+
type ChoiceItem = { name: string; icon?: string; iconStyle?: React.CSSProperties }
|
|
65
67
|
type Choice = {
|
|
66
|
-
choices:
|
|
68
|
+
choices: (string | ChoiceItem)[]
|
|
67
69
|
default: string
|
|
68
70
|
}
|