mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-17 11:07:07 +02:00
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:
parent
0f5f6f31e0
commit
5273a534d3
10 changed files with 341 additions and 201 deletions
|
@ -148,10 +148,10 @@ declare module '@theme/BlogLayout' {
|
|||
}
|
||||
|
||||
declare module '@theme/CodeBlock' {
|
||||
import type {ReactElement} from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly children: string | ReactElement;
|
||||
readonly children: ReactNode;
|
||||
readonly className?: string;
|
||||
readonly metastring?: string;
|
||||
readonly title?: string;
|
||||
|
@ -170,6 +170,56 @@ declare module '@theme/CodeBlock/CopyButton' {
|
|||
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' {
|
||||
import type {PropSidebarItem} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
|
|
|
@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -5,14 +5,6 @@
|
|||
* 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 {
|
||||
position: relative;
|
||||
/* rtl:ignore */
|
||||
|
@ -62,31 +54,3 @@
|
|||
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);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -5,172 +5,41 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {isValidElement, useEffect, useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
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 React, {isValidElement, type ReactNode} from 'react';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
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({
|
||||
children,
|
||||
className: blockClassName = '',
|
||||
metastring,
|
||||
title,
|
||||
showLineNumbers,
|
||||
language: languageProp,
|
||||
children: rawChildren,
|
||||
...props
|
||||
}: Props): JSX.Element {
|
||||
const {prism} = useThemeConfig();
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
// 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 from SSR. Hence force a re-render after mounting to apply the
|
||||
// current relevant styles. There will be a flash seen of the original
|
||||
// styles seen using this current approach but that's probably ok. Fixing
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
// from SSR. Hence force a re-render after mounting to apply the current
|
||||
// relevant styles.
|
||||
const isBrowser = useIsBrowser();
|
||||
const children = maybeStringifyChildren(rawChildren);
|
||||
const CodeBlockComp =
|
||||
typeof children === 'string' ? StringContent : ElementContent;
|
||||
return (
|
||||
<Highlight
|
||||
{...defaultProps}
|
||||
key={String(mounted)}
|
||||
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>
|
||||
<CodeBlockComp key={String(isBrowser)} {...props}>
|
||||
{children as string}
|
||||
</CodeBlockComp>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -182,11 +182,10 @@ export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
|||
backgroundColor: '--prism-background-color',
|
||||
};
|
||||
|
||||
const properties: CSSProperties = {};
|
||||
const properties: {[key: string]: string} = {};
|
||||
Object.entries(prismTheme.plain).forEach(([key, value]) => {
|
||||
const varName = mapping[key];
|
||||
if (varName && typeof value === 'string') {
|
||||
// @ts-expect-error: why css variables not in inline style type?
|
||||
properties[varName] = value;
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue