@brillout/docpress 0.16.34 → 0.16.35-commit-3867953
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.css +4 -67
- package/code-blocks/components/ChoiceGroup.tsx +4 -4
- package/code-blocks/components/Pre.css +1 -8
- package/code-blocks/components/Tabs.css +53 -0
- package/code-blocks/components/Tabs.tsx +91 -0
- package/code-blocks/hooks/useRestoreScroll.ts +6 -2
- package/code-blocks/remarkChoiceGroup.ts +5 -5
- package/code-blocks/remarkDetype.ts +5 -12
- package/code-blocks/remarkPkgManager.ts +0 -9
- package/code-blocks/utils/generateChoiceGroupCode.ts +47 -60
- package/components/index.ts +1 -0
- package/dist/code-blocks/components/Tabs.d.ts +6 -0
- package/dist/code-blocks/components/Tabs.js +57 -0
- package/dist/code-blocks/hooks/useCurrentSelection.d.ts +11 -0
- package/dist/code-blocks/hooks/useCurrentSelection.js +34 -0
- package/dist/code-blocks/hooks/useLocalStorage.d.ts +10 -0
- package/dist/code-blocks/hooks/useLocalStorage.js +31 -0
- package/dist/code-blocks/hooks/useRestoreScroll.d.ts +12 -0
- package/dist/code-blocks/hooks/useRestoreScroll.js +27 -0
- package/dist/code-blocks/remarkChoiceGroup.d.ts +1 -0
- package/dist/code-blocks/remarkChoiceGroup.js +4 -5
- package/dist/code-blocks/remarkDetype.js +5 -11
- package/dist/code-blocks/remarkPkgManager.js +0 -8
- package/dist/code-blocks/utils/generateChoiceGroupCode.js +40 -56
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/index.ts +10 -1
- package/package.json +2 -2
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
.choice-group {
|
|
2
2
|
position: relative;
|
|
3
3
|
|
|
4
|
-
&:hover {
|
|
5
|
-
.choice-group__selects,
|
|
6
|
-
.copy-button {
|
|
7
|
-
opacity: 1;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
4
|
/* layout */
|
|
12
5
|
--top-position: 10px;
|
|
13
6
|
--border-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
|
|
@@ -17,16 +10,14 @@
|
|
|
17
10
|
display: flex;
|
|
18
11
|
flex-direction: row-reverse;
|
|
19
12
|
gap: 2px;
|
|
20
|
-
z-index: 3;
|
|
21
13
|
top: var(--top-position);
|
|
22
14
|
right: 42px;
|
|
23
|
-
opacity: 0;
|
|
24
|
-
transition: opacity 0.5s ease-in-out;
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
.choice-select {
|
|
28
18
|
background: #eee;
|
|
29
19
|
font-size: 13.3333px;
|
|
20
|
+
z-index: 3;
|
|
30
21
|
|
|
31
22
|
-webkit-user-select: none; /* Safari */
|
|
32
23
|
-moz-user-select: none; /* Firefox */
|
|
@@ -53,65 +44,10 @@
|
|
|
53
44
|
align-items: center;
|
|
54
45
|
flex-wrap: nowrap;
|
|
55
46
|
background: #fff;
|
|
56
|
-
padding: 0
|
|
47
|
+
padding: 0 6px;
|
|
57
48
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
58
49
|
cursor: pointer;
|
|
59
50
|
transition: background 120ms ease;
|
|
60
|
-
|
|
61
|
-
span {
|
|
62
|
-
flex: 1;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
&::after {
|
|
66
|
-
width: 15px;
|
|
67
|
-
text-align: end;
|
|
68
|
-
--animation-width: 2px;
|
|
69
|
-
position: relative;
|
|
70
|
-
padding-right: var(--animation-width);
|
|
71
|
-
padding-left: var(--animation-width);
|
|
72
|
-
font-size: 1.13em;
|
|
73
|
-
color: #666;
|
|
74
|
-
left: 0;
|
|
75
|
-
transition: left 500ms ease, opacity 150ms ease-in-out;
|
|
76
|
-
opacity: 0;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.choice-select__list:hover &:hover::after {
|
|
80
|
-
opacity: 1;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
&[aria-selected='true'] {
|
|
84
|
-
&::after {
|
|
85
|
-
content: '»' !important;
|
|
86
|
-
opacity: 1;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.choice-select__list:hover &:not(:hover)::after {
|
|
90
|
-
opacity: 0;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
&:hover::after {
|
|
94
|
-
left: var(--animation-width);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
&[aria-disabled='true'] {
|
|
99
|
-
&::after {
|
|
100
|
-
content: '⊘';
|
|
101
|
-
font-size: 1em;
|
|
102
|
-
left: 1px;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
&:not([aria-selected='true']):not([aria-disabled='true']) {
|
|
107
|
-
&::after {
|
|
108
|
-
content: '«';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
&:hover::after {
|
|
112
|
-
left: calc(-1 * var(--animation-width));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
51
|
}
|
|
116
52
|
|
|
117
53
|
.choice-select[aria-expanded='false'] {
|
|
@@ -122,6 +58,7 @@
|
|
|
122
58
|
.choice-select[aria-expanded='true'] {
|
|
123
59
|
overflow: visible;
|
|
124
60
|
border-width: 0;
|
|
61
|
+
z-index: 5;
|
|
125
62
|
|
|
126
63
|
.choice-select__list {
|
|
127
64
|
border-style: solid;
|
|
@@ -143,7 +80,7 @@
|
|
|
143
80
|
|
|
144
81
|
.choice-select__option[aria-disabled='true'] {
|
|
145
82
|
color: #999;
|
|
146
|
-
cursor:
|
|
83
|
+
cursor: not-allowed;
|
|
147
84
|
opacity: 0.8;
|
|
148
85
|
}
|
|
149
86
|
|
|
@@ -109,7 +109,7 @@ function ChoiceGroup({ children, choiceGroup, parentChoiceGroup }: ChoiceGroupPr
|
|
|
109
109
|
))}
|
|
110
110
|
</select>
|
|
111
111
|
{children}
|
|
112
|
-
{lvl === 0 && (
|
|
112
|
+
{lvl === 0 && !choiceGroup.hidden && (
|
|
113
113
|
<div className={`choice-group__selects`}>
|
|
114
114
|
{choiceGroupAll
|
|
115
115
|
.slice()
|
|
@@ -133,7 +133,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
133
133
|
parentChoiceGroup,
|
|
134
134
|
} = choiceGroup
|
|
135
135
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
136
|
-
const
|
|
136
|
+
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
137
137
|
const [expanded, setExpanded] = useState(false)
|
|
138
138
|
const selectedIndex = choices.indexOf(selectedChoice)
|
|
139
139
|
const height = 25
|
|
@@ -188,7 +188,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
188
188
|
style={{ height }}
|
|
189
189
|
onClick={handleOnClick}
|
|
190
190
|
>
|
|
191
|
-
|
|
191
|
+
{choice}
|
|
192
192
|
</div>
|
|
193
193
|
))}
|
|
194
194
|
</div>
|
|
@@ -197,7 +197,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
197
197
|
function handleOnClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
|
198
198
|
e.stopPropagation()
|
|
199
199
|
const el = e.currentTarget
|
|
200
|
-
|
|
200
|
+
setPrevPosition(el)
|
|
201
201
|
if (el.getAttribute('aria-selected') === 'true') {
|
|
202
202
|
next()
|
|
203
203
|
} else if (el.getAttribute('aria-disabled') === 'false') {
|
|
@@ -13,12 +13,6 @@ pre {
|
|
|
13
13
|
position: relative;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
&:hover {
|
|
17
|
-
.copy-button {
|
|
18
|
-
opacity: 1;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
16
|
.copy-button {
|
|
23
17
|
position: absolute !important;
|
|
24
18
|
top: 10px;
|
|
@@ -28,8 +22,7 @@ pre {
|
|
|
28
22
|
height: 25px;
|
|
29
23
|
width: 30px;
|
|
30
24
|
background-color: #f7f7f7;
|
|
31
|
-
|
|
32
|
-
transition: opacity 0.5s ease-in-out, background-color 0.4s ease-in-out;
|
|
25
|
+
transition: background-color 0.4s ease-in-out;
|
|
33
26
|
|
|
34
27
|
&:not(:hover) {
|
|
35
28
|
background-color: #eee;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
.choice-tabs {
|
|
2
|
+
-webkit-tap-highlight-color: transparent;
|
|
3
|
+
|
|
4
|
+
/* tablist style logic */
|
|
5
|
+
select:has(option:nth-of-type(1):checked) ~ ul[role='tablist'] li:nth-of-type(1),
|
|
6
|
+
select:has(option:nth-of-type(2):checked) ~ ul[role='tablist'] li:nth-of-type(2),
|
|
7
|
+
select:has(option:nth-of-type(3):checked) ~ ul[role='tablist'] li:nth-of-type(3),
|
|
8
|
+
select:has(option:nth-of-type(4):checked) ~ ul[role='tablist'] li:nth-of-type(4),
|
|
9
|
+
select:has(option:nth-of-type(5):checked) ~ ul[role='tablist'] li:nth-of-type(5),
|
|
10
|
+
select:has(option:nth-of-type(6):checked) ~ ul[role='tablist'] li:nth-of-type(6),
|
|
11
|
+
select:has(option:nth-of-type(7):checked) ~ ul[role='tablist'] li:nth-of-type(7) {
|
|
12
|
+
border-bottom: 2px solid #aaa;
|
|
13
|
+
color: var(--color-text);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.choice-tabs__tab-list {
|
|
18
|
+
border-bottom: 1px solid #eaeaea;
|
|
19
|
+
margin: 0 0 10px;
|
|
20
|
+
padding: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.choice-tabs__tab {
|
|
24
|
+
display: inline-block;
|
|
25
|
+
bottom: -1px;
|
|
26
|
+
position: relative;
|
|
27
|
+
list-style: none;
|
|
28
|
+
padding: 6px 12px;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
font-weight: 500;
|
|
31
|
+
/* lighten --color-text by 20% */
|
|
32
|
+
color: color-mix(in srgb, var(--color-text) 80%, white 20%);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@media screen and (max-width: 400px) {
|
|
36
|
+
.choice-tabs__tab {
|
|
37
|
+
padding: 6px 4px;
|
|
38
|
+
font-size: 0.87em;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.choice-tabs__tab:hover {
|
|
43
|
+
color: var(--color-text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.choice-tabs__tab--disabled {
|
|
47
|
+
color: GrayText;
|
|
48
|
+
cursor: default;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.choice-tabs__tab:focus {
|
|
52
|
+
outline: none;
|
|
53
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export { Tabs }
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
5
|
+
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
6
|
+
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
7
|
+
import { assertUsage } from '../../utils/assert.js'
|
|
8
|
+
import './Tabs.css'
|
|
9
|
+
|
|
10
|
+
function Tabs({ choice }: { choice: string }) {
|
|
11
|
+
const groupName = choice
|
|
12
|
+
const pageContext = usePageContext()
|
|
13
|
+
const choicesAll = pageContext.config.docpress.choices
|
|
14
|
+
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`)
|
|
15
|
+
|
|
16
|
+
const { choices, default: defaultChoice } = choicesAll[groupName]
|
|
17
|
+
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
18
|
+
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
19
|
+
const selectedIndex = choices.indexOf(selectedChoice)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="choice-tabs" data-choice-group={groupName}>
|
|
23
|
+
{/* Hidden select used to control tablist styling via CSS. */}
|
|
24
|
+
<select name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
|
|
25
|
+
{choices.map((choice, i) => (
|
|
26
|
+
<option key={i} value={choice}>
|
|
27
|
+
{choice}
|
|
28
|
+
</option>
|
|
29
|
+
))}
|
|
30
|
+
</select>
|
|
31
|
+
<ul id={`choicesFor-${groupName}`} className="choice-tabs__tab-list" role="tablist">
|
|
32
|
+
{choices.map((choice, i) => (
|
|
33
|
+
<li
|
|
34
|
+
key={i}
|
|
35
|
+
id={choice}
|
|
36
|
+
className="choice-tabs__tab"
|
|
37
|
+
role="tab"
|
|
38
|
+
aria-selected={i === selectedIndex}
|
|
39
|
+
tabIndex={i === selectedIndex ? 0 : -1}
|
|
40
|
+
onClick={(e) => handleOnClick(e, i)}
|
|
41
|
+
onKeyDown={handleOnKeyDown}
|
|
42
|
+
>
|
|
43
|
+
{choice}
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
46
|
+
</ul>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
function handleOnClick(e: React.MouseEvent<HTMLLIElement, MouseEvent>, index: number) {
|
|
51
|
+
const el = e.currentTarget
|
|
52
|
+
setPrevPosition(el)
|
|
53
|
+
setSelectedChoice(choices[index]!)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleOnKeyDown(e: React.KeyboardEvent<HTMLLIElement>) {
|
|
57
|
+
const el = e.currentTarget
|
|
58
|
+
let nextIndex = selectedIndex
|
|
59
|
+
|
|
60
|
+
switch (e.key) {
|
|
61
|
+
case 'ArrowRight':
|
|
62
|
+
nextIndex = (selectedIndex + 1) % choices.length
|
|
63
|
+
break
|
|
64
|
+
case 'ArrowLeft':
|
|
65
|
+
nextIndex = (selectedIndex - 1 + choices.length) % choices.length
|
|
66
|
+
break
|
|
67
|
+
case 'Home':
|
|
68
|
+
nextIndex = 0
|
|
69
|
+
break
|
|
70
|
+
case 'End':
|
|
71
|
+
nextIndex = choices.length - 1
|
|
72
|
+
break
|
|
73
|
+
default:
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
e.preventDefault()
|
|
78
|
+
setPrevPosition(el)
|
|
79
|
+
const nextChoice = choices[nextIndex]!
|
|
80
|
+
setSelectedChoice(nextChoice)
|
|
81
|
+
const tabEl = el.parentElement?.parentElement as HTMLDivElement
|
|
82
|
+
|
|
83
|
+
if (!isInViewport(tabEl)) tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
|
84
|
+
el.focus()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isInViewport(el: Element) {
|
|
89
|
+
const rect = el.getBoundingClientRect()
|
|
90
|
+
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth
|
|
91
|
+
}
|
|
@@ -11,7 +11,7 @@ type ScrollPosition = { top: number; el: Element }
|
|
|
11
11
|
* preserving the user’s scroll position.
|
|
12
12
|
*
|
|
13
13
|
* @param deps Dependencies that trigger scroll restoration
|
|
14
|
-
* @returns
|
|
14
|
+
* @returns Function to set the previous top position
|
|
15
15
|
*/
|
|
16
16
|
function useRestoreScroll(deps: React.DependencyList) {
|
|
17
17
|
const prevPositionRef = useRef<ScrollPosition | null>(null)
|
|
@@ -27,5 +27,9 @@ function useRestoreScroll(deps: React.DependencyList) {
|
|
|
27
27
|
prevPositionRef.current = null
|
|
28
28
|
}, deps)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
const setPrevPosition = (el: Element) => {
|
|
31
|
+
prevPositionRef.current = { top: el.getBoundingClientRect().top, el }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return setPrevPosition
|
|
31
35
|
}
|
|
@@ -28,9 +28,8 @@ function remarkChoiceGroup() {
|
|
|
28
28
|
}
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
const replaced = new WeakSet()
|
|
32
31
|
visit(tree, (node) => {
|
|
33
|
-
if (!('children' in node) ||
|
|
32
|
+
if (!('children' in node) || node.data?.customDataIsVisited) return 'skip'
|
|
34
33
|
|
|
35
34
|
let start = -1
|
|
36
35
|
let end = 0
|
|
@@ -43,9 +42,10 @@ function remarkChoiceGroup() {
|
|
|
43
42
|
|
|
44
43
|
for (const choiceNodes of choiceNodesFiltered) {
|
|
45
44
|
const replacement = generateChoiceGroupCode(choiceNodes, node)
|
|
45
|
+
replacement.data ??= {}
|
|
46
|
+
replacement.data.customDataIsVisited = true
|
|
46
47
|
replacements.push(replacement)
|
|
47
48
|
}
|
|
48
|
-
replaced.add(replacements)
|
|
49
49
|
|
|
50
50
|
node.children.splice(start, end - start, ...replacements)
|
|
51
51
|
|
|
@@ -56,7 +56,7 @@ function remarkChoiceGroup() {
|
|
|
56
56
|
for (; end < node.children.length; end++) {
|
|
57
57
|
const child = node.children[end]!
|
|
58
58
|
|
|
59
|
-
if (!['code', '
|
|
59
|
+
if (!['code', 'containerDirective'].includes(child.type)) {
|
|
60
60
|
process()
|
|
61
61
|
continue
|
|
62
62
|
}
|
|
@@ -85,7 +85,6 @@ function filterChoices(nodes: ChoiceNode['children']) {
|
|
|
85
85
|
.map((node) => {
|
|
86
86
|
const choice = node.data!.customDataChoice!
|
|
87
87
|
const nodes = nodesByChoice.get(choice) ?? []
|
|
88
|
-
node.data!.customDataChoice = undefined
|
|
89
88
|
nodes.push(node)
|
|
90
89
|
nodesByChoice.set(choice, nodes)
|
|
91
90
|
})
|
|
@@ -99,6 +98,7 @@ function filterChoices(nodes: ChoiceNode['children']) {
|
|
|
99
98
|
|
|
100
99
|
declare module 'mdast' {
|
|
101
100
|
export interface Data {
|
|
101
|
+
customDataIsVisited?: boolean
|
|
102
102
|
customDataChoice?: string
|
|
103
103
|
customDataFilter?: string
|
|
104
104
|
customDataParentChoiceGroup?: {
|
|
@@ -33,6 +33,9 @@ function remarkDetype() {
|
|
|
33
33
|
// Skip if 'ts-only' meta is present
|
|
34
34
|
if (node.meta?.includes('ts-only')) return
|
|
35
35
|
|
|
36
|
+
// Skip if 'customDataChoice' is 'TypeScript'
|
|
37
|
+
if (node.data?.customDataChoice === 'TypeScript') return
|
|
38
|
+
|
|
36
39
|
// Collect this node for post-processing
|
|
37
40
|
code_nodes.push({ codeBlock: node, index, parent })
|
|
38
41
|
})
|
|
@@ -54,9 +57,6 @@ function transformYaml(node: CodeNode) {
|
|
|
54
57
|
// Skip wrapping if the YAML code block hasn't changed
|
|
55
58
|
if (codeBlockContentJs === codeBlock.value) return
|
|
56
59
|
|
|
57
|
-
const meta = parseMetaString(codeBlock.meta, ['choice'])
|
|
58
|
-
const { choice } = meta.props
|
|
59
|
-
codeBlock.meta = meta.rest
|
|
60
60
|
const { position, ...rest } = codeBlock
|
|
61
61
|
|
|
62
62
|
// Create a new code node for the JS version based on the modified YAML
|
|
@@ -71,21 +71,16 @@ function transformYaml(node: CodeNode) {
|
|
|
71
71
|
{ choiceValue: 'TypeScript', children: [codeBlock] },
|
|
72
72
|
]
|
|
73
73
|
const replacement = generateChoiceGroupCode(choiceNodes, parent, true)
|
|
74
|
-
replacement.data ??= { customDataChoice: choice, customDataFilter: 'codeLang' }
|
|
75
74
|
|
|
76
75
|
parent.children.splice(index, 1, replacement)
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
80
79
|
const { codeBlock, index, parent } = node
|
|
81
|
-
const meta = parseMetaString(codeBlock.meta, ['max-width'
|
|
80
|
+
const meta = parseMetaString(codeBlock.meta, ['max-width'])
|
|
82
81
|
const maxWidth = Number(meta.props['max-width'])
|
|
83
|
-
const { choice } = meta.props
|
|
84
82
|
codeBlock.meta = meta.rest
|
|
85
83
|
|
|
86
|
-
codeBlock.data ??= { customDataChoice: choice, customDataFilter: 'codeLang' }
|
|
87
|
-
if (choice === 'TypeScript') return
|
|
88
|
-
|
|
89
84
|
let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value)
|
|
90
85
|
let codeBlockContentJs = ''
|
|
91
86
|
|
|
@@ -126,7 +121,7 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
|
126
121
|
// No wrapping needed if JS and TS code are still identical
|
|
127
122
|
if (codeBlockContentJs === codeBlock.value) return
|
|
128
123
|
|
|
129
|
-
const { position, lang,
|
|
124
|
+
const { position, lang, ...rest } = codeBlock
|
|
130
125
|
|
|
131
126
|
const tsCode: Code = { ...rest, lang }
|
|
132
127
|
const jsCode: Code = {
|
|
@@ -145,8 +140,6 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
|
145
140
|
const hide = codeBlockReplacedJs === codeBlockContentJs
|
|
146
141
|
const replacement = generateChoiceGroupCode(choiceNodes, parent, hide)
|
|
147
142
|
|
|
148
|
-
replacement.data ??= { ...data }
|
|
149
|
-
|
|
150
143
|
parent.children.splice(index, 1, replacement)
|
|
151
144
|
}
|
|
152
145
|
|
|
@@ -4,7 +4,6 @@ import type { Code, Root } from 'mdast'
|
|
|
4
4
|
import type { VFile } from '@mdx-js/mdx/internal-create-format-aware-processors'
|
|
5
5
|
import { visit } from 'unist-util-visit'
|
|
6
6
|
import convert_ from 'npm-to-yarn'
|
|
7
|
-
import { parseMetaString } from './rehypeMetaToProps.js'
|
|
8
7
|
import { generateChoiceGroupCode } from './utils/generateChoiceGroupCode.js'
|
|
9
8
|
import { assertUsage } from '../utils/assert.js'
|
|
10
9
|
import pc from '@brillout/picocolors'
|
|
@@ -25,15 +24,8 @@ function remarkPkgManager() {
|
|
|
25
24
|
}. Replace it with the equivalent 'npm' command for the package manager toggle to work.`,
|
|
26
25
|
)
|
|
27
26
|
if (!node.value.includes('npm ') && !node.value.includes('npx ')) return
|
|
28
|
-
let choice: string | undefined = undefined
|
|
29
27
|
const nodes = new Map<string, Code>()
|
|
30
28
|
|
|
31
|
-
if (node.meta) {
|
|
32
|
-
const meta = parseMetaString(node.meta, ['choice'])
|
|
33
|
-
choice = meta.props['choice']
|
|
34
|
-
node.meta = meta.rest
|
|
35
|
-
}
|
|
36
|
-
|
|
37
29
|
node.value = node.value.replaceAll('npm i ', 'npm install ')
|
|
38
30
|
nodes.set('npm', node)
|
|
39
31
|
|
|
@@ -49,7 +41,6 @@ function remarkPkgManager() {
|
|
|
49
41
|
const choiceNodes = [...nodes].map(([name, node]) => ({ choiceValue: name, children: [node] }))
|
|
50
42
|
const replacement = generateChoiceGroupCode(choiceNodes, parent)
|
|
51
43
|
|
|
52
|
-
replacement.data ??= { customDataChoice: choice, customDataFilter: replacement.type }
|
|
53
44
|
parent.children.splice(index, 1, replacement)
|
|
54
45
|
})
|
|
55
46
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { generateChoiceGroupCode }
|
|
2
2
|
export type { ChoiceNode }
|
|
3
3
|
|
|
4
|
-
import type { VikeConfig } from 'vike/types'
|
|
5
4
|
import type { BlockContent, DefinitionContent, Parent } from 'mdast'
|
|
6
5
|
import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
|
|
7
6
|
import { getVikeConfig } from 'vike/plugin'
|
|
@@ -26,20 +25,12 @@ const CHOICES_BUILT_IN: Record<string, { choices: string[]; default: string }> =
|
|
|
26
25
|
|
|
27
26
|
function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide: boolean = false): MdxJsxFlowElement {
|
|
28
27
|
let lvl: number = 0
|
|
28
|
+
const customHidden = choiceNodes.some((node) =>
|
|
29
|
+
node.children.some((node) => node.type === 'containerDirective' && node.children[0]!.type !== 'code'),
|
|
30
|
+
)
|
|
31
|
+
const hidden = hide || customHidden
|
|
29
32
|
|
|
30
|
-
const
|
|
31
|
-
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue)
|
|
32
|
-
const choiceGroup = findChoiceGroup(vikeConfig, choices)
|
|
33
|
-
|
|
34
|
-
const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
|
|
35
|
-
const node = choiceNodes.find((n) => n.choiceValue === choice)
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
choiceValue: choice,
|
|
39
|
-
children: node?.children ?? [],
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
33
|
+
const { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes)
|
|
43
34
|
const attributes: MdxJsxAttribute[] = []
|
|
44
35
|
const children: MdxJsxFlowElement[] = []
|
|
45
36
|
|
|
@@ -73,55 +64,15 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
73
64
|
if (parent.data?.customDataParentChoiceGroup) {
|
|
74
65
|
const { lvl: parentLvl, ...parentChoiceGroup } = parent.data.customDataParentChoiceGroup
|
|
75
66
|
|
|
76
|
-
attributes.push(
|
|
77
|
-
type: 'mdxJsxAttribute',
|
|
78
|
-
name: 'parentChoiceGroup',
|
|
79
|
-
value: {
|
|
80
|
-
type: 'mdxJsxAttributeValueExpression',
|
|
81
|
-
value: '',
|
|
82
|
-
data: {
|
|
83
|
-
estree: {
|
|
84
|
-
type: 'Program',
|
|
85
|
-
sourceType: 'module',
|
|
86
|
-
comments: [],
|
|
87
|
-
body: [
|
|
88
|
-
// @ts-ignore: Missing properties in type definition
|
|
89
|
-
{
|
|
90
|
-
type: 'ExpressionStatement',
|
|
91
|
-
expression: valueToEstree(parentChoiceGroup),
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
})
|
|
67
|
+
attributes.push(expressionToAttribute('parentChoiceGroup', parentChoiceGroup))
|
|
98
68
|
|
|
99
69
|
lvl = parentLvl + 1
|
|
100
70
|
parent.data.customDataParentChoiceGroup = undefined
|
|
101
71
|
}
|
|
102
72
|
|
|
103
|
-
attributes.push(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
value: {
|
|
107
|
-
type: 'mdxJsxAttributeValueExpression',
|
|
108
|
-
value: '',
|
|
109
|
-
data: {
|
|
110
|
-
estree: {
|
|
111
|
-
type: 'Program',
|
|
112
|
-
sourceType: 'module',
|
|
113
|
-
comments: [],
|
|
114
|
-
body: [
|
|
115
|
-
// @ts-ignore: Missing properties in type definition
|
|
116
|
-
{
|
|
117
|
-
type: 'ExpressionStatement',
|
|
118
|
-
expression: valueToEstree({ ...choiceGroup, hidden: choiceNodes.length === 1 || hide, lvl }),
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
})
|
|
73
|
+
attributes.push(
|
|
74
|
+
expressionToAttribute('choiceGroup', { ...choiceGroup, hidden: choiceNodes.length === 1 || hidden, lvl }),
|
|
75
|
+
)
|
|
125
76
|
|
|
126
77
|
const choiceGroupNode: MdxJsxFlowElement = {
|
|
127
78
|
type: 'mdxJsxFlowElement',
|
|
@@ -142,7 +93,9 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
142
93
|
return choiceGroupNode
|
|
143
94
|
}
|
|
144
95
|
|
|
145
|
-
function
|
|
96
|
+
function resolveChoiceGroupNodes(choiceNodes: ChoiceNode[]) {
|
|
97
|
+
const vikeConfig = getVikeConfig()
|
|
98
|
+
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue)
|
|
146
99
|
const { choices: choicesConfig } = vikeConfig.config.docpress
|
|
147
100
|
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig }
|
|
148
101
|
|
|
@@ -163,5 +116,39 @@ function findChoiceGroup(vikeConfig: VikeConfig, choices: string[]) {
|
|
|
163
116
|
disabled,
|
|
164
117
|
}
|
|
165
118
|
|
|
166
|
-
|
|
119
|
+
const mergedChoiceNodes: ChoiceNode[] = choiceGroup.choices.map((choice) => {
|
|
120
|
+
const node = choiceNodes.find((node) => node.choiceValue === choice)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
choiceValue: choice,
|
|
124
|
+
children: node?.children ?? [],
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return { choiceGroup, mergedChoiceNodes }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function expressionToAttribute(name: string, value: unknown): MdxJsxAttribute {
|
|
132
|
+
return {
|
|
133
|
+
type: 'mdxJsxAttribute',
|
|
134
|
+
name,
|
|
135
|
+
value: {
|
|
136
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
137
|
+
value: '',
|
|
138
|
+
data: {
|
|
139
|
+
estree: {
|
|
140
|
+
type: 'Program',
|
|
141
|
+
sourceType: 'module',
|
|
142
|
+
comments: [],
|
|
143
|
+
body: [
|
|
144
|
+
// @ts-ignore: Missing properties in type definition
|
|
145
|
+
{
|
|
146
|
+
type: 'ExpressionStatement',
|
|
147
|
+
expression: valueToEstree(value),
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}
|
|
167
154
|
}
|
package/components/index.ts
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export { Tabs };
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useCurrentSelection } from '../hooks/useCurrentSelection.js';
|
|
4
|
+
import { useRestoreScroll } from '../hooks/useRestoreScroll.js';
|
|
5
|
+
import { usePageContext } from '../../renderer/usePageContext.js';
|
|
6
|
+
import { assertUsage } from '../../utils/assert.js';
|
|
7
|
+
import './Tabs.css';
|
|
8
|
+
function Tabs({ choice }) {
|
|
9
|
+
const groupName = choice;
|
|
10
|
+
const pageContext = usePageContext();
|
|
11
|
+
const choicesAll = pageContext.config.docpress.choices;
|
|
12
|
+
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`);
|
|
13
|
+
const { choices, default: defaultChoice } = choicesAll[groupName];
|
|
14
|
+
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
|
|
15
|
+
const setPrevPosition = useRestoreScroll([selectedChoice]);
|
|
16
|
+
const selectedIndex = choices.indexOf(selectedChoice);
|
|
17
|
+
return (React.createElement("div", { className: "choice-tabs", "data-choice-group": groupName },
|
|
18
|
+
React.createElement("select", { name: `choicesFor-${groupName}`, value: selectedChoice, hidden: true, disabled: true }, choices.map((choice, i) => (React.createElement("option", { key: i, value: choice }, choice)))),
|
|
19
|
+
React.createElement("ul", { id: `choicesFor-${groupName}`, className: "choice-tabs__tab-list", role: "tablist" }, choices.map((choice, i) => (React.createElement("li", { key: i, id: choice, className: "choice-tabs__tab", role: "tab", "aria-selected": i === selectedIndex, tabIndex: i === selectedIndex ? 0 : -1, onClick: (e) => handleOnClick(e, i), onKeyDown: handleOnKeyDown }, choice))))));
|
|
20
|
+
function handleOnClick(e, index) {
|
|
21
|
+
const el = e.currentTarget;
|
|
22
|
+
setPrevPosition(el);
|
|
23
|
+
setSelectedChoice(choices[index]);
|
|
24
|
+
}
|
|
25
|
+
function handleOnKeyDown(e) {
|
|
26
|
+
const el = e.currentTarget;
|
|
27
|
+
let nextIndex = selectedIndex;
|
|
28
|
+
switch (e.key) {
|
|
29
|
+
case 'ArrowRight':
|
|
30
|
+
nextIndex = (selectedIndex + 1) % choices.length;
|
|
31
|
+
break;
|
|
32
|
+
case 'ArrowLeft':
|
|
33
|
+
nextIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
34
|
+
break;
|
|
35
|
+
case 'Home':
|
|
36
|
+
nextIndex = 0;
|
|
37
|
+
break;
|
|
38
|
+
case 'End':
|
|
39
|
+
nextIndex = choices.length - 1;
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setPrevPosition(el);
|
|
46
|
+
const nextChoice = choices[nextIndex];
|
|
47
|
+
setSelectedChoice(nextChoice);
|
|
48
|
+
const tabEl = el.parentElement?.parentElement;
|
|
49
|
+
if (!isInViewport(tabEl))
|
|
50
|
+
tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
51
|
+
el.focus();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isInViewport(el) {
|
|
55
|
+
const rect = el.getBoundingClientRect();
|
|
56
|
+
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { useCurrentSelection };
|
|
2
|
+
export { initializeChoiceGroup_SSR };
|
|
3
|
+
/**
|
|
4
|
+
* Stores and retrieves a selected choice from local storage.
|
|
5
|
+
*
|
|
6
|
+
* @param choiceGroupName Group name for the stored choice.
|
|
7
|
+
* @param defaultValue Default choice value.
|
|
8
|
+
* @returns `[selectedChoice, setSelectedChoice]`.
|
|
9
|
+
*/
|
|
10
|
+
declare function useCurrentSelection(choiceGroupName: string, defaultValue: string): readonly [string, (value: string) => void];
|
|
11
|
+
declare const initializeChoiceGroup_SSR: string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { useCurrentSelection };
|
|
2
|
+
export { initializeChoiceGroup_SSR };
|
|
3
|
+
import { useLocalStorage } from './useLocalStorage.js';
|
|
4
|
+
const keyPrefix = 'docpress';
|
|
5
|
+
/**
|
|
6
|
+
* Stores and retrieves a selected choice from local storage.
|
|
7
|
+
*
|
|
8
|
+
* @param choiceGroupName Group name for the stored choice.
|
|
9
|
+
* @param defaultValue Default choice value.
|
|
10
|
+
* @returns `[selectedChoice, setSelectedChoice]`.
|
|
11
|
+
*/
|
|
12
|
+
function useCurrentSelection(choiceGroupName, defaultValue) {
|
|
13
|
+
return useLocalStorage(`${keyPrefix}:choice:${choiceGroupName}`, defaultValue);
|
|
14
|
+
}
|
|
15
|
+
// WARNING: We cannot use `keyPrefix` here: closures don't work because we serialize the function.
|
|
16
|
+
const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`;
|
|
17
|
+
function initializeChoiceGroup() {
|
|
18
|
+
const groupsElements = document.querySelectorAll('[data-choice-group]');
|
|
19
|
+
for (const groupEl of groupsElements) {
|
|
20
|
+
const choiceGroupName = groupEl.getAttribute('data-choice-group');
|
|
21
|
+
const storageKey = `docpress:choice:${choiceGroupName}`;
|
|
22
|
+
const selectedChoice = localStorage.getItem(storageKey);
|
|
23
|
+
if (selectedChoice) {
|
|
24
|
+
const selectEl = groupEl.querySelector(`select[name="choicesFor-${choiceGroupName}"]`);
|
|
25
|
+
const selectedIndex = [...selectEl.options].findIndex((option) => option.value === selectedChoice);
|
|
26
|
+
if (selectedIndex === -1) {
|
|
27
|
+
localStorage.removeItem(storageKey);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
selectEl.value = selectedChoice;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { useLocalStorage };
|
|
2
|
+
/**
|
|
3
|
+
* A simple, generic `useLocalStorage` hook with SSR and cross-tab support.
|
|
4
|
+
*
|
|
5
|
+
* @param storageKey The key used in localStorage.
|
|
6
|
+
* @param clientValue Default value for the client.
|
|
7
|
+
* @param ssrValue Optional fallback for server-side rendering.
|
|
8
|
+
* @returns A tuple `[value, setValue]`.
|
|
9
|
+
*/
|
|
10
|
+
declare function useLocalStorage(storageKey: string, clientValue: string, ssrValue?: string): readonly [string, (value: string) => void];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { useLocalStorage };
|
|
2
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* A simple, generic `useLocalStorage` hook with SSR and cross-tab support.
|
|
5
|
+
*
|
|
6
|
+
* @param storageKey The key used in localStorage.
|
|
7
|
+
* @param clientValue Default value for the client.
|
|
8
|
+
* @param ssrValue Optional fallback for server-side rendering.
|
|
9
|
+
* @returns A tuple `[value, setValue]`.
|
|
10
|
+
*/
|
|
11
|
+
function useLocalStorage(storageKey, clientValue, ssrValue) {
|
|
12
|
+
const subscribe = useCallback((callback) => {
|
|
13
|
+
const listener = (e) => {
|
|
14
|
+
if (e.key === storageKey)
|
|
15
|
+
callback();
|
|
16
|
+
};
|
|
17
|
+
window.addEventListener('storage', listener);
|
|
18
|
+
return () => window.removeEventListener('storage', listener);
|
|
19
|
+
}, [storageKey]);
|
|
20
|
+
const getSnapshot = useCallback(() => {
|
|
21
|
+
const storedValue = localStorage.getItem(storageKey);
|
|
22
|
+
return storedValue || clientValue;
|
|
23
|
+
}, [storageKey, clientValue]);
|
|
24
|
+
const setValue = (value) => {
|
|
25
|
+
localStorage.setItem(storageKey, value);
|
|
26
|
+
// Manually dispatch a storage event to force update in the current tab
|
|
27
|
+
window.dispatchEvent(new StorageEvent('storage', { key: storageKey }));
|
|
28
|
+
};
|
|
29
|
+
const value = useSyncExternalStore(subscribe, getSnapshot, () => ssrValue || clientValue);
|
|
30
|
+
return [value, setValue];
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { useRestoreScroll };
|
|
2
|
+
import React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* useRestoreScroll
|
|
5
|
+
*
|
|
6
|
+
* Keeps the page from jumping when content changes,
|
|
7
|
+
* preserving the user’s scroll position.
|
|
8
|
+
*
|
|
9
|
+
* @param deps Dependencies that trigger scroll restoration
|
|
10
|
+
* @returns Function to set the previous top position
|
|
11
|
+
*/
|
|
12
|
+
declare function useRestoreScroll(deps: React.DependencyList): (el: Element) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { useRestoreScroll };
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* useRestoreScroll
|
|
5
|
+
*
|
|
6
|
+
* Keeps the page from jumping when content changes,
|
|
7
|
+
* preserving the user’s scroll position.
|
|
8
|
+
*
|
|
9
|
+
* @param deps Dependencies that trigger scroll restoration
|
|
10
|
+
* @returns Function to set the previous top position
|
|
11
|
+
*/
|
|
12
|
+
function useRestoreScroll(deps) {
|
|
13
|
+
const prevPositionRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!prevPositionRef.current)
|
|
16
|
+
return;
|
|
17
|
+
const { top, el } = prevPositionRef.current;
|
|
18
|
+
const delta = el.getBoundingClientRect().top - top;
|
|
19
|
+
if (delta !== 0)
|
|
20
|
+
window.scrollBy(0, delta);
|
|
21
|
+
prevPositionRef.current = null;
|
|
22
|
+
}, deps);
|
|
23
|
+
const setPrevPosition = (el) => {
|
|
24
|
+
prevPositionRef.current = { top: el.getBoundingClientRect().top, el };
|
|
25
|
+
};
|
|
26
|
+
return setPrevPosition;
|
|
27
|
+
}
|
|
@@ -24,9 +24,8 @@ function remarkChoiceGroup() {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
|
-
const replaced = new WeakSet();
|
|
28
27
|
visit(tree, (node) => {
|
|
29
|
-
if (!('children' in node) ||
|
|
28
|
+
if (!('children' in node) || node.data?.customDataIsVisited)
|
|
30
29
|
return 'skip';
|
|
31
30
|
let start = -1;
|
|
32
31
|
let end = 0;
|
|
@@ -38,16 +37,17 @@ function remarkChoiceGroup() {
|
|
|
38
37
|
const replacements = [];
|
|
39
38
|
for (const choiceNodes of choiceNodesFiltered) {
|
|
40
39
|
const replacement = generateChoiceGroupCode(choiceNodes, node);
|
|
40
|
+
replacement.data ?? (replacement.data = {});
|
|
41
|
+
replacement.data.customDataIsVisited = true;
|
|
41
42
|
replacements.push(replacement);
|
|
42
43
|
}
|
|
43
|
-
replaced.add(replacements);
|
|
44
44
|
node.children.splice(start, end - start, ...replacements);
|
|
45
45
|
end = start;
|
|
46
46
|
start = -1;
|
|
47
47
|
};
|
|
48
48
|
for (; end < node.children.length; end++) {
|
|
49
49
|
const child = node.children[end];
|
|
50
|
-
if (!['code', '
|
|
50
|
+
if (!['code', 'containerDirective'].includes(child.type)) {
|
|
51
51
|
process();
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
@@ -72,7 +72,6 @@ function filterChoices(nodes) {
|
|
|
72
72
|
.map((node) => {
|
|
73
73
|
const choice = node.data.customDataChoice;
|
|
74
74
|
const nodes = nodesByChoice.get(choice) ?? [];
|
|
75
|
-
node.data.customDataChoice = undefined;
|
|
76
75
|
nodes.push(node);
|
|
77
76
|
nodesByChoice.set(choice, nodes);
|
|
78
77
|
});
|
|
@@ -22,6 +22,9 @@ function remarkDetype() {
|
|
|
22
22
|
// Skip if 'ts-only' meta is present
|
|
23
23
|
if (node.meta?.includes('ts-only'))
|
|
24
24
|
return;
|
|
25
|
+
// Skip if 'customDataChoice' is 'TypeScript'
|
|
26
|
+
if (node.data?.customDataChoice === 'TypeScript')
|
|
27
|
+
return;
|
|
25
28
|
// Collect this node for post-processing
|
|
26
29
|
code_nodes.push({ codeBlock: node, index, parent });
|
|
27
30
|
});
|
|
@@ -41,9 +44,6 @@ function transformYaml(node) {
|
|
|
41
44
|
// Skip wrapping if the YAML code block hasn't changed
|
|
42
45
|
if (codeBlockContentJs === codeBlock.value)
|
|
43
46
|
return;
|
|
44
|
-
const meta = parseMetaString(codeBlock.meta, ['choice']);
|
|
45
|
-
const { choice } = meta.props;
|
|
46
|
-
codeBlock.meta = meta.rest;
|
|
47
47
|
const { position, ...rest } = codeBlock;
|
|
48
48
|
// Create a new code node for the JS version based on the modified YAML
|
|
49
49
|
const yamlJsCode = {
|
|
@@ -56,18 +56,13 @@ function transformYaml(node) {
|
|
|
56
56
|
{ choiceValue: 'TypeScript', children: [codeBlock] },
|
|
57
57
|
];
|
|
58
58
|
const replacement = generateChoiceGroupCode(choiceNodes, parent, true);
|
|
59
|
-
replacement.data ?? (replacement.data = { customDataChoice: choice, customDataFilter: 'codeLang' });
|
|
60
59
|
parent.children.splice(index, 1, replacement);
|
|
61
60
|
}
|
|
62
61
|
async function transformTsToJs(node, file) {
|
|
63
62
|
const { codeBlock, index, parent } = node;
|
|
64
|
-
const meta = parseMetaString(codeBlock.meta, ['max-width'
|
|
63
|
+
const meta = parseMetaString(codeBlock.meta, ['max-width']);
|
|
65
64
|
const maxWidth = Number(meta.props['max-width']);
|
|
66
|
-
const { choice } = meta.props;
|
|
67
65
|
codeBlock.meta = meta.rest;
|
|
68
|
-
codeBlock.data ?? (codeBlock.data = { customDataChoice: choice, customDataFilter: 'codeLang' });
|
|
69
|
-
if (choice === 'TypeScript')
|
|
70
|
-
return;
|
|
71
66
|
let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value);
|
|
72
67
|
let codeBlockContentJs = '';
|
|
73
68
|
// Remove TypeScript from the TS/TSX/Vue code node
|
|
@@ -104,7 +99,7 @@ async function transformTsToJs(node, file) {
|
|
|
104
99
|
// No wrapping needed if JS and TS code are still identical
|
|
105
100
|
if (codeBlockContentJs === codeBlock.value)
|
|
106
101
|
return;
|
|
107
|
-
const { position, lang,
|
|
102
|
+
const { position, lang, ...rest } = codeBlock;
|
|
108
103
|
const tsCode = { ...rest, lang };
|
|
109
104
|
const jsCode = {
|
|
110
105
|
...rest,
|
|
@@ -120,7 +115,6 @@ async function transformTsToJs(node, file) {
|
|
|
120
115
|
// Add `hide` prop to `<ChoiceGroup>` if the only change was replacing `.ts` with `.js`
|
|
121
116
|
const hide = codeBlockReplacedJs === codeBlockContentJs;
|
|
122
117
|
const replacement = generateChoiceGroupCode(choiceNodes, parent, hide);
|
|
123
|
-
replacement.data ?? (replacement.data = { ...data });
|
|
124
118
|
parent.children.splice(index, 1, replacement);
|
|
125
119
|
}
|
|
126
120
|
// Replace all '.ts' extensions with '.js'
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export { remarkPkgManager };
|
|
2
2
|
import { visit } from 'unist-util-visit';
|
|
3
3
|
import convert_ from 'npm-to-yarn';
|
|
4
|
-
import { parseMetaString } from './rehypeMetaToProps.js';
|
|
5
4
|
import { generateChoiceGroupCode } from './utils/generateChoiceGroupCode.js';
|
|
6
5
|
import { assertUsage } from '../utils/assert.js';
|
|
7
6
|
import pc from '@brillout/picocolors';
|
|
@@ -18,13 +17,7 @@ function remarkPkgManager() {
|
|
|
18
17
|
assertUsage(!node.value.includes('pnpm'), `Found a 'pnpm' command in the code block at: ${pc.bold(pc.blue(file.path))}, line ${node.position?.start.line}. Replace it with the equivalent 'npm' command for the package manager toggle to work.`);
|
|
19
18
|
if (!node.value.includes('npm ') && !node.value.includes('npx '))
|
|
20
19
|
return;
|
|
21
|
-
let choice = undefined;
|
|
22
20
|
const nodes = new Map();
|
|
23
|
-
if (node.meta) {
|
|
24
|
-
const meta = parseMetaString(node.meta, ['choice']);
|
|
25
|
-
choice = meta.props['choice'];
|
|
26
|
-
node.meta = meta.rest;
|
|
27
|
-
}
|
|
28
21
|
node.value = node.value.replaceAll('npm i ', 'npm install ');
|
|
29
22
|
nodes.set('npm', node);
|
|
30
23
|
for (const pm of PKG_MANAGERS) {
|
|
@@ -37,7 +30,6 @@ function remarkPkgManager() {
|
|
|
37
30
|
}
|
|
38
31
|
const choiceNodes = [...nodes].map(([name, node]) => ({ choiceValue: name, children: [node] }));
|
|
39
32
|
const replacement = generateChoiceGroupCode(choiceNodes, parent);
|
|
40
|
-
replacement.data ?? (replacement.data = { customDataChoice: choice, customDataFilter: replacement.type });
|
|
41
33
|
parent.children.splice(index, 1, replacement);
|
|
42
34
|
});
|
|
43
35
|
};
|
|
@@ -14,16 +14,9 @@ const CHOICES_BUILT_IN = {
|
|
|
14
14
|
};
|
|
15
15
|
function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
16
16
|
let lvl = 0;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const choiceGroup =
|
|
20
|
-
const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
|
|
21
|
-
const node = choiceNodes.find((n) => n.choiceValue === choice);
|
|
22
|
-
return {
|
|
23
|
-
choiceValue: choice,
|
|
24
|
-
children: node?.children ?? [],
|
|
25
|
-
};
|
|
26
|
-
});
|
|
17
|
+
const customHidden = choiceNodes.some((node) => node.children.some((node) => node.type === 'containerDirective' && node.children[0].type !== 'code'));
|
|
18
|
+
const hidden = hide || customHidden;
|
|
19
|
+
const { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes);
|
|
27
20
|
const attributes = [];
|
|
28
21
|
const children = [];
|
|
29
22
|
for (const choiceNode of mergedChoiceNodes) {
|
|
@@ -54,53 +47,11 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
54
47
|
}
|
|
55
48
|
if (parent.data?.customDataParentChoiceGroup) {
|
|
56
49
|
const { lvl: parentLvl, ...parentChoiceGroup } = parent.data.customDataParentChoiceGroup;
|
|
57
|
-
attributes.push(
|
|
58
|
-
type: 'mdxJsxAttribute',
|
|
59
|
-
name: 'parentChoiceGroup',
|
|
60
|
-
value: {
|
|
61
|
-
type: 'mdxJsxAttributeValueExpression',
|
|
62
|
-
value: '',
|
|
63
|
-
data: {
|
|
64
|
-
estree: {
|
|
65
|
-
type: 'Program',
|
|
66
|
-
sourceType: 'module',
|
|
67
|
-
comments: [],
|
|
68
|
-
body: [
|
|
69
|
-
// @ts-ignore: Missing properties in type definition
|
|
70
|
-
{
|
|
71
|
-
type: 'ExpressionStatement',
|
|
72
|
-
expression: valueToEstree(parentChoiceGroup),
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
});
|
|
50
|
+
attributes.push(expressionToAttribute('parentChoiceGroup', parentChoiceGroup));
|
|
79
51
|
lvl = parentLvl + 1;
|
|
80
52
|
parent.data.customDataParentChoiceGroup = undefined;
|
|
81
53
|
}
|
|
82
|
-
attributes.push({
|
|
83
|
-
type: 'mdxJsxAttribute',
|
|
84
|
-
name: 'choiceGroup',
|
|
85
|
-
value: {
|
|
86
|
-
type: 'mdxJsxAttributeValueExpression',
|
|
87
|
-
value: '',
|
|
88
|
-
data: {
|
|
89
|
-
estree: {
|
|
90
|
-
type: 'Program',
|
|
91
|
-
sourceType: 'module',
|
|
92
|
-
comments: [],
|
|
93
|
-
body: [
|
|
94
|
-
// @ts-ignore: Missing properties in type definition
|
|
95
|
-
{
|
|
96
|
-
type: 'ExpressionStatement',
|
|
97
|
-
expression: valueToEstree({ ...choiceGroup, hidden: choiceNodes.length === 1 || hide, lvl }),
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
});
|
|
54
|
+
attributes.push(expressionToAttribute('choiceGroup', { ...choiceGroup, hidden: choiceNodes.length === 1 || hidden, lvl }));
|
|
104
55
|
const choiceGroupNode = {
|
|
105
56
|
type: 'mdxJsxFlowElement',
|
|
106
57
|
name: 'ChoiceGroup',
|
|
@@ -117,7 +68,9 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
117
68
|
}
|
|
118
69
|
return choiceGroupNode;
|
|
119
70
|
}
|
|
120
|
-
function
|
|
71
|
+
function resolveChoiceGroupNodes(choiceNodes) {
|
|
72
|
+
const vikeConfig = getVikeConfig();
|
|
73
|
+
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue);
|
|
121
74
|
const { choices: choicesConfig } = vikeConfig.config.docpress;
|
|
122
75
|
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig };
|
|
123
76
|
const groupName = Object.keys(choicesAll).find((key) => {
|
|
@@ -135,5 +88,36 @@ function findChoiceGroup(vikeConfig, choices) {
|
|
|
135
88
|
...choicesAll[groupName],
|
|
136
89
|
disabled,
|
|
137
90
|
};
|
|
138
|
-
|
|
91
|
+
const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
|
|
92
|
+
const node = choiceNodes.find((node) => node.choiceValue === choice);
|
|
93
|
+
return {
|
|
94
|
+
choiceValue: choice,
|
|
95
|
+
children: node?.children ?? [],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
return { choiceGroup, mergedChoiceNodes };
|
|
99
|
+
}
|
|
100
|
+
function expressionToAttribute(name, value) {
|
|
101
|
+
return {
|
|
102
|
+
type: 'mdxJsxAttribute',
|
|
103
|
+
name,
|
|
104
|
+
value: {
|
|
105
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
106
|
+
value: '',
|
|
107
|
+
data: {
|
|
108
|
+
estree: {
|
|
109
|
+
type: 'Program',
|
|
110
|
+
sourceType: 'module',
|
|
111
|
+
comments: [],
|
|
112
|
+
body: [
|
|
113
|
+
// @ts-ignore: Missing properties in type definition
|
|
114
|
+
{
|
|
115
|
+
type: 'ExpressionStatement',
|
|
116
|
+
expression: valueToEstree(value),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
139
123
|
}
|
package/dist/components/index.js
CHANGED
package/index.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/**********/
|
|
2
2
|
/* PUBLIC */
|
|
3
3
|
/**********/
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
CodeBlockTransformer,
|
|
6
|
+
Link,
|
|
7
|
+
RepoLink,
|
|
8
|
+
FileAdded,
|
|
9
|
+
FileRemoved,
|
|
10
|
+
ImportMeta,
|
|
11
|
+
Emoji,
|
|
12
|
+
Tabs,
|
|
13
|
+
} from './components/index.js'
|
|
5
14
|
export { MenuToggle } from './Layout.js'
|
|
6
15
|
export * from './components/Note.js'
|
|
7
16
|
export * from './icons/index.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brillout/docpress",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.35-commit-3867953",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@brillout/picocolors": "^1.0.10",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@brillout/release-me": "^0.4.8",
|
|
57
|
-
"@vitejs/plugin-react": "^6.0.1",
|
|
58
57
|
"@types/hast": "^3.0.4",
|
|
59
58
|
"@types/mdast": "^4.0.4",
|
|
60
59
|
"@types/node": "^24.10.0",
|
|
61
60
|
"@types/react": "^19.2.2",
|
|
62
61
|
"@types/react-dom": "^19.2.2",
|
|
62
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
63
63
|
"mdast-util-directive": "^3.1.0",
|
|
64
64
|
"mdast-util-mdx-jsx": "^3.2.0",
|
|
65
65
|
"vike": "^0.4.255",
|