feat(theme-classic): extensible code block magic comment system (#7178)

This commit is contained in:
Joshua Chen 2022-05-04 18:31:13 +08:00 committed by GitHub
parent 785fed723f
commit 51815c12c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 692 additions and 161 deletions

View file

@ -36,6 +36,13 @@ describe('themeConfig', () => {
darkTheme, darkTheme,
defaultLanguage: 'javascript', defaultLanguage: 'javascript',
additionalLanguages: ['kotlin', 'java'], additionalLanguages: ['kotlin', 'java'],
magicComments: [
{
className: 'theme-code-block-highlighted-line',
line: 'highlight-next-line',
block: {start: 'highlight-start', end: 'highlight-end'},
},
],
}, },
docs: { docs: {
versionPersistence: 'localStorage', versionPersistence: 'localStorage',
@ -549,16 +556,72 @@ describe('themeConfig', () => {
}); });
}); });
it('accepts valid prism config', () => { describe('prism config', () => {
const prismConfig = { it('accepts a range of magic comments', () => {
prism: { const prismConfig = {
additionalLanguages: ['kotlin', 'java'], prism: {
theme: darkTheme, additionalLanguages: ['kotlin', 'java'],
}, theme: darkTheme,
}; magicComments: [],
expect(testValidateThemeConfig(prismConfig)).toEqual({ },
...DEFAULT_CONFIG, };
...prismConfig, 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"`,
);
}); });
}); });

View file

