@brillout/docpress 0.17.5 → 0.17.6
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/getHighlighter.spec.ts +30 -0
- package/code-blocks/getHighlighter.ts +77 -0
- package/code-blocks/remarkChoiceGroup.ts +6 -2
- package/code-blocks/remarkPkgManager.ts +15 -1
- package/code-blocks/utils/generateChoiceGroupCode.ts +22 -12
- package/dist/code-blocks/getHighlighter.d.ts +8 -0
- package/dist/code-blocks/getHighlighter.js +67 -0
- package/dist/code-blocks/remarkChoiceGroup.js +7 -2
- package/dist/code-blocks/remarkPkgManager.js +14 -1
- package/dist/code-blocks/utils/generateChoiceGroupCode.js +14 -4
- package/dist/vite.config.js +6 -1
- package/package.json +1 -1
- package/vite.config.ts +6 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getHighlighter } from './getHighlighter.js'
|
|
3
|
+
import type { Highlighter } from 'shiki'
|
|
4
|
+
|
|
5
|
+
describe('getHighlighter()', () => {
|
|
6
|
+
it('highlights lazily embedded grammars regardless of the order in which grammars are loaded', async () => {
|
|
7
|
+
// The markdown grammar lazily embeds the `yaml` grammar for the frontmatter
|
|
8
|
+
const code = ['---', 'name: "vike"', '---', '', 'See https://vike.dev/llms.txt'].join('\n')
|
|
9
|
+
const highlight = (highlighter: Highlighter) => highlighter.codeToHtml(code, { lang: 'md', theme: 'github-light' })
|
|
10
|
+
// Same options as Rehype Pretty Code
|
|
11
|
+
const options = { themes: ['github-light' as const], langs: ['plaintext' as const] }
|
|
12
|
+
|
|
13
|
+
const highlighter1 = await getHighlighter(options)
|
|
14
|
+
await highlighter1.loadLanguage('md')
|
|
15
|
+
const html1 = highlight(highlighter1)
|
|
16
|
+
|
|
17
|
+
const highlighter2 = await getHighlighter(options)
|
|
18
|
+
await highlighter2.loadLanguage('yaml')
|
|
19
|
+
await highlighter2.loadLanguage('md')
|
|
20
|
+
const html2 = highlight(highlighter2)
|
|
21
|
+
|
|
22
|
+
expect(html1).toBe(html2)
|
|
23
|
+
// The frontmatter is highlighted as YAML: `name` is a YAML key
|
|
24
|
+
expect(html1).toContain('>name</span>')
|
|
25
|
+
})
|
|
26
|
+
it('caches the highlighter per options', () => {
|
|
27
|
+
const options = { themes: ['github-light' as const], langs: ['plaintext' as const] }
|
|
28
|
+
expect(getHighlighter(options)).toBe(getHighlighter({ ...options }))
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export { getHighlighter }
|
|
2
|
+
export { warmUpHighlighter }
|
|
3
|
+
export { highlighterTheme }
|
|
4
|
+
|
|
5
|
+
import { getHighlighter as getHighlighterShiki, bundledLanguagesBase } from 'shiki'
|
|
6
|
+
import type { BundledHighlighterOptions, BundledLanguage, BundledTheme, Highlighter, LanguageRegistration } from 'shiki'
|
|
7
|
+
import type { Plugin } from 'vite'
|
|
8
|
+
|
|
9
|
+
const highlighterTheme = 'github-light'
|
|
10
|
+
// The options Rehype Pretty Code passes to `getHighlighter()`, see `rehypePrettyCode()` in
|
|
11
|
+
// node_modules/rehype-pretty-code/dist/index.js
|
|
12
|
+
const highlighterOptions: BundledHighlighterOptions<BundledLanguage, BundledTheme> = {
|
|
13
|
+
themes: [highlighterTheme],
|
|
14
|
+
langs: ['plaintext'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// TEMPORARY WORKAROUND
|
|
18
|
+
// TO-DO/eventually: remove this file (and its usages in vite.config.ts) once Rehype Pretty Code loads lazily embedded
|
|
19
|
+
// grammars deterministically, e.g. by using Shiki's `guessEmbeddedLanguages()`.
|
|
20
|
+
//
|
|
21
|
+
// Rehype Pretty Code loads grammars on-demand into a single highlighter that is shared by the client-side and
|
|
22
|
+
// server-side builds. Some grammars *lazily* embed other grammars, e.g. the markdown grammar lazily embeds the `yaml`
|
|
23
|
+
// grammar (for frontmatter) and most other grammars (for fenced code blocks): a lazily embedded grammar is used only if
|
|
24
|
+
// it happens to be already loaded.
|
|
25
|
+
//
|
|
26
|
+
// The highlighting of a ```md code block would thus depend on whether a ```yaml code block was processed before it —
|
|
27
|
+
// which differs between the client-side and server-side builds, leading to a hydration mismatch (React error #418).
|
|
28
|
+
//
|
|
29
|
+
// We make the highlighting deterministic by loading all grammars upfront (~0.5s and ~30MB, once per process).
|
|
30
|
+
//
|
|
31
|
+
// See:
|
|
32
|
+
// - https://github.com/brillout/docpress/pull/195
|
|
33
|
+
// - https://github.com/shikijs/shiki/pull/791 (Shiki lazily embeds grammars for performance: "users should already
|
|
34
|
+
// be loading the languages they need")
|
|
35
|
+
// - https://github.com/shikijs/shiki/issues/979 (same problem: `wikitext` doesn't load `html`)
|
|
36
|
+
// - https://github.com/shikijs/shiki/pull/1299 (`guessEmbeddedLanguages()` detects frontmatter, but Rehype Pretty Code
|
|
37
|
+
// doesn't use it — the problem still exists with rehype-pretty-code@0.14.5 and shiki@4.4.3)
|
|
38
|
+
const highlighters = new Map<string, Promise<Highlighter>>()
|
|
39
|
+
function getHighlighter(options: BundledHighlighterOptions<BundledLanguage, BundledTheme>): Promise<Highlighter> {
|
|
40
|
+
const key = JSON.stringify(options)
|
|
41
|
+
let highlighter = highlighters.get(key)
|
|
42
|
+
if (!highlighter) {
|
|
43
|
+
highlighter = createHighlighter(options)
|
|
44
|
+
highlighters.set(key, highlighter)
|
|
45
|
+
}
|
|
46
|
+
return highlighter
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function createHighlighter(
|
|
50
|
+
options: BundledHighlighterOptions<BundledLanguage, BundledTheme>,
|
|
51
|
+
): Promise<Highlighter> {
|
|
52
|
+
const highlighter = await getHighlighterShiki(options)
|
|
53
|
+
const langsAll = await Promise.all(
|
|
54
|
+
Object.values(bundledLanguagesBase).map(async (importLang) => (await importLang()).default),
|
|
55
|
+
)
|
|
56
|
+
// We load the grammars that lazily embed other grammars (markdown, mdx, vue, ...) last: Shiki re-compiles such a
|
|
57
|
+
// grammar each time one of its lazily embedded grammars is loaded.
|
|
58
|
+
const hasLangsEmbeddedLazy = (langs: LanguageRegistration[]) => langs.some((lang) => lang.embeddedLangsLazy?.length)
|
|
59
|
+
const langsOrdered = [
|
|
60
|
+
...langsAll.filter((langs) => !hasLangsEmbeddedLazy(langs)),
|
|
61
|
+
...langsAll.filter(hasLangsEmbeddedLazy),
|
|
62
|
+
]
|
|
63
|
+
for (const langs of langsOrdered) await highlighter.loadLanguage(...langs)
|
|
64
|
+
return highlighter
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Part of the TEMPORARY WORKAROUND above.
|
|
68
|
+
// We load the grammars before the build starts (instead of during the first MDX transform): Rolldown warns when
|
|
69
|
+
// plugins take a significant share of the build time, which the ~0.5s of grammar loading does for small builds.
|
|
70
|
+
function warmUpHighlighter(): Plugin {
|
|
71
|
+
return {
|
|
72
|
+
name: '@brillout/docpress:warmUpHighlighter',
|
|
73
|
+
async configResolved() {
|
|
74
|
+
await getHighlighter(highlighterOptions)
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -83,7 +83,7 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
|
|
|
83
83
|
remarkPkgManager.call(this)(tree, file)
|
|
84
84
|
|
|
85
85
|
visit(tree, 'mdxJsxFlowElement', (node) => {
|
|
86
|
-
// Descend into non-container nodes so that a `
|
|
86
|
+
// Descend into non-container nodes so that a `ChoiceGroupContainer` nested inside another JSX
|
|
87
87
|
// element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
|
|
88
88
|
// `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
|
|
89
89
|
if (node.name !== 'ChoiceGroupContainer') return
|
|
@@ -91,6 +91,10 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
|
|
|
91
91
|
const choiceGroupAll: ChoiceGroupWithParent[] = []
|
|
92
92
|
|
|
93
93
|
visit(node, 'mdxJsxFlowElement', (child) => {
|
|
94
|
+
// A nested container renders its own dropdowns: e.g. a code block inside a choice of a hidden group (a
|
|
95
|
+
// `:::Choice` toggled by `<Tabs>`), or inside a blockquote. Don't collect its groups here — it gets its own
|
|
96
|
+
// `choiceGroupAll` attribute when the outer traversal reaches it.
|
|
97
|
+
if (child !== node && child.name === 'ChoiceGroupContainer') return 'skip'
|
|
94
98
|
if (child.name !== 'ChoiceGroup') return
|
|
95
99
|
|
|
96
100
|
const choiceGroup = child.data?.customDataChoiceGroup
|
|
@@ -127,7 +131,7 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
|
|
|
127
131
|
|
|
128
132
|
node.attributes.push(expressionToAttribute('choiceGroupAll', choiceGroupAll))
|
|
129
133
|
|
|
130
|
-
return 'skip'
|
|
134
|
+
// Don't return 'skip': nested containers need their own `choiceGroupAll` attribute.
|
|
131
135
|
})
|
|
132
136
|
}
|
|
133
137
|
}
|
|
@@ -34,7 +34,7 @@ function remarkPkgManager() {
|
|
|
34
34
|
type: node.type,
|
|
35
35
|
lang: node.lang,
|
|
36
36
|
meta: node.meta,
|
|
37
|
-
value:
|
|
37
|
+
value: convertCommands(node.value, pm.toLowerCase() as 'pnpm' | 'bun' | 'yarn'),
|
|
38
38
|
})
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -45,3 +45,17 @@ function remarkPkgManager() {
|
|
|
45
45
|
})
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
// If the string contains `npx`, npm-to-yarn only replaces its first `npx` occurrence and leaves the rest of the string
|
|
50
|
+
// untouched (e.g. the `npm install` line of a multi-line command): convert each line separately.
|
|
51
|
+
function convertCommands(value: string, pm: 'pnpm' | 'yarn' | 'bun'): string {
|
|
52
|
+
return value
|
|
53
|
+
.split('\n')
|
|
54
|
+
.map((line) => (hasNpmCommand(line) ? convert(line, pm) : line))
|
|
55
|
+
.join('\n')
|
|
56
|
+
}
|
|
57
|
+
// Whether the line contains an `npm`/`npx` command. (Not a package name such as `skills-npm`, nor a comment like
|
|
58
|
+
// `# Make sure you install skills-npm`, which npm-to-yarn would mangle.)
|
|
59
|
+
function hasNpmCommand(line: string): boolean {
|
|
60
|
+
return /(^|\s)np[mx](\s|$)/.test(line)
|
|
61
|
+
}
|
|
@@ -56,9 +56,11 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
56
56
|
const customHidden = choiceNodes.some((node) =>
|
|
57
57
|
node.children.some((node) => node.type === 'containerDirective' && node.children[0]!.type !== 'code'),
|
|
58
58
|
)
|
|
59
|
-
|
|
59
|
+
// A hidden group doesn't render a dropdown: its choices are toggled by `<Tabs>`, or there is only one choice.
|
|
60
|
+
const hidden = hide || customHidden || choiceNodes.length === 1
|
|
60
61
|
|
|
61
62
|
const { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes)
|
|
63
|
+
const isBuiltIn = Object.keys(CHOICES_BUILT_IN).includes(choiceGroup.name)
|
|
62
64
|
const attributes: MdxJsxAttribute[] = []
|
|
63
65
|
const children: MdxJsxFlowElement[] = []
|
|
64
66
|
let data: MdxJsxFlowElementData = {}
|
|
@@ -95,24 +97,32 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
95
97
|
],
|
|
96
98
|
children: choiceChildren,
|
|
97
99
|
data: {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
// Mark the choice as parent so that the groups nested in it (e.g. the npm/pnpm toggle of a code block, or
|
|
101
|
+
// the JS/TS toggle) render their dropdown in this group's `<ChoiceGroupContainer>`, next to this group's
|
|
102
|
+
// dropdown, and only while this choice is selected.
|
|
103
|
+
// A hidden group renders no dropdown and its choices usually start with prose (it's toggled by `<Tabs>`), so
|
|
104
|
+
// the top-right corner of its container isn't the top-right corner of a code block: don't mark its choices,
|
|
105
|
+
// so that nested groups get their own `<ChoiceGroupContainer>` positioned at the code block instead. (That
|
|
106
|
+
// container is shown/hidden along with the choice, so no parent tracking is needed.)
|
|
107
|
+
...(!isBuiltIn &&
|
|
108
|
+
!hidden && {
|
|
109
|
+
customDataParentChoiceGroup: {
|
|
110
|
+
name: choiceGroup.name,
|
|
111
|
+
choice: choiceNode.choiceValue,
|
|
112
|
+
default: choiceGroup.default,
|
|
113
|
+
emptyChoices: choiceGroup.emptyChoices,
|
|
114
|
+
lvl,
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
107
117
|
},
|
|
108
118
|
})
|
|
109
119
|
}
|
|
110
120
|
|
|
111
121
|
const choiceGroupAttr: ChoiceGroup = {
|
|
112
122
|
...choiceGroup,
|
|
113
|
-
hidden
|
|
123
|
+
hidden,
|
|
114
124
|
lvl,
|
|
115
|
-
isBuiltIn
|
|
125
|
+
isBuiltIn,
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
attributes.push(expressionToAttribute('choiceGroup', choiceGroupAttr))
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { getHighlighter };
|
|
2
|
+
export { warmUpHighlighter };
|
|
3
|
+
export { highlighterTheme };
|
|
4
|
+
import type { BundledHighlighterOptions, BundledLanguage, BundledTheme, Highlighter } from 'shiki';
|
|
5
|
+
import type { Plugin } from 'vite';
|
|
6
|
+
declare const highlighterTheme = "github-light";
|
|
7
|
+
declare function getHighlighter(options: BundledHighlighterOptions<BundledLanguage, BundledTheme>): Promise<Highlighter>;
|
|
8
|
+
declare function warmUpHighlighter(): Plugin;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export { getHighlighter };
|
|
2
|
+
export { warmUpHighlighter };
|
|
3
|
+
export { highlighterTheme };
|
|
4
|
+
import { getHighlighter as getHighlighterShiki, bundledLanguagesBase } from 'shiki';
|
|
5
|
+
const highlighterTheme = 'github-light';
|
|
6
|
+
// The options Rehype Pretty Code passes to `getHighlighter()`, see `rehypePrettyCode()` in
|
|
7
|
+
// node_modules/rehype-pretty-code/dist/index.js
|
|
8
|
+
const highlighterOptions = {
|
|
9
|
+
themes: [highlighterTheme],
|
|
10
|
+
langs: ['plaintext'],
|
|
11
|
+
};
|
|
12
|
+
// TEMPORARY WORKAROUND
|
|
13
|
+
// TO-DO/eventually: remove this file (and its usages in vite.config.ts) once Rehype Pretty Code loads lazily embedded
|
|
14
|
+
// grammars deterministically, e.g. by using Shiki's `guessEmbeddedLanguages()`.
|
|
15
|
+
//
|
|
16
|
+
// Rehype Pretty Code loads grammars on-demand into a single highlighter that is shared by the client-side and
|
|
17
|
+
// server-side builds. Some grammars *lazily* embed other grammars, e.g. the markdown grammar lazily embeds the `yaml`
|
|
18
|
+
// grammar (for frontmatter) and most other grammars (for fenced code blocks): a lazily embedded grammar is used only if
|
|
19
|
+
// it happens to be already loaded.
|
|
20
|
+
//
|
|
21
|
+
// The highlighting of a ```md code block would thus depend on whether a ```yaml code block was processed before it —
|
|
22
|
+
// which differs between the client-side and server-side builds, leading to a hydration mismatch (React error #418).
|
|
23
|
+
//
|
|
24
|
+
// We make the highlighting deterministic by loading all grammars upfront (~0.5s and ~30MB, once per process).
|
|
25
|
+
//
|
|
26
|
+
// See:
|
|
27
|
+
// - https://github.com/brillout/docpress/pull/195
|
|
28
|
+
// - https://github.com/shikijs/shiki/pull/791 (Shiki lazily embeds grammars for performance: "users should already
|
|
29
|
+
// be loading the languages they need")
|
|
30
|
+
// - https://github.com/shikijs/shiki/issues/979 (same problem: `wikitext` doesn't load `html`)
|
|
31
|
+
// - https://github.com/shikijs/shiki/pull/1299 (`guessEmbeddedLanguages()` detects frontmatter, but Rehype Pretty Code
|
|
32
|
+
// doesn't use it — the problem still exists with rehype-pretty-code@0.14.5 and shiki@4.4.3)
|
|
33
|
+
const highlighters = new Map();
|
|
34
|
+
function getHighlighter(options) {
|
|
35
|
+
const key = JSON.stringify(options);
|
|
36
|
+
let highlighter = highlighters.get(key);
|
|
37
|
+
if (!highlighter) {
|
|
38
|
+
highlighter = createHighlighter(options);
|
|
39
|
+
highlighters.set(key, highlighter);
|
|
40
|
+
}
|
|
41
|
+
return highlighter;
|
|
42
|
+
}
|
|
43
|
+
async function createHighlighter(options) {
|
|
44
|
+
const highlighter = await getHighlighterShiki(options);
|
|
45
|
+
const langsAll = await Promise.all(Object.values(bundledLanguagesBase).map(async (importLang) => (await importLang()).default));
|
|
46
|
+
// We load the grammars that lazily embed other grammars (markdown, mdx, vue, ...) last: Shiki re-compiles such a
|
|
47
|
+
// grammar each time one of its lazily embedded grammars is loaded.
|
|
48
|
+
const hasLangsEmbeddedLazy = (langs) => langs.some((lang) => lang.embeddedLangsLazy?.length);
|
|
49
|
+
const langsOrdered = [
|
|
50
|
+
...langsAll.filter((langs) => !hasLangsEmbeddedLazy(langs)),
|
|
51
|
+
...langsAll.filter(hasLangsEmbeddedLazy),
|
|
52
|
+
];
|
|
53
|
+
for (const langs of langsOrdered)
|
|
54
|
+
await highlighter.loadLanguage(...langs);
|
|
55
|
+
return highlighter;
|
|
56
|
+
}
|
|
57
|
+
// Part of the TEMPORARY WORKAROUND above.
|
|
58
|
+
// We load the grammars before the build starts (instead of during the first MDX transform): Rolldown warns when
|
|
59
|
+
// plugins take a significant share of the build time, which the ~0.5s of grammar loading does for small builds.
|
|
60
|
+
function warmUpHighlighter() {
|
|
61
|
+
return {
|
|
62
|
+
name: '@brillout/docpress:warmUpHighlighter',
|
|
63
|
+
async configResolved() {
|
|
64
|
+
await getHighlighter(highlighterOptions);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -67,13 +67,18 @@ const remarkChoiceGroup = () => {
|
|
|
67
67
|
await remarkDetype.call(this)(tree, file);
|
|
68
68
|
remarkPkgManager.call(this)(tree, file);
|
|
69
69
|
visit(tree, 'mdxJsxFlowElement', (node) => {
|
|
70
|
-
// Descend into non-container nodes so that a `
|
|
70
|
+
// Descend into non-container nodes so that a `ChoiceGroupContainer` nested inside another JSX
|
|
71
71
|
// element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
|
|
72
72
|
// `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
|
|
73
73
|
if (node.name !== 'ChoiceGroupContainer')
|
|
74
74
|
return;
|
|
75
75
|
const choiceGroupAll = [];
|
|
76
76
|
visit(node, 'mdxJsxFlowElement', (child) => {
|
|
77
|
+
// A nested container renders its own dropdowns: e.g. a code block inside a choice of a hidden group (a
|
|
78
|
+
// `:::Choice` toggled by `<Tabs>`), or inside a blockquote. Don't collect its groups here — it gets its own
|
|
79
|
+
// `choiceGroupAll` attribute when the outer traversal reaches it.
|
|
80
|
+
if (child !== node && child.name === 'ChoiceGroupContainer')
|
|
81
|
+
return 'skip';
|
|
77
82
|
if (child.name !== 'ChoiceGroup')
|
|
78
83
|
return;
|
|
79
84
|
const choiceGroup = child.data?.customDataChoiceGroup;
|
|
@@ -104,7 +109,7 @@ const remarkChoiceGroup = () => {
|
|
|
104
109
|
}
|
|
105
110
|
});
|
|
106
111
|
node.attributes.push(expressionToAttribute('choiceGroupAll', choiceGroupAll));
|
|
107
|
-
return 'skip'
|
|
112
|
+
// Don't return 'skip': nested containers need their own `choiceGroupAll` attribute.
|
|
108
113
|
});
|
|
109
114
|
};
|
|
110
115
|
};
|
|
@@ -25,7 +25,7 @@ function remarkPkgManager() {
|
|
|
25
25
|
type: node.type,
|
|
26
26
|
lang: node.lang,
|
|
27
27
|
meta: node.meta,
|
|
28
|
-
value:
|
|
28
|
+
value: convertCommands(node.value, pm.toLowerCase()),
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
const choiceNodes = [...nodes].map(([name, node]) => ({ choiceValue: name, children: [node] }));
|
|
@@ -34,3 +34,16 @@ function remarkPkgManager() {
|
|
|
34
34
|
});
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
// If the string contains `npx`, npm-to-yarn only replaces its first `npx` occurrence and leaves the rest of the string
|
|
38
|
+
// untouched (e.g. the `npm install` line of a multi-line command): convert each line separately.
|
|
39
|
+
function convertCommands(value, pm) {
|
|
40
|
+
return value
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => (hasNpmCommand(line) ? convert(line, pm) : line))
|
|
43
|
+
.join('\n');
|
|
44
|
+
}
|
|
45
|
+
// Whether the line contains an `npm`/`npx` command. (Not a package name such as `skills-npm`, nor a comment like
|
|
46
|
+
// `# Make sure you install skills-npm`, which npm-to-yarn would mangle.)
|
|
47
|
+
function hasNpmCommand(line) {
|
|
48
|
+
return /(^|\s)np[mx](\s|$)/.test(line);
|
|
49
|
+
}
|
|
@@ -41,8 +41,10 @@ const CHOICES_BUILT_IN = {
|
|
|
41
41
|
function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
42
42
|
let lvl = 0;
|
|
43
43
|
const customHidden = choiceNodes.some((node) => node.children.some((node) => node.type === 'containerDirective' && node.children[0].type !== 'code'));
|
|
44
|
-
|
|
44
|
+
// A hidden group doesn't render a dropdown: its choices are toggled by `<Tabs>`, or there is only one choice.
|
|
45
|
+
const hidden = hide || customHidden || choiceNodes.length === 1;
|
|
45
46
|
const { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes);
|
|
47
|
+
const isBuiltIn = Object.keys(CHOICES_BUILT_IN).includes(choiceGroup.name);
|
|
46
48
|
const attributes = [];
|
|
47
49
|
const children = [];
|
|
48
50
|
let data = {};
|
|
@@ -76,7 +78,15 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
76
78
|
],
|
|
77
79
|
children: choiceChildren,
|
|
78
80
|
data: {
|
|
79
|
-
|
|
81
|
+
// Mark the choice as parent so that the groups nested in it (e.g. the npm/pnpm toggle of a code block, or
|
|
82
|
+
// the JS/TS toggle) render their dropdown in this group's `<ChoiceGroupContainer>`, next to this group's
|
|
83
|
+
// dropdown, and only while this choice is selected.
|
|
84
|
+
// A hidden group renders no dropdown and its choices usually start with prose (it's toggled by `<Tabs>`), so
|
|
85
|
+
// the top-right corner of its container isn't the top-right corner of a code block: don't mark its choices,
|
|
86
|
+
// so that nested groups get their own `<ChoiceGroupContainer>` positioned at the code block instead. (That
|
|
87
|
+
// container is shown/hidden along with the choice, so no parent tracking is needed.)
|
|
88
|
+
...(!isBuiltIn &&
|
|
89
|
+
!hidden && {
|
|
80
90
|
customDataParentChoiceGroup: {
|
|
81
91
|
name: choiceGroup.name,
|
|
82
92
|
choice: choiceNode.choiceValue,
|
|
@@ -90,9 +100,9 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
90
100
|
}
|
|
91
101
|
const choiceGroupAttr = {
|
|
92
102
|
...choiceGroup,
|
|
93
|
-
hidden
|
|
103
|
+
hidden,
|
|
94
104
|
lvl,
|
|
95
|
-
isBuiltIn
|
|
105
|
+
isBuiltIn,
|
|
96
106
|
};
|
|
97
107
|
attributes.push(expressionToAttribute('choiceGroup', choiceGroupAttr));
|
|
98
108
|
const choiceGroupNode = {
|
package/dist/vite.config.js
CHANGED
|
@@ -14,12 +14,15 @@ import { transformerNotationHighlight } from '@brillout/shiki-transformers';
|
|
|
14
14
|
import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js';
|
|
15
15
|
import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js';
|
|
16
16
|
import { remarkChoiceGroup } from './code-blocks/remarkChoiceGroup.js';
|
|
17
|
+
import { getHighlighter, warmUpHighlighter, highlighterTheme } from './code-blocks/getHighlighter.js';
|
|
17
18
|
const root = process.cwd();
|
|
18
19
|
const prettyCode = [
|
|
19
20
|
rehypePrettyCode,
|
|
20
21
|
{
|
|
21
|
-
theme:
|
|
22
|
+
theme: highlighterTheme,
|
|
22
23
|
keepBackground: false,
|
|
24
|
+
// TEMPORARY WORKAROUND, see getHighlighter.ts
|
|
25
|
+
getHighlighter,
|
|
23
26
|
transformers: [
|
|
24
27
|
transformerNotationDiff(),
|
|
25
28
|
transformerNotationHighlight(),
|
|
@@ -36,6 +39,8 @@ const config = {
|
|
|
36
39
|
parsePageSections(),
|
|
37
40
|
mdx({ rehypePlugins, remarkPlugins, providerImportSource: '@brillout/docpress' }),
|
|
38
41
|
react(),
|
|
42
|
+
// TEMPORARY WORKAROUND, see getHighlighter.ts
|
|
43
|
+
warmUpHighlighter(),
|
|
39
44
|
],
|
|
40
45
|
optimizeDeps: {
|
|
41
46
|
include: ['react', 'react-dom', 'react-dom/client'],
|
package/package.json
CHANGED
package/vite.config.ts
CHANGED
|
@@ -16,13 +16,16 @@ import { transformerNotationHighlight } from '@brillout/shiki-transformers'
|
|
|
16
16
|
import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js'
|
|
17
17
|
import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js'
|
|
18
18
|
import { remarkChoiceGroup } from './code-blocks/remarkChoiceGroup.js'
|
|
19
|
+
import { getHighlighter, warmUpHighlighter, highlighterTheme } from './code-blocks/getHighlighter.js'
|
|
19
20
|
|
|
20
21
|
const root = process.cwd()
|
|
21
22
|
const prettyCode = [
|
|
22
23
|
rehypePrettyCode,
|
|
23
24
|
{
|
|
24
|
-
theme:
|
|
25
|
+
theme: highlighterTheme,
|
|
25
26
|
keepBackground: false,
|
|
27
|
+
// TEMPORARY WORKAROUND, see getHighlighter.ts
|
|
28
|
+
getHighlighter,
|
|
26
29
|
transformers: [
|
|
27
30
|
transformerNotationDiff(),
|
|
28
31
|
transformerNotationHighlight(),
|
|
@@ -40,6 +43,8 @@ const config: UserConfig = {
|
|
|
40
43
|
parsePageSections(),
|
|
41
44
|
mdx({ rehypePlugins, remarkPlugins, providerImportSource: '@brillout/docpress' }) as PluginOption,
|
|
42
45
|
react(),
|
|
46
|
+
// TEMPORARY WORKAROUND, see getHighlighter.ts
|
|
47
|
+
warmUpHighlighter(),
|
|
43
48
|
],
|
|
44
49
|
optimizeDeps: {
|
|
45
50
|
include: ['react', 'react-dom', 'react-dom/client'],
|