refactor(theme-classic): split CodeBlock (#7175)

* extract CodeBlockLine

* stable refactor

* stable refactor

* stable refactor

* add CodeBlockContainer

* refactor

* refactor

* do the actual split

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
This commit is contained in:
Sébastien Lorber 2022-04-15 11:58:12 +02:00 committed by GitHub
parent 0f5f6f31e0
commit 5273a534d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 341 additions and 201 deletions

View file

@ -148,10 +148,10 @@ declare module '@theme/BlogLayout' {
} }
declare module '@theme/CodeBlock' { declare module '@theme/CodeBlock' {
import type {ReactElement} from 'react'; import type {ReactNode} from 'react';
export interface Props { export interface Props {
readonly children: string | ReactElement; readonly children: ReactNode;
readonly className?: string; readonly className?: string;
readonly metastring?: string; readonly metastring?: string;
readonly title?: string; readonly title?: string;
@ -170,6 +170,56 @@ declare module '@theme/CodeBlock/CopyButton' {
export default function CopyButton(props: Props): JSX.Element; export default function CopyButton(props: Props): JSX.Element;
} }
declare module '@theme/CodeBlock/Container' {
import type {ComponentProps} from 'react';
export default function CodeBlockContainer<T extends 'div' | 'pre'>({
as: As,
...props
}: {as: T} & ComponentProps<T>): JSX.Element;
}
declare module '@theme/CodeBlock/Content/Element' {
import type {Props} from '@theme/CodeBlock';
export type {Props};
export default function CodeBlockElementContent(props: Props): JSX.Element;
}
declare module '@theme/CodeBlock/Content/String' {
import type {Props as CodeBlockProps} from '@theme/CodeBlock';
export interface Props extends Omit<CodeBlockProps, 'children'> {
readonly children: string;
}
export default function CodeBlockStringContent(props: Props): JSX.Element;
}
declare module '@theme/CodeBlock/Line' {
import type {ComponentProps} from 'react';
import type Highlight from 'prism-react-renderer';
// Lib does not make this easy
type RenderProps = Parameters<
ComponentProps<typeof Highlight>['children']
>[0];
type GetLineProps = RenderProps['getLineProps'];
type GetTokenProps = RenderProps['getTokenProps'];
type Token = RenderProps['tokens'][number][number];
export interface Props {
readonly line: Token[];
readonly highlight: boolean;
readonly showLineNumbers: boolean;
readonly getLineProps: GetLineProps;
readonly getTokenProps: GetTokenProps;
}
export default function CodeBlockLine(props: Props): JSX.Element;
}
declare module '@theme/DocCard' { declare module '@theme/DocCard' {
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';

View file

@ -0,0 +1,35 @@
/**
* 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 ComponentProps} from 'react';
import clsx from 'clsx';
import {
usePrismTheme,
getPrismCssVariables,
ThemeClassNames,
} from '@docusaurus/theme-common';
import styles from './styles.module.css';
export default function CodeBlockContainer<T extends 'div' | 'pre'>({
as: As,
...props
}: {as: T} & ComponentProps<T>): JSX.Element {
const prismTheme = usePrismTheme();
const prismCssVariables = getPrismCssVariables(prismTheme);
return (
<As
// Polymorphic components are hard to type, without `oneOf` generics
{...(props as never)}
style={prismCssVariables}
className={clsx(
props.className,
styles.codeBlockContainer,
ThemeClassNames.common.codeBlock,
)}
/>
);
}

View file

@ -0,0 +1,14 @@
/**
* 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.
*/
.codeBlockContainer {
background: var(--prism-background-color);
color: var(--prism-color);
margin-bottom: var(--ifm-leading);
box-shadow: var(--ifm-global-shadow-lw);
border-radius: var(--ifm-code-border-radius);
}

View file

@ -0,0 +1,30 @@
/**
* 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 from 'react';
import Container from '@theme/CodeBlock/Container';
import clsx from 'clsx';
import type {Props} from '@theme/CodeBlock/Content/Element';
import styles from './styles.module.css';
// <pre> tags in markdown map to CodeBlocks. They may contain JSX children. When
// the children is not a simple string, we just return a styled block without
// actually highlighting.
export default function CodeBlockJSX({
children,
className,
}: Props): JSX.Element {
return (
<Container
as="pre"
tabIndex={0}
className={clsx(styles.codeBlockStandalone, 'thin-scrollbar', className)}>
<code className={styles.codeBlockLines}>{children}</code>
</Container>
);
}

View file

@ -0,0 +1,94 @@
/**
* 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 from 'react';
import {
useThemeConfig,
parseCodeBlockTitle,
parseLanguage,
parseLines,
containsLineNumbers,
usePrismTheme,
} from '@docusaurus/theme-common';
import clsx from 'clsx';
import Highlight, {defaultProps, type Language} from 'prism-react-renderer';
import Line from '@theme/CodeBlock/Line';
import CopyButton from '@theme/CodeBlock/CopyButton';
import Container from '@theme/CodeBlock/Container';
import type {Props} from '@theme/CodeBlock/Content/String';
import styles from './styles.module.css';
export default function CodeBlockString({
children,
className: blockClassName = '',
metastring,
title: titleProp,
showLineNumbers: showLineNumbersProp,
language: languageProp,
}: Props): JSX.Element {
const {
prism: {defaultLanguage},
} = useThemeConfig();
const language =
languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage;
const prismTheme = usePrismTheme();
// 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 {highlightLines, code} = parseLines(children, metastring, language);
const showLineNumbers =
showLineNumbersProp || containsLineNumbers(metastring);
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
{...defaultProps}
theme={prismTheme}
code={code}
language={(language ?? 'text') as Language}>
{({className, tokens, getLineProps, getTokenProps}) => (
<pre
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
tabIndex={0}
className={clsx(className, styles.codeBlock, 'thin-scrollbar')}>
<code
className={clsx(
styles.codeBlockLines,
showLineNumbers && styles.codeBlockLinesWithNumbering,
)}>
{tokens.map((line, i) => (
<Line
key={i}
line={line}
getLineProps={getLineProps}
getTokenProps={getTokenProps}
highlight={highlightLines.includes(i)}
showLineNumbers={showLineNumbers}
/>
))}
</code>
</pre>
)}
</Highlight>
<CopyButton code={code} />
</div>
</Container>
);
}

View file

@ -5,14 +5,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
.codeBlockContainer {
background: var(--prism-background-color);
color: var(--prism-color);
margin-bottom: var(--ifm-leading);
box-shadow: var(--ifm-global-shadow-lw);
border-radius: var(--ifm-code-border-radius);
}
.codeBlockContent { .codeBlockContent {
position: relative; position: relative;
/* rtl:ignore */ /* rtl:ignore */
@ -62,31 +54,3 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
} }
.codeLine {
display: table-row;
counter-increment: line-count;
}
.codeLineNumber {
display: table-cell;
text-align: right;
width: 1%;
position: sticky;
left: 0;
padding: 0 var(--ifm-pre-padding);
background: var(--ifm-pre-background);
}
.codeLineNumber::before {
content: counter(line-count);
opacity: 0.4;
}
:global(.docusaurus-highlight-code-line) .codeLineNumber::before {
opacity: 0.8;
}
.codeLineContent {
padding-right: var(--ifm-pre-padding);
}

View file

@ -0,0 +1,51 @@
/**
* 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 from 'react';
import type {Props} from '@theme/CodeBlock/Line';
import styles from './styles.module.css';
export default function CodeBlockLine({
line,
highlight,
showLineNumbers,
getLineProps,
getTokenProps,
}: Props): JSX.Element {
if (line.length === 1 && line[0]!.content === '\n') {
line[0]!.content = '';
}
const lineProps = getLineProps({
line,
...(showLineNumbers && {className: styles.codeLine}),
});
if (highlight) {
lineProps.className += ' docusaurus-highlight-code-line';
}
const lineTokens = line.map((token, key) => (
<span key={key} {...getTokenProps({token, key})} />
));
return (
<span {...lineProps}>
{showLineNumbers ? (
<>
<span className={styles.codeLineNumber} />
<span className={styles.codeLineContent}>{lineTokens}</span>
</>
) : (
<>
{lineTokens}
<br />
</>
)}
</span>
);
}

View file

@ -0,0 +1,34 @@
/**
* 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.
*/
.codeLine {
display: table-row;
counter-increment: line-count;
}
.codeLineNumber {
display: table-cell;
text-align: right;
width: 1%;
position: sticky;
left: 0;
padding: 0 var(--ifm-pre-padding);
background: var(--ifm-pre-background);
}
.codeLineNumber::before {
content: counter(line-count);
opacity: 0.4;
}
:global(.docusaurus-highlight-code-line) .codeLineNumber::before {
opacity: 0.8;
}
.codeLineContent {
padding-right: var(--ifm-pre-padding);
}

View file

@ -5,172 +5,41 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import React, {isValidElement, useEffect, useState} from 'react'; import React, {isValidElement, type ReactNode} from 'react';
import clsx from 'clsx'; import useIsBrowser from '@docusaurus/useIsBrowser';
import Highlight, {defaultProps, type Language} from 'prism-react-renderer';
import {
useThemeConfig,
parseCodeBlockTitle,
parseLanguage,
parseLines,
containsLineNumbers,
ThemeClassNames,
usePrismTheme,
getPrismCssVariables,
} from '@docusaurus/theme-common';
import CopyButton from '@theme/CodeBlock/CopyButton';
import type {Props} from '@theme/CodeBlock'; import type {Props} from '@theme/CodeBlock';
import ElementContent from '@theme/CodeBlock/Content/Element';
import StringContent from '@theme/CodeBlock/Content/String';
import styles from './styles.module.css'; /**
* Best attempt to make the children a plain string so it is copyable. If there
* are react elements, we will not be able to copy the content, and it will
* return `children` as-is; otherwise, it concatenates the string children
* together.
*/
function maybeStringifyChildren(children: ReactNode): ReactNode {
if (React.Children.toArray(children).some((el) => isValidElement(el))) {
return children;
}
// The children is now guaranteed to be one/more plain strings
return Array.isArray(children) ? children.join('') : (children as string);
}
export default function CodeBlock({ export default function CodeBlock({
children, children: rawChildren,
className: blockClassName = '', ...props
metastring,
title,
showLineNumbers,
language: languageProp,
}: Props): JSX.Element { }: Props): JSX.Element {
const {prism} = useThemeConfig(); // The Prism theme on SSR is always the default theme but the site theme can
// be in a different mode. React hydration doesn't update DOM styles that come
const [mounted, setMounted] = useState(false); // from SSR. Hence force a re-render after mounting to apply the current
// The Prism theme on SSR is always the default theme but the site theme // relevant styles.
// can be in a different mode. React hydration doesn't update DOM styles const isBrowser = useIsBrowser();
// that come from SSR. Hence force a re-render after mounting to apply the const children = maybeStringifyChildren(rawChildren);
// current relevant styles. There will be a flash seen of the original const CodeBlockComp =
// styles seen using this current approach but that's probably ok. Fixing typeof children === 'string' ? StringContent : ElementContent;
// the flash will require changing the theming approach and is not worth it
// at this point.
useEffect(() => {
setMounted(true);
}, []);
// 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 codeBlockTitle = parseCodeBlockTitle(metastring) || title;
const prismTheme = usePrismTheme();
const prismCssVariables = getPrismCssVariables(prismTheme);
// <pre> tags in markdown map to CodeBlocks and they may contain JSX children.
// When the children is not a simple string, we just return a styled block
// without actually highlighting.
if (React.Children.toArray(children).some((el) => isValidElement(el))) {
return (
<Highlight
{...defaultProps}
key={String(mounted)}
theme={prismTheme}
code=""
language={'text' as Language}>
{({className}) => (
<pre
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
tabIndex={0}
className={clsx(
className,
styles.codeBlockStandalone,
'thin-scrollbar',
styles.codeBlockContainer,
blockClassName,
ThemeClassNames.common.codeBlock,
)}
style={prismCssVariables}>
<code className={styles.codeBlockLines}>{children}</code>
</pre>
)}
</Highlight>
);
}
// The children is now guaranteed to be one/more plain strings
const content = Array.isArray(children)
? children.join('')
: (children as string);
const language =
languageProp ?? parseLanguage(blockClassName) ?? prism.defaultLanguage;
const {highlightLines, code} = parseLines(content, metastring, language);
const shouldShowLineNumbers =
showLineNumbers || containsLineNumbers(metastring);
return ( return (
<Highlight <CodeBlockComp key={String(isBrowser)} {...props}>
{...defaultProps} {children as string}
key={String(mounted)} </CodeBlockComp>
theme={prismTheme}
code={code}
language={(language ?? 'text') as Language}>
{({className, tokens, getLineProps, getTokenProps}) => (
<div
className={clsx(
styles.codeBlockContainer,
blockClassName,
{
[`language-${language}`]:
language && !blockClassName.includes(`language-${language}`),
},
ThemeClassNames.common.codeBlock,
)}
style={prismCssVariables}>
{codeBlockTitle && (
<div className={styles.codeBlockTitle}>{codeBlockTitle}</div>
)}
<div className={styles.codeBlockContent}>
<pre
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
tabIndex={0}
className={clsx(className, styles.codeBlock, 'thin-scrollbar')}>
<code
className={clsx(
styles.codeBlockLines,
shouldShowLineNumbers && styles.codeBlockLinesWithNumbering,
)}>
{tokens.map((line, i) => {
if (line.length === 1 && line[0]!.content === '\n') {
line[0]!.content = '';
}
const lineProps = getLineProps({
line,
key: i,
...(shouldShowLineNumbers && {className: styles.codeLine}),
});
if (highlightLines.includes(i)) {
lineProps.className += ' docusaurus-highlight-code-line';
}
const lineTokens = line.map((token, key) => (
<span key={key} {...getTokenProps({token, key})} />
));
return (
<span key={i} {...lineProps}>
{shouldShowLineNumbers ? (
<>
<span className={styles.codeLineNumber} />
<span className={styles.codeLineContent}>
{lineTokens}
</span>
</>
) : (
<>
{lineTokens}
<br />
</>
)}
</span>
);
})}
</code>
</pre>
<CopyButton code={code} />
</div>
</div>
)}
</Highlight>
); );
} }

View file

@ -182,11 +182,10 @@ export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
backgroundColor: '--prism-background-color', backgroundColor: '--prism-background-color',
}; };
const properties: CSSProperties = {}; const properties: {[key: string]: string} = {};
Object.entries(prismTheme.plain).forEach(([key, value]) => { Object.entries(prismTheme.plain).forEach(([key, value]) => {
const varName = mapping[key]; const varName = mapping[key];
if (varName && typeof value === 'string') { if (varName && typeof value === 'string') {
// @ts-expect-error: why css variables not in inline style type?
properties[varName] = value; properties[varName] = value;
} }
}); });