mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-29 02:08:36 +02:00
refactor(theme): CodeBlock, centralize metadata parsing + refactor theme component (#11059)
* fix import duplicated * centralize parsing of code block metadata * split logic into many subcomponents * extract getCodeBlockClassName * fix duplicate useCodeWordWrap() call * simplify JSX * move ensureLanguageClassName logic to theme-common * fix line highlighting bug * rename tokens to lines * Extract Pre/Code subcomponents * Add tests for metadata language * Add tests for metadata className * Add tests for metadata title * Add tests for metadata line highlighting * Add tests for metadata lineNumbersStart
This commit is contained in:
parent
f6bdc3123b
commit
d28210d35b
4 changed files with 489 additions and 146 deletions
|
@ -5,18 +5,16 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {type ReactNode} from 'react';
|
import React, {type ComponentProps, type ReactNode} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {useThemeConfig, usePrismTheme} from '@docusaurus/theme-common';
|
import {useThemeConfig, usePrismTheme} from '@docusaurus/theme-common';
|
||||||
import {
|
import {
|
||||||
parseCodeBlockTitle,
|
|
||||||
parseLanguage,
|
|
||||||
parseLines,
|
|
||||||
getLineNumbersStart,
|
|
||||||
useCodeWordWrap,
|
useCodeWordWrap,
|
||||||
|
createCodeBlockMetadata,
|
||||||
|
type CodeBlockMetadata,
|
||||||
} from '@docusaurus/theme-common/internal';
|
} from '@docusaurus/theme-common/internal';
|
||||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||||
import {Highlight, type Language} from 'prism-react-renderer';
|
import {Highlight} from 'prism-react-renderer';
|
||||||
import Line from '@theme/CodeBlock/Line';
|
import Line from '@theme/CodeBlock/Line';
|
||||||
import CopyButton from '@theme/CodeBlock/CopyButton';
|
import CopyButton from '@theme/CodeBlock/CopyButton';
|
||||||
import WordWrapButton from '@theme/CodeBlock/WordWrapButton';
|
import WordWrapButton from '@theme/CodeBlock/WordWrapButton';
|
||||||
|
@ -25,81 +23,67 @@ import type {Props} from '@theme/CodeBlock/Content/String';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
// Prism languages are always lowercase
|
type WordWrap = ReturnType<typeof useCodeWordWrap>;
|
||||||
// We want to fail-safe and allow both "php" and "PHP"
|
|
||||||
// See https://github.com/facebook/docusaurus/issues/9012
|
function CodeBlockTitle({children}: {children: ReactNode}): ReactNode {
|
||||||
function normalizeLanguage(language: string | undefined): string | undefined {
|
// Just a pass-through for now
|
||||||
return language?.toLowerCase();
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CodeBlockString({
|
// TODO Docusaurus v4: remove useless forwardRef
|
||||||
children,
|
const Pre = React.forwardRef<HTMLPreElement, ComponentProps<'pre'>>(
|
||||||
className: blockClassName = '',
|
(props, ref) => {
|
||||||
metastring,
|
|
||||||
title: titleProp,
|
|
||||||
showLineNumbers: showLineNumbersProp,
|
|
||||||
language: languageProp,
|
|
||||||
}: Props): ReactNode {
|
|
||||||
const {
|
|
||||||
prism: {defaultLanguage, magicComments},
|
|
||||||
} = useThemeConfig();
|
|
||||||
const language = normalizeLanguage(
|
|
||||||
languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage,
|
|
||||||
);
|
|
||||||
|
|
||||||
const prismTheme = usePrismTheme();
|
|
||||||
const wordWrap = useCodeWordWrap();
|
|
||||||
const isBrowser = useIsBrowser();
|
|
||||||
|
|
||||||
// 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 {lineClassNames, code} = parseLines(children, {
|
|
||||||
metastring,
|
|
||||||
language,
|
|
||||||
magicComments,
|
|
||||||
});
|
|
||||||
const lineNumbersStart = getLineNumbersStart({
|
|
||||||
showLineNumbers: showLineNumbersProp,
|
|
||||||
metastring,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
|
||||||
as="div"
|
|
||||||
className={clsx(
|
|
||||||
blockClassName,
|
|
||||||
language &&
|
|
||||||
!blockClassName.includes(`language-${language}`) &&
|
|
||||||
`language-${language}`,
|
|
||||||
)}>
|
|
||||||
{title && <div className={styles.codeBlockTitle}>{title}</div>}
|
|
||||||
<div className={styles.codeBlockContent}>
|
|
||||||
<Highlight
|
|
||||||
theme={prismTheme}
|
|
||||||
code={code}
|
|
||||||
language={(language ?? 'text') as Language}>
|
|
||||||
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
|
||||||
<pre
|
<pre
|
||||||
|
ref={ref}
|
||||||
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
|
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
ref={wordWrap.codeBlockRef}
|
{...props}
|
||||||
className={clsx(className, styles.codeBlock, 'thin-scrollbar')}
|
className={clsx(props.className, styles.codeBlock, 'thin-scrollbar')}
|
||||||
style={style}>
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function Code({
|
||||||
|
metadata,
|
||||||
|
...props
|
||||||
|
}: {metadata: CodeBlockMetadata} & ComponentProps<'code'>) {
|
||||||
|
return (
|
||||||
<code
|
<code
|
||||||
|
{...props}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
props.className,
|
||||||
styles.codeBlockLines,
|
styles.codeBlockLines,
|
||||||
lineNumbersStart !== undefined &&
|
metadata.lineNumbersStart !== undefined &&
|
||||||
styles.codeBlockLinesWithNumbering,
|
styles.codeBlockLinesWithNumbering,
|
||||||
)}
|
)}
|
||||||
style={
|
style={{
|
||||||
lineNumbersStart === undefined
|
...props.style,
|
||||||
|
counterReset:
|
||||||
|
metadata.lineNumbersStart === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: {counterReset: `line-count ${lineNumbersStart - 1}`}
|
: `line-count ${metadata.lineNumbersStart - 1}`,
|
||||||
}>
|
}}
|
||||||
{tokens.map((line, i) => (
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeBlockContent({
|
||||||
|
metadata,
|
||||||
|
wordWrap,
|
||||||
|
}: {
|
||||||
|
metadata: CodeBlockMetadata;
|
||||||
|
wordWrap: WordWrap;
|
||||||
|
}): ReactNode {
|
||||||
|
const prismTheme = usePrismTheme();
|
||||||
|
const {code, language, lineNumbersStart, lineClassNames} = metadata;
|
||||||
|
return (
|
||||||
|
<Highlight theme={prismTheme} code={code} language={language}>
|
||||||
|
{({className, style, tokens: lines, getLineProps, getTokenProps}) => (
|
||||||
|
<Pre ref={wordWrap.codeBlockRef} className={className} style={style}>
|
||||||
|
<Code metadata={metadata}>
|
||||||
|
{lines.map((line, i) => (
|
||||||
<Line
|
<Line
|
||||||
key={i}
|
key={i}
|
||||||
line={line}
|
line={line}
|
||||||
|
@ -109,11 +93,21 @@ export default function CodeBlockString({
|
||||||
showLineNumbers={lineNumbersStart !== undefined}
|
showLineNumbers={lineNumbersStart !== undefined}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</code>
|
</Code>
|
||||||
</pre>
|
</Pre>
|
||||||
)}
|
)}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
{isBrowser ? (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeBlockButtons({
|
||||||
|
metadata,
|
||||||
|
wordWrap,
|
||||||
|
}: {
|
||||||
|
metadata: CodeBlockMetadata;
|
||||||
|
wordWrap: WordWrap;
|
||||||
|
}): ReactNode {
|
||||||
|
return (
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
{(wordWrap.isEnabled || wordWrap.isCodeScrollable) && (
|
{(wordWrap.isEnabled || wordWrap.isCodeScrollable) && (
|
||||||
<WordWrapButton
|
<WordWrapButton
|
||||||
|
@ -122,10 +116,46 @@ export default function CodeBlockString({
|
||||||
isEnabled={wordWrap.isEnabled}
|
isEnabled={wordWrap.isEnabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CopyButton className={styles.codeButton} code={code} />
|
<CopyButton className={styles.codeButton} code={metadata.code} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeBlockLayout({metadata}: {metadata: CodeBlockMetadata}): ReactNode {
|
||||||
|
const isBrowser = useIsBrowser();
|
||||||
|
const wordWrap = useCodeWordWrap();
|
||||||
|
return (
|
||||||
|
<Container as="div" className={metadata.className}>
|
||||||
|
{metadata.title && (
|
||||||
|
<div className={styles.codeBlockTitle}>
|
||||||
|
<CodeBlockTitle>{metadata.title}</CodeBlockTitle>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.codeBlockContent}>
|
||||||
|
<CodeBlockContent metadata={metadata} wordWrap={wordWrap} />
|
||||||
|
{isBrowser && (
|
||||||
|
<CodeBlockButtons metadata={metadata} wordWrap={wordWrap} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useCodeBlockMetadata(props: Props): CodeBlockMetadata {
|
||||||
|
const {prism} = useThemeConfig();
|
||||||
|
return createCodeBlockMetadata({
|
||||||
|
code: props.children,
|
||||||
|
className: props.className,
|
||||||
|
metastring: props.metastring,
|
||||||
|
magicComments: prism.magicComments,
|
||||||
|
defaultLanguage: prism.defaultLanguage,
|
||||||
|
language: props.language,
|
||||||
|
title: props.title,
|
||||||
|
showLineNumbers: props.showLineNumbers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockString(props: Props): ReactNode {
|
||||||
|
const metadata = useCodeBlockMetadata(props);
|
||||||
|
return <CodeBlockLayout metadata={metadata} />;
|
||||||
|
}
|
||||||
|
|
|
@ -34,10 +34,9 @@ export {ColorModeProvider} from './contexts/colorMode';
|
||||||
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
parseCodeBlockTitle,
|
type CodeBlockMetadata,
|
||||||
parseLanguage,
|
createCodeBlockMetadata,
|
||||||
parseLines,
|
getPrismCssVariables,
|
||||||
getLineNumbersStart,
|
|
||||||
} from './utils/codeBlockUtils';
|
} from './utils/codeBlockUtils';
|
||||||
|
|
||||||
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||||
|
@ -88,7 +87,6 @@ export {
|
||||||
} from './hooks/useKeyboardNavigation';
|
} from './hooks/useKeyboardNavigation';
|
||||||
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
|
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
|
||||||
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
||||||
export {getPrismCssVariables} from './utils/codeBlockUtils';
|
|
||||||
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -9,10 +9,19 @@ import {
|
||||||
getLineNumbersStart,
|
getLineNumbersStart,
|
||||||
type MagicCommentConfig,
|
type MagicCommentConfig,
|
||||||
parseCodeBlockTitle,
|
parseCodeBlockTitle,
|
||||||
parseLanguage,
|
parseClassNameLanguage,
|
||||||
parseLines,
|
parseLines,
|
||||||
|
createCodeBlockMetadata,
|
||||||
} from '../codeBlockUtils';
|
} from '../codeBlockUtils';
|
||||||
|
|
||||||
|
const defaultMagicComments: MagicCommentConfig[] = [
|
||||||
|
{
|
||||||
|
className: 'theme-code-block-highlighted-line',
|
||||||
|
line: 'highlight-next-line',
|
||||||
|
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
describe('parseCodeBlockTitle', () => {
|
describe('parseCodeBlockTitle', () => {
|
||||||
it('parses double quote delimited title', () => {
|
it('parses double quote delimited title', () => {
|
||||||
expect(parseCodeBlockTitle(`title="index.js"`)).toBe(`index.js`);
|
expect(parseCodeBlockTitle(`title="index.js"`)).toBe(`index.js`);
|
||||||
|
@ -59,24 +68,16 @@ describe('parseCodeBlockTitle', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('parseLanguage', () => {
|
describe('parseClassNameLanguage', () => {
|
||||||
it('works', () => {
|
it('works', () => {
|
||||||
expect(parseLanguage('language-foo xxx yyy')).toBe('foo');
|
expect(parseClassNameLanguage('language-foo xxx yyy')).toBe('foo');
|
||||||
expect(parseLanguage('xxxxx language-foo yyy')).toBe('foo');
|
expect(parseClassNameLanguage('xxxxx language-foo yyy')).toBe('foo');
|
||||||
expect(parseLanguage('xx-language-foo yyyy')).toBeUndefined();
|
expect(parseClassNameLanguage('xx-language-foo yyyy')).toBeUndefined();
|
||||||
expect(parseLanguage('xxx yyy zzz')).toBeUndefined();
|
expect(parseClassNameLanguage('xxx yyy zzz')).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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(
|
expect(
|
||||||
parseLines('aaaaa\nnnnnn', {
|
parseLines('aaaaa\nnnnnn', {
|
||||||
|
@ -810,3 +811,214 @@ describe('getLineNumbersStart', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createCodeBlockMetadata', () => {
|
||||||
|
type Params = Parameters<typeof createCodeBlockMetadata>[0];
|
||||||
|
|
||||||
|
const defaultParams: Params = {
|
||||||
|
code: '',
|
||||||
|
className: undefined,
|
||||||
|
metastring: '',
|
||||||
|
language: undefined,
|
||||||
|
defaultLanguage: undefined,
|
||||||
|
magicComments: defaultMagicComments,
|
||||||
|
title: undefined,
|
||||||
|
showLineNumbers: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
function create(params?: Partial<Params>) {
|
||||||
|
return createCodeBlockMetadata({...defaultParams, ...params});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('creates basic metadata', () => {
|
||||||
|
const meta = create();
|
||||||
|
expect(meta).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"className": "language-text",
|
||||||
|
"code": "",
|
||||||
|
"codeInput": "",
|
||||||
|
"language": "text",
|
||||||
|
"lineClassNames": {},
|
||||||
|
"lineNumbersStart": undefined,
|
||||||
|
"title": undefined,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('language', () => {
|
||||||
|
it('returns input language', () => {
|
||||||
|
const meta = create({language: 'js'});
|
||||||
|
expect(meta.language).toBe('js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns className language', () => {
|
||||||
|
const meta = create({className: 'x language-ts y z'});
|
||||||
|
expect(meta.language).toBe('ts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default language', () => {
|
||||||
|
const meta = create({defaultLanguage: 'jsx'});
|
||||||
|
expect(meta.language).toBe('jsx');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns fallback language', () => {
|
||||||
|
const meta = create();
|
||||||
|
expect(meta.language).toBe('text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns language with expected precedence', () => {
|
||||||
|
expect(
|
||||||
|
create({
|
||||||
|
language: 'js',
|
||||||
|
className: 'x language-ts y z',
|
||||||
|
defaultLanguage: 'jsx',
|
||||||
|
}).language,
|
||||||
|
).toBe('js');
|
||||||
|
expect(
|
||||||
|
create({
|
||||||
|
language: undefined,
|
||||||
|
className: 'x language-ts y z',
|
||||||
|
defaultLanguage: 'jsx',
|
||||||
|
}).language,
|
||||||
|
).toBe('ts');
|
||||||
|
expect(
|
||||||
|
create({
|
||||||
|
language: undefined,
|
||||||
|
className: 'x y z',
|
||||||
|
defaultLanguage: 'jsx',
|
||||||
|
}).language,
|
||||||
|
).toBe('jsx');
|
||||||
|
expect(
|
||||||
|
create({
|
||||||
|
language: undefined,
|
||||||
|
className: 'x y z',
|
||||||
|
defaultLanguage: undefined,
|
||||||
|
}).language,
|
||||||
|
).toBe('text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('code highlighting', () => {
|
||||||
|
it('returns code with no highlighting', () => {
|
||||||
|
const code = 'const x = 42;';
|
||||||
|
const meta = create({code});
|
||||||
|
expect(meta.codeInput).toBe(code);
|
||||||
|
expect(meta.code).toBe(code);
|
||||||
|
expect(meta.lineClassNames).toMatchInlineSnapshot(`{}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns code with metastring highlighting', () => {
|
||||||
|
const code = 'const x = 42;';
|
||||||
|
const meta = create({code, metastring: '{1}'});
|
||||||
|
expect(meta.codeInput).toBe(code);
|
||||||
|
expect(meta.code).toBe(code);
|
||||||
|
expect(meta.lineClassNames).toMatchInlineSnapshot(
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"0": [
|
||||||
|
"theme-code-block-highlighted-line",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns code with magic comment highlighting', () => {
|
||||||
|
const code = 'const x = 42;';
|
||||||
|
const inputCode = `// highlight-next-line\n${code}`;
|
||||||
|
|
||||||
|
const meta = create({code: inputCode});
|
||||||
|
expect(meta.codeInput).toBe(inputCode);
|
||||||
|
expect(meta.code).toBe(code);
|
||||||
|
expect(meta.lineClassNames).toMatchInlineSnapshot(
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"0": [
|
||||||
|
"theme-code-block-highlighted-line",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('className', () => {
|
||||||
|
it('returns provided className with current language', () => {
|
||||||
|
const meta = create({language: 'js', className: 'some-class'});
|
||||||
|
expect(meta.className).toBe('some-class language-js');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns provided className with fallback language', () => {
|
||||||
|
const meta = create({className: 'some-class'});
|
||||||
|
expect(meta.className).toBe('some-class language-text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns provided className without duplicating className language', () => {
|
||||||
|
const meta = create({
|
||||||
|
language: 'js',
|
||||||
|
className: 'some-class language-js',
|
||||||
|
});
|
||||||
|
expect(meta.className).toBe('some-class language-js');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('title', () => {
|
||||||
|
it('returns no title', () => {
|
||||||
|
const meta = create();
|
||||||
|
expect(meta.title).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns title from metastring', () => {
|
||||||
|
const meta = create({metastring: "title='my title meta'"});
|
||||||
|
expect(meta.title).toBe('my title meta');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns title from param', () => {
|
||||||
|
const meta = create({title: 'my title param'});
|
||||||
|
expect(meta.title).toBe('my title param');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns title from meta over params', () => {
|
||||||
|
const meta = create({
|
||||||
|
metastring: "title='my title meta'",
|
||||||
|
title: 'my title param',
|
||||||
|
});
|
||||||
|
expect(meta.title).toBe('my title meta');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showLineNumbers', () => {
|
||||||
|
it('returns no lineNumbersStart', () => {
|
||||||
|
const meta = create();
|
||||||
|
expect(meta.lineNumbersStart).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns lineNumbersStart - params.showLineNumbers=true', () => {
|
||||||
|
const meta = create({showLineNumbers: true});
|
||||||
|
expect(meta.lineNumbersStart).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns lineNumbersStart - params.showLineNumbers=3', () => {
|
||||||
|
const meta = create({showLineNumbers: 3});
|
||||||
|
expect(meta.lineNumbersStart).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns lineNumbersStart - meta showLineNumbers', () => {
|
||||||
|
const meta = create({metastring: 'showLineNumbers'});
|
||||||
|
expect(meta.lineNumbersStart).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns lineNumbersStart - meta showLineNumbers=2', () => {
|
||||||
|
const meta = create({metastring: 'showLineNumbers=2'});
|
||||||
|
expect(meta.lineNumbersStart).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns lineNumbersStart - params.showLineNumbers=3 + meta showLineNumbers=2', () => {
|
||||||
|
const meta = create({
|
||||||
|
showLineNumbers: 3,
|
||||||
|
metastring: 'showLineNumbers=2',
|
||||||
|
});
|
||||||
|
expect(meta.lineNumbersStart).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {CSSProperties} from 'react';
|
import type {CSSProperties, ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
import rangeParser from 'parse-numeric-range';
|
import rangeParser from 'parse-numeric-range';
|
||||||
import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer';
|
import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer';
|
||||||
|
|
||||||
|
@ -184,18 +185,6 @@ export function getLineNumbersStart({
|
||||||
return getMetaLineNumbersStart(metastring);
|
return getMetaLineNumbersStart(metastring);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the language name from the class name (set by MDX).
|
|
||||||
* e.g. `"language-javascript"` => `"javascript"`.
|
|
||||||
* Returns undefined if there is no language class name.
|
|
||||||
*/
|
|
||||||
export function parseLanguage(className: string): string | undefined {
|
|
||||||
const languageClassName = className
|
|
||||||
.split(' ')
|
|
||||||
.find((str) => str.startsWith('language-'));
|
|
||||||
return languageClassName?.replace(/language-/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParseCodeLinesParam = {
|
type ParseCodeLinesParam = {
|
||||||
/**
|
/**
|
||||||
* The full metastring, as received from MDX. Line ranges declared here
|
* The full metastring, as received from MDX. Line ranges declared here
|
||||||
|
@ -215,26 +204,23 @@ type ParseCodeLinesParam = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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;
|
|
||||||
/**
|
|
||||||
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
|
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
|
||||||
* means the 1st line should have `highlight` and `sample` as class names.
|
* means the 1st line should have `highlight` and `sample` as class names.
|
||||||
*/
|
*/
|
||||||
lineClassNames: {[lineIndex: number]: string[]};
|
type CodeLineClassNames = {[lineIndex: number]: string[]};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code lines after applying magic comments or metastring highlight ranges
|
||||||
|
*/
|
||||||
|
type ParsedCodeLines = {
|
||||||
|
code: string;
|
||||||
|
lineClassNames: CodeLineClassNames;
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseCodeLinesFromMetastring(
|
function parseCodeLinesFromMetastring(
|
||||||
code: string,
|
code: string,
|
||||||
{metastring, magicComments}: ParseCodeLinesParam,
|
{metastring, magicComments}: ParseCodeLinesParam,
|
||||||
): CodeLines | null {
|
): ParsedCodeLines | null {
|
||||||
// Highlighted lines specified in props: don't parse the content
|
// Highlighted lines specified in props: don't parse the content
|
||||||
if (metastring && metastringLinesRangeRegex.test(metastring)) {
|
if (metastring && metastringLinesRangeRegex.test(metastring)) {
|
||||||
const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
|
const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
|
||||||
|
@ -256,7 +242,7 @@ function parseCodeLinesFromMetastring(
|
||||||
function parseCodeLinesFromContent(
|
function parseCodeLinesFromContent(
|
||||||
code: string,
|
code: string,
|
||||||
params: ParseCodeLinesParam,
|
params: ParseCodeLinesParam,
|
||||||
): CodeLines {
|
): ParsedCodeLines {
|
||||||
const {language, magicComments} = params;
|
const {language, magicComments} = params;
|
||||||
if (language === undefined) {
|
if (language === undefined) {
|
||||||
return {lineClassNames: {}, code};
|
return {lineClassNames: {}, code};
|
||||||
|
@ -331,17 +317,134 @@ function parseCodeLinesFromContent(
|
||||||
export function parseLines(
|
export function parseLines(
|
||||||
code: string,
|
code: string,
|
||||||
params: ParseCodeLinesParam,
|
params: ParseCodeLinesParam,
|
||||||
): CodeLines {
|
): ParsedCodeLines {
|
||||||
// Historical behavior: we remove last line break
|
// Historical behavior: we remove last line break
|
||||||
const newCode = code.replace(/\r?\n$/, '');
|
const newCode = code.replace(/\r?\n$/, '');
|
||||||
// Historical behavior: we try one strategy after the other
|
// Historical behavior: we try one strategy after the other
|
||||||
// we don't support mixing metastring ranges + magic comments
|
// we don't support mixing metastring ranges + magic comments
|
||||||
|
console.log('params', {params, code});
|
||||||
|
console.log('from meta', parseCodeLinesFromMetastring(newCode, {...params}));
|
||||||
|
console.log('from content', parseCodeLinesFromContent(newCode, {...params}));
|
||||||
return (
|
return (
|
||||||
parseCodeLinesFromMetastring(newCode, {...params}) ??
|
parseCodeLinesFromMetastring(newCode, {...params}) ??
|
||||||
parseCodeLinesFromContent(newCode, {...params})
|
parseCodeLinesFromContent(newCode, {...params})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the language name from the class name (set by MDX).
|
||||||
|
* e.g. `"language-javascript"` => `"javascript"`.
|
||||||
|
* Returns undefined if there is no language class name.
|
||||||
|
*/
|
||||||
|
export function parseClassNameLanguage(
|
||||||
|
className: string | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
if (!className) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const languageClassName = className
|
||||||
|
.split(' ')
|
||||||
|
.find((str) => str.startsWith('language-'));
|
||||||
|
return languageClassName?.replace(/language-/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prism languages are always lowercase
|
||||||
|
// We want to fail-safe and allow both "php" and "PHP"
|
||||||
|
// See https://github.com/facebook/docusaurus/issues/9012
|
||||||
|
function normalizeLanguage(language: string | undefined): string | undefined {
|
||||||
|
return language?.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLanguage(params: {
|
||||||
|
language: string | undefined;
|
||||||
|
className: string | undefined;
|
||||||
|
defaultLanguage: string | undefined;
|
||||||
|
}): string {
|
||||||
|
return (
|
||||||
|
normalizeLanguage(
|
||||||
|
params.language ??
|
||||||
|
parseClassNameLanguage(params.className) ??
|
||||||
|
params.defaultLanguage,
|
||||||
|
) ?? 'text'
|
||||||
|
); // There's always a language, required by Prism;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This ensures that we always have the code block language as className
|
||||||
|
* For MDX code blocks this is provided automatically by MDX
|
||||||
|
* For JSX code blocks, the language gets added by this function
|
||||||
|
* This ensures both cases lead to a consistent HTML output
|
||||||
|
*/
|
||||||
|
function ensureLanguageClassName({
|
||||||
|
className,
|
||||||
|
language,
|
||||||
|
}: {
|
||||||
|
className: string | undefined;
|
||||||
|
language: string;
|
||||||
|
}): string {
|
||||||
|
return clsx(
|
||||||
|
className,
|
||||||
|
language &&
|
||||||
|
!className?.includes(`language-${language}`) &&
|
||||||
|
`language-${language}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeBlockMetadata {
|
||||||
|
codeInput: string; // Including magic comments
|
||||||
|
code: string; // Rendered code, excluding magic comments
|
||||||
|
className: string; // There's always a "language-<lang>" className
|
||||||
|
language: string;
|
||||||
|
title: ReactNode;
|
||||||
|
lineNumbersStart: number | undefined;
|
||||||
|
lineClassNames: CodeLineClassNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCodeBlockMetadata(params: {
|
||||||
|
code: string;
|
||||||
|
className: string | undefined;
|
||||||
|
language: string | undefined;
|
||||||
|
defaultLanguage: string | undefined;
|
||||||
|
metastring: string | undefined;
|
||||||
|
magicComments: MagicCommentConfig[];
|
||||||
|
title: ReactNode;
|
||||||
|
showLineNumbers: boolean | number | undefined;
|
||||||
|
}): CodeBlockMetadata {
|
||||||
|
const language = getLanguage({
|
||||||
|
language: params.language,
|
||||||
|
defaultLanguage: params.defaultLanguage,
|
||||||
|
className: params.className,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {lineClassNames, code} = parseLines(params.code, {
|
||||||
|
metastring: params.metastring,
|
||||||
|
magicComments: params.magicComments,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
|
const className = ensureLanguageClassName({
|
||||||
|
className: params.className,
|
||||||
|
language,
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = parseCodeBlockTitle(params.metastring) || params.title;
|
||||||
|
|
||||||
|
const lineNumbersStart = getLineNumbersStart({
|
||||||
|
showLineNumbers: params.showLineNumbers,
|
||||||
|
metastring: params.metastring,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
codeInput: params.code,
|
||||||
|
code,
|
||||||
|
className,
|
||||||
|
language,
|
||||||
|
title,
|
||||||
|
lineNumbersStart,
|
||||||
|
lineClassNames,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
||||||
const mapping: PrismThemeEntry = {
|
const mapping: PrismThemeEntry = {
|
||||||
color: '--prism-color',
|
color: '--prism-color',
|
||||||
|
|
Loading…
Add table
Reference in a new issue