mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 09:47:48 +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.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import React, {type ComponentProps, type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {useThemeConfig, usePrismTheme} from '@docusaurus/theme-common';
|
||||
import {
|
||||
parseCodeBlockTitle,
|
||||
parseLanguage,
|
||||
parseLines,
|
||||
getLineNumbersStart,
|
||||
useCodeWordWrap,
|
||||
createCodeBlockMetadata,
|
||||
type CodeBlockMetadata,
|
||||
} from '@docusaurus/theme-common/internal';
|
||||
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 CopyButton from '@theme/CodeBlock/CopyButton';
|
||||
import WordWrapButton from '@theme/CodeBlock/WordWrapButton';
|
||||
|
@ -25,107 +23,139 @@ import type {Props} from '@theme/CodeBlock/Content/String';
|
|||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// 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();
|
||||
type WordWrap = ReturnType<typeof useCodeWordWrap>;
|
||||
|
||||
function CodeBlockTitle({children}: {children: ReactNode}): ReactNode {
|
||||
// Just a pass-through for now
|
||||
return children;
|
||||
}
|
||||
|
||||
export default function CodeBlockString({
|
||||
children,
|
||||
className: blockClassName = '',
|
||||
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,
|
||||
});
|
||||
// TODO Docusaurus v4: remove useless forwardRef
|
||||
const Pre = React.forwardRef<HTMLPreElement, ComponentProps<'pre'>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<pre
|
||||
ref={ref}
|
||||
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
className={clsx(props.className, styles.codeBlock, 'thin-scrollbar')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function Code({
|
||||
metadata,
|
||||
...props
|
||||
}: {metadata: CodeBlockMetadata} & ComponentProps<'code'>) {
|
||||
return (
|
||||
<Container
|
||||
as="div"
|
||||
<code
|
||||
{...props}
|
||||
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
|
||||
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
|
||||
tabIndex={0}
|
||||
ref={wordWrap.codeBlockRef}
|
||||
className={clsx(className, styles.codeBlock, 'thin-scrollbar')}
|
||||
style={style}>
|
||||
<code
|
||||
className={clsx(
|
||||
styles.codeBlockLines,
|
||||
lineNumbersStart !== undefined &&
|
||||
styles.codeBlockLinesWithNumbering,
|
||||
)}
|
||||
style={
|
||||
lineNumbersStart === undefined
|
||||
? undefined
|
||||
: {counterReset: `line-count ${lineNumbersStart - 1}`}
|
||||
}>
|
||||
{tokens.map((line, i) => (
|
||||
<Line
|
||||
key={i}
|
||||
line={line}
|
||||
getLineProps={getLineProps}
|
||||
getTokenProps={getTokenProps}
|
||||
classNames={lineClassNames[i]}
|
||||
showLineNumbers={lineNumbersStart !== undefined}
|
||||
/>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
{isBrowser ? (
|
||||
<div className={styles.buttonGroup}>
|
||||
{(wordWrap.isEnabled || wordWrap.isCodeScrollable) && (
|
||||
<WordWrapButton
|
||||
className={styles.codeButton}
|
||||
onClick={() => wordWrap.toggle()}
|
||||
isEnabled={wordWrap.isEnabled}
|
||||
props.className,
|
||||
styles.codeBlockLines,
|
||||
metadata.lineNumbersStart !== undefined &&
|
||||
styles.codeBlockLinesWithNumbering,
|
||||
)}
|
||||
style={{
|
||||
...props.style,
|
||||
counterReset:
|
||||
metadata.lineNumbersStart === undefined
|
||||
? undefined
|
||||
: `line-count ${metadata.lineNumbersStart - 1}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
key={i}
|
||||
line={line}
|
||||
getLineProps={getLineProps}
|
||||
getTokenProps={getTokenProps}
|
||||
classNames={lineClassNames[i]}
|
||||
showLineNumbers={lineNumbersStart !== undefined}
|
||||
/>
|
||||
)}
|
||||
<CopyButton className={styles.codeButton} code={code} />
|
||||
</div>
|
||||
) : null}
|
||||
))}
|
||||
</Code>
|
||||
</Pre>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlockButtons({
|
||||
metadata,
|
||||
wordWrap,
|
||||
}: {
|
||||
metadata: CodeBlockMetadata;
|
||||
wordWrap: WordWrap;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<div className={styles.buttonGroup}>
|
||||
{(wordWrap.isEnabled || wordWrap.isCodeScrollable) && (
|
||||
<WordWrapButton
|
||||
className={styles.codeButton}
|
||||
onClick={() => wordWrap.toggle()}
|
||||
isEnabled={wordWrap.isEnabled}
|
||||
/>
|
||||
)}
|
||||
<CopyButton className={styles.codeButton} code={metadata.code} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
</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 {
|
||||
parseCodeBlockTitle,
|
||||
parseLanguage,
|
||||
parseLines,
|
||||
getLineNumbersStart,
|
||||
type CodeBlockMetadata,
|
||||
createCodeBlockMetadata,
|
||||
getPrismCssVariables,
|
||||
} from './utils/codeBlockUtils';
|
||||
|
||||
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||
|
@ -88,7 +87,6 @@ export {
|
|||
} from './hooks/useKeyboardNavigation';
|
||||
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
|
||||
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
||||
export {getPrismCssVariables} from './utils/codeBlockUtils';
|
||||
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
||||
|
||||
export {
|
||||
|
|
|
@ -9,10 +9,19 @@ import {
|
|||
getLineNumbersStart,
|
||||
type MagicCommentConfig,
|
||||
parseCodeBlockTitle,
|
||||
parseLanguage,
|
||||
parseClassNameLanguage,
|
||||
parseLines,
|
||||
createCodeBlockMetadata,
|
||||
} from '../codeBlockUtils';
|
||||
|
||||
const defaultMagicComments: MagicCommentConfig[] = [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
];
|
||||
|
||||
describe('parseCodeBlockTitle', () => {
|
||||
it('parses double quote delimited title', () => {
|
||||
expect(parseCodeBlockTitle(`title="index.js"`)).toBe(`index.js`);
|
||||
|
@ -59,24 +68,16 @@ describe('parseCodeBlockTitle', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('parseLanguage', () => {
|
||||
describe('parseClassNameLanguage', () => {
|
||||
it('works', () => {
|
||||
expect(parseLanguage('language-foo xxx yyy')).toBe('foo');
|
||||
expect(parseLanguage('xxxxx language-foo yyy')).toBe('foo');
|
||||
expect(parseLanguage('xx-language-foo yyyy')).toBeUndefined();
|
||||
expect(parseLanguage('xxx yyy zzz')).toBeUndefined();
|
||||
expect(parseClassNameLanguage('language-foo xxx yyy')).toBe('foo');
|
||||
expect(parseClassNameLanguage('xxxxx language-foo yyy')).toBe('foo');
|
||||
expect(parseClassNameLanguage('xx-language-foo yyyy')).toBeUndefined();
|
||||
expect(parseClassNameLanguage('xxx yyy zzz')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
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', {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
||||
import type {CSSProperties} from 'react';
|
||||
import type {CSSProperties, ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import rangeParser from 'parse-numeric-range';
|
||||
import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer';
|
||||
|
||||
|
@ -184,18 +185,6 @@ export function getLineNumbersStart({
|
|||
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 = {
|
||||
/**
|
||||
* The full metastring, as received from MDX. Line ranges declared here
|
||||
|
@ -214,27 +203,24 @@ type ParseCodeLinesParam = {
|
|||
magicComments: MagicCommentConfig[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
|
||||
* means the 1st line should have `highlight` and `sample` as class names.
|
||||
*/
|
||||
type CodeLineClassNames = {[lineIndex: number]: string[]};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
type ParsedCodeLines = {
|
||||
code: string;
|
||||
/**
|
||||
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
|
||||
* means the 1st line should have `highlight` and `sample` as class names.
|
||||
*/
|
||||
lineClassNames: {[lineIndex: number]: string[]};
|
||||
lineClassNames: CodeLineClassNames;
|
||||
};
|
||||
|
||||
function parseCodeLinesFromMetastring(
|
||||
code: string,
|
||||
{metastring, magicComments}: ParseCodeLinesParam,
|
||||
): CodeLines | null {
|
||||
): ParsedCodeLines | null {
|
||||
// Highlighted lines specified in props: don't parse the content
|
||||
if (metastring && metastringLinesRangeRegex.test(metastring)) {
|
||||
const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
|
||||
|
@ -256,7 +242,7 @@ function parseCodeLinesFromMetastring(
|
|||
function parseCodeLinesFromContent(
|
||||
code: string,
|
||||
params: ParseCodeLinesParam,
|
||||
): CodeLines {
|
||||
): ParsedCodeLines {
|
||||
const {language, magicComments} = params;
|
||||
if (language === undefined) {
|
||||
return {lineClassNames: {}, code};
|
||||
|
@ -331,17 +317,134 @@ function parseCodeLinesFromContent(
|
|||
export function parseLines(
|
||||
code: string,
|
||||
params: ParseCodeLinesParam,
|
||||
): CodeLines {
|
||||
): ParsedCodeLines {
|
||||
// Historical behavior: we remove last line break
|
||||
const newCode = code.replace(/\r?\n$/, '');
|
||||
// Historical behavior: we try one strategy after the other
|
||||
// we don't support mixing metastring ranges + magic comments
|
||||
console.log('params', {params, code});
|
||||
console.log('from meta', parseCodeLinesFromMetastring(newCode, {...params}));
|
||||
console.log('from content', parseCodeLinesFromContent(newCode, {...params}));
|
||||
return (
|
||||
parseCodeLinesFromMetastring(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 {
|
||||
const mapping: PrismThemeEntry = {
|
||||
color: '--prism-color',
|
||||
|
|
Loading…
Add table
Reference in a new issue