mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-12 00:27:21 +02:00
feat(theme-classic): extensible code block magic comment system (#7178)
This commit is contained in:
parent
785fed723f
commit
51815c12c9
14 changed files with 692 additions and 161 deletions
|
@ -36,6 +36,13 @@ describe('themeConfig', () => {
|
|||
darkTheme,
|
||||
defaultLanguage: 'javascript',
|
||||
additionalLanguages: ['kotlin', 'java'],
|
||||
magicComments: [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
],
|
||||
},
|
||||
docs: {
|
||||
versionPersistence: 'localStorage',
|
||||
|
@ -549,16 +556,72 @@ describe('themeConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('accepts valid prism config', () => {
|
||||
const prismConfig = {
|
||||
prism: {
|
||||
additionalLanguages: ['kotlin', 'java'],
|
||||
theme: darkTheme,
|
||||
},
|
||||
};
|
||||
expect(testValidateThemeConfig(prismConfig)).toEqual({
|
||||
...DEFAULT_CONFIG,
|
||||
...prismConfig,
|
||||
describe('prism config', () => {
|
||||
it('accepts a range of magic comments', () => {
|
||||
const prismConfig = {
|
||||
prism: {
|
||||
additionalLanguages: ['kotlin', 'java'],
|
||||
theme: darkTheme,
|
||||
magicComments: [],
|
||||
},
|
||||
};
|
||||
expect(testValidateThemeConfig(prismConfig)).toEqual({
|
||||
...DEFAULT_CONFIG,
|
||||
...prismConfig,
|
||||
});
|
||||
const prismConfig2 = {
|
||||
prism: {
|
||||
additionalLanguages: [],
|
||||
theme: darkTheme,
|
||||
magicComments: [
|
||||
{
|
||||
className: 'a',
|
||||
line: 'a-next-line',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(testValidateThemeConfig(prismConfig2)).toEqual({
|
||||
...DEFAULT_CONFIG,
|
||||
...prismConfig2,
|
||||
});
|
||||
const prismConfig3 = {
|
||||
prism: {
|
||||
additionalLanguages: [],
|
||||
theme: darkTheme,
|
||||
magicComments: [
|
||||
{
|
||||
className: 'a',
|
||||
block: {start: 'a-start', end: 'a-end'},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(testValidateThemeConfig(prismConfig3)).toEqual({
|
||||
...DEFAULT_CONFIG,
|
||||
...prismConfig3,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects incomplete magic comments', () => {
|
||||
expect(() =>
|
||||
testValidateThemeConfig({
|
||||
prism: {
|
||||
magicComments: [{className: 'a'}],
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""prism.magicComments[0]" must contain at least one of [line, block]"`,
|
||||
);
|
||||
expect(() =>
|
||||
testValidateThemeConfig({
|
||||
prism: {
|
||||
magicComments: [{className: 'a', block: {start: 'start'}}],
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`""prism.magicComments[0].block.end" is required"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -219,7 +219,7 @@ declare module '@theme/CodeBlock/Line' {
|
|||
|
||||
export interface Props {
|
||||
readonly line: Token[];
|
||||
readonly highlight: boolean;
|
||||
readonly classNames: string[] | undefined;
|
||||
readonly showLineNumbers: boolean;
|
||||
readonly getLineProps: GetLineProps;
|
||||
readonly getTokenProps: GetTokenProps;
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function CodeBlockString({
|
|||
language: languageProp,
|
||||
}: Props): JSX.Element {
|
||||
const {
|
||||
prism: {defaultLanguage},
|
||||
prism: {defaultLanguage, magicComments},
|
||||
} = useThemeConfig();
|
||||
const language =
|
||||
languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage;
|
||||
|
@ -46,7 +46,11 @@ export default function CodeBlockString({
|
|||
// "title=\"xyz\"" => title: "\"xyz\""
|
||||
const title = parseCodeBlockTitle(metastring) || titleProp;
|
||||
|
||||
const {highlightLines, code} = parseLines(children, metastring, language);
|
||||
const {lineClassNames, code} = parseLines(children, {
|
||||
metastring,
|
||||
language,
|
||||
magicComments,
|
||||
});
|
||||
const showLineNumbers =
|
||||
showLineNumbersProp || containsLineNumbers(metastring);
|
||||
|
||||
|
@ -83,7 +87,7 @@ export default function CodeBlockString({
|
|||
line={line}
|
||||
getLineProps={getLineProps}
|
||||
getTokenProps={getTokenProps}
|
||||
highlight={highlightLines.includes(i)}
|
||||
classNames={lineClassNames[i]}
|
||||
showLineNumbers={showLineNumbers}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -12,7 +12,7 @@ import styles from './styles.module.css';
|
|||
|
||||
export default function CodeBlockLine({
|
||||
line,
|
||||
highlight,
|
||||
classNames,
|
||||
showLineNumbers,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
|
@ -23,17 +23,9 @@ export default function CodeBlockLine({
|
|||
|
||||
const lineProps = getLineProps({
|
||||
line,
|
||||
...(showLineNumbers && {className: styles.codeLine}),
|
||||
className: clsx(classNames, showLineNumbers && styles.codeLine),
|
||||
});
|
||||
|
||||
if (highlight) {
|
||||
lineProps.className = clsx(
|
||||
lineProps.className,
|
||||
styles.highlightedCodeLine,
|
||||
'theme-code-block-highlighted-line',
|
||||
);
|
||||
}
|
||||
|
||||
const lineTokens = line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({token, key})} />
|
||||
));
|
||||
|
|
|
@ -5,6 +5,13 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
:global(.theme-code-block-highlighted-line) {
|
||||
background-color: var(--docusaurus-highlighted-code-line-bg);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
}
|
||||
|
||||
/* Intentionally has zero specificity, so that to be able to override
|
||||
the background in custom CSS file due bug https://github.com/facebook/docusaurus/issues/3678 */
|
||||
:where(:root) {
|
||||
|
@ -15,13 +22,6 @@ the background in custom CSS file due bug https://github.com/facebook/docusaurus
|
|||
--docusaurus-highlighted-code-line-bg: rgb(100 100 100);
|
||||
}
|
||||
|
||||
.highlightedCodeLine {
|
||||
background-color: var(--docusaurus-highlighted-code-line-bg);
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
}
|
||||
|
||||
.codeLine {
|
||||
display: table-row;
|
||||
counter-increment: line-count;
|
||||
|
|
|
@ -44,6 +44,13 @@ export const DEFAULT_CONFIG = {
|
|||
prism: {
|
||||
additionalLanguages: [],
|
||||
theme: defaultPrismTheme,
|
||||
magicComments: [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
],
|
||||
},
|
||||
navbar: {
|
||||
hideOnScroll: false,
|
||||
|
@ -386,6 +393,18 @@ export const ThemeConfigSchema = Joi.object({
|
|||
additionalLanguages: Joi.array()
|
||||
.items(Joi.string())
|
||||
.default(DEFAULT_CONFIG.prism.additionalLanguages),
|
||||
magicComments: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
className: Joi.string().required(),
|
||||
line: Joi.string(),
|
||||
block: Joi.object({
|
||||
start: Joi.string().required(),
|
||||
end: Joi.string().required(),
|
||||
}),
|
||||
}).or('line', 'block'),
|
||||
)
|
||||
.default(DEFAULT_CONFIG.prism.magicComments),
|
||||
})
|
||||
.default(DEFAULT_CONFIG.prism)
|
||||
.unknown(),
|
||||
|
|
|
@ -4,9 +4,11 @@ exports[`parseLines does not parse content with metastring 1`] = `
|
|||
{
|
||||
"code": "aaaaa
|
||||
nnnnn",
|
||||
"highlightLines": [
|
||||
0,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -15,9 +17,11 @@ exports[`parseLines does not parse content with metastring 2`] = `
|
|||
"code": "// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"highlightLines": [
|
||||
0,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -25,9 +29,11 @@ exports[`parseLines does not parse content with metastring 3`] = `
|
|||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"highlightLines": [
|
||||
0,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -36,7 +42,100 @@ exports[`parseLines does not parse content with no language 1`] = `
|
|||
"code": "// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"highlightLines": [],
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines handles one line with multiple class names 1`] = `
|
||||
{
|
||||
"code": "
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
Only highlighted
|
||||
Only collapsed
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
Only collapsed
|
||||
highlighted and collapsed",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"2": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"3": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"4": [
|
||||
"highlight",
|
||||
],
|
||||
"5": [
|
||||
"collapse",
|
||||
],
|
||||
"6": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"7": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"8": [
|
||||
"collapse",
|
||||
],
|
||||
"9": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines handles one line with multiple class names 2`] = `
|
||||
{
|
||||
"code": "line
|
||||
line",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
],
|
||||
"1": [
|
||||
"b",
|
||||
"d",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines parses multiple types of magic comments 1`] = `
|
||||
{
|
||||
"code": "
|
||||
highlighted
|
||||
collapsed
|
||||
collapsed
|
||||
collapsed",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"highlight",
|
||||
],
|
||||
"2": [
|
||||
"collapse",
|
||||
],
|
||||
"3": [
|
||||
"collapse",
|
||||
],
|
||||
"4": [
|
||||
"collapse",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -44,9 +143,11 @@ exports[`parseLines removes lines correctly 1`] = `
|
|||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"highlightLines": [
|
||||
0,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -54,9 +155,11 @@ exports[`parseLines removes lines correctly 2`] = `
|
|||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"highlightLines": [
|
||||
0,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -65,12 +168,18 @@ exports[`parseLines removes lines correctly 3`] = `
|
|||
"code": "aaaaa
|
||||
bbbbbbb
|
||||
bbbbb",
|
||||
"highlightLines": [
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"2": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -80,10 +189,14 @@ exports[`parseLines respects language: html 1`] = `
|
|||
{/* highlight-next-line */}
|
||||
bbbbb
|
||||
dddd",
|
||||
"highlightLines": [
|
||||
0,
|
||||
3,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"3": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -92,7 +205,7 @@ exports[`parseLines respects language: js 1`] = `
|
|||
"code": "# highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"highlightLines": [],
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -102,10 +215,14 @@ exports[`parseLines respects language: jsx 1`] = `
|
|||
bbbbb
|
||||
<!-- highlight-next-line -->
|
||||
dddd",
|
||||
"highlightLines": [
|
||||
0,
|
||||
1,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -128,11 +245,17 @@ dddd
|
|||
// highlight-next-line
|
||||
console.log("preserved");
|
||||
\`\`\`",
|
||||
"highlightLines": [
|
||||
1,
|
||||
7,
|
||||
11,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"11": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"7": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -142,12 +265,20 @@ exports[`parseLines respects language: none 1`] = `
|
|||
bbbbb
|
||||
ccccc
|
||||
dddd",
|
||||
"highlightLines": [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"2": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"3": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -156,7 +287,7 @@ exports[`parseLines respects language: py 1`] = `
|
|||
"code": "/* highlight-next-line */
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"highlightLines": [],
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -169,8 +300,10 @@ bbbbb
|
|||
ccccc
|
||||
<!-- highlight-next-line -->
|
||||
dddd",
|
||||
"highlightLines": [
|
||||
4,
|
||||
],
|
||||
"lineClassNames": {
|
||||
"4": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
type MagicCommentConfig,
|
||||
parseCodeBlockTitle,
|
||||
parseLanguage,
|
||||
parseLines,
|
||||
|
@ -67,24 +68,58 @@ describe('parseLanguage', () => {
|
|||
});
|
||||
|
||||
describe('parseLines', () => {
|
||||
const defaultMagicComments: MagicCommentConfig[] = [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
];
|
||||
|
||||
it('does not parse content with metastring', () => {
|
||||
expect(parseLines('aaaaa\nnnnnn', '{1}', 'js')).toMatchSnapshot();
|
||||
expect(
|
||||
parseLines('aaaaa\nnnnnn', {
|
||||
metastring: '{1}',
|
||||
language: 'js',
|
||||
magicComments: defaultMagicComments,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb`,
|
||||
'{1}',
|
||||
'js',
|
||||
{
|
||||
metastring: '{1}',
|
||||
language: 'js',
|
||||
magicComments: defaultMagicComments,
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
parseLines(
|
||||
`aaaaa
|
||||
bbbbb`,
|
||||
'{1}',
|
||||
{
|
||||
metastring: '{1}',
|
||||
language: 'undefined',
|
||||
magicComments: defaultMagicComments,
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
expect(() =>
|
||||
parseLines(
|
||||
`aaaaa
|
||||
bbbbb`,
|
||||
{
|
||||
metastring: '{1}',
|
||||
language: 'js',
|
||||
magicComments: [],
|
||||
},
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"A highlight range has been given in code block's metastring (\`\`\` {1}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges."`,
|
||||
);
|
||||
});
|
||||
it('does not parse content with no language', () => {
|
||||
expect(
|
||||
|
@ -92,8 +127,11 @@ bbbbb`,
|
|||
`// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb`,
|
||||
'',
|
||||
undefined,
|
||||
{
|
||||
metastring: '',
|
||||
language: undefined,
|
||||
magicComments: defaultMagicComments,
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
@ -103,8 +141,7 @@ bbbbb`,
|
|||
`// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb`,
|
||||
'',
|
||||
'js',
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
|
@ -113,8 +150,7 @@ bbbbb`,
|
|||
aaaaa
|
||||
// highlight-end
|
||||
bbbbb`,
|
||||
'',
|
||||
'js',
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
|
@ -126,8 +162,7 @@ bbbbbbb
|
|||
// highlight-next-line
|
||||
// highlight-end
|
||||
bbbbb`,
|
||||
'',
|
||||
'js',
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
@ -137,8 +172,7 @@ bbbbb`,
|
|||
`# highlight-next-line
|
||||
aaaaa
|
||||
bbbbb`,
|
||||
'',
|
||||
'js',
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('js');
|
||||
expect(
|
||||
|
@ -146,8 +180,7 @@ bbbbb`,
|
|||
`/* highlight-next-line */
|
||||
aaaaa
|
||||
bbbbb`,
|
||||
'',
|
||||
'py',
|
||||
{metastring: '', language: 'py', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('py');
|
||||
expect(
|
||||
|
@ -160,8 +193,7 @@ bbbbb
|
|||
ccccc
|
||||
<!-- highlight-next-line -->
|
||||
dddd`,
|
||||
'',
|
||||
'py',
|
||||
{metastring: '', language: 'py', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('py');
|
||||
expect(
|
||||
|
@ -174,8 +206,7 @@ bbbbb
|
|||
ccccc
|
||||
<!-- highlight-next-line -->
|
||||
dddd`,
|
||||
'',
|
||||
'',
|
||||
{metastring: '', language: '', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('none');
|
||||
expect(
|
||||
|
@ -186,8 +217,7 @@ aaaa
|
|||
bbbbb
|
||||
<!-- highlight-next-line -->
|
||||
dddd`,
|
||||
'',
|
||||
'jsx',
|
||||
{metastring: '', language: 'jsx', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('jsx');
|
||||
expect(
|
||||
|
@ -198,8 +228,7 @@ aaaa
|
|||
bbbbb
|
||||
<!-- highlight-next-line -->
|
||||
dddd`,
|
||||
'',
|
||||
'html',
|
||||
{metastring: '', language: 'html', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('html');
|
||||
expect(
|
||||
|
@ -225,9 +254,109 @@ dddd
|
|||
console.log("preserved");
|
||||
\`\`\`
|
||||
`,
|
||||
'',
|
||||
'md',
|
||||
{metastring: '', language: 'md', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('md');
|
||||
});
|
||||
|
||||
it('parses multiple types of magic comments', () => {
|
||||
expect(
|
||||
parseLines(
|
||||
`
|
||||
// highlight-next-line
|
||||
highlighted
|
||||
// collapse-next-line
|
||||
collapsed
|
||||
/* collapse-start */
|
||||
collapsed
|
||||
collapsed
|
||||
/* collapse-end */
|
||||
`,
|
||||
{
|
||||
language: 'js',
|
||||
metastring: '',
|
||||
magicComments: [
|
||||
{
|
||||
className: 'highlight',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
{
|
||||
className: 'collapse',
|
||||
line: 'collapse-next-line',
|
||||
block: {start: 'collapse-start', end: 'collapse-end'},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('handles one line with multiple class names', () => {
|
||||
expect(
|
||||
parseLines(
|
||||
`
|
||||
// highlight-next-line
|
||||
// collapse-next-line
|
||||
highlighted and collapsed
|
||||
/* collapse-start */
|
||||
/* highlight-start */
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
/* collapse-end */
|
||||
Only highlighted
|
||||
/* highlight-end */
|
||||
/* collapse-start */
|
||||
Only collapsed
|
||||
/* highlight-start */
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
/* highlight-end */
|
||||
Only collapsed
|
||||
// highlight-next-line
|
||||
highlighted and collapsed
|
||||
/* collapse-end */
|
||||
`,
|
||||
{
|
||||
language: 'js',
|
||||
metastring: '',
|
||||
magicComments: [
|
||||
{
|
||||
className: 'highlight',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
{
|
||||
className: 'collapse',
|
||||
line: 'collapse-next-line',
|
||||
block: {start: 'collapse-start', end: 'collapse-end'},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
expect(
|
||||
parseLines(
|
||||
`// a
|
||||
// b
|
||||
// c
|
||||
// d
|
||||
line
|
||||
// b
|
||||
// d
|
||||
line
|
||||
`,
|
||||
{
|
||||
language: 'js',
|
||||
metastring: '',
|
||||
magicComments: [
|
||||
{className: 'a', line: 'a'},
|
||||
{className: 'b', line: 'b'},
|
||||
{className: 'c', line: 'c'},
|
||||
{className: 'd', line: 'd'},
|
||||
],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ import type {PrismTheme} from 'prism-react-renderer';
|
|||
import type {CSSProperties} from 'react';
|
||||
|
||||
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
|
||||
const highlightLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
|
||||
const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
|
||||
|
||||
// Supported types of highlight comments
|
||||
const commentPatterns = {
|
||||
|
@ -23,18 +23,23 @@ const commentPatterns = {
|
|||
|
||||
type CommentType = keyof typeof commentPatterns;
|
||||
|
||||
const magicCommentDirectives = [
|
||||
'highlight-next-line',
|
||||
'highlight-start',
|
||||
'highlight-end',
|
||||
];
|
||||
export type MagicCommentConfig = {
|
||||
className: string;
|
||||
line?: string;
|
||||
block?: {start: string; end: string};
|
||||
};
|
||||
|
||||
function getCommentPattern(languages: CommentType[]) {
|
||||
function getCommentPattern(
|
||||
languages: CommentType[],
|
||||
magicCommentDirectives: MagicCommentConfig[],
|
||||
) {
|
||||
// To be more reliable, the opening and closing comment must match
|
||||
const commentPattern = languages
|
||||
.map((lang) => {
|
||||
const {start, end} = commentPatterns[lang];
|
||||
return `(?:${start}\\s*(${magicCommentDirectives.join('|')})\\s*${end})`;
|
||||
return `(?:${start}\\s*(${magicCommentDirectives
|
||||
.flatMap((d) => [d.line, d.block?.start, d.block?.end].filter(Boolean))
|
||||
.join('|')})\\s*${end})`;
|
||||
})
|
||||
.join('|');
|
||||
// White space is allowed, but otherwise it should be on it's own line
|
||||
|
@ -44,34 +49,46 @@ function getCommentPattern(languages: CommentType[]) {
|
|||
/**
|
||||
* Select comment styles based on language
|
||||
*/
|
||||
function getAllMagicCommentDirectiveStyles(lang: string) {
|
||||
function getAllMagicCommentDirectiveStyles(
|
||||
lang: string,
|
||||
magicCommentDirectives: MagicCommentConfig[],
|
||||
) {
|
||||
switch (lang) {
|
||||
case 'js':
|
||||
case 'javascript':
|
||||
case 'ts':
|
||||
case 'typescript':
|
||||
return getCommentPattern(['js', 'jsBlock']);
|
||||
return getCommentPattern(['js', 'jsBlock'], magicCommentDirectives);
|
||||
|
||||
case 'jsx':
|
||||
case 'tsx':
|
||||
return getCommentPattern(['js', 'jsBlock', 'jsx']);
|
||||
return getCommentPattern(
|
||||
['js', 'jsBlock', 'jsx'],
|
||||
magicCommentDirectives,
|
||||
);
|
||||
|
||||
case 'html':
|
||||
return getCommentPattern(['js', 'jsBlock', 'html']);
|
||||
return getCommentPattern(
|
||||
['js', 'jsBlock', 'html'],
|
||||
magicCommentDirectives,
|
||||
);
|
||||
|
||||
case 'python':
|
||||
case 'py':
|
||||
case 'bash':
|
||||
return getCommentPattern(['bash']);
|
||||
return getCommentPattern(['bash'], magicCommentDirectives);
|
||||
|
||||
case 'markdown':
|
||||
case 'md':
|
||||
// Text uses HTML, front matter uses bash
|
||||
return getCommentPattern(['html', 'jsx', 'bash']);
|
||||
return getCommentPattern(['html', 'jsx', 'bash'], magicCommentDirectives);
|
||||
|
||||
default:
|
||||
// All comment types
|
||||
return getCommentPattern(Object.keys(commentPatterns) as CommentType[]);
|
||||
return getCommentPattern(
|
||||
Object.keys(commentPatterns) as CommentType[],
|
||||
magicCommentDirectives,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,50 +116,91 @@ export function parseLanguage(className: string): string | undefined {
|
|||
* Parses the code content, strips away any magic comments, and returns the
|
||||
* clean content and the highlighted lines marked by the comments or metastring.
|
||||
*
|
||||
* If the metastring contains highlight range, the `content` will be returned
|
||||
* as-is without any parsing.
|
||||
* If the metastring contains a range, the `content` will be returned as-is
|
||||
* without any parsing. The returned `lineClassNames` will be a map from that
|
||||
* number range to the first magic comment config entry (which _should_ be for
|
||||
* line highlight directives.)
|
||||
*
|
||||
* @param content The raw code with magic comments. Trailing newline will be
|
||||
* trimmed upfront.
|
||||
* @param metastring The full metastring, as received from MDX. Highlight range
|
||||
* declared here starts at 1.
|
||||
* @param language Language of the code block, used to determine which kinds of
|
||||
* magic comment styles to enable.
|
||||
* @param options Options for parsing behavior.
|
||||
*/
|
||||
export function parseLines(
|
||||
content: string,
|
||||
metastring?: string,
|
||||
language?: string,
|
||||
options: {
|
||||
/**
|
||||
* The full metastring, as received from MDX. Line ranges declared here
|
||||
* start at 1.
|
||||
*/
|
||||
metastring: string | undefined;
|
||||
/**
|
||||
* Language of the code block, used to determine which kinds of magic
|
||||
* comment styles to enable.
|
||||
*/
|
||||
language: string | undefined;
|
||||
/**
|
||||
* Magic comment types that we should try to parse. Each entry would
|
||||
* correspond to one class name to apply to each line.
|
||||
*/
|
||||
magicComments: MagicCommentConfig[];
|
||||
},
|
||||
): {
|
||||
/**
|
||||
* The highlighted lines, 0-indexed. e.g. `[0, 1, 4]` means the 1st, 2nd, and
|
||||
* 5th lines are highlighted.
|
||||
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
|
||||
* means the 1st line should have `highlight` and `sample` as class names.
|
||||
*/
|
||||
highlightLines: number[];
|
||||
lineClassNames: {[lineIndex: number]: string[]};
|
||||
/**
|
||||
* The clean code without any magic comments (only if highlight range isn't
|
||||
* present in the metastring).
|
||||
* If there's number range declared in the metastring, the code block is
|
||||
* returned as-is (no parsing); otherwise, this is the clean code with all
|
||||
* magic comments stripped away.
|
||||
*/
|
||||
code: string;
|
||||
} {
|
||||
let code = content.replace(/\n$/, '');
|
||||
const {language, magicComments, metastring} = options;
|
||||
// Highlighted lines specified in props: don't parse the content
|
||||
if (metastring && highlightLinesRangeRegex.test(metastring)) {
|
||||
const highlightLinesRange = metastring.match(highlightLinesRangeRegex)!
|
||||
.groups!.range!;
|
||||
const highlightLines = rangeParser(highlightLinesRange)
|
||||
if (metastring && metastringLinesRangeRegex.test(metastring)) {
|
||||
const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
|
||||
.range!;
|
||||
if (magicComments.length === 0) {
|
||||
throw new Error(
|
||||
`A highlight range has been given in code block's metastring (\`\`\` ${metastring}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges.`,
|
||||
);
|
||||
}
|
||||
const metastringRangeClassName = magicComments[0]!.className;
|
||||
const lines = rangeParser(linesRange)
|
||||
.filter((n) => n > 0)
|
||||
.map((n) => n - 1);
|
||||
return {highlightLines, code};
|
||||
.map((n) => [n - 1, [metastringRangeClassName]]);
|
||||
return {lineClassNames: Object.fromEntries(lines), code};
|
||||
}
|
||||
if (language === undefined) {
|
||||
return {highlightLines: [], code};
|
||||
return {lineClassNames: {}, code};
|
||||
}
|
||||
const directiveRegex = getAllMagicCommentDirectiveStyles(language);
|
||||
const directiveRegex = getAllMagicCommentDirectiveStyles(
|
||||
language,
|
||||
magicComments,
|
||||
);
|
||||
// Go through line by line
|
||||
const lines = code.split('\n');
|
||||
let highlightBlockStart: number;
|
||||
let highlightRange = '';
|
||||
const blocks = Object.fromEntries(
|
||||
magicComments.map((d) => [d.className, {start: 0, range: ''}]),
|
||||
);
|
||||
const lineToClassName: {[comment: string]: string} = Object.fromEntries(
|
||||
magicComments
|
||||
.filter((d) => d.line)
|
||||
.map(({className, line}) => [line, className]),
|
||||
);
|
||||
const blockStartToClassName: {[comment: string]: string} = Object.fromEntries(
|
||||
magicComments
|
||||
.filter((d) => d.block)
|
||||
.map(({className, block}) => [block!.start, className]),
|
||||
);
|
||||
const blockEndToClassName: {[comment: string]: string} = Object.fromEntries(
|
||||
magicComments
|
||||
.filter((d) => d.block)
|
||||
.map(({className, block}) => [block!.end, className]),
|
||||
);
|
||||
for (let lineNumber = 0; lineNumber < lines.length; ) {
|
||||
const line = lines[lineNumber]!;
|
||||
const match = line.match(directiveRegex);
|
||||
|
@ -151,28 +209,27 @@ export function parseLines(
|
|||
lineNumber += 1;
|
||||
continue;
|
||||
}
|
||||
const directive = match.slice(1).find((item) => item !== undefined);
|
||||
switch (directive) {
|
||||
case 'highlight-next-line':
|
||||
highlightRange += `${lineNumber},`;
|
||||
break;
|
||||
|
||||
case 'highlight-start':
|
||||
highlightBlockStart = lineNumber;
|
||||
break;
|
||||
|
||||
case 'highlight-end':
|
||||
highlightRange += `${highlightBlockStart!}-${lineNumber - 1},`;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
const directive = match.slice(1).find((item) => item !== undefined)!;
|
||||
if (lineToClassName[directive]) {
|
||||
blocks[lineToClassName[directive]!]!.range += `${lineNumber},`;
|
||||
} else if (blockStartToClassName[directive]) {
|
||||
blocks[blockStartToClassName[directive]!]!.start = lineNumber;
|
||||
} else if (blockEndToClassName[directive]) {
|
||||
blocks[blockEndToClassName[directive]!]!.range += `${
|
||||
blocks[blockEndToClassName[directive]!]!.start
|
||||
}-${lineNumber - 1},`;
|
||||
}
|
||||
lines.splice(lineNumber, 1);
|
||||
}
|
||||
const highlightLines = rangeParser(highlightRange);
|
||||
code = lines.join('\n');
|
||||
return {highlightLines, code};
|
||||
const lineClassNames: {[lineIndex: number]: string[]} = {};
|
||||
Object.entries(blocks).forEach(([className, {range}]) => {
|
||||
rangeParser(range).forEach((l) => {
|
||||
lineClassNames[l] ??= [];
|
||||
lineClassNames[l]!.push(className);
|
||||
});
|
||||
});
|
||||
return {lineClassNames, code};
|
||||
}
|
||||
|
||||
export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import type {PrismTheme} from 'prism-react-renderer';
|
||||
import type {DeepPartial} from 'utility-types';
|
||||
import type {MagicCommentConfig} from './codeBlockUtils';
|
||||
|
||||
export type DocsVersionPersistence = 'localStorage' | 'none';
|
||||
|
||||
|
@ -57,6 +58,7 @@ export type PrismConfig = {
|
|||
darkTheme?: PrismTheme;
|
||||
defaultLanguage?: string;
|
||||
additionalLanguages: string[];
|
||||
magicComments: MagicCommentConfig[];
|
||||
};
|
||||
|
||||
export type FooterLinkItem = {
|
||||
|
|
|
@ -700,9 +700,28 @@ Accepted fields:
|
|||
| `theme` | `PrismTheme` | `palenight` | The Prism theme to use for light-theme code blocks. |
|
||||
| `darkTheme` | `PrismTheme` | `palenight` | The Prism theme to use for dark-theme code blocks. |
|
||||
| `defaultLanguage` | `string` | `undefined` | The side of the navbar this item should appear on. |
|
||||
| `magicComments` | `MagicCommentConfig[]` | _see below_ | The list of [magic comments](../../guides/markdown-features/markdown-features-code-blocks.mdx#custom-magic-comments). |
|
||||
|
||||
</APITable>
|
||||
|
||||
```ts
|
||||
type MagicCommentConfig = {
|
||||
className: string;
|
||||
line?: string;
|
||||
block?: {start: string; end: string};
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
const defaultMagicComments = [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Theme {#theme}
|
||||
|
||||
By default, we use [Palenight](https://github.com/FormidableLabs/prism-react-renderer/blob/master/src/themes/palenight.js) as syntax highlighting theme. You can specify a custom theme from the [list of available themes](https://github.com/FormidableLabs/prism-react-renderer/tree/master/src/themes). You may also use a different syntax highlighting theme when the site is in dark mode.
|
||||
|
|
|
@ -196,7 +196,7 @@ Supported commenting syntax:
|
|||
| Bash-style | `# ...` |
|
||||
| HTML-style | `<!-- ... -->` |
|
||||
|
||||
We will do our best to infer which set of comment styles to use based on the language, and default to allowing _all_ comment styles. If there's a comment style that is not currently supported, we are open to adding them! Pull requests welcome.
|
||||
We will do our best to infer which set of comment styles to use based on the language, and default to allowing _all_ comment styles. If there's a comment style that is not currently supported, we are open to adding them! Pull requests welcome. Note that different comment styles have no semantic difference, only their content does.
|
||||
|
||||
You can set your own background color for highlighted code line in your `src/css/custom.css` which will better fit to your selected syntax highlighting theme. The color given below works for the default highlighting theme (Palenight), so if you are using another theme, you will have to tweak the color accordingly.
|
||||
|
||||
|
@ -272,10 +272,104 @@ Prefer highlighting with comments where you can. By inlining highlight in the co
|
|||
```
|
||||
````
|
||||
|
||||
In the future, we may extend the magic comment system and let you define custom directives and their functionalities. The magic comments would only be parsed if a highlight metastring is not present.
|
||||
Below, we will introduce how the magic comment system can be extended to define custom directives and their functionalities. The magic comments would only be parsed if a highlight metastring is not present.
|
||||
|
||||
:::
|
||||
|
||||
### Custom magic comments {#custom-magic-comments}
|
||||
|
||||
`// highlight-next-line` and `// highlight-start` etc. are called "magic comments", because they will be parsed and removed, and their purposes are to add metadata to the next line, or the section that the pair of start- and end-comments enclose.
|
||||
|
||||
You can declare custom magic comments through theme config. For example, you can register another magic comment that adds a `code-block-error-line` class name:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="docusaurus.config.js">
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
themeConfig: {
|
||||
prism: {
|
||||
magicComments: [
|
||||
// Remember to extend the default highlight class name as well!
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
// highlight-start
|
||||
{
|
||||
className: 'code-block-error-line',
|
||||
line: 'This will error',
|
||||
},
|
||||
// highlight-end
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="src/css/custom.css">
|
||||
|
||||
```css
|
||||
.code-block-error-line {
|
||||
background-color: #ff000020;
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
border-left: 3px solid #ff000080;
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="myDoc.md">
|
||||
|
||||
````md
|
||||
In TypeScript, types help prevent runtime errors.
|
||||
|
||||
```ts
|
||||
function greet(name: string) {
|
||||
// This will error
|
||||
console.log(name.toUpper());
|
||||
// .toUpper doesn't exist on string
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
````mdx-code-block
|
||||
<BrowserWindow>
|
||||
|
||||
In TypeScript, types help prevent runtime errors.
|
||||
|
||||
```ts
|
||||
function greet(name: string) {
|
||||
// This will error
|
||||
console.log(name.toUpper());
|
||||
// .toUpper doesn't exist on string
|
||||
}
|
||||
```
|
||||
|
||||
</BrowserWindow>
|
||||
````
|
||||
|
||||
If you use number ranges in metastring (the `{1,3-4}` syntax), Docusaurus will apply the **first `magicComments` entry**'s class name. This, by default, is `theme-code-block-highlighted-line`, but if you change the `magicComments` config and use a different entry as the first one, the meaning of the metastring range will change as well.
|
||||
|
||||
You can disable the default line highlighting comments with `magicComments: []`. If there's no magic comment config, but Docusaurus encounters a code block containing a metastring range, it will error because there will be no class name to apply—the highlighting class name, after all, is just a magic comment entry.
|
||||
|
||||
Every magic comment entry will contain three keys: `className` (required), `line`, which applies to the directly next line, or `block` (containing `start` and `end`), which applies to the entire block enclosed by the two comments.
|
||||
|
||||
Using CSS to target the class can already do a lot, but you can unlock the full potential of this feature through [swizzling](../../swizzling.md).
|
||||
|
||||
```bash npm2yarn
|
||||
npm run swizzle @docusaurus/theme-classic CodeBlock/Line
|
||||
```
|
||||
|
||||
The `Line` component will receive the list of class names, based on which you can conditionally render different markup.
|
||||
|
||||
## Line numbering {#line-numbering}
|
||||
|
||||
You can enable line numbering for your code block by using `showLineNumbers` key within the language meta string (don't forget to add space directly before the key).
|
||||
|
|
|
@ -378,6 +378,17 @@ const config = {
|
|||
// TODO after we have forked prism-react-renderer, we should tweak the
|
||||
// import order and fix it there
|
||||
additionalLanguages: ['java', 'markdown', 'latex'],
|
||||
magicComments: [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
{
|
||||
className: 'code-block-error-line',
|
||||
line: 'This will error',
|
||||
},
|
||||
],
|
||||
},
|
||||
image: 'img/docusaurus-soc.png',
|
||||
// metadata: [{name: 'twitter:card', content: 'summary'}],
|
||||
|
|
|
@ -207,3 +207,11 @@ div[class^='announcementBar_'] {
|
|||
font-size: 0.875rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
|
||||
.code-block-error-line {
|
||||
background-color: #ff000020;
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 var(--ifm-pre-padding);
|
||||
border-left: 3px solid #ff000080;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue