From 9e842aec717431d2876ac8f0facf4fc06e444a56 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 22 Mar 2025 14:09:04 +0100 Subject: [PATCH 01/11] test(theme-common): Add tests for getLineNumbersStart --- .../__snapshots__/codeBlockUtils.test.ts.snap | 24 ++++ .../utils/__tests__/codeBlockUtils.test.ts | 118 ++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap index 7ab1326fddc8..ca223444b461 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap +++ b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap @@ -1,5 +1,29 @@ // 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 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 47c62a91ca69..91d2efe65951 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts @@ -6,6 +6,7 @@ */ import { + getLineNumbersStart, type MagicCommentConfig, parseCodeBlockTitle, parseLanguage, @@ -360,3 +361,120 @@ line ).toMatchSnapshot(); }); }); + +describe('getLineNumbersStart', () => { + it('with nothing set', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metastring: undefined, + }), + ).toMatchSnapshot(); + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metastring: '', + }), + ).toMatchSnapshot(); + }); + + describe('handles prop', () => { + describe('combined with metastring', () => { + it('set to true', () => { + expect( + getLineNumbersStart({ + showLineNumbers: true, + metastring: 'showLineNumbers=2', + }), + ).toMatchSnapshot(); + }); + + it('set to false', () => { + expect( + getLineNumbersStart({ + showLineNumbers: false, + metastring: 'showLineNumbers=2', + }), + ).toMatchSnapshot(); + }); + + it('set to number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: 10, + metastring: 'showLineNumbers=2', + }), + ).toMatchSnapshot(); + }); + }); + + describe('standalone', () => { + it('set to true', () => { + expect( + getLineNumbersStart({ + showLineNumbers: true, + metastring: undefined, + }), + ).toMatchSnapshot(); + }); + + it('set to false', () => { + expect( + getLineNumbersStart({ + showLineNumbers: false, + metastring: undefined, + }), + ).toMatchSnapshot(); + }); + + it('set to number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: 10, + metastring: undefined, + }), + ).toMatchSnapshot(); + }); + }); + }); + + describe('handles metadata', () => { + describe('standalone', () => { + it('set as flag', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metastring: 'showLineNumbers', + }), + ).toMatchSnapshot(); + }); + it('set with number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metastring: 'showLineNumbers=10', + }), + ).toMatchSnapshot(); + }); + }); + + describe('combined with other options', () => { + it('set as flag', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metastring: '{1,2-3} title="file.txt" showLineNumbers noInline', + }), + ).toMatchSnapshot(); + }); + it('set with number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metastring: '{1,2-3} title="file.txt" showLineNumbers=10 noInline', + }), + ).toMatchSnapshot(); + }); + }); + }); +}); From 1d82bd0b111c382dba1c7eae0221827fa967cc6e Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 22 Mar 2025 14:26:56 +0100 Subject: [PATCH 02/11] refactor(theme-classic, theme-common): Parse CodeBlock title into new metaOptions --- .../src/theme/CodeBlock/Content/String.tsx | 10 +- .../docusaurus-theme-common/src/internal.ts | 3 +- .../__snapshots__/codeBlockUtils.test.ts.snap | 12 ++ .../utils/__tests__/codeBlockUtils.test.ts | 161 +++++++++++++----- .../src/utils/codeBlockUtils.ts | 36 +++- 5 files changed, 173 insertions(+), 49 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx index fb6c6763dfbd..9bd19f00f4e4 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx @@ -9,9 +9,10 @@ import React, {type ReactNode} from 'react'; import clsx from 'clsx'; import {useThemeConfig, usePrismTheme} from '@docusaurus/theme-common'; import { - parseCodeBlockTitle, + parseCodeBlockMetaOptions, parseLanguage, parseLines, + getCodeBlockTitle, getLineNumbersStart, useCodeWordWrap, } from '@docusaurus/theme-common/internal'; @@ -51,10 +52,15 @@ export default function CodeBlockString({ const wordWrap = useCodeWordWrap(); const isBrowser = useIsBrowser(); + const metaOptions = parseCodeBlockMetaOptions(metastring); + // We still parse the metastring in case we want to support more syntax in the // future. Note that MDX doesn't strip quotes when parsing metastring: // "title=\"xyz\"" => title: "\"xyz\"" - const title = parseCodeBlockTitle(metastring) || titleProp; + const title = getCodeBlockTitle({ + titleProp, + metaOptions, + }); const {lineClassNames, code} = parseLines(children, { metastring, diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index e4a3852774d8..149e1f054852 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -34,9 +34,10 @@ export {ColorModeProvider} from './contexts/colorMode'; export {useAlternatePageUtils} from './utils/useAlternatePageUtils'; export { - parseCodeBlockTitle, + parseCodeBlockMetaOptions, parseLanguage, parseLines, + getCodeBlockTitle, getLineNumbersStart, } from './utils/codeBlockUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap index ca223444b461..9fa075f3c6d1 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap +++ b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`getCodeBlockTitle returns option with empty prop 1`] = `"Option"`; + +exports[`getCodeBlockTitle returns option with filled prop 1`] = `"Option"`; + +exports[`getCodeBlockTitle returns option with undefined prop 1`] = `"Option"`; + +exports[`getCodeBlockTitle returns titleProp with empty options 1`] = `"Prop"`; + +exports[`getCodeBlockTitle returns titleProp with empty string on option 1`] = `"Prop"`; + +exports[`getCodeBlockTitle with nothing set 1`] = `undefined`; + 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`; 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 91d2efe65951..cc23683f5468 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts @@ -8,57 +8,71 @@ import { getLineNumbersStart, type MagicCommentConfig, - parseCodeBlockTitle, + getCodeBlockTitle, parseLanguage, parseLines, + parseCodeBlockMetaOptions, } from '../codeBlockUtils'; -describe('parseCodeBlockTitle', () => { - it('parses double quote delimited title', () => { - expect(parseCodeBlockTitle(`title="index.js"`)).toBe(`index.js`); - }); +const defaultMagicComments: MagicCommentConfig[] = [ + { + className: 'theme-code-block-highlighted-line', + line: 'highlight-next-line', + block: {start: 'highlight-start', end: 'highlight-end'}, + }, +]; - it('parses single quote delimited title', () => { - expect(parseCodeBlockTitle(`title='index.js'`)).toBe(`index.js`); - }); +describe('parseCodeBlockMetaOptions', () => { + describe('title', () => { + it('parses double quote delimited title', () => { + expect(parseCodeBlockMetaOptions(`title="index.js"`).title).toBe( + `index.js`, + ); + }); - it('does not parse mismatched quote delimiters', () => { - expect(parseCodeBlockTitle(`title="index.js'`)).toBe(``); - }); + it('parses single quote delimited title', () => { + expect(parseCodeBlockMetaOptions(`title='index.js'`).title).toBe( + `index.js`, + ); + }); - it('parses undefined metastring', () => { - expect(parseCodeBlockTitle(undefined)).toBe(``); - }); + it('does not parse mismatched quote delimiters', () => { + expect(parseCodeBlockMetaOptions(`title="index.js'`).title).toBe(``); + }); - it('parses metastring with no title specified', () => { - expect(parseCodeBlockTitle(`{1,2-3}`)).toBe(``); - }); + it('parses undefined metastring', () => { + expect(parseCodeBlockMetaOptions(undefined).title).toBe(``); + }); - it('parses with multiple metadata title first', () => { - expect(parseCodeBlockTitle(`title="index.js" label="JavaScript"`)).toBe( - `index.js`, - ); - }); + it('parses metastring with no title specified', () => { + expect(parseCodeBlockMetaOptions(`{1,2-3}`).title).toBe(``); + }); - it('parses with multiple metadata title last', () => { - expect(parseCodeBlockTitle(`label="JavaScript" title="index.js"`)).toBe( - `index.js`, - ); - }); + it('parses with multiple metadata title first', () => { + expect( + parseCodeBlockMetaOptions(`title="index.js" label="JavaScript"`).title, + ).toBe(`index.js`); + }); - it('parses double quotes when delimited by single quotes', () => { - expect(parseCodeBlockTitle(`title='console.log("Hello, World!")'`)).toBe( - `console.log("Hello, World!")`, - ); - }); + it('parses with multiple metadata title last', () => { + expect( + parseCodeBlockMetaOptions(`label="JavaScript" title="index.js"`).title, + ).toBe(`index.js`); + }); - it('parses single quotes when delimited by double quotes', () => { - expect(parseCodeBlockTitle(`title="console.log('Hello, World!')"`)).toBe( - `console.log('Hello, World!')`, - ); + it('parses double quotes when delimited by single quotes', () => { + expect( + parseCodeBlockMetaOptions(`title='console.log("Hello, World!")'`).title, + ).toBe(`console.log("Hello, World!")`); + }); + + it('parses single quotes when delimited by double quotes', () => { + expect( + parseCodeBlockMetaOptions(`title="console.log('Hello, World!')"`).title, + ).toBe(`console.log('Hello, World!')`); + }); }); }); - describe('parseLanguage', () => { it('works', () => { expect(parseLanguage('language-foo xxx yyy')).toBe('foo'); @@ -69,14 +83,6 @@ 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', { @@ -478,3 +484,68 @@ describe('getLineNumbersStart', () => { }); }); }); + +describe('getCodeBlockTitle', () => { + it('with nothing set', () => { + expect( + getCodeBlockTitle({ + titleProp: undefined, + metaOptions: {}, + }), + ).toMatchSnapshot(); + }); + + describe('returns titleProp', () => { + it('with empty options', () => { + expect( + getCodeBlockTitle({ + titleProp: 'Prop', + metaOptions: {}, + }), + ).toMatchSnapshot(); + }); + it('with empty string on option', () => { + expect( + getCodeBlockTitle({ + titleProp: 'Prop', + metaOptions: { + title: '', + }, + }), + ).toMatchSnapshot(); + }); + }); + + describe('returns option', () => { + it('with undefined prop', () => { + expect( + getCodeBlockTitle({ + titleProp: undefined, + metaOptions: { + title: 'Option', + }, + }), + ).toMatchSnapshot(); + }); + it('with empty prop', () => { + expect( + getCodeBlockTitle({ + titleProp: '', + metaOptions: { + title: 'Option', + }, + }), + ).toMatchSnapshot(); + }); + it('with filled prop', () => { + expect( + getCodeBlockTitle({ + titleProp: 'Prop', + metaOptions: { + title: 'Option', + }, + }), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts index 305ba3e87703..d6fa6bbe6dcf 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts @@ -147,7 +147,7 @@ function getAllMagicCommentDirectiveStyles( } } -export function parseCodeBlockTitle(metastring?: string): string { +function parseCodeBlockTitle(metastring?: string): string { return metastring?.match(codeBlockTitleRegex)?.groups!.title ?? ''; } @@ -167,6 +167,20 @@ function getMetaLineNumbersStart(metastring?: string): number | undefined { return undefined; } +export function getCodeBlockTitle({ + titleProp, + metaOptions, +}: { + titleProp: React.ReactNode; + metaOptions: CodeBlockMetaOptions; +}): React.ReactNode { + // NOTE: historically the metastring option overruled + // any `title=""` prop specified on `` + // this is the reversed logic to getLineNumbersStart + // but would be a breaking change so we keep it. + return metaOptions.title || titleProp; +} + export function getLineNumbersStart({ showLineNumbers, metastring, @@ -318,6 +332,26 @@ export function parseLines( return {lineClassNames, code}; } +/** + * Parses {@link CodeBlockParsedLines.metaOptions} from the given metastring. + * @param metastring The metastring to parse + * @returns The parsed options. + */ +export function parseCodeBlockMetaOptions( + metastring: string | undefined, +): CodeBlockMetaOptions { + const parsedOptions: CodeBlockMetaOptions = {}; + + parsedOptions.title = parseCodeBlockTitle(metastring); + + // parsedOptions.live = TODO; + // parsedOptions.noInline = TODO; + + // parsedOptions.showLineNumbers = TODO; + + return parsedOptions; +} + export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties { const mapping: PrismThemeEntry = { color: '--prism-color', From dd31c32087139eb54836d2b4f84ec45224d82a47 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 22 Mar 2025 16:32:59 +0100 Subject: [PATCH 03/11] refactor(theme-common, theme-classic): Use metaOptions for live --- .../src/remark/mdx1Compat/codeCompatPlugin.ts | 4 ---- .../src/theme-classic.d.ts | 2 ++ .../src/theme/CodeBlock/Content/String.tsx | 3 ++- .../docusaurus-theme-common/src/internal.ts | 1 + .../src/utils/codeBlockUtils.ts | 17 ++++++++++++++--- .../src/theme/CodeBlock/index.tsx | 19 ++++++++++++++++--- 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/docusaurus-mdx-loader/src/remark/mdx1Compat/codeCompatPlugin.ts b/packages/docusaurus-mdx-loader/src/remark/mdx1Compat/codeCompatPlugin.ts index f71dca66eed2..3f8ce8e0f17b 100644 --- a/packages/docusaurus-mdx-loader/src/remark/mdx1Compat/codeCompatPlugin.ts +++ b/packages/docusaurus-mdx-loader/src/remark/mdx1Compat/codeCompatPlugin.ts @@ -24,10 +24,6 @@ const plugin: Plugin = function plugin(): Transformer { node.data.hProperties = node.data.hProperties || {}; node.data.hProperties.metastring = node.meta; - - // Retrocompatible support for live codeblock metastring - // Not really the appropriate place to handle that :s - node.data.hProperties.live = node.meta?.split(' ').includes('live'); }); }; }; diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 323ef6effb24..8ce0a2d28943 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -405,6 +405,7 @@ declare module '@theme/BlogLayout' { declare module '@theme/CodeBlock' { import type {ReactNode} from 'react'; + import type {CodeBlockMetaOptions} from '@docusaurus/theme-common/internal'; export interface Props { readonly children: ReactNode; @@ -413,6 +414,7 @@ declare module '@theme/CodeBlock' { readonly title?: ReactNode; readonly language?: string; readonly showLineNumbers?: boolean | number; + readonly metaOptions?: CodeBlockMetaOptions; } export default function CodeBlock(props: Props): ReactNode; diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx index 9bd19f00f4e4..3c2706099790 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx @@ -40,6 +40,7 @@ export default function CodeBlockString({ title: titleProp, showLineNumbers: showLineNumbersProp, language: languageProp, + metaOptions: metaOptionsProp, }: Props): ReactNode { const { prism: {defaultLanguage, magicComments}, @@ -52,7 +53,7 @@ export default function CodeBlockString({ const wordWrap = useCodeWordWrap(); const isBrowser = useIsBrowser(); - const metaOptions = parseCodeBlockMetaOptions(metastring); + const metaOptions = parseCodeBlockMetaOptions(metastring, metaOptionsProp); // We still parse the metastring in case we want to support more syntax in the // future. Note that MDX doesn't strip quotes when parsing metastring: diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index 149e1f054852..6346d0cb1d49 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -39,6 +39,7 @@ export { parseLines, getCodeBlockTitle, getLineNumbersStart, + type CodeBlockMetaOptions, } from './utils/codeBlockUtils'; export {DEFAULT_SEARCH_TAG} from './utils/searchUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts index d6fa6bbe6dcf..76e3c6df91b9 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts @@ -335,20 +335,31 @@ export function parseLines( /** * Parses {@link CodeBlockParsedLines.metaOptions} from the given metastring. * @param metastring The metastring to parse + * @param metaOptionsProp any meta options defined via component props. * @returns The parsed options. */ export function parseCodeBlockMetaOptions( metastring: string | undefined, + metaOptionsProp: CodeBlockMetaOptions | undefined, ): CodeBlockMetaOptions { + // If we already have options via props use them as they are + if (metaOptionsProp) { + return metaOptionsProp; + } + const parsedOptions: CodeBlockMetaOptions = {}; + // NOTE: until we parse generally all options contained in this string + // we keep the old custom logic which was moved from their old spots to here. + + // normal codeblock parsedOptions.title = parseCodeBlockTitle(metastring); + // parsedOptions.showLineNumbers = TODO; - // parsedOptions.live = TODO; + // interactive code editor (theme-live-codeblock => Playground) + parsedOptions.live = metastring?.split(' ').includes('live'); // parsedOptions.noInline = TODO; - // parsedOptions.showLineNumbers = TODO; - return parsedOptions; } diff --git a/packages/docusaurus-theme-live-codeblock/src/theme/CodeBlock/index.tsx b/packages/docusaurus-theme-live-codeblock/src/theme/CodeBlock/index.tsx index 225073c0ee64..66a10b7a1ec0 100644 --- a/packages/docusaurus-theme-live-codeblock/src/theme/CodeBlock/index.tsx +++ b/packages/docusaurus-theme-live-codeblock/src/theme/CodeBlock/index.tsx @@ -6,17 +6,30 @@ */ import React from 'react'; +import {parseCodeBlockMetaOptions} from '@docusaurus/theme-common/internal'; import Playground from '@theme/Playground'; import ReactLiveScope from '@theme/ReactLiveScope'; import CodeBlock, {type Props} from '@theme-init/CodeBlock'; const withLiveEditor = (Component: typeof CodeBlock) => { function WrappedComponent(props: Props) { - if (props.live) { - return ; + const metaOptions = parseCodeBlockMetaOptions( + props.metastring, + props.metaOptions, + ); + + const live = props.live ?? metaOptions.live; + if (live) { + return ( + + ); } - return ; + return ; } return WrappedComponent; From b9df87793e0c5ae4adb98756a3281dea0dfc4ad8 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 22 Mar 2025 16:44:19 +0100 Subject: [PATCH 04/11] refactor(theme-common, theme-live-codeblock): Use metaOptions for noInline --- .../docusaurus-theme-common/src/utils/codeBlockUtils.ts | 2 +- .../src/theme/Playground/index.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts index 76e3c6df91b9..331fb7a69cf5 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts @@ -358,7 +358,7 @@ export function parseCodeBlockMetaOptions( // interactive code editor (theme-live-codeblock => Playground) parsedOptions.live = metastring?.split(' ').includes('live'); - // parsedOptions.noInline = TODO; + parsedOptions.noInline = metastring?.includes('noInline'); return parsedOptions; } diff --git a/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx b/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx index 37cda3ee0280..d4cfca3aed4b 100644 --- a/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx +++ b/packages/docusaurus-theme-live-codeblock/src/theme/Playground/index.tsx @@ -16,6 +16,7 @@ import { ErrorBoundaryErrorMessageFallback, usePrismTheme, } from '@docusaurus/theme-common'; +import {parseCodeBlockMetaOptions} from '@docusaurus/theme-common/internal'; import ErrorBoundary from '@docusaurus/ErrorBoundary'; import type {Props} from '@theme/Playground'; @@ -105,6 +106,8 @@ const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`; export default function Playground({ children, transformCode, + metastring, + metaOptions: metaOptionsProp, ...props }: Props): ReactNode { const { @@ -115,7 +118,8 @@ export default function Playground({ } = themeConfig as ThemeConfig; const prismTheme = usePrismTheme(); - const noInline = props.metastring?.includes('noInline') ?? false; + const metaOptions = parseCodeBlockMetaOptions(metastring, metaOptionsProp); + const noInline = !!metaOptions.noInline; return (
From 0ea23e6a83a645c170c7734573df6852c6d7afe4 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 22 Mar 2025 17:00:04 +0100 Subject: [PATCH 05/11] refactor(theme-common, theme-classic): Use metaOptions for showLineNumbers --- .../src/theme/CodeBlock/Content/String.tsx | 2 +- .../src/utils/codeBlockUtils.ts | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx index 3c2706099790..02d39836ec3d 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx @@ -70,7 +70,7 @@ export default function CodeBlockString({ }); const lineNumbersStart = getLineNumbersStart({ showLineNumbers: showLineNumbersProp, - metastring, + metaOptions, }); return ( diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts index 331fb7a69cf5..629b1fc219a7 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts @@ -183,19 +183,22 @@ export function getCodeBlockTitle({ export function getLineNumbersStart({ showLineNumbers, - metastring, + metaOptions, }: { showLineNumbers: boolean | number | undefined; - metastring: string | undefined; + metaOptions: CodeBlockMetaOptions; }): number | undefined { + const showLineNumbersValue = showLineNumbers ?? metaOptions.showLineNumbers; + const defaultStart = 1; - if (typeof showLineNumbers === 'boolean') { - return showLineNumbers ? defaultStart : undefined; + if (typeof showLineNumbersValue === 'boolean') { + return showLineNumbersValue ? defaultStart : undefined; } - if (typeof showLineNumbers === 'number') { - return showLineNumbers; + if (typeof showLineNumbersValue === 'number') { + return showLineNumbersValue; } - return getMetaLineNumbersStart(metastring); + + return undefined; } /** @@ -354,7 +357,7 @@ export function parseCodeBlockMetaOptions( // normal codeblock parsedOptions.title = parseCodeBlockTitle(metastring); - // parsedOptions.showLineNumbers = TODO; + parsedOptions.showLineNumbers = getMetaLineNumbersStart(metastring); // interactive code editor (theme-live-codeblock => Playground) parsedOptions.live = metastring?.split(' ').includes('live'); From f26b52fc8fa24aa870a8f3fbe1384e2bf5e16aae Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sat, 22 Mar 2025 17:00:39 +0100 Subject: [PATCH 06/11] test(theme-common): Update codeBlockUtils tests --- .../__snapshots__/codeBlockUtils.test.ts.snap | 6 +- .../utils/__tests__/codeBlockUtils.test.ts | 150 +++++++++++++----- 2 files changed, 117 insertions(+), 39 deletions(-) diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap index 9fa075f3c6d1..93d4bfbf0d4e 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap +++ b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap @@ -20,11 +20,11 @@ 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 metaoptions set to false 1`] = `undefined`; -exports[`getLineNumbersStart handles prop combined with metastring set to number 1`] = `10`; +exports[`getLineNumbersStart handles prop combined with metaoptions set to number 1`] = `10`; -exports[`getLineNumbersStart handles prop combined with metastring set to true 1`] = `1`; +exports[`getLineNumbersStart handles prop combined with metaoptions set to true 1`] = `1`; exports[`getLineNumbersStart handles prop standalone set to false 1`] = `undefined`; 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 cc23683f5468..b0f065c9eb7d 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts @@ -25,50 +25,64 @@ const defaultMagicComments: MagicCommentConfig[] = [ describe('parseCodeBlockMetaOptions', () => { describe('title', () => { it('parses double quote delimited title', () => { - expect(parseCodeBlockMetaOptions(`title="index.js"`).title).toBe( - `index.js`, - ); + expect( + parseCodeBlockMetaOptions(`title="index.js"`, undefined).title, + ).toBe(`index.js`); }); it('parses single quote delimited title', () => { - expect(parseCodeBlockMetaOptions(`title='index.js'`).title).toBe( - `index.js`, - ); + expect( + parseCodeBlockMetaOptions(`title='index.js'`, undefined).title, + ).toBe(`index.js`); }); it('does not parse mismatched quote delimiters', () => { - expect(parseCodeBlockMetaOptions(`title="index.js'`).title).toBe(``); + expect( + parseCodeBlockMetaOptions(`title="index.js'`, undefined).title, + ).toBe(``); }); it('parses undefined metastring', () => { - expect(parseCodeBlockMetaOptions(undefined).title).toBe(``); + expect(parseCodeBlockMetaOptions(undefined, undefined).title).toBe(``); }); it('parses metastring with no title specified', () => { - expect(parseCodeBlockMetaOptions(`{1,2-3}`).title).toBe(``); + expect(parseCodeBlockMetaOptions(`{1,2-3}`, undefined).title).toBe(``); }); it('parses with multiple metadata title first', () => { expect( - parseCodeBlockMetaOptions(`title="index.js" label="JavaScript"`).title, + parseCodeBlockMetaOptions( + `title="index.js" label="JavaScript"`, + undefined, + ).title, ).toBe(`index.js`); }); it('parses with multiple metadata title last', () => { expect( - parseCodeBlockMetaOptions(`label="JavaScript" title="index.js"`).title, + parseCodeBlockMetaOptions( + `label="JavaScript" title="index.js"`, + undefined, + ).title, ).toBe(`index.js`); }); it('parses double quotes when delimited by single quotes', () => { expect( - parseCodeBlockMetaOptions(`title='console.log("Hello, World!")'`).title, + parseCodeBlockMetaOptions( + `title='console.log("Hello, World!")'`, + undefined, + ).title, ).toBe(`console.log("Hello, World!")`); }); it('parses single quotes when delimited by double quotes', () => { expect( - parseCodeBlockMetaOptions(`title="console.log('Hello, World!')"`).title, + parseCodeBlockMetaOptions( + `title="console.log('Hello, World!')"`, + undefined, + ).title, ).toBe(`console.log('Hello, World!')`); }); }); @@ -148,7 +162,11 @@ bbbbb`, `// highlight-next-line aaaaa bbbbb`, - {metastring: '', language: 'js', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'js', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot(); expect( @@ -157,7 +175,11 @@ bbbbb`, aaaaa // highlight-end bbbbb`, - {metastring: '', language: 'js', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'js', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot(); expect( @@ -169,7 +191,11 @@ bbbbbbb // highlight-next-line // highlight-end bbbbb`, - {metastring: '', language: 'js', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'js', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot(); }); @@ -179,7 +205,11 @@ bbbbb`, `# highlight-next-line aaaaa bbbbb`, - {metastring: '', language: 'js', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'js', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot('js'); expect( @@ -187,7 +217,11 @@ bbbbb`, `/* highlight-next-line */ aaaaa bbbbb`, - {metastring: '', language: 'py', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'py', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot('py'); expect( @@ -200,7 +234,11 @@ bbbbb ccccc dddd`, - {metastring: '', language: 'py', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'py', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot('py'); expect( @@ -213,7 +251,11 @@ bbbbb ccccc dddd`, - {metastring: '', language: '', magicComments: defaultMagicComments}, + { + metastring: '', + language: '', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot('none'); expect( @@ -224,7 +266,11 @@ aaaa bbbbb dddd`, - {metastring: '', language: 'jsx', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'jsx', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot('jsx'); expect( @@ -235,7 +281,11 @@ aaaa bbbbb dddd`, - {metastring: '', language: 'html', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'html', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot('html'); expect( @@ -261,7 +311,11 @@ dddd console.log("preserved"); \`\`\` `, - {metastring: '', language: 'md', magicComments: defaultMagicComments}, + { + metastring: '', + language: 'md', + magicComments: defaultMagicComments, + }, ), ).toMatchSnapshot('md'); }); @@ -373,24 +427,26 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, - metastring: undefined, + metaOptions: {}, }), ).toMatchSnapshot(); expect( getLineNumbersStart({ showLineNumbers: undefined, - metastring: '', + metaOptions: {}, }), ).toMatchSnapshot(); }); describe('handles prop', () => { - describe('combined with metastring', () => { + describe('combined with metaoptions', () => { it('set to true', () => { expect( getLineNumbersStart({ showLineNumbers: true, - metastring: 'showLineNumbers=2', + metaOptions: { + showLineNumbers: 2, + }, }), ).toMatchSnapshot(); }); @@ -399,7 +455,9 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: false, - metastring: 'showLineNumbers=2', + metaOptions: { + showLineNumbers: 2, + }, }), ).toMatchSnapshot(); }); @@ -408,7 +466,9 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: 10, - metastring: 'showLineNumbers=2', + metaOptions: { + showLineNumbers: 2, + }, }), ).toMatchSnapshot(); }); @@ -419,7 +479,9 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: true, - metastring: undefined, + metaOptions: { + showLineNumbers: 2, + }, }), ).toMatchSnapshot(); }); @@ -428,7 +490,9 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: false, - metastring: undefined, + metaOptions: { + showLineNumbers: 2, + }, }), ).toMatchSnapshot(); }); @@ -437,7 +501,9 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: 10, - metastring: undefined, + metaOptions: { + showLineNumbers: 2, + }, }), ).toMatchSnapshot(); }); @@ -450,7 +516,9 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, - metastring: 'showLineNumbers', + metaOptions: { + showLineNumbers: true, + }, }), ).toMatchSnapshot(); }); @@ -458,7 +526,9 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, - metastring: 'showLineNumbers=10', + metaOptions: { + showLineNumbers: 10, + }, }), ).toMatchSnapshot(); }); @@ -469,7 +539,11 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, - metastring: '{1,2-3} title="file.txt" showLineNumbers noInline', + metaOptions: { + title: 'file.txt', + showLineNumbers: true, + noInline: true, + }, }), ).toMatchSnapshot(); }); @@ -477,7 +551,11 @@ describe('getLineNumbersStart', () => { expect( getLineNumbersStart({ showLineNumbers: undefined, - metastring: '{1,2-3} title="file.txt" showLineNumbers=10 noInline', + metaOptions: { + title: 'file.txt', + showLineNumbers: 10, + noInline: true, + }, }), ).toMatchSnapshot(); }); From 547a7c2488a35568c7fe02fad4811bbb31656415 Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 23 Mar 2025 12:00:43 +0100 Subject: [PATCH 07/11] refactor(theme-common): Do not fill invalid/missing options --- .../src/utils/codeBlockUtils.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts index 629b1fc219a7..3cb2ff8e8ade 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts @@ -147,10 +147,6 @@ function getAllMagicCommentDirectiveStyles( } } -function parseCodeBlockTitle(metastring?: string): string { - return metastring?.match(codeBlockTitleRegex)?.groups!.title ?? ''; -} - function getMetaLineNumbersStart(metastring?: string): number | undefined { const showLineNumbersMeta = metastring ?.split(' ') @@ -356,12 +352,23 @@ export function parseCodeBlockMetaOptions( // we keep the old custom logic which was moved from their old spots to here. // normal codeblock - parsedOptions.title = parseCodeBlockTitle(metastring); - parsedOptions.showLineNumbers = getMetaLineNumbersStart(metastring); + const title = metastring?.match(codeBlockTitleRegex)?.groups!.title; + if (title !== undefined) { + parsedOptions.title = title; + } + const showLineNumbers = getMetaLineNumbersStart(metastring); + if (showLineNumbers !== undefined) { + parsedOptions.showLineNumbers = showLineNumbers; + } // interactive code editor (theme-live-codeblock => Playground) - parsedOptions.live = metastring?.split(' ').includes('live'); - parsedOptions.noInline = metastring?.includes('noInline'); + if (metastring?.split(' ').includes('live')) { + parsedOptions.live = true; + } + + if (metastring?.includes('noInline')) { + parsedOptions.noInline = true; + } return parsedOptions; } From 0b4addf6ba4663dda7c25409021ed504a0b658bc Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 23 Mar 2025 12:01:07 +0100 Subject: [PATCH 08/11] test(theme-common): More tests for currently known options --- .../__snapshots__/codeBlockUtils.test.ts.snap | 28 ++ .../utils/__tests__/codeBlockUtils.test.ts | 307 +++++++++++------- 2 files changed, 211 insertions(+), 124 deletions(-) diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap index 93d4bfbf0d4e..16335a3520fd 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap +++ b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap @@ -12,6 +12,10 @@ exports[`getCodeBlockTitle returns titleProp with empty string on option 1`] = ` exports[`getCodeBlockTitle with nothing set 1`] = `undefined`; +exports[`getLineNumbersStart from metastring parses flags as 1 1`] = `1`; + +exports[`getLineNumbersStart from metastring parses value 1`] = `10`; + 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`; @@ -36,6 +40,30 @@ exports[`getLineNumbersStart with nothing set 1`] = `undefined`; exports[`getLineNumbersStart with nothing set 2`] = `undefined`; +exports[`getLineNumbersStart with parsed metaoption handles metadata combined with other options set as flag 1`] = `1`; + +exports[`getLineNumbersStart with parsed metaoption handles metadata combined with other options set with number 1`] = `10`; + +exports[`getLineNumbersStart with parsed metaoption handles metadata standalone set as flag 1`] = `1`; + +exports[`getLineNumbersStart with parsed metaoption handles metadata standalone set with number 1`] = `10`; + +exports[`getLineNumbersStart with parsed metaoption handles prop combined with metaoptions set to false 1`] = `undefined`; + +exports[`getLineNumbersStart with parsed metaoption handles prop combined with metaoptions set to number 1`] = `10`; + +exports[`getLineNumbersStart with parsed metaoption handles prop combined with metaoptions set to true 1`] = `1`; + +exports[`getLineNumbersStart with parsed metaoption handles prop standalone set to false 1`] = `undefined`; + +exports[`getLineNumbersStart with parsed metaoption handles prop standalone set to number 1`] = `10`; + +exports[`getLineNumbersStart with parsed metaoption handles prop standalone set to true 1`] = `1`; + +exports[`getLineNumbersStart with parsed metaoption with nothing set 1`] = `undefined`; + +exports[`getLineNumbersStart with parsed metaoption with nothing set 2`] = `undefined`; + exports[`parseLines does not parse content with metastring 1`] = ` { "code": "aaaaa 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 b0f065c9eb7d..d8c2bcdaa5da 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts @@ -39,15 +39,19 @@ describe('parseCodeBlockMetaOptions', () => { it('does not parse mismatched quote delimiters', () => { expect( parseCodeBlockMetaOptions(`title="index.js'`, undefined).title, - ).toBe(``); + ).toBeUndefined(); }); it('parses undefined metastring', () => { - expect(parseCodeBlockMetaOptions(undefined, undefined).title).toBe(``); + expect( + parseCodeBlockMetaOptions(undefined, undefined).title, + ).toBeUndefined(); }); it('parses metastring with no title specified', () => { - expect(parseCodeBlockMetaOptions(`{1,2-3}`, undefined).title).toBe(``); + expect( + parseCodeBlockMetaOptions(`{1,2-3}`, undefined).title, + ).toBeUndefined(); }); it('parses with multiple metadata title first', () => { @@ -86,7 +90,34 @@ describe('parseCodeBlockMetaOptions', () => { ).toBe(`console.log('Hello, World!')`); }); }); + + // showLineNumber logic is tested in combination with getLineNumber below + + describe('live', () => { + it('parses live as true', () => { + expect(parseCodeBlockMetaOptions(`live`, undefined).live).toBe(true); + }); + it('parses no live as undefined', () => { + expect( + parseCodeBlockMetaOptions(` otherOption `, undefined).live, + ).toBeUndefined(); + }); + }); + + describe('noInline', () => { + it('parses noInline as true', () => { + expect(parseCodeBlockMetaOptions(`noInline`, undefined).noInline).toBe( + true, + ); + }); + it('parses no noInline as undefined', () => { + expect( + parseCodeBlockMetaOptions(` otherOption `, undefined).noInline, + ).toBeUndefined(); + }); + }); }); + describe('parseLanguage', () => { it('works', () => { expect(parseLanguage('language-foo xxx yyy')).toBe('foo'); @@ -423,142 +454,170 @@ line }); describe('getLineNumbersStart', () => { - it('with nothing set', () => { - expect( - getLineNumbersStart({ - showLineNumbers: undefined, - metaOptions: {}, - }), - ).toMatchSnapshot(); - expect( - getLineNumbersStart({ - showLineNumbers: undefined, - metaOptions: {}, - }), - ).toMatchSnapshot(); - }); + describe('with parsed metaoption', () => { + it('with nothing set', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: {}, + }), + ).toMatchSnapshot(); + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: {}, + }), + ).toMatchSnapshot(); + }); - describe('handles prop', () => { - describe('combined with metaoptions', () => { - it('set to true', () => { - expect( - getLineNumbersStart({ - showLineNumbers: true, - metaOptions: { - showLineNumbers: 2, - }, - }), - ).toMatchSnapshot(); - }); + describe('handles prop', () => { + describe('combined with metaoptions', () => { + it('set to true', () => { + expect( + getLineNumbersStart({ + showLineNumbers: true, + metaOptions: { + showLineNumbers: 2, + }, + }), + ).toMatchSnapshot(); + }); - it('set to false', () => { - expect( - getLineNumbersStart({ - showLineNumbers: false, - metaOptions: { - showLineNumbers: 2, - }, - }), - ).toMatchSnapshot(); - }); + it('set to false', () => { + expect( + getLineNumbersStart({ + showLineNumbers: false, + metaOptions: { + showLineNumbers: 2, + }, + }), + ).toMatchSnapshot(); + }); - it('set to number', () => { - expect( - getLineNumbersStart({ - showLineNumbers: 10, - metaOptions: { - showLineNumbers: 2, - }, - }), - ).toMatchSnapshot(); + it('set to number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: 10, + metaOptions: { + showLineNumbers: 2, + }, + }), + ).toMatchSnapshot(); + }); }); - }); - describe('standalone', () => { - it('set to true', () => { - expect( - getLineNumbersStart({ - showLineNumbers: true, - metaOptions: { - showLineNumbers: 2, - }, - }), - ).toMatchSnapshot(); + describe('standalone', () => { + it('set to true', () => { + expect( + getLineNumbersStart({ + showLineNumbers: true, + metaOptions: { + showLineNumbers: 2, + }, + }), + ).toMatchSnapshot(); + }); + + it('set to false', () => { + expect( + getLineNumbersStart({ + showLineNumbers: false, + metaOptions: { + showLineNumbers: 2, + }, + }), + ).toMatchSnapshot(); + }); + + it('set to number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: 10, + metaOptions: { + showLineNumbers: 2, + }, + }), + ).toMatchSnapshot(); + }); }); + }); - it('set to false', () => { - expect( - getLineNumbersStart({ - showLineNumbers: false, - metaOptions: { - showLineNumbers: 2, - }, - }), - ).toMatchSnapshot(); + describe('handles metadata', () => { + describe('standalone', () => { + it('set as flag', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: { + showLineNumbers: true, + }, + }), + ).toMatchSnapshot(); + }); + it('set with number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: { + showLineNumbers: 10, + }, + }), + ).toMatchSnapshot(); + }); }); - it('set to number', () => { - expect( - getLineNumbersStart({ - showLineNumbers: 10, - metaOptions: { - showLineNumbers: 2, - }, - }), - ).toMatchSnapshot(); + describe('combined with other options', () => { + it('set as flag', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: { + title: 'file.txt', + showLineNumbers: true, + noInline: true, + }, + }), + ).toMatchSnapshot(); + }); + it('set with number', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: { + title: 'file.txt', + showLineNumbers: 10, + noInline: true, + }, + }), + ).toMatchSnapshot(); + }); }); }); }); - describe('handles metadata', () => { - describe('standalone', () => { - it('set as flag', () => { - expect( - getLineNumbersStart({ - showLineNumbers: undefined, - metaOptions: { - showLineNumbers: true, - }, - }), - ).toMatchSnapshot(); - }); - it('set with number', () => { - expect( - getLineNumbersStart({ - showLineNumbers: undefined, - metaOptions: { - showLineNumbers: 10, - }, - }), - ).toMatchSnapshot(); - }); + describe('from metastring', () => { + it('parses flags as 1', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: parseCodeBlockMetaOptions( + ' showLineNumbers ', + undefined, + ), + }), + ).toMatchSnapshot(); }); - describe('combined with other options', () => { - it('set as flag', () => { - expect( - getLineNumbersStart({ - showLineNumbers: undefined, - metaOptions: { - title: 'file.txt', - showLineNumbers: true, - noInline: true, - }, - }), - ).toMatchSnapshot(); - }); - it('set with number', () => { - expect( - getLineNumbersStart({ - showLineNumbers: undefined, - metaOptions: { - title: 'file.txt', - showLineNumbers: 10, - noInline: true, - }, - }), - ).toMatchSnapshot(); - }); + it('parses value', () => { + expect( + getLineNumbersStart({ + showLineNumbers: undefined, + metaOptions: parseCodeBlockMetaOptions( + ' showLineNumbers=10 ', + undefined, + ), + }), + ).toMatchSnapshot(); }); }); }); From 97f85ef834b1e4039637ad0e1aa72fabf24a0ddc Mon Sep 17 00:00:00 2001 From: Danielku15 Date: Sun, 23 Mar 2025 11:48:45 +0100 Subject: [PATCH 09/11] feat(theme-common): Parse all options from metastring --- .../src/utils/codeBlockUtils.ts | 81 +++++++++++-------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts index 3cb2ff8e8ade..23f67f45e79e 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts @@ -9,7 +9,12 @@ import type {CSSProperties} from 'react'; import rangeParser from 'parse-numeric-range'; import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer'; -const codeBlockTitleRegex = /title=(?["'])(?.*?)\1/; +// note: regexp/no-useless-non-capturing-group is a false positive +// the group is required or it breaks the correct alternation of +// <quote><stringValue><quote> | <rawValue> +const metaOptionRegex = + // eslint-disable-next-line regexp/no-useless-non-capturing-group + /(?<key>[^\s=]+)(?:=(?:(?:(?<quote>["'])(?<stringValue>.*?)\k<quote>)|(?<rawValue>\S*)))?/g; const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/; // Supported types of highlight comments @@ -147,22 +152,6 @@ function getAllMagicCommentDirectiveStyles( } } -function getMetaLineNumbersStart(metastring?: string): number | undefined { - const showLineNumbersMeta = metastring - ?.split(' ') - .find((str) => str.startsWith('showLineNumbers')); - - if (showLineNumbersMeta) { - if (showLineNumbersMeta.startsWith('showLineNumbers=')) { - const value = showLineNumbersMeta.replace('showLineNumbers=', ''); - return parseInt(value, 10); - } - return 1; - } - - return undefined; -} - export function getCodeBlockTitle({ titleProp, metaOptions, @@ -331,6 +320,40 @@ export function parseLines( return {lineClassNames, code}; } +function parseMetaOptionValue(match: RegExpExecArray): CodeMetaOptionValue { + const {stringValue, rawValue} = match.groups!; + + // flag options without values (e.g. `showLineNumbers`) + if (stringValue === undefined && rawValue === undefined) { + return true; + } + + // NOTE: we currently on-purpose do not use JSON.parse here to avoid + // parsing object literals with unclear consequences. + + // quoted string option (e.g. `title="file.ts"` or `title='file.ts'`) + if (stringValue !== undefined) { + return stringValue; + } + + // boolean option (e.g. `live=true` or `showCopyButton=false`) + if (rawValue === 'true') { + return true; + } else if (rawValue === 'false') { + return false; + } + + // number value (e.g. `showLineNumbers=10`) + const number = parseFloat(rawValue!); + if (!Number.isNaN(number)) { + // number value + return number; + } + + // non quoted string (e.g. `live-tabMode=focus`) + return rawValue!; +} + /** * Parses {@link CodeBlockParsedLines.metaOptions} from the given metastring. * @param metastring The metastring to parse @@ -348,26 +371,16 @@ export function parseCodeBlockMetaOptions( const parsedOptions: CodeBlockMetaOptions = {}; - // NOTE: until we parse generally all options contained in this string - // we keep the old custom logic which was moved from their old spots to here. + if (metastring) { + metaOptionRegex.lastIndex = 0; - // normal codeblock - const title = metastring?.match(codeBlockTitleRegex)?.groups!.title; - if (title !== undefined) { - parsedOptions.title = title; - } - const showLineNumbers = getMetaLineNumbersStart(metastring); - if (showLineNumbers !== undefined) { - parsedOptions.showLineNumbers = showLineNumbers; - } + let match = metaOptionRegex.exec(metastring); - // interactive code editor (theme-live-codeblock => Playground) - if (metastring?.split(' ').includes('live')) { - parsedOptions.live = true; - } + while (match) { + parsedOptions[match.groups!.key!] = parseMetaOptionValue(match); - if (metastring?.includes('noInline')) { - parsedOptions.noInline = true; + match = metaOptionRegex.exec(metastring); + } } return parsedOptions; From 0c262f45ab4a5cd063a64b9bd4b31b113341038b Mon Sep 17 00:00:00 2001 From: Danielku15 <danielku15@coderline.net> Date: Sun, 23 Mar 2025 12:04:27 +0100 Subject: [PATCH 10/11] test(theme-common): Extend tests for new parsing --- .../__snapshots__/codeBlockUtils.test.ts.snap | 80 +++++++++++++++++++ .../utils/__tests__/codeBlockUtils.test.ts | 78 +++++++++++++++++- 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap index 16335a3520fd..4150912a95be 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap +++ b/packages/docusaurus-theme-common/src/utils/__tests__/__snapshots__/codeBlockUtils.test.ts.snap @@ -64,6 +64,86 @@ exports[`getLineNumbersStart with parsed metaoption with nothing set 1`] = `unde exports[`getLineNumbersStart with parsed metaoption with nothing set 2`] = `undefined`; +exports[`parseCodeBlockMetaOptions any option double quotes as string 1`] = ` +{ + "PascalCase": "Hello'Docusaurus Options", + "UPPER_CASE": "Hello'Docusaurus Options", + "camelCase": "Hello'Docusaurus Options", + "kebab-case": "Hello'Docusaurus Options", + "lowercase": "Hello'Docusaurus Options", +} +`; + +exports[`parseCodeBlockMetaOptions any option false 1`] = ` +{ + "PascalCase": false, + "UPPER_CASE": false, + "camelCase": false, + "kebab-case": false, + "lowercase": false, +} +`; + +exports[`parseCodeBlockMetaOptions any option flag as true 1`] = ` +{ + "PascalCase": true, + "UPPER_CASE": true, + "camelCase": true, + "kebab-case": true, + "lowercase": true, +} +`; + +exports[`parseCodeBlockMetaOptions any option float numbers 1`] = ` +{ + "PascalCase": 3.3, + "UPPER_CASE": 4.4, + "camelCase": 2.2, + "kebab-case": 5.5, + "lowercase": 1.1, +} +`; + +exports[`parseCodeBlockMetaOptions any option integer numbers 1`] = ` +{ + "PascalCase": 3, + "UPPER_CASE": 4, + "camelCase": 2, + "kebab-case": 5, + "lowercase": 1, +} +`; + +exports[`parseCodeBlockMetaOptions any option non quoted value as string 1`] = ` +{ + "PascalCase": "simple", + "UPPER_CASE": "simple", + "camelCase": "simple", + "kebab-case": "simple", + "lowercase": "simple", +} +`; + +exports[`parseCodeBlockMetaOptions any option single quotes as string 1`] = ` +{ + "PascalCase": "Hello"Docusaurus Options", + "UPPER_CASE": "Hello"Docusaurus Options", + "camelCase": "Hello"Docusaurus Options", + "kebab-case": "Hello"Docusaurus Options", + "lowercase": "Hello"Docusaurus Options", +} +`; + +exports[`parseCodeBlockMetaOptions any option true 1`] = ` +{ + "PascalCase": true, + "UPPER_CASE": true, + "camelCase": true, + "kebab-case": true, + "lowercase": true, +} +`; + exports[`parseLines does not parse content with metastring 1`] = ` { "code": "aaaaa 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 d8c2bcdaa5da..f59f29f199ca 100644 --- a/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts +++ b/packages/docusaurus-theme-common/src/utils/__tests__/codeBlockUtils.test.ts @@ -36,10 +36,10 @@ describe('parseCodeBlockMetaOptions', () => { ).toBe(`index.js`); }); - it('does not parse mismatched quote delimiters', () => { + it('parses mismatched quote delimiters as literal', () => { expect( parseCodeBlockMetaOptions(`title="index.js'`, undefined).title, - ).toBeUndefined(); + ).toBe(`"index.js'`); }); it('parses undefined metastring', () => { @@ -116,6 +116,80 @@ describe('parseCodeBlockMetaOptions', () => { ).toBeUndefined(); }); }); + + describe('any option', () => { + it('flag as true', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase camelCase PascalCase UPPER_CASE kebab-case`, + undefined, + ), + ).toMatchSnapshot(); + }); + + it('single quotes as string', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase='Hello"Docusaurus Options' camelCase='Hello"Docusaurus Options' PascalCase='Hello"Docusaurus Options' UPPER_CASE='Hello"Docusaurus Options' kebab-case='Hello"Docusaurus Options'`, + undefined, + ), + ).toMatchSnapshot(); + }); + + it('double quotes as string', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase="Hello'Docusaurus Options" camelCase="Hello'Docusaurus Options" PascalCase="Hello'Docusaurus Options" UPPER_CASE="Hello'Docusaurus Options" kebab-case="Hello'Docusaurus Options"`, + undefined, + ), + ).toMatchSnapshot(); + }); + + it('true', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase=true camelCase=true PascalCase=true UPPER_CASE=true kebab-case=true`, + undefined, + ), + ).toMatchSnapshot(); + }); + + it('false', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase=false camelCase=false PascalCase=false UPPER_CASE=false kebab-case=false`, + undefined, + ), + ).toMatchSnapshot(); + }); + + it('integer numbers', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase=1 camelCase=2 PascalCase=3 UPPER_CASE=4 kebab-case=5`, + undefined, + ), + ).toMatchSnapshot(); + }); + + it('float numbers', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase=1.1 camelCase=2.2 PascalCase=3.3 UPPER_CASE=4.4 kebab-case=5.5`, + undefined, + ), + ).toMatchSnapshot(); + }); + + it('non quoted value as string', () => { + expect( + parseCodeBlockMetaOptions( + `lowercase=simple camelCase=simple PascalCase=simple UPPER_CASE=simple kebab-case=simple`, + undefined, + ), + ).toMatchSnapshot(); + }); + }); }); describe('parseLanguage', () => { From 2acb5bd435a029788876bcf687087f65b6ba96a0 Mon Sep 17 00:00:00 2001 From: Danielku15 <danielku15@coderline.net> Date: Sun, 23 Mar 2025 12:32:35 +0100 Subject: [PATCH 11/11] feat(theme-classic): Add CodeBlockToken component for swizzling --- .../src/theme-classic.d.ts | 12 ++++++++++++ .../src/theme/CodeBlock/Line/index.tsx | 18 ++++++++++-------- .../src/theme/CodeBlock/Token/index.tsx | 16 ++++++++++++++++ .../markdown-features-code-blocks.mdx | 6 ++++++ 4 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 packages/docusaurus-theme-classic/src/theme/CodeBlock/Token/index.tsx diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 8ce0a2d28943..4e7efd074cbe 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -502,6 +502,18 @@ declare module '@theme/CodeBlock/WordWrapButton' { export default function WordWrapButton(props: Props): ReactNode; } +declare module '@theme/CodeBlock/Token' { + import type {ReactNode} from 'react'; + import type {Props as LineProps} from '@theme/CodeBlock/Line'; + import type {TokenOutputProps} from 'prism-react-renderer'; + + export interface Props extends TokenOutputProps { + line: LineProps; + } + + export default function CodeBlockToken(props: Props): ReactNode; +} + declare module '@theme/DocCard' { import type {ReactNode} from 'react'; import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx index e56bba27ed66..fe0a150204de 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx @@ -8,6 +8,7 @@ import React, {type ReactNode} from 'react'; import clsx from 'clsx'; import type {Props} from '@theme/CodeBlock/Line'; +import CodeBlockToken from '@theme/CodeBlock/Token'; import styles from './styles.module.css'; @@ -26,13 +27,14 @@ function fixLineBreak(line: Token[]) { return line; } -export default function CodeBlockLine({ - line: lineProp, - classNames, - showLineNumbers, - getLineProps, - getTokenProps, -}: Props): ReactNode { +export default function CodeBlockLine(props: Props): ReactNode { + const { + line: lineProp, + classNames, + showLineNumbers, + getLineProps, + getTokenProps, + } = props; const line = fixLineBreak(lineProp); const lineProps = getLineProps({ @@ -41,7 +43,7 @@ export default function CodeBlockLine({ }); const lineTokens = line.map((token, key) => ( - <span key={key} {...getTokenProps({token})} /> + <CodeBlockToken key={key} line={props} {...getTokenProps({token})} /> )); return ( diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Token/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Token/index.tsx new file mode 100644 index 000000000000..077264466490 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Token/index.tsx @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ReactNode} from 'react'; +import type {Props} from '@theme/CodeBlock/Token'; + +export default function CodeBlockToken(props: Props): ReactNode { + // We omit the "line" information in the default rendering, + // but its meant for devs who devs who Swizzle this component. + const {line, ...tokenProps} = props; + return <span {...tokenProps} />; +} diff --git a/website/docs/guides/markdown-features/markdown-features-code-blocks.mdx b/website/docs/guides/markdown-features/markdown-features-code-blocks.mdx index da7e5d020591..75db636a2e1b 100644 --- a/website/docs/guides/markdown-features/markdown-features-code-blocks.mdx +++ b/website/docs/guides/markdown-features/markdown-features-code-blocks.mdx @@ -390,6 +390,12 @@ 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. +```bash npm2yarn +npm run swizzle @docusaurus/theme-classic CodeBlock/Token +``` + +The `Token` component will receive the CSS styles, a className, and the raw syntax token content, based on which you can conditionally render different markup. You also have access to the parent line via `props.line` to access information like the class names applied from magic comments. + ## 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).