@ -219,7 +219,7 @@ declare module '@theme/CodeBlock/Line' {
export interface Props { export interface Props {
readonly line: Token[]; readonly line: Token[];
readonly highlight: boolean; readonly classNames: string[] | undefined;
readonly showLineNumbers: boolean; readonly showLineNumbers: boolean;
readonly getLineProps: GetLineProps; readonly getLineProps: GetLineProps;
readonly getTokenProps: GetTokenProps; readonly getTokenProps: GetTokenProps;

View file

@ -34,7 +34,7 @@ export default function CodeBlockString({
language: languageProp, language: languageProp,
}: Props): JSX.Element { }: Props): JSX.Element {
const { const {
prism: {defaultLanguage}, prism: {defaultLanguage, magicComments},
} = useThemeConfig(); } = useThemeConfig();
const language = const language =
languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage; languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage;
@ -46,7 +46,11 @@ export default function CodeBlockString({
// "title=\"xyz\"" => title: "\"xyz\"" // "title=\"xyz\"" => title: "\"xyz\""
const title = parseCodeBlockTitle(metastring) || titleProp; const title = parseCodeBlockTitle(metastring) || titleProp;
const {highlightLines, code} = parseLines(children, metastring, language); const {lineClassNames, code} = parseLines(children, {
metastring,
language,
magicComments,
});
const showLineNumbers = const showLineNumbers =
showLineNumbersProp || containsLineNumbers(metastring); showLineNumbersProp || containsLineNumbers(metastring);
@ -83,7 +87,7 @@ export default function CodeBlockString({
line={line} line={line}
getLineProps={getLineProps} getLineProps={getLineProps}
getTokenProps={getTokenProps} getTokenProps={getTokenProps}
highlight={highlightLines.includes(i)} classNames={lineClassNames[i]}
showLineNumbers={showLineNumbers} showLineNumbers={showLineNumbers}
/> />
))} ))}

View file

@ -12,7 +12,7 @@ import styles from './styles.module.css';
export default function CodeBlockLine({ export default function CodeBlockLine({
line, line,
highlight, classNames,
showLineNumbers, showLineNumbers,
getLineProps, getLineProps,
getTokenProps, getTokenProps,
@ -23,17 +23,9 @@ export default function CodeBlockLine({
const lineProps = getLineProps({ const lineProps = getLineProps({
line, 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) => ( const lineTokens = line.map((token, key) => (
<span key={key} {...getTokenProps({token, key})} /> <span key={key} {...getTokenProps({token, key})} />
)); ));

View file

@ -5,6 +5,13 @@
* LICENSE file in the root directory of this source tree. * 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 /* 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 */ the background in custom CSS file due bug https://github.com/facebook/docusaurus/issues/3678 */
:where(:root) { :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); --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 { .codeLine {
display: table-row; display: table-row;
counter-increment: line-count; counter-increment: line-count;

View file

@ -44,6 +44,13 @@ export const DEFAULT_CONFIG = {
prism: { prism: {
additionalLanguages: [], additionalLanguages: [],
theme: defaultPrismTheme, theme: defaultPrismTheme,
magicComments: [
{
className: 'theme-code-block-highlighted-line',
line: 'highlight-next-line',
block: {start: 'highlight-start', end: 'highlight-end'},
},
],
}, },
navbar: { navbar: {
hideOnScroll: false, hideOnScroll: false,
@ -386,6 +393,18 @@ export const ThemeConfigSchema = Joi.object({
additionalLanguages: Joi.array() additionalLanguages: Joi.array()
.items(Joi.string()) .items(Joi.string())
.default(DEFAULT_CONFIG.prism.additionalLanguages), .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) .default(DEFAULT_CONFIG.prism)
.unknown(), .unknown(),

View file

@ -4,9 +4,11 @@ exports[`parseLines does not parse content with metastring 1`] = `
{ {
"code": "aaaaa "code": "aaaaa
nnnnn", nnnnn",
"highlightLines": [ "lineClassNames": {
0, "0": [
], "theme-code-block-highlighted-line",
],
},
} }
`; `;
@ -15,9 +17,11 @@ exports[`parseLines does not parse content with metastring 2`] = `
"code": "// highlight-next-line "code": "// highlight-next-line
aaaaa aaaaa
bbbbb", bbbbb",
"highlightLines": [ "lineClassNames": {
0, "0": [
], "theme-code-block-highlighted-line",
],
},
} }
`; `;
@ -25,9 +29,11 @@ exports[`parseLines does not parse content with metastring 3`] = `
{ {
"code": "aaaaa "code": "aaaaa
bbbbb", bbbbb",
"highlightLines": [ "lineClassNames": {
0, "0": [
], "theme-code-block-highlighted-line",
],
},
} }
`; `;
@ -36,7 +42,100 @@ exports[`parseLines does not parse content with no language 1`] = `
"code": "// highlight-next-line "code": "// highlight-next-line
aaaaa aaaaa
bbbbb", 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 "code": "aaaaa
bbbbb", bbbbb",
"highlightLines": [ "lineClassNames": {
0, "0": [
], "theme-code-block-highlighted-line",
],
},
} }
`; `;
@ -54,9 +155,11 @@ exports[`parseLines removes lines correctly 2`] = `
{ {
"code": "aaaaa "code": "aaaaa
bbbbb", bbbbb",
"highlightLines": [ "lineClassNames": {
0, "0": [
], "theme-code-block-highlighted-line",
],
},
} }
`; `;
@ -65,12 +168,18 @@ exports[`parseLines removes lines correctly 3`] = `
"code": "aaaaa "code": "aaaaa
bbbbbbb bbbbbbb
bbbbb", bbbbb",
"highlightLines": [ "lineClassNames": {
0, "0": [
2, "theme-code-block-highlighted-line",
0, "theme-code-block-highlighted-line",
1, ],
], "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 */} {/* highlight-next-line */}
bbbbb bbbbb
dddd", dddd",
"highlightLines": [ "lineClassNames": {
0, "0": [
3, "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 "code": "# highlight-next-line
aaaaa aaaaa
bbbbb", bbbbb",
"highlightLines": [], "lineClassNames": {},
} }
`; `;
@ -102,10 +215,14 @@ exports[`parseLines respects language: jsx 1`] = `
bbbbb bbbbb
<!-- highlight-next-line --> <!-- highlight-next-line -->
dddd", dddd",
"highlightLines": [ "lineClassNames": {
0, "0": [
1, "theme-code-block-highlighted-line",
], ],
"1": [
"theme-code-block-highlighted-line",
],
},
} }
`; `;
@ -128,11 +245,17 @@ dddd
// highlight-next-line // highlight-next-line
console.log("preserved"); console.log("preserved");
\`\`\`", \`\`\`",
"highlightLines": [ "lineClassNames": {
1, "1": [
7, "theme-code-block-highlighted-line",
11, ],
], "11": [
"theme-code-block-highlighted-line",
],
"7": [
"theme-code-block-highlighted-line",
],
},
} }
`; `;
@ -142,12 +265,20 @@ exports[`parseLines respects language: none 1`] = `
bbbbb bbbbb
ccccc ccccc
dddd", dddd",
"highlightLines": [ "lineClassNames": {
0, "0": [
1, "theme-code-block-highlighted-line",
2, ],
3, "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 */ "code": "/* highlight-next-line */
aaaaa aaaaa
bbbbb", bbbbb",
"highlightLines": [], "lineClassNames": {},
} }
`; `;
@ -169,8 +300,10 @@ bbbbb
ccccc ccccc
<!-- highlight-next-line --> <!-- highlight-next-line -->
dddd", dddd",
"highlightLines": [ "lineClassNames": {
4, "4": [
], "theme-code-block-highlighted-line",
],
},
} }
`; `;

View file

@ -6,6 +6,7 @@
*/ */
import { import {
type MagicCommentConfig,
parseCodeBlockTitle, parseCodeBlockTitle,
parseLanguage, parseLanguage,
parseLines, parseLines,
@ -67,24 +68,58 @@ describe('parseLanguage', () => {
}); });
describe('parseLines', () => { 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', () => { 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( expect(
parseLines( parseLines(
`// highlight-next-line `// highlight-next-line
aaaaa aaaaa
bbbbb`, bbbbb`,
'{1}', {
'js', metastring: '{1}',
language: 'js',
magicComments: defaultMagicComments,
},
), ),
).toMatchSnapshot(); ).toMatchSnapshot();
expect( expect(
parseLines( parseLines(
`aaaaa `aaaaa
bbbbb`, bbbbb`,
'{1}', {
metastring: '{1}',
language: 'undefined',
magicComments: defaultMagicComments,
},
), ),
).toMatchSnapshot(); ).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', () => { it('does not parse content with no language', () => {
expect( expect(
@ -92,8 +127,11 @@ bbbbb`,
`// highlight-next-line `// highlight-next-line
aaaaa aaaaa
bbbbb`, bbbbb`,
'', {
undefined, metastring: '',
language: undefined,
magicComments: defaultMagicComments,
},
), ),
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
@ -103,8 +141,7 @@ bbbbb`,
`// highlight-next-line `// highlight-next-line
aaaaa aaaaa
bbbbb`, bbbbb`,
'', {metastring: '', language: 'js', magicComments: defaultMagicComments},
'js',
), ),
).toMatchSnapshot(); ).toMatchSnapshot();
expect( expect(
@ -113,8 +150,7 @@ bbbbb`,
aaaaa aaaaa
// highlight-end // highlight-end
bbbbb`, bbbbb`,
'', {metastring: '', language: 'js', magicComments: defaultMagicComments},
'js',
), ),
).toMatchSnapshot(); ).toMatchSnapshot();
expect( expect(
@ -126,8 +162,7 @@ bbbbbbb
// highlight-next-line // highlight-next-line
// highlight-end // highlight-end
bbbbb`, bbbbb`,
'', {metastring: '', language: 'js', magicComments: defaultMagicComments},
'js',
), ),
).toMatchSnapshot(); ).toMatchSnapshot();
}); });
@ -137,8 +172,7 @@ bbbbb`,
`# highlight-next-line `# highlight-next-line
aaaaa aaaaa
bbbbb`, bbbbb`,
'', {metastring: '', language: 'js', magicComments: defaultMagicComments},
'js',
), ),
).toMatchSnapshot('js'); ).toMatchSnapshot('js');
expect( expect(
@ -146,8 +180,7 @@ bbbbb`,
`/* highlight-next-line */ `/* highlight-next-line */
aaaaa aaaaa
bbbbb`, bbbbb`,
'', {metastring: '', language: 'py', magicComments: defaultMagicComments},
'py',
), ),
).toMatchSnapshot('py'); ).toMatchSnapshot('py');
expect( expect(
@ -160,8 +193,7 @@ bbbbb
ccccc ccccc
<!-- highlight-next-line --> <!-- highlight-next-line -->
dddd`, dddd`,
'', {metastring: '', language: 'py', magicComments: defaultMagicComments},
'py',
), ),
).toMatchSnapshot('py'); ).toMatchSnapshot('py');
expect( expect(
@ -174,8 +206,7 @@ bbbbb
ccccc ccccc
<!-- highlight-next-line --> <!-- highlight-next-line -->
dddd`, dddd`,
'', {metastring: '', language: '', magicComments: defaultMagicComments},
'',
), ),
).toMatchSnapshot('none'); ).toMatchSnapshot('none');
expect( expect(
@ -186,8 +217,7 @@ aaaa
bbbbb bbbbb
<!-- highlight-next-line --> <!-- highlight-next-line -->
dddd`, dddd`,
'', {metastring: '', language: 'jsx', magicComments: defaultMagicComments},
'jsx',
), ),
).toMatchSnapshot('jsx'); ).toMatchSnapshot('jsx');
expect( expect(
@ -198,8 +228,7 @@ aaaa
bbbbb bbbbb
<!-- highlight-next-line --> <!-- highlight-next-line -->
dddd`, dddd`,
'', {metastring: '', language: 'html', magicComments: defaultMagicComments},
'html',
), ),
).toMatchSnapshot('html'); ).toMatchSnapshot('html');
expect( expect(
@ -225,9 +254,109 @@ dddd
console.log("preserved"); console.log("preserved");
\`\`\` \`\`\`
`, `,
'', {metastring: '', language: 'md', magicComments: defaultMagicComments},
'md',
), ),
).toMatchSnapshot('md'); ).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();
});
}); });

View file

@ -10,7 +10,7 @@ import type {PrismTheme} from 'prism-react-renderer';
import type {CSSProperties} from 'react'; import type {CSSProperties} from 'react';
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/; const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
const highlightLinesRangeRegex = /\{(?<range>[\d,-]+)\}/; const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
// Supported types of highlight comments // Supported types of highlight comments
const commentPatterns = { const commentPatterns = {
@ -23,18 +23,23 @@ const commentPatterns = {
type CommentType = keyof typeof commentPatterns; type CommentType = keyof typeof commentPatterns;
const magicCommentDirectives = [ export type MagicCommentConfig = {
'highlight-next-line', className: string;
'highlight-start', line?: string;
'highlight-end', 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 // To be more reliable, the opening and closing comment must match
const commentPattern = languages const commentPattern = languages
.map((lang) => { .map((lang) => {
const {start, end} = commentPatterns[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('|'); .join('|');
// White space is allowed, but otherwise it should be on it's own line // 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 * Select comment styles based on language
*/ */
function getAllMagicCommentDirectiveStyles(lang: string) { function getAllMagicCommentDirectiveStyles(
lang: string,
magicCommentDirectives: MagicCommentConfig[],
) {
switch (lang) { switch (lang) {
case 'js': case 'js':
case 'javascript': case 'javascript':
case 'ts': case 'ts':
case 'typescript': case 'typescript':
return getCommentPattern(['js', 'jsBlock']); return getCommentPattern(['js', 'jsBlock'], magicCommentDirectives);
case 'jsx': case 'jsx':
case 'tsx': case 'tsx':
return getCommentPattern(['js', 'jsBlock', 'jsx']); return getCommentPattern(
['js', 'jsBlock', 'jsx'],
magicCommentDirectives,
);
case 'html': case 'html':
return getCommentPattern(['js', 'jsBlock', 'html']); return getCommentPattern(
['js', 'jsBlock', 'html'],
magicCommentDirectives,
);
case 'python': case 'python':
case 'py': case 'py':
case 'bash': case 'bash':
return getCommentPattern(['bash']); return getCommentPattern(['bash'], magicCommentDirectives);
case 'markdown': case 'markdown':
case 'md': case 'md':
// Text uses HTML, front matter uses bash // Text uses HTML, front matter uses bash
return getCommentPattern(['html', 'jsx', 'bash']); return getCommentPattern(['html', 'jsx', 'bash'], magicCommentDirectives);
default: default:
// All comment types // 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 * Parses the code content, strips away any magic comments, and returns the
* clean content and the highlighted lines marked by the comments or metastring. * clean content and the highlighted lines marked by the comments or metastring.
* *
* If the metastring contains highlight range, the `content` will be returned * If the metastring contains a range, the `content` will be returned as-is
* as-is without any parsing. * 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 * @param content The raw code with magic comments. Trailing newline will be
* trimmed upfront. * trimmed upfront.
* @param metastring The full metastring, as received from MDX. Highlight range * @param options Options for parsing behavior.
* declared here starts at 1.
* @param language Language of the code block, used to determine which kinds of
* magic comment styles to enable.
*/ */
export function parseLines( export function parseLines(
content: string, content: string,
metastring?: string, options: {
language?: string, /**
* 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 * The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
* 5th lines are highlighted. * 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 * If there's number range declared in the metastring, the code block is
* present in the metastring). * returned as-is (no parsing); otherwise, this is the clean code with all
* magic comments stripped away.
*/ */
code: string; code: string;
} { } {
let code = content.replace(/\n$/, ''); let code = content.replace(/\n$/, '');
const {language, magicComments, metastring} = options;
// Highlighted lines specified in props: don't parse the content // Highlighted lines specified in props: don't parse the content
if (metastring && highlightLinesRangeRegex.test(metastring)) { if (metastring && metastringLinesRangeRegex.test(metastring)) {
const highlightLinesRange = metastring.match(highlightLinesRangeRegex)! const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
.groups!.range!; .range!;
const highlightLines = rangeParser(highlightLinesRange) 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) .filter((n) => n > 0)
.map((n) => n - 1); .map((n) => [n - 1, [metastringRangeClassName]]);
return {highlightLines, code}; return {lineClassNames: Object.fromEntries(lines), code};
} }
if (language === undefined) { if (language === undefined) {
return {highlightLines: [], code}; return {lineClassNames: {}, code};
} }
const directiveRegex = getAllMagicCommentDirectiveStyles(language); const directiveRegex = getAllMagicCommentDirectiveStyles(
language,
magicComments,
);
// Go through line by line // Go through line by line
const lines = code.split('\n'); const lines = code.split('\n');
let highlightBlockStart: number; const blocks = Object.fromEntries(
let highlightRange = ''; 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; ) { for (let lineNumber = 0; lineNumber < lines.length; ) {
const line = lines[lineNumber]!; const line = lines[lineNumber]!;
const match = line.match(directiveRegex); const match = line.match(directiveRegex);
@ -151,28 +209,27 @@ export function parseLines(
lineNumber += 1; lineNumber += 1;
continue; continue;
} }
const directive = match.slice(1).find((item) => item !== undefined); const directive = match.slice(1).find((item) => item !== undefined)!;
switch (directive) { if (lineToClassName[directive]) {
case 'highlight-next-line': blocks[lineToClassName[directive]!]!.range += `${lineNumber},`;
highlightRange += `${lineNumber},`; } else if (blockStartToClassName[directive]) {
break; blocks[blockStartToClassName[directive]!]!.start = lineNumber;
} else if (blockEndToClassName[directive]) {
case 'highlight-start': blocks[blockEndToClassName[directive]!]!.range += `${
highlightBlockStart = lineNumber; blocks[blockEndToClassName[directive]!]!.start
break; }-${lineNumber - 1},`;
case 'highlight-end':
highlightRange += `${highlightBlockStart!}-${lineNumber - 1},`;
break;
default:
break;
} }
lines.splice(lineNumber, 1); lines.splice(lineNumber, 1);
} }
const highlightLines = rangeParser(highlightRange);
code = lines.join('\n'); 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 { export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {

View file

@ -8,6 +8,7 @@
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import type {PrismTheme} from 'prism-react-renderer'; import type {PrismTheme} from 'prism-react-renderer';
import type {DeepPartial} from 'utility-types'; import type {DeepPartial} from 'utility-types';
import type {MagicCommentConfig} from './codeBlockUtils';
export type DocsVersionPersistence = 'localStorage' | 'none'; export type DocsVersionPersistence = 'localStorage' | 'none';
@ -57,6 +58,7 @@ export type PrismConfig = {
darkTheme?: PrismTheme; darkTheme?: PrismTheme;
defaultLanguage?: string; defaultLanguage?: string;
additionalLanguages: string[]; additionalLanguages: string[];
magicComments: MagicCommentConfig[];
}; };
export type FooterLinkItem = { export type FooterLinkItem = {

View file

@ -700,9 +700,28 @@ Accepted fields:
| `theme` | `PrismTheme` | `palenight` | The Prism theme to use for light-theme code blocks. | | `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. | | `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. | | `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> </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} ### 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. 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.

View file

@ -196,7 +196,7 @@ Supported commenting syntax:
| Bash-style | `# ...` | | Bash-style | `# ...` |
| HTML-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. 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} ## 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). 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).

View file

@ -378,6 +378,17 @@ const config = {
// TODO after we have forked prism-react-renderer, we should tweak the // TODO after we have forked prism-react-renderer, we should tweak the
// import order and fix it there // import order and fix it there
additionalLanguages: ['java', 'markdown', 'latex'], 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', image: 'img/docusaurus-soc.png',
// metadata: [{name: 'twitter:card', content: 'summary'}], // metadata: [{name: 'twitter:card', content: 'summary'}],

View file

@ -207,3 +207,11 @@ div[class^='announcementBar_'] {
font-size: 0.875rem; font-size: 0.875rem;
padding: 0.2rem 0.5rem; 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;
}