@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 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, { useEffect, useRef } from '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 = useRef<null | { top: number; el: Element }>(null)
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, { ComponentPropsWithoutRef, useState } from 'react'
3
+ import React, { useState } from 'react'
4
+ import { cls } from '../../utils/cls'
4
5
  import './Pre.css'
5
6
 
6
- function Pre({ children, ...props }: ComponentPropsWithoutRef<'pre'> & { 'hide-menu'?: string }) {
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 {...props}>
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 { useState, useEffect, useCallback } from 'react'
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
- const [codeLangSelected, setCodeLangSelected] = useState(codeLangDefaultSsr)
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 props = parseMetaString(node.data?.meta)
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
- * Minimal parser for a metadata string into key-value pairs.
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="value".
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
- * Example:
43
- * parseMetaString('foo fooBar="value"')
44
- * => { foo: 'true', foo_bar: 'value' }
45
- *
46
- * @param metaString - The input metadata string.
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(metaString: ElementData['meta']): Record<string, string> {
50
- if (!metaString) return {}
51
+ function parseMetaString<Name extends string = string>(meta: ElementData['meta'], propNames?: Name[]): PropsType<Name> {
52
+ if (!meta) return { props: {}, rest: '' }
51
53
 
52
- const props: Record<string, string> = {}
54
+ let str = meta
53
55
 
54
- const keyValuePairRE = /([a-zA-Z_-]+)(?:=([^"'\s]+))?(?=\s|$)/g
55
- for (const match of metaString.matchAll(keyValuePairRE)) {
56
- let [_, key, value] = match
57
- props[kebabCase(key)] = value || 'true'
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
- return props
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 maxWidth = Number(parseMetaString(codeBlock.meta)['max-width'])
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 = /^\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm
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) {