diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/index.tsx index dec2ddd8f22a..e6ff65c1fbbf 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/index.tsx @@ -7,8 +7,11 @@ import React, {type ComponentProps, type ReactNode} from 'react'; import clsx from 'clsx'; -import {useCodeBlockContext} from '@docusaurus/theme-common/internal'; -import {usePrismTheme} from '@docusaurus/theme-common'; +import { + parseCodeLinesFromTokens, + useCodeBlockContext, +} from '@docusaurus/theme-common/internal'; +import {usePrismTheme, useThemeConfig} from '@docusaurus/theme-common'; import {Highlight} from 'prism-react-renderer'; import type {Props} from '@theme/CodeBlock/Content'; import Line from '@theme/CodeBlock/Line'; @@ -57,28 +60,39 @@ export default function CodeBlockContent({ }: Props): ReactNode { const {metadata, wordWrap} = useCodeBlockContext(); const prismTheme = usePrismTheme(); - const {code, language, lineNumbersStart, lineClassNames} = metadata; + const {prism} = useThemeConfig(); + const {codeInput, language, lineNumbersStart, metastring} = metadata; return ( - - {({className, style, tokens: lines, getLineProps, getTokenProps}) => ( -
-          
-            {lines.map((line, i) => (
-              
-            ))}
-          
-        
- )} + + {({className, style, tokens: lines, getLineProps, getTokenProps}) => { + const {lineClassNames, lineIndexes} = parseCodeLinesFromTokens({ + codeInput, + tokens: lines, + metastring, + magicComments: prism.magicComments, + language, + }); + const visibleLines = lineIndexes.map((index) => lines[index]!); + return ( +
+            
+              {visibleLines.map((line, i) => (
+                
+              ))}
+            
+          
+ ); + }}
); } diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index d8d5821c95f8..5650c1357a83 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -45,6 +45,7 @@ export { parseCodeBlockTitle, parseClassNameLanguage as parseLanguage, parseLines, + parseCodeLinesFromTokens, getLineNumbersStart, containsLineNumbers, } from './utils/codeBlockUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts b/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts index 73ae42eb49e1..9a369f5ca1b0 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts @@ -8,6 +8,7 @@ import { getLineNumbersStart, type MagicCommentConfig, + parseCodeLinesFromTokens, parseCodeBlockTitle, parseClassNameLanguage, parseLines, @@ -812,6 +813,65 @@ describe('getLineNumbersStart', () => { }); }); +describe('parseCodeLinesFromTokens', () => { + function createTokens(code: string) { + return code.split(/\r?\n/).map((line) => [{content: line}]); + } + + it('removes magic comment lines and returns line indexes', () => { + const codeInput = `// highlight-next-line\nconst x = 42;`; + const result = parseCodeLinesFromTokens({ + codeInput, + tokens: createTokens(codeInput), + metastring: '', + language: 'js', + magicComments: defaultMagicComments, + }); + + expect(result.lineClassNames).toMatchInlineSnapshot(` + { + "0": [ + "theme-code-block-highlighted-line", + ], + } + `); + expect(result.lineIndexes).toEqual([1]); + }); + + it('respects metastring ranges', () => { + const codeInput = `const x = 42;`; + const result = parseCodeLinesFromTokens({ + codeInput, + tokens: createTokens(codeInput), + metastring: '{1}', + language: 'js', + magicComments: defaultMagicComments, + }); + + expect(result.lineClassNames).toMatchInlineSnapshot(` + { + "0": [ + "theme-code-block-highlighted-line", + ], + } + `); + expect(result.lineIndexes).toEqual([0]); + }); + + it('drops trailing empty line when input ends with newline', () => { + const codeInput = `const x = 42;\n`; + const result = parseCodeLinesFromTokens({ + codeInput, + tokens: createTokens(codeInput), + metastring: '', + language: 'js', + magicComments: defaultMagicComments, + }); + + expect(result.lineIndexes).toEqual([0]); + }); +}); + describe('createCodeBlockMetadata', () => { type Params = Parameters[0]; @@ -840,6 +900,7 @@ describe('createCodeBlockMetadata', () => { "language": "text", "lineClassNames": {}, "lineNumbersStart": undefined, + "metastring": "", "title": undefined, } `); diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.tsx b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.tsx index fc18e4d44b57..f412706263f1 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.tsx +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.tsx @@ -248,20 +248,20 @@ function parseCodeLinesFromMetastring( return null; } -function parseCodeLinesFromContent( - code: string, +function parseCodeLinesFromLineArray( + lines: string[], params: ParseCodeLinesParam, -): ParsedCodeLines { +): {lineClassNames: CodeLineClassNames; lineIndexes: number[]} { const {language, magicComments} = params; + const lineIndexes = lines.map((_, index) => index); if (language === undefined) { - return {lineClassNames: {}, code}; + return {lineClassNames: {}, lineIndexes}; } const directiveRegex = getAllMagicCommentDirectiveStyles( language, magicComments, ); // Go through line by line - const lines = code.split(/\r?\n/); const blocks = Object.fromEntries( magicComments.map((d) => [d.className, {start: 0, range: ''}]), ); @@ -301,6 +301,7 @@ function parseCodeLinesFromContent( }-${lineNumber - 1},`; } lines.splice(lineNumber, 1); + lineIndexes.splice(lineNumber, 1); } const lineClassNames: {[lineIndex: number]: string[]} = {}; @@ -311,7 +312,16 @@ function parseCodeLinesFromContent( }); }); - return {code: lines.join('\n'), lineClassNames}; + return {lineClassNames, lineIndexes}; +} + +function parseCodeLinesFromContent( + code: string, + params: ParseCodeLinesParam, +): ParsedCodeLines { + const lines = code.split(/\r?\n/); + const {lineClassNames} = parseCodeLinesFromLineArray(lines, params); + return {lineClassNames, code: lines.join('\n')}; } /** @@ -337,6 +347,46 @@ export function parseLines( ); } +type TokenLine = T[]; + +export function parseCodeLinesFromTokens( + params: { + codeInput: string; + tokens: TokenLine[]; + } & ParseCodeLinesParam, +): { + lineClassNames: CodeLineClassNames; + lineIndexes: number[]; +} { + const {codeInput, tokens, metastring, magicComments, language} = params; + const lines = tokens.map((line) => + line.map((token) => token.content).join(''), + ); + + if (codeInput.match(/\r?\n$/) && lines.at(-1) === '') { + lines.pop(); + } + + const metastringResult = parseCodeLinesFromMetastring(lines.join('\n'), { + metastring, + magicComments, + language, + }); + + if (metastringResult) { + return { + lineClassNames: metastringResult.lineClassNames, + lineIndexes: lines.map((_, index) => index), + }; + } + + return parseCodeLinesFromLineArray(lines, { + metastring, + magicComments, + language, + }); +} + /** * Gets the language name from the class name (set by MDX). * e.g. `"language-javascript"` => `"javascript"`. @@ -402,6 +452,7 @@ export interface CodeBlockMetadata { className: string; // There's always a "language-" className language: string; title: ReactNode; + metastring: string | undefined; lineNumbersStart: number | undefined; lineClassNames: CodeLineClassNames; } @@ -446,6 +497,7 @@ export function createCodeBlockMetadata(params: { className, language, title, + metastring: params.metastring, lineNumbersStart, lineClassNames, }; diff --git a/website/netlify.toml b/website/netlify.toml index 2e2e8fec7d52..05e868076928 100644 --- a/website/netlify.toml +++ b/website/netlify.toml @@ -27,6 +27,9 @@ [context.deploy-preview] command = "(echo 'Build packages start' && yarn --cwd .. build:packages && echo 'Build packages end') & (echo 'Git backfill start' && git backfill && echo 'Git backfill end' ) & wait && yarn netlify:build:deployPreview" + [context.deploy-preview.environment] + DISABLE_RSPACK_INCREMENTAL = "true" + DOCUSAURUS_NO_PERSISTENT_CACHE = "true" [[plugins]] package = "netlify-plugin-cache"