@brillout/docpress 0.16.5 → 0.16.7-commit-fc2ed19
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/Layout.tsx +6 -2
- package/code-blocks/components/ChoiceGroup.css +48 -0
- package/code-blocks/components/ChoiceGroup.tsx +85 -0
- package/code-blocks/components/CodeSnippets.tsx +3 -13
- package/code-blocks/components/Pre.tsx +27 -3
- package/code-blocks/hooks/useLocalStorage.ts +39 -0
- package/code-blocks/hooks/useMDXComponents.tsx +2 -0
- package/code-blocks/hooks/useRestoreScroll.ts +31 -0
- package/code-blocks/hooks/useSelectCodeLang.ts +2 -51
- package/code-blocks/hooks/useSelectedChoice.ts +34 -0
- package/code-blocks/rehypeMetaToProps.ts +32 -21
- package/code-blocks/remarkChoiceGroup.ts +112 -0
- package/code-blocks/remarkDetype.ts +10 -3
- package/code-blocks/remarkPkgManager.ts +45 -0
- package/code-blocks/utils/generateChoiceGroup.ts +87 -0
- package/dist/code-blocks/rehypeMetaToProps.d.ts +17 -11
- package/dist/code-blocks/rehypeMetaToProps.js +26 -21
- package/dist/code-blocks/remarkChoiceGroup.d.ts +8 -0
- package/dist/code-blocks/remarkChoiceGroup.js +84 -0
- package/dist/code-blocks/remarkDetype.js +8 -3
- package/dist/code-blocks/remarkPkgManager.d.ts +3 -0
- package/dist/code-blocks/remarkPkgManager.js +38 -0
- package/dist/code-blocks/utils/generateChoiceGroup.d.ts +9 -0
- package/dist/code-blocks/utils/generateChoiceGroup.js +66 -0
- package/dist/types/Config.d.ts +9 -1
- package/dist/vite.config.js +4 -1
- package/package.json +4 -1
- package/renderer/onRenderHtml.tsx +42 -4
- package/types/Config.ts +12 -1
- package/vite.config.ts +4 -1
package/Layout.tsx
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from './MenuModal/toggleMenuModal'
|
|
28
28
|
import { MenuModal } from './MenuModal'
|
|
29
29
|
import { autoScrollNav_SSR } from './autoScrollNav'
|
|
30
|
+
import { initializeChoiceGroup_SSR } from './code-blocks/hooks/useSelectedChoice'
|
|
30
31
|
import { initializeJsToggle_SSR } from './code-blocks/hooks/useSelectCodeLang'
|
|
31
32
|
import { SearchLink } from './docsearch/SearchLink'
|
|
32
33
|
import { navigate } from 'vike/client/router'
|
|
@@ -100,7 +101,7 @@ function Layout({ children }: { children: React.ReactNode }) {
|
|
|
100
101
|
{content}
|
|
101
102
|
</div>
|
|
102
103
|
{/* Early toggling, to avoid layout jumps */}
|
|
103
|
-
<script dangerouslySetInnerHTML={{ __html: initializeJsToggle_SSR }}></script>
|
|
104
|
+
<script dangerouslySetInnerHTML={{ __html: `${initializeChoiceGroup_SSR}\n${initializeJsToggle_SSR}` }}></script>
|
|
104
105
|
<Style>{getStyleLayout()}</Style>
|
|
105
106
|
</div>
|
|
106
107
|
)
|
|
@@ -589,7 +590,10 @@ function NavHeadLogo({ isNavLeft }: { isNavLeft?: true }) {
|
|
|
589
590
|
paddingLeft: 'var(--main-view-padding)',
|
|
590
591
|
paddingRight: 'var(--padding-side)',
|
|
591
592
|
}
|
|
592
|
-
: {
|
|
593
|
+
: {
|
|
594
|
+
paddingLeft: 15,
|
|
595
|
+
marginLeft: -10,
|
|
596
|
+
}),
|
|
593
597
|
}}
|
|
594
598
|
href="/"
|
|
595
599
|
onContextMenu={!navLogo ? undefined : onContextMenu}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.choice-group {
|
|
2
|
+
position: relative;
|
|
3
|
+
|
|
4
|
+
&:hover {
|
|
5
|
+
.select-choice,
|
|
6
|
+
.copy-button,
|
|
7
|
+
.code-lang-toggle {
|
|
8
|
+
opacity: 1;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
--select-width: 100px;
|
|
13
|
+
--select-top-position: 10px;
|
|
14
|
+
--select-right-position: 42px;
|
|
15
|
+
--has-toggle: calc(var(--select-right-position) + 61px);
|
|
16
|
+
|
|
17
|
+
.select-choice {
|
|
18
|
+
position: absolute;
|
|
19
|
+
z-index: 3;
|
|
20
|
+
height: 25px;
|
|
21
|
+
top: var(--select-top-position);
|
|
22
|
+
right: var(--select-right-position);
|
|
23
|
+
width: var(--select-width);
|
|
24
|
+
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transition: opacity 0.5s ease-in-out, background-color 0.4s ease-in-out;
|
|
27
|
+
|
|
28
|
+
&.has-toggle {
|
|
29
|
+
--select-right-position: var(--has-toggle);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&:has(+ .choice .choice-group) {
|
|
33
|
+
right: calc(var(--select-width) + var(--select-right-position) + 2px);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.hidden {
|
|
38
|
+
display: none !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
select:has(option:nth-of-type(1):not(:checked)) ~ .choice:nth-of-type(1),
|
|
42
|
+
select:has(option:nth-of-type(2):not(:checked)) ~ .choice:nth-of-type(2),
|
|
43
|
+
select:has(option:nth-of-type(3):not(:checked)) ~ .choice:nth-of-type(3),
|
|
44
|
+
select:has(option:nth-of-type(4):not(:checked)) ~ .choice:nth-of-type(4),
|
|
45
|
+
select:has(option:nth-of-type(5):not(:checked)) ~ .choice:nth-of-type(5) {
|
|
46
|
+
display: none;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export { ChoiceGroup }
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { usePageContext } from '../../renderer/usePageContext'
|
|
5
|
+
import { useSelectedChoice } from '../hooks/useSelectedChoice'
|
|
6
|
+
import { useRestoreScroll } from '../hooks/useRestoreScroll'
|
|
7
|
+
import { assertUsage } from '../../utils/assert'
|
|
8
|
+
import { cls } from '../../utils/cls'
|
|
9
|
+
import type { PageContext } from 'vike/types'
|
|
10
|
+
import './ChoiceGroup.css'
|
|
11
|
+
|
|
12
|
+
function ChoiceGroup({ children, choices }: { children: React.ReactNode; choices: string[] }) {
|
|
13
|
+
const pageContext = usePageContext()
|
|
14
|
+
const group = findGroup(pageContext, choices)
|
|
15
|
+
const [selectedChoice, setSelectedChoice] = useSelectedChoice(group.name, group.default)
|
|
16
|
+
const [hasJsToggle, setHasJsToggle] = useState(false)
|
|
17
|
+
const choiceGroupRef = useRef<HTMLDivElement>(null)
|
|
18
|
+
const prevPositionRef = useRestoreScroll([selectedChoice])
|
|
19
|
+
const isHidden = choices.length === 1 || !choices.includes(selectedChoice)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!choiceGroupRef.current) return
|
|
23
|
+
const selectedChoiceEl = choiceGroupRef.current.querySelector<HTMLDivElement>(`div[id="${selectedChoice}"]`)
|
|
24
|
+
setHasJsToggle(!!selectedChoiceEl?.classList.contains('has-toggle'))
|
|
25
|
+
}, [selectedChoice])
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div ref={choiceGroupRef} data-group-name={group.name} className="choice-group">
|
|
29
|
+
<select
|
|
30
|
+
name={`${group.name}-choices`}
|
|
31
|
+
value={selectedChoice}
|
|
32
|
+
onChange={onChange}
|
|
33
|
+
className={cls(['select-choice', hasJsToggle && 'has-toggle', isHidden && 'hidden'])}
|
|
34
|
+
>
|
|
35
|
+
{group.choices.map((choice, i) => (
|
|
36
|
+
<option key={i} value={choice} disabled={!choices.includes(choice)}>
|
|
37
|
+
{choice}
|
|
38
|
+
</option>
|
|
39
|
+
))}
|
|
40
|
+
</select>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
|
46
|
+
const el = e.target
|
|
47
|
+
prevPositionRef.current = { top: el.getBoundingClientRect().top, el }
|
|
48
|
+
setSelectedChoice(el.value)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findGroup(pageContext: PageContext, choices: string[]) {
|
|
53
|
+
const { choices: choicesGroup } = pageContext.globalContext.config.docpress
|
|
54
|
+
assertUsage(choicesGroup, `+docpress.choices is not defined.`)
|
|
55
|
+
|
|
56
|
+
const groupName = Object.keys(choicesGroup).find((key) => {
|
|
57
|
+
// get only the values that exist in both choices and choicesGroup[key].choices
|
|
58
|
+
const relevantChoices = choicesGroup[key].choices.filter((choice) => choices.includes(choice))
|
|
59
|
+
// if nothing exists, skip this key
|
|
60
|
+
if (relevantChoices.length === 0) return false
|
|
61
|
+
|
|
62
|
+
// check order
|
|
63
|
+
let i = 0
|
|
64
|
+
for (const choice of choices) {
|
|
65
|
+
if (choice === relevantChoices[i]) i++
|
|
66
|
+
}
|
|
67
|
+
assertUsage(
|
|
68
|
+
i === relevantChoices.length,
|
|
69
|
+
`Choices exist for key "${key}" but NOT in order. Expected order: [${relevantChoices}], got: [${choices}]`,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
})
|
|
74
|
+
assertUsage(groupName, `the group name for [${choices}] was not found.`)
|
|
75
|
+
|
|
76
|
+
const mergedChoices = [...new Set([...choices, ...choicesGroup[groupName].choices])]
|
|
77
|
+
|
|
78
|
+
const group = {
|
|
79
|
+
name: groupName,
|
|
80
|
+
...choicesGroup[groupName],
|
|
81
|
+
choices: mergedChoices,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return group
|
|
85
|
+
}
|
|
@@ -4,8 +4,9 @@ export { TypescriptOnly }
|
|
|
4
4
|
// Internal
|
|
5
5
|
export { CodeSnippets }
|
|
6
6
|
|
|
7
|
-
import React
|
|
7
|
+
import React from 'react'
|
|
8
8
|
import { useSelectCodeLang } from '../hooks/useSelectCodeLang'
|
|
9
|
+
import { useRestoreScroll } from '../hooks/useRestoreScroll'
|
|
9
10
|
import './CodeSnippets.css'
|
|
10
11
|
|
|
11
12
|
/** Only show if TypeScript is selected */
|
|
@@ -16,18 +17,7 @@ function TypescriptOnly({ children }: { children: React.ReactNode }) {
|
|
|
16
17
|
|
|
17
18
|
function CodeSnippets({ children, hideToggle = false }: { children: React.ReactNode; hideToggle: boolean }) {
|
|
18
19
|
const [codeLangSelected, selectCodeLang] = useSelectCodeLang()
|
|
19
|
-
const prevPositionRef =
|
|
20
|
-
|
|
21
|
-
// Restores the scroll position of the toggle element after toggling languages.
|
|
22
|
-
useEffect(() => {
|
|
23
|
-
if (!prevPositionRef.current) return
|
|
24
|
-
const { top, el } = prevPositionRef.current
|
|
25
|
-
const delta = el.getBoundingClientRect().top - top
|
|
26
|
-
if (delta !== 0) {
|
|
27
|
-
window.scrollBy(0, delta)
|
|
28
|
-
}
|
|
29
|
-
prevPositionRef.current = null
|
|
30
|
-
}, [codeLangSelected])
|
|
20
|
+
const prevPositionRef = useRestoreScroll([codeLangSelected])
|
|
31
21
|
|
|
32
22
|
return (
|
|
33
23
|
<div className="code-snippets">
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
export { Pre }
|
|
2
2
|
|
|
3
|
-
import React, {
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { cls } from '../../utils/cls'
|
|
4
5
|
import './Pre.css'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// Styling defined in src/css/code/diff.css
|
|
8
|
+
const classRemoved = [
|
|
9
|
+
//
|
|
10
|
+
'diff-entire-file',
|
|
11
|
+
'diff-entire-file-removed',
|
|
12
|
+
].join(' ')
|
|
13
|
+
const classAdded = [
|
|
14
|
+
//
|
|
15
|
+
'diff-entire-file',
|
|
16
|
+
'diff-entire-file-added',
|
|
17
|
+
].join(' ')
|
|
18
|
+
|
|
19
|
+
type AdditionalProps = {
|
|
20
|
+
'hide-menu'?: string
|
|
21
|
+
'file-added'?: string
|
|
22
|
+
'file-removed'?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Pre({ children, ...props }: React.ComponentPropsWithoutRef<'pre'> & AdditionalProps) {
|
|
26
|
+
const { className, ...rest } = props
|
|
27
|
+
|
|
7
28
|
return (
|
|
8
|
-
<pre
|
|
29
|
+
<pre
|
|
30
|
+
className={cls([className, props['file-added'] && classAdded, props['file-removed'] && classRemoved])}
|
|
31
|
+
{...rest}
|
|
32
|
+
>
|
|
9
33
|
{!props['hide-menu'] && <CopyButton />}
|
|
10
34
|
{children}
|
|
11
35
|
</pre>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export { useLocalStorage }
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A simple, generic `useLocalStorage` hook with SSR and cross-tab support.
|
|
7
|
+
*
|
|
8
|
+
* @param storageKey The key used in localStorage.
|
|
9
|
+
* @param clientValue Default value for the client.
|
|
10
|
+
* @param ssrValue Optional fallback for server-side rendering.
|
|
11
|
+
* @returns A tuple `[value, setValue]`.
|
|
12
|
+
*/
|
|
13
|
+
function useLocalStorage(storageKey: string, clientValue: string, ssrValue?: string) {
|
|
14
|
+
const subscribe = useCallback(
|
|
15
|
+
(callback: () => void) => {
|
|
16
|
+
const listener = (e: StorageEvent) => {
|
|
17
|
+
if (e.key === storageKey) callback()
|
|
18
|
+
}
|
|
19
|
+
window.addEventListener('storage', listener)
|
|
20
|
+
return () => window.removeEventListener('storage', listener)
|
|
21
|
+
},
|
|
22
|
+
[storageKey],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const getSnapshot = useCallback(() => {
|
|
26
|
+
const storedValue = localStorage.getItem(storageKey)
|
|
27
|
+
return storedValue || clientValue
|
|
28
|
+
}, [storageKey, clientValue])
|
|
29
|
+
|
|
30
|
+
const setValue = (value: string) => {
|
|
31
|
+
localStorage.setItem(storageKey, value)
|
|
32
|
+
// Manually dispatch a storage event to force update in the current tab
|
|
33
|
+
window.dispatchEvent(new StorageEvent('storage', { key: storageKey }))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const value = useSyncExternalStore(subscribe, getSnapshot, () => ssrValue || clientValue)
|
|
37
|
+
|
|
38
|
+
return [value, setValue] as const
|
|
39
|
+
}
|
|
@@ -4,9 +4,11 @@ import React from 'react'
|
|
|
4
4
|
import type { UseMdxComponents } from '@mdx-js/mdx'
|
|
5
5
|
import { Pre } from '../components/Pre.js'
|
|
6
6
|
import { CodeSnippets } from '../components/CodeSnippets.js'
|
|
7
|
+
import { ChoiceGroup } from '../components/ChoiceGroup.js'
|
|
7
8
|
|
|
8
9
|
const useMDXComponents: UseMdxComponents = () => {
|
|
9
10
|
return {
|
|
11
|
+
ChoiceGroup,
|
|
10
12
|
CodeSnippets,
|
|
11
13
|
pre: (props) => <Pre {...props} />,
|
|
12
14
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { useRestoreScroll }
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
type ScrollPosition = { top: number; el: Element }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* useRestoreScroll
|
|
9
|
+
*
|
|
10
|
+
* Keeps the page from jumping when content changes,
|
|
11
|
+
* preserving the user’s scroll position.
|
|
12
|
+
*
|
|
13
|
+
* @param deps Dependencies that trigger scroll restoration
|
|
14
|
+
* @returns Ref holding the tracked element and its previous top position
|
|
15
|
+
*/
|
|
16
|
+
function useRestoreScroll(deps: React.DependencyList) {
|
|
17
|
+
const prevPositionRef = useRef<ScrollPosition | null>(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!prevPositionRef.current) return
|
|
21
|
+
|
|
22
|
+
const { top, el } = prevPositionRef.current
|
|
23
|
+
const delta = el.getBoundingClientRect().top - top
|
|
24
|
+
|
|
25
|
+
if (delta !== 0) window.scrollBy(0, delta)
|
|
26
|
+
|
|
27
|
+
prevPositionRef.current = null
|
|
28
|
+
}, deps)
|
|
29
|
+
|
|
30
|
+
return prevPositionRef
|
|
31
|
+
}
|
|
@@ -1,57 +1,14 @@
|
|
|
1
1
|
export { useSelectCodeLang }
|
|
2
2
|
export { initializeJsToggle_SSR }
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { assertWarning } from '../../utils/assert'
|
|
4
|
+
import { useLocalStorage } from './useLocalStorage'
|
|
6
5
|
|
|
7
6
|
const storageKey = 'docpress:code-lang'
|
|
8
7
|
const codeLangDefaultSsr = 'ts'
|
|
9
8
|
const codeLangDefaultClient = 'js'
|
|
10
9
|
|
|
11
10
|
function useSelectCodeLang() {
|
|
12
|
-
|
|
13
|
-
const updateState = () => {
|
|
14
|
-
setCodeLangSelected(getCodeLangStorage())
|
|
15
|
-
}
|
|
16
|
-
const updateStateOnStorageEvent = (event: StorageEvent) => {
|
|
17
|
-
if (event.key === storageKey) updateState()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const getCodeLangStorage = () => {
|
|
21
|
-
try {
|
|
22
|
-
return window.localStorage.getItem(storageKey) ?? codeLangDefaultClient
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.error(error)
|
|
25
|
-
assertWarning(false, 'Error reading from localStorage')
|
|
26
|
-
return codeLangDefaultClient
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const selectCodeLang = useCallback((value: string) => {
|
|
31
|
-
try {
|
|
32
|
-
window.localStorage.setItem(storageKey, value)
|
|
33
|
-
setCodeLangSelected(value)
|
|
34
|
-
window.dispatchEvent(new CustomEvent('code-lang-storage'))
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error(error)
|
|
37
|
-
assertWarning(false, 'Error setting localStorage')
|
|
38
|
-
}
|
|
39
|
-
}, [])
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
// Initial load from localStorage
|
|
43
|
-
updateState()
|
|
44
|
-
// Update code lang in current tab
|
|
45
|
-
window.addEventListener('code-lang-storage', updateState)
|
|
46
|
-
// Update code lang if changed in another tab
|
|
47
|
-
window.addEventListener('storage', updateStateOnStorageEvent)
|
|
48
|
-
return () => {
|
|
49
|
-
window.removeEventListener('code-lang-storage', updateState)
|
|
50
|
-
window.removeEventListener('storage', updateStateOnStorageEvent)
|
|
51
|
-
}
|
|
52
|
-
}, [])
|
|
53
|
-
|
|
54
|
-
return [codeLangSelected, selectCodeLang] as const
|
|
11
|
+
return useLocalStorage(storageKey, codeLangDefaultClient, codeLangDefaultSsr)
|
|
55
12
|
}
|
|
56
13
|
|
|
57
14
|
// WARNING: We cannot use the variables storageKey nor codeLangDefaultClient here: closures
|
|
@@ -66,9 +23,3 @@ function initializeJsToggle() {
|
|
|
66
23
|
for (const input of inputs) input.checked = false
|
|
67
24
|
}
|
|
68
25
|
}
|
|
69
|
-
|
|
70
|
-
declare global {
|
|
71
|
-
interface WindowEventMap {
|
|
72
|
-
'code-lang-storage': CustomEvent
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { useSelectedChoice }
|
|
2
|
+
export { initializeChoiceGroup_SSR }
|
|
3
|
+
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { useLocalStorage } from './useLocalStorage'
|
|
6
|
+
|
|
7
|
+
const keyPrefix = 'docpress'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tracks the selected choice.
|
|
11
|
+
* Uses `useLocalStorage` if `persistId` is provided, otherwise regular state.
|
|
12
|
+
*
|
|
13
|
+
* @param persistId Optional ID to persist selection.
|
|
14
|
+
* @param defaultValue Default choice value.
|
|
15
|
+
* @returns `[selectedChoice, setSelectedChoice]`.
|
|
16
|
+
*/
|
|
17
|
+
function useSelectedChoice(persistId: string | null, defaultValue: string) {
|
|
18
|
+
if (!persistId) return useState(defaultValue)
|
|
19
|
+
|
|
20
|
+
return useLocalStorage(`${keyPrefix}:${persistId}`, defaultValue)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// WARNING: We cannot use the keyPrefix variable here: closures don't work because we serialize the function.
|
|
24
|
+
const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`
|
|
25
|
+
function initializeChoiceGroup() {
|
|
26
|
+
const groupsElements = document.querySelectorAll<HTMLDivElement>('[data-group-name]')
|
|
27
|
+
for (const groupEl of groupsElements) {
|
|
28
|
+
const groupName = groupEl.getAttribute('data-group-name')!
|
|
29
|
+
const selectedChoice = localStorage.getItem(`docpress:${groupName}`)
|
|
30
|
+
if (!selectedChoice) continue
|
|
31
|
+
const selectEl = groupEl.querySelector<HTMLSelectElement>(`.select-choice`)
|
|
32
|
+
if (selectEl) selectEl.value = selectedChoice
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -23,41 +23,47 @@ function rehypeMetaToProps() {
|
|
|
23
23
|
return (tree: Root) => {
|
|
24
24
|
visit(tree, 'element', (node, _index, parent) => {
|
|
25
25
|
if (node.tagName === 'code' && parent?.type === 'element' && parent.tagName === 'pre') {
|
|
26
|
-
const
|
|
26
|
+
const meta = parseMetaString(node.data?.meta)
|
|
27
27
|
parent.properties ??= {}
|
|
28
|
-
parent.properties = { ...parent.properties, ...props }
|
|
28
|
+
parent.properties = { ...parent.properties, ...meta.props }
|
|
29
29
|
}
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Simple parser for a metadata string into key-value pairs and a remaining unparsed string.
|
|
36
36
|
*
|
|
37
|
-
* Supports simple patterns: key or key=
|
|
37
|
+
* Supports simple patterns: key or key=value.
|
|
38
38
|
*
|
|
39
|
-
* Keys must contain only letters, dashes, or underscores (no digits).
|
|
40
|
-
* Keys are converted to kebab-case. Values default to "true" if missing.
|
|
39
|
+
* - Keys must contain only letters, dashes, or underscores (no digits).
|
|
40
|
+
* - Keys are converted to kebab-case. Values default to "true" if missing.
|
|
41
|
+
* - Keys and values are stored in `props`.
|
|
42
|
+
* - If `propNames` is provided, only keys included in that list are extracted.
|
|
43
|
+
* - Unextracted tokens remain in `rest`.
|
|
41
44
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* @returns A plain object of parsed key-value pairs.
|
|
45
|
+
* @param meta - The input metadata string.
|
|
46
|
+
* @param propNames - Optional whitelist of property names to extract.
|
|
47
|
+
* @returns An object containing:
|
|
48
|
+
* - `props`: a map of extracted properties
|
|
49
|
+
* - `rest`: the remaining metadata string after extraction
|
|
48
50
|
*/
|
|
49
|
-
function parseMetaString(
|
|
50
|
-
if (!
|
|
51
|
+
function parseMetaString<Name extends string = string>(meta: ElementData['meta'], propNames?: Name[]): PropsType<Name> {
|
|
52
|
+
if (!meta) return { props: {}, rest: '' }
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
let str = meta
|
|
53
55
|
|
|
54
|
-
const keyValuePairRE = /([a-zA-Z_-]+)(?:=([^"'\s]+))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const keyValuePairRE = /(?<name>[a-zA-Z_-]+)(?:=([^"'\s]+))?/g
|
|
57
|
+
const props: PropsType['props'] = {}
|
|
58
|
+
|
|
59
|
+
str = str.replaceAll(keyValuePairRE, (match, name, value) => {
|
|
60
|
+
if (propNames && !propNames.includes(name)) return match
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
props[kebabCase(name)] = value || 'true'
|
|
63
|
+
return ''
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return { props, rest: str.trim() }
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
// Simple function to convert a camelCase or PascalCase string to kebab-case.
|
|
@@ -67,3 +73,8 @@ function kebabCase(str: string) {
|
|
|
67
73
|
.replace('_', '-')
|
|
68
74
|
.toLowerCase()
|
|
69
75
|
}
|
|
76
|
+
|
|
77
|
+
interface PropsType<Name extends string = string> {
|
|
78
|
+
props: Partial<Record<Name, string>>
|
|
79
|
+
rest: string
|
|
80
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export { remarkChoiceGroup }
|
|
2
|
+
|
|
3
|
+
import type { Code, Root } from 'mdast'
|
|
4
|
+
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
|
|
5
|
+
import type { ContainerDirective } from 'mdast-util-directive'
|
|
6
|
+
import { visit } from 'unist-util-visit'
|
|
7
|
+
import { parseMetaString } from './rehypeMetaToProps.js'
|
|
8
|
+
import { generateChoiceGroup } from './utils/generateChoiceGroup.js'
|
|
9
|
+
|
|
10
|
+
type Node = Code | MdxJsxFlowElement | ContainerDirective
|
|
11
|
+
|
|
12
|
+
function remarkChoiceGroup() {
|
|
13
|
+
return function (tree: Root) {
|
|
14
|
+
visit(tree, (node) => {
|
|
15
|
+
if (node.type === 'code') {
|
|
16
|
+
if (!node.meta) return
|
|
17
|
+
const meta = parseMetaString(node.meta, ['choice'])
|
|
18
|
+
const { choice } = meta.props
|
|
19
|
+
node.meta = meta.rest
|
|
20
|
+
|
|
21
|
+
if (choice) node.data ??= { choice }
|
|
22
|
+
}
|
|
23
|
+
if (node.type === 'containerDirective' && node.name === 'Choice') {
|
|
24
|
+
if (!node.attributes) return
|
|
25
|
+
const { id: choice } = node.attributes
|
|
26
|
+
if (choice) {
|
|
27
|
+
node.data ??= { choice }
|
|
28
|
+
node.attributes = {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const replaced = new WeakSet()
|
|
34
|
+
visit(tree, (node) => {
|
|
35
|
+
if (!('children' in node) || replaced.has(node)) return 'skip'
|
|
36
|
+
if (node.type === 'mdxJsxFlowElement') return 'skip'
|
|
37
|
+
|
|
38
|
+
let start = -1
|
|
39
|
+
let end = 0
|
|
40
|
+
|
|
41
|
+
const process = () => {
|
|
42
|
+
if (start === -1 || start === end) return
|
|
43
|
+
const nodes = node.children.slice(start, end) as Node[]
|
|
44
|
+
const groupedNodes = groupByNodeType(nodes)
|
|
45
|
+
const replacements: MdxJsxFlowElement[] = []
|
|
46
|
+
|
|
47
|
+
for (const groupedNode of groupedNodes) {
|
|
48
|
+
const replacement = generateChoiceGroup(groupedNode)
|
|
49
|
+
|
|
50
|
+
replacements.push(replacement)
|
|
51
|
+
replaced.add(replacement)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
node.children.splice(start, end - start, ...replacements)
|
|
55
|
+
|
|
56
|
+
end = start
|
|
57
|
+
start = -1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (; end < node.children.length; end++) {
|
|
61
|
+
const child = node.children[end]
|
|
62
|
+
|
|
63
|
+
if (!['code', 'mdxJsxFlowElement', 'containerDirective'].includes(child.type)) {
|
|
64
|
+
process()
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!child.data?.choice) {
|
|
69
|
+
process()
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (start === -1) start = end
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
process()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type NodeGroup = {
|
|
82
|
+
value: string
|
|
83
|
+
children: Node[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function groupByNodeType(nodes: Node[]) {
|
|
87
|
+
const groupedNodes = new Set<NodeGroup[]>()
|
|
88
|
+
const filters = [...new Set(nodes.flat().map((node) => (node.type === 'code' ? node.lang! : node.name)))]
|
|
89
|
+
|
|
90
|
+
filters.map((filter) => {
|
|
91
|
+
const nodesByChoice = new Map<string, Node[]>()
|
|
92
|
+
nodes
|
|
93
|
+
.filter((node) => (node.type === 'code' ? node.lang! : node.name) === filter)
|
|
94
|
+
.map((node) => {
|
|
95
|
+
const choice = node.data!.choice!
|
|
96
|
+
const nodes = nodesByChoice.get(choice) ?? []
|
|
97
|
+
nodes.push(node)
|
|
98
|
+
node.data = {}
|
|
99
|
+
nodesByChoice.set(choice, nodes)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
groupedNodes.add([...nodesByChoice].map(([name, nodes]) => ({ value: name, children: nodes })))
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return [...groupedNodes]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
declare module 'mdast' {
|
|
109
|
+
export interface Data {
|
|
110
|
+
choice?: string
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -82,7 +82,11 @@ function transformYaml(node: CodeNode) {
|
|
|
82
82
|
|
|
83
83
|
async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
84
84
|
const { codeBlock, index, parent } = node
|
|
85
|
-
const
|
|
85
|
+
const meta = parseMetaString(codeBlock.meta, ['max-width', 'choice'])
|
|
86
|
+
const maxWidth = Number(meta.props['max-width'])
|
|
87
|
+
const { choice } = meta.props
|
|
88
|
+
codeBlock.meta = meta.rest
|
|
89
|
+
|
|
86
90
|
let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value)
|
|
87
91
|
let codeBlockContentJs = ''
|
|
88
92
|
|
|
@@ -136,8 +140,8 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
|
136
140
|
// Add `hideToggle` attribute (prop) to `CodeSnippets` if the only change was replacing `.ts` with `.js`
|
|
137
141
|
if (codeBlockReplacedJs === codeBlockContentJs) {
|
|
138
142
|
attributes.push({
|
|
139
|
-
name: 'hideToggle',
|
|
140
143
|
type: 'mdxJsxAttribute',
|
|
144
|
+
name: 'hideToggle',
|
|
141
145
|
})
|
|
142
146
|
}
|
|
143
147
|
|
|
@@ -148,6 +152,9 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
|
|
|
148
152
|
children: [jsCode, codeBlock],
|
|
149
153
|
attributes,
|
|
150
154
|
}
|
|
155
|
+
|
|
156
|
+
if (choice) container.data ??= { choice }
|
|
157
|
+
|
|
151
158
|
parent.children.splice(index, 1, container)
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -164,7 +171,7 @@ function cleanUpCode(code: string, isJsCode: boolean = false) {
|
|
|
164
171
|
}
|
|
165
172
|
function processMagicComments(code: string) {
|
|
166
173
|
// @detype-replace DummyLayout Layout
|
|
167
|
-
const renameCommentRE =
|
|
174
|
+
const renameCommentRE = /^\s*\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm
|
|
168
175
|
const matches = Array.from(code.matchAll(renameCommentRE))
|
|
169
176
|
|
|
170
177
|
if (matches.length) {
|