refactor(theme): refactor CodeBlock parseLines logic + use inline snapshots to ease review (#11058)

* refactor codeblock parseLines logic + use inline snapshots

* refactor: apply lint autofix

* eslint

---------

Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
Sébastien Lorber 2025-04-04 13:22:51 +02:00 committed by GitHub
parent 16e3002911
commit f6bdc3123b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 417 additions and 444 deletions

2
.eslintrc.js vendored
View file

@ -298,7 +298,7 @@ module.exports = {
'jest/expect-expect': OFF,
'jest/no-large-snapshots': [
WARNING,
{maxSize: Infinity, inlineMaxSize: 10},
{maxSize: Infinity, inlineMaxSize: 50},
],
'jest/no-test-return-statement': ERROR,
'jest/prefer-expect-resolves': WARNING,

View file

@ -23,7 +23,6 @@ describe('normalizeSocials', () => {
mastodon: 'Mastodon',
};
// eslint-disable-next-line jest/no-large-snapshots
expect(normalizeSocials(socials)).toMatchInlineSnapshot(`
{
"bluesky": "https://bsky.app/profile/gingergeek.co.uk",

View file

@ -42,19 +42,16 @@ describe('validateSidebars', () => {
});
it('sidebar category wrong label', () => {
expect(
() =>
validateSidebars({
docs: [
{
type: 'category',
label: true,
items: [{type: 'doc', id: 'doc1'}],
},
],
}),
// eslint-disable-next-line jest/no-large-snapshots
expect(() =>
validateSidebars({
docs: [
{
type: 'category',
label: true,
items: [{type: 'doc', id: 'doc1'}],
},
],
}),
).toThrowErrorMatchingInlineSnapshot(`
"{
"type": "category",

View file

@ -1,357 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getLineNumbersStart handles metadata combined with other options set as flag 1`] = `1`;
exports[`getLineNumbersStart handles metadata combined with other options set with number 1`] = `10`;
exports[`getLineNumbersStart handles metadata standalone set as flag 1`] = `1`;
exports[`getLineNumbersStart handles metadata standalone set with number 1`] = `10`;
exports[`getLineNumbersStart handles prop combined with metastring set to false 1`] = `undefined`;
exports[`getLineNumbersStart handles prop combined with metastring set to number 1`] = `10`;
exports[`getLineNumbersStart handles prop combined with metastring set to true 1`] = `1`;
exports[`getLineNumbersStart handles prop standalone set to false 1`] = `undefined`;
exports[`getLineNumbersStart handles prop standalone set to number 1`] = `10`;
exports[`getLineNumbersStart handles prop standalone set to true 1`] = `1`;
exports[`getLineNumbersStart with nothing set 1`] = `undefined`;
exports[`getLineNumbersStart with nothing set 2`] = `undefined`;
exports[`parseLines does not parse content with metastring 1`] = `
{
"code": "aaaaa
nnnnn",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines does not parse content with metastring 2`] = `
{
"code": "// highlight-next-line
aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines does not parse content with metastring 3`] = `
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines does not parse content with no language 1`] = `
{
"code": "// highlight-next-line
aaaaa
bbbbb",
"lineClassNames": {},
}
`;
exports[`parseLines handles CRLF line breaks with highlight comments correctly 1`] = `
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"1": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines handles CRLF line breaks with highlight metastring 1`] = `
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"1": [
"theme-code-block-highlighted-line",
],
},
}
`;
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",
],
},
}
`;
exports[`parseLines removes lines correctly 1`] = `
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines removes lines correctly 2`] = `
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines removes lines correctly 3`] = `
{
"code": "aaaaa
bbbbbbb
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
"theme-code-block-highlighted-line",
],
"1": [
"theme-code-block-highlighted-line",
],
"2": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines respects language: html 1`] = `
{
"code": "aaaa
{/* highlight-next-line */}
bbbbb
dddd",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
"3": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines respects language: js 1`] = `
{
"code": "# highlight-next-line
aaaaa
bbbbb",
"lineClassNames": {},
}
`;
exports[`parseLines respects language: jsx 1`] = `
{
"code": "aaaa
bbbbb
<!-- highlight-next-line -->
dddd",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
"1": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines respects language: md 1`] = `
{
"code": "---
aaa: boo
---
aaaa
<div>
foo
</div>
bbbbb
dddd
\`\`\`js
// highlight-next-line
console.log("preserved");
\`\`\`",
"lineClassNames": {
"1": [
"theme-code-block-highlighted-line",
],
"11": [
"theme-code-block-highlighted-line",
],
"7": [
"theme-code-block-highlighted-line",
],
},
}
`;
exports[`parseLines respects language: none 1`] = `
{
"code": "aaaa
bbbbb
ccccc
dddd",
"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",
],
},
}
`;
exports[`parseLines respects language: py 1`] = `
{
"code": "/* highlight-next-line */
aaaaa
bbbbb",
"lineClassNames": {},
}
`;
exports[`parseLines respects language: py 2`] = `
{
"code": "// highlight-next-line
aaaa
/* highlight-next-line */
bbbbb
ccccc
<!-- highlight-next-line -->
dddd",
"lineClassNames": {
"4": [
"theme-code-block-highlighted-line",
],
},
}
`;

View file

@ -84,7 +84,18 @@ describe('parseLines', () => {
language: 'js',
magicComments: defaultMagicComments,
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "aaaaa
nnnnn",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(
parseLines(
`// highlight-next-line
@ -96,7 +107,19 @@ bbbbb`,
magicComments: defaultMagicComments,
},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "// highlight-next-line
aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(
parseLines(
`aaaaa
@ -107,7 +130,18 @@ bbbbb`,
magicComments: defaultMagicComments,
},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(() =>
parseLines(
`aaaaa
@ -122,6 +156,7 @@ bbbbb`,
`"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(
parseLines(
@ -134,8 +169,16 @@ bbbbb`,
magicComments: defaultMagicComments,
},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "// highlight-next-line
aaaaa
bbbbb",
"lineClassNames": {},
}
`);
});
it('removes lines correctly', () => {
expect(
parseLines(
@ -144,7 +187,18 @@ aaaaa
bbbbb`,
{metastring: '', language: 'js', magicComments: defaultMagicComments},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(
parseLines(
`// highlight-start
@ -153,7 +207,18 @@ aaaaa
bbbbb`,
{metastring: '', language: 'js', magicComments: defaultMagicComments},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(
parseLines(
`// highlight-start
@ -165,8 +230,27 @@ bbbbbbb
bbbbb`,
{metastring: '', language: 'js', magicComments: defaultMagicComments},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "aaaaa
bbbbbbb
bbbbb",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
"theme-code-block-highlighted-line",
],
"1": [
"theme-code-block-highlighted-line",
],
"2": [
"theme-code-block-highlighted-line",
],
},
}
`);
});
it('respects language', () => {
expect(
parseLines(
@ -175,7 +259,15 @@ aaaaa
bbbbb`,
{metastring: '', language: 'js', magicComments: defaultMagicComments},
),
).toMatchSnapshot('js');
).toMatchInlineSnapshot(`
{
"code": "# highlight-next-line
aaaaa
bbbbb",
"lineClassNames": {},
}
`);
expect(
parseLines(
`/* highlight-next-line */
@ -183,7 +275,15 @@ aaaaa
bbbbb`,
{metastring: '', language: 'py', magicComments: defaultMagicComments},
),
).toMatchSnapshot('py');
).toMatchInlineSnapshot(`
{
"code": "/* highlight-next-line */
aaaaa
bbbbb",
"lineClassNames": {},
}
`);
expect(
parseLines(
`// highlight-next-line
@ -196,7 +296,23 @@ ccccc
dddd`,
{metastring: '', language: 'py', magicComments: defaultMagicComments},
),
).toMatchSnapshot('py');
).toMatchInlineSnapshot(`
{
"code": "// highlight-next-line
aaaa
/* highlight-next-line */
bbbbb
ccccc
<!-- highlight-next-line -->
dddd",
"lineClassNames": {
"4": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(
parseLines(
`// highlight-next-line
@ -209,7 +325,29 @@ ccccc
dddd`,
{metastring: '', language: '', magicComments: defaultMagicComments},
),
).toMatchSnapshot('none');
).toMatchInlineSnapshot(`
{
"code": "aaaa
bbbbb
ccccc
dddd",
"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",
],
},
}
`);
expect(
parseLines(
`// highlight-next-line
@ -220,7 +358,23 @@ bbbbb
dddd`,
{metastring: '', language: 'jsx', magicComments: defaultMagicComments},
),
).toMatchSnapshot('jsx');
).toMatchInlineSnapshot(`
{
"code": "aaaa
bbbbb
<!-- highlight-next-line -->
dddd",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
"1": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(
parseLines(
`// highlight-next-line
@ -231,7 +385,23 @@ bbbbb
dddd`,
{metastring: '', language: 'html', magicComments: defaultMagicComments},
),
).toMatchSnapshot('html');
).toMatchInlineSnapshot(`
{
"code": "aaaa
{/* highlight-next-line */}
bbbbb
dddd",
"lineClassNames": {
"0": [
"theme-code-block-highlighted-line",
],
"3": [
"theme-code-block-highlighted-line",
],
},
}
`);
expect(
parseLines(
`---
@ -257,7 +427,38 @@ console.log("preserved");
`,
{metastring: '', language: 'md', magicComments: defaultMagicComments},
),
).toMatchSnapshot('md');
).toMatchInlineSnapshot(`
{
"code": "---
aaa: boo
---
aaaa
<div>
foo
</div>
bbbbb
dddd
\`\`\`js
// highlight-next-line
console.log("preserved");
\`\`\`",
"lineClassNames": {
"1": [
"theme-code-block-highlighted-line",
],
"11": [
"theme-code-block-highlighted-line",
],
"7": [
"theme-code-block-highlighted-line",
],
},
}
`);
});
it('parses multiple types of magic comments', () => {
@ -290,7 +491,29 @@ collapsed
],
},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "
highlighted
collapsed
collapsed
collapsed",
"lineClassNames": {
"1": [
"highlight",
],
"2": [
"collapse",
],
"3": [
"collapse",
],
"4": [
"collapse",
],
},
}
`);
});
it('handles one line with multiple class names', () => {
@ -335,7 +558,56 @@ highlighted and collapsed
],
},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"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",
],
},
}
`);
expect(
parseLines(
`// a
@ -358,7 +630,24 @@ line
],
},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "line
line",
"lineClassNames": {
"0": [
"a",
"b",
"c",
"d",
],
"1": [
"b",
"d",
],
},
}
`);
});
it('handles CRLF line breaks with highlight comments correctly', () => {
@ -371,7 +660,17 @@ line
magicComments: defaultMagicComments,
},
),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"1": [
"theme-code-block-highlighted-line",
],
},
}
`);
});
it('handles CRLF line breaks with highlight metastring', () => {
@ -381,7 +680,17 @@ line
language: 'js',
magicComments: defaultMagicComments,
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`
{
"code": "aaaaa
bbbbb",
"lineClassNames": {
"1": [
"theme-code-block-highlighted-line",
],
},
}
`);
});
});
@ -392,13 +701,13 @@ describe('getLineNumbersStart', () => {
showLineNumbers: undefined,
metastring: undefined,
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`undefined`);
expect(
getLineNumbersStart({
showLineNumbers: undefined,
metastring: '',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`undefined`);
});
describe('handles prop', () => {
@ -409,7 +718,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: true,
metastring: 'showLineNumbers=2',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`1`);
});
it('set to false', () => {
@ -418,7 +727,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: false,
metastring: 'showLineNumbers=2',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`undefined`);
});
it('set to number', () => {
@ -427,7 +736,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: 10,
metastring: 'showLineNumbers=2',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`10`);
});
});
@ -438,7 +747,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: true,
metastring: undefined,
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`1`);
});
it('set to false', () => {
@ -447,7 +756,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: false,
metastring: undefined,
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`undefined`);
});
it('set to number', () => {
@ -456,7 +765,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: 10,
metastring: undefined,
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`10`);
});
});
});
@ -469,7 +778,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: undefined,
metastring: 'showLineNumbers',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`1`);
});
it('set with number', () => {
expect(
@ -477,7 +786,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: undefined,
metastring: 'showLineNumbers=10',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`10`);
});
});
@ -488,7 +797,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: undefined,
metastring: '{1,2-3} title="file.txt" showLineNumbers noInline',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`1`);
});
it('set with number', () => {
expect(
@ -496,7 +805,7 @@ describe('getLineNumbersStart', () => {
showLineNumbers: undefined,
metastring: '{1,2-3} title="file.txt" showLineNumbers=10 noInline',
}),
).toMatchSnapshot();
).toMatchInlineSnapshot(`10`);
});
});
});

View file

@ -196,53 +196,45 @@ export function parseLanguage(className: string): string | undefined {
return languageClassName?.replace(/language-/, '');
}
/**
* 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 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 options Options for parsing behavior.
*/
export function parseLines(
content: 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[];
},
): {
type ParseCodeLinesParam = {
/**
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
* means the 1st line should have `highlight` and `sample` as class names.
* The full metastring, as received from MDX. Line ranges declared here
* start at 1.
*/
lineClassNames: {[lineIndex: number]: string[]};
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[];
};
/**
* Code lines after applying magic comments or metastring highlight ranges
*/
type CodeLines = {
/**
* 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(/\r?\n$/, '');
const {language, magicComments, metastring} = options;
/**
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
* means the 1st line should have `highlight` and `sample` as class names.
*/
lineClassNames: {[lineIndex: number]: string[]};
};
function parseCodeLinesFromMetastring(
code: string,
{metastring, magicComments}: ParseCodeLinesParam,
): CodeLines | null {
// Highlighted lines specified in props: don't parse the content
if (metastring && metastringLinesRangeRegex.test(metastring)) {
const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
@ -258,6 +250,14 @@ export function parseLines(
.map((n) => [n - 1, [metastringRangeClassName]] as [number, string[]]);
return {lineClassNames: Object.fromEntries(lines), code};
}
return null;
}
function parseCodeLinesFromContent(
code: string,
params: ParseCodeLinesParam,
): CodeLines {
const {language, magicComments} = params;
if (language === undefined) {
return {lineClassNames: {}, code};
}
@ -307,7 +307,7 @@ export function parseLines(
}
lines.splice(lineNumber, 1);
}
code = lines.join('\n');
const lineClassNames: {[lineIndex: number]: string[]} = {};
Object.entries(blocks).forEach(([className, {range}]) => {
rangeParser(range).forEach((l) => {
@ -315,7 +315,31 @@ export function parseLines(
lineClassNames[l]!.push(className);
});
});
return {lineClassNames, code};
return {code: lines.join('\n'), lineClassNames};
}
/**
* 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 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.)
*/
export function parseLines(
code: string,
params: ParseCodeLinesParam,
): CodeLines {
// Historical behavior: we remove last line break
const newCode = code.replace(/\r?\n$/, '');
// Historical behavior: we try one strategy after the other
// we don't support mixing metastring ranges + magic comments
return (
parseCodeLinesFromMetastring(newCode, {...params}) ??
parseCodeLinesFromContent(newCode, {...params})
);
}
export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {

View file

@ -175,6 +175,7 @@ Meilisearch
merveilleuse
metadatum
metastring
Metastring
metrica
Metrika
microdata