mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-05 21:27:24 +02:00
refactor(theme): introduce CodeBlockContextProvider + split into smaller components (#11062)
* introduce CodeBlockContextProvider * refactor: apply lint autofix * add comment * move wordWrap to context * Refactor button components * remove console logs * Extract more code block components * Extract CodeBlockLineToken subcomponent * add TODOs --------- Co-authored-by: slorber <749374+slorber@users.noreply.github.com>
This commit is contained in:
parent
d28210d35b
commit
31b279fea6
21 changed files with 474 additions and 243 deletions
|
@ -159,7 +159,7 @@ export default function getSwizzleConfig(): SwizzleConfig {
|
||||||
'CodeBlock/Content': {
|
'CodeBlock/Content': {
|
||||||
actions: {
|
actions: {
|
||||||
eject: 'unsafe',
|
eject: 'unsafe',
|
||||||
wrap: 'forbidden',
|
wrap: 'unsafe',
|
||||||
},
|
},
|
||||||
description:
|
description:
|
||||||
'The folder containing components responsible for rendering different types of CodeBlock content.',
|
'The folder containing components responsible for rendering different types of CodeBlock content.',
|
||||||
|
|
|
@ -426,17 +426,76 @@ declare module '@theme/CodeInline' {
|
||||||
export default function CodeInline(props: Props): ReactNode;
|
export default function CodeInline(props: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/CodeBlock/CopyButton' {
|
declare module '@theme/CodeBlock/Provider' {
|
||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
readonly code: string;
|
readonly children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockProvider(props: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/CodeBlock/Title' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockTitle(props: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/CodeBlock/Layout' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockLayout(props: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/CodeBlock/Buttons' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockButtons(props: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/CodeBlock/Buttons/Button' {
|
||||||
|
import type {ComponentProps, ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props extends ComponentProps<'button'> {
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyButton(props: Props): ReactNode;
|
export default function CopyButton(props: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/CodeBlock/Buttons/CopyButton' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockButtonCopy(props: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@theme/CodeBlock/Buttons/WordWrapButton' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
readonly className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockButtonWordWrap(props: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/CodeBlock/Container' {
|
declare module '@theme/CodeBlock/Container' {
|
||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
import type {ComponentProps} from 'react';
|
import type {ComponentProps} from 'react';
|
||||||
|
@ -447,13 +506,23 @@ declare module '@theme/CodeBlock/Container' {
|
||||||
}: {as: T} & ComponentProps<T>): ReactNode;
|
}: {as: T} & ComponentProps<T>): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module '@theme/CodeBlock/Content' {
|
||||||
|
import type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockContent(props: Props): ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@theme/CodeBlock/Content/Element' {
|
declare module '@theme/CodeBlock/Content/Element' {
|
||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
import type {Props} from '@theme/CodeBlock';
|
import type {Props} from '@theme/CodeBlock';
|
||||||
|
|
||||||
export type {Props};
|
export type {Props};
|
||||||
|
|
||||||
export default function CodeBlockElementContent(props: Props): ReactNode;
|
export default function CodeBlockContentElement(props: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/CodeBlock/Content/String' {
|
declare module '@theme/CodeBlock/Content/String' {
|
||||||
|
@ -464,7 +533,7 @@ declare module '@theme/CodeBlock/Content/String' {
|
||||||
readonly children: string;
|
readonly children: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CodeBlockStringContent(props: Props): ReactNode;
|
export default function CodeBlockContentString(props: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/CodeBlock/Line' {
|
declare module '@theme/CodeBlock/Line' {
|
||||||
|
@ -488,16 +557,16 @@ declare module '@theme/CodeBlock/Line' {
|
||||||
export default function CodeBlockLine(props: Props): ReactNode;
|
export default function CodeBlockLine(props: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/CodeBlock/WordWrapButton' {
|
declare module '@theme/CodeBlock/Line/Token' {
|
||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
|
import type {Token, TokenOutputProps} from 'prism-react-renderer';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props extends TokenOutputProps {
|
||||||
readonly className?: string;
|
readonly token: Token;
|
||||||
readonly onClick: React.MouseEventHandler;
|
readonly line: Token[];
|
||||||
readonly isEnabled: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WordWrapButton(props: Props): ReactNode;
|
export default function CodeBlockLine(props: Props): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@theme/DocCard' {
|
declare module '@theme/DocCard' {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type {Props} from '@theme/CodeBlock/Buttons/Button';
|
||||||
|
|
||||||
|
export default function CodeBlockButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: Props): ReactNode {
|
||||||
|
return (
|
||||||
|
<button type="button" {...props} className={clsx('clean-btn', className)} />
|
||||||
|
);
|
||||||
|
}
|
|
@ -15,30 +15,24 @@ import React, {
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import copy from 'copy-text-to-clipboard';
|
import copy from 'copy-text-to-clipboard';
|
||||||
import {translate} from '@docusaurus/Translate';
|
import {translate} from '@docusaurus/Translate';
|
||||||
import type {Props} from '@theme/CodeBlock/CopyButton';
|
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
|
||||||
|
import Button from '@theme/CodeBlock/Buttons/Button';
|
||||||
|
import type {Props} from '@theme/CodeBlock/Buttons/CopyButton';
|
||||||
import IconCopy from '@theme/Icon/Copy';
|
import IconCopy from '@theme/Icon/Copy';
|
||||||
import IconSuccess from '@theme/Icon/Success';
|
import IconSuccess from '@theme/Icon/Success';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
export default function CopyButton({code, className}: Props): ReactNode {
|
function title() {
|
||||||
const [isCopied, setIsCopied] = useState(false);
|
return translate({
|
||||||
const copyTimeout = useRef<number | undefined>(undefined);
|
id: 'theme.CodeBlock.copy',
|
||||||
const handleCopyCode = useCallback(() => {
|
message: 'Copy',
|
||||||
copy(code);
|
description: 'The copy button label on code blocks',
|
||||||
setIsCopied(true);
|
});
|
||||||
copyTimeout.current = window.setTimeout(() => {
|
}
|
||||||
setIsCopied(false);
|
|
||||||
}, 1000);
|
|
||||||
}, [code]);
|
|
||||||
|
|
||||||
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
|
function ariaLabel(isCopied: boolean) {
|
||||||
|
return isCopied
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label={
|
|
||||||
isCopied
|
|
||||||
? translate({
|
? translate({
|
||||||
id: 'theme.CodeBlock.copied',
|
id: 'theme.CodeBlock.copied',
|
||||||
message: 'Copied',
|
message: 'Copied',
|
||||||
|
@ -48,24 +42,46 @@ export default function CopyButton({code, className}: Props): ReactNode {
|
||||||
id: 'theme.CodeBlock.copyButtonAriaLabel',
|
id: 'theme.CodeBlock.copyButtonAriaLabel',
|
||||||
message: 'Copy code to clipboard',
|
message: 'Copy code to clipboard',
|
||||||
description: 'The ARIA label for copy code blocks button',
|
description: 'The ARIA label for copy code blocks button',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
title={translate({
|
|
||||||
id: 'theme.CodeBlock.copy',
|
function useCopyButton() {
|
||||||
message: 'Copy',
|
const {
|
||||||
description: 'The copy button label on code blocks',
|
metadata: {code},
|
||||||
})}
|
} = useCodeBlockContext();
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const copyTimeout = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const copyCode = useCallback(() => {
|
||||||
|
copy(code);
|
||||||
|
setIsCopied(true);
|
||||||
|
copyTimeout.current = window.setTimeout(() => {
|
||||||
|
setIsCopied(false);
|
||||||
|
}, 1000);
|
||||||
|
}, [code]);
|
||||||
|
|
||||||
|
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
|
||||||
|
|
||||||
|
return {copyCode, isCopied};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CopyButton({className}: Props): ReactNode {
|
||||||
|
const {copyCode, isCopied} = useCopyButton();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
aria-label={ariaLabel(isCopied)}
|
||||||
|
title={title()}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'clean-btn',
|
|
||||||
className,
|
className,
|
||||||
styles.copyButton,
|
styles.copyButton,
|
||||||
isCopied && styles.copyButtonCopied,
|
isCopied && styles.copyButtonCopied,
|
||||||
)}
|
)}
|
||||||
onClick={handleCopyCode}>
|
onClick={copyCode}>
|
||||||
<span className={styles.copyButtonIcons} aria-hidden="true">
|
<span className={styles.copyButtonIcons} aria-hidden="true">
|
||||||
<IconCopy className={styles.copyButtonIcon} />
|
<IconCopy className={styles.copyButtonIcon} />
|
||||||
<IconSuccess className={styles.copyButtonSuccessIcon} />
|
<IconSuccess className={styles.copyButtonSuccessIcon} />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -8,16 +8,21 @@
|
||||||
import React, {type ReactNode} from 'react';
|
import React, {type ReactNode} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {translate} from '@docusaurus/Translate';
|
import {translate} from '@docusaurus/Translate';
|
||||||
import type {Props} from '@theme/CodeBlock/WordWrapButton';
|
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
|
||||||
|
import Button from '@theme/CodeBlock/Buttons/Button';
|
||||||
|
import type {Props} from '@theme/CodeBlock/Buttons/WordWrapButton';
|
||||||
import IconWordWrap from '@theme/Icon/WordWrap';
|
import IconWordWrap from '@theme/Icon/WordWrap';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
export default function WordWrapButton({
|
export default function WordWrapButton({className}: Props): ReactNode {
|
||||||
className,
|
const {wordWrap} = useCodeBlockContext();
|
||||||
onClick,
|
|
||||||
isEnabled,
|
const canShowButton = wordWrap.isEnabled || wordWrap.isCodeScrollable;
|
||||||
}: Props): ReactNode {
|
if (!canShowButton) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const title = translate({
|
const title = translate({
|
||||||
id: 'theme.CodeBlock.wordWrapToggle',
|
id: 'theme.CodeBlock.wordWrapToggle',
|
||||||
message: 'Toggle word wrap',
|
message: 'Toggle word wrap',
|
||||||
|
@ -26,17 +31,15 @@ export default function WordWrapButton({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
onClick={() => wordWrap.toggle()}
|
||||||
onClick={onClick}
|
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'clean-btn',
|
|
||||||
className,
|
className,
|
||||||
isEnabled && styles.wordWrapButtonEnabled,
|
wordWrap.isEnabled && styles.wordWrapButtonEnabled,
|
||||||
)}
|
)}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
title={title}>
|
title={title}>
|
||||||
<IconWordWrap className={styles.wordWrapButtonIcon} aria-hidden="true" />
|
<IconWordWrap className={styles.wordWrapButtonIcon} aria-hidden="true" />
|
||||||
</button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||||
|
|
||||||
|
import CopyButton from '@theme/CodeBlock/Buttons/CopyButton';
|
||||||
|
import WordWrapButton from '@theme/CodeBlock/Buttons/WordWrapButton';
|
||||||
|
import type {Props} from '@theme/CodeBlock/Buttons';
|
||||||
|
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
// Code block buttons are not server-rendered on purpose
|
||||||
|
// Adding them to the initial HTML is useless and expensive (due to JSX SVG)
|
||||||
|
// They are hidden by default and require React to become interactive
|
||||||
|
export default function CodeBlockButtons({className}: Props): ReactNode {
|
||||||
|
return (
|
||||||
|
<BrowserOnly>
|
||||||
|
{() => (
|
||||||
|
<div className={clsx(className, styles.buttonGroup)}>
|
||||||
|
<WordWrapButton />
|
||||||
|
<CopyButton />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BrowserOnly>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.buttonGroup {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 0.2rem;
|
||||||
|
position: absolute;
|
||||||
|
/* rtl:ignore */
|
||||||
|
right: calc(var(--ifm-pre-padding) / 2);
|
||||||
|
top: calc(var(--ifm-pre-padding) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGroup button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--prism-background-color);
|
||||||
|
color: var(--prism-color);
|
||||||
|
border: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
border-radius: var(--ifm-global-radius);
|
||||||
|
padding: 0.4rem;
|
||||||
|
line-height: 0;
|
||||||
|
transition: opacity var(--ifm-transition-fast) ease-in-out;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonGroup button:focus-visible,
|
||||||
|
.buttonGroup button:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.theme-code-block:hover) .buttonGroup button {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
|
@ -12,8 +12,10 @@ import type {Props} from '@theme/CodeBlock/Content/Element';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
// <pre> tags in markdown map to CodeBlocks. They may contain JSX children. When
|
// TODO Docusaurus v4: move this component at the root?
|
||||||
// the children is not a simple string, we just return a styled block without
|
// This component only handles a rare edge-case: <pre><MyComp/></pre> in MDX
|
||||||
|
// <pre> tags in markdown map to CodeBlocks. They may contain JSX children.
|
||||||
|
// When children is not a simple string, we just return a styled block without
|
||||||
// actually highlighting.
|
// actually highlighting.
|
||||||
export default function CodeBlockJSX({children, className}: Props): ReactNode {
|
export default function CodeBlockJSX({children, className}: Props): ReactNode {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,141 +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 ComponentProps, type ReactNode} from 'react';
|
import React, {type ReactNode} from 'react';
|
||||||
import clsx from 'clsx';
|
import {useThemeConfig} from '@docusaurus/theme-common';
|
||||||
import {useThemeConfig, usePrismTheme} from '@docusaurus/theme-common';
|
|
||||||
import {
|
import {
|
||||||
useCodeWordWrap,
|
CodeBlockContextProvider,
|
||||||
createCodeBlockMetadata,
|
|
||||||
type CodeBlockMetadata,
|
type CodeBlockMetadata,
|
||||||
|
createCodeBlockMetadata,
|
||||||
|
useCodeWordWrap,
|
||||||
} from '@docusaurus/theme-common/internal';
|
} from '@docusaurus/theme-common/internal';
|
||||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
|
||||||
import {Highlight} from 'prism-react-renderer';
|
|
||||||
import Line from '@theme/CodeBlock/Line';
|
|
||||||
import CopyButton from '@theme/CodeBlock/CopyButton';
|
|
||||||
import WordWrapButton from '@theme/CodeBlock/WordWrapButton';
|
|
||||||
import Container from '@theme/CodeBlock/Container';
|
|
||||||
import type {Props} from '@theme/CodeBlock/Content/String';
|
import type {Props} from '@theme/CodeBlock/Content/String';
|
||||||
|
import CodeBlockLayout from '@theme/CodeBlock/Layout';
|
||||||
import styles from './styles.module.css';
|
|
||||||
|
|
||||||
type WordWrap = ReturnType<typeof useCodeWordWrap>;
|
|
||||||
|
|
||||||
function CodeBlockTitle({children}: {children: ReactNode}): ReactNode {
|
|
||||||
// Just a pass-through for now
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<code
|
|
||||||
{...props}
|
|
||||||
className={clsx(
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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 {
|
function useCodeBlockMetadata(props: Props): CodeBlockMetadata {
|
||||||
const {prism} = useThemeConfig();
|
const {prism} = useThemeConfig();
|
||||||
|
@ -155,7 +30,13 @@ function useCodeBlockMetadata(props: Props): CodeBlockMetadata {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Docusaurus v4: move this component at the root?
|
||||||
export default function CodeBlockString(props: Props): ReactNode {
|
export default function CodeBlockString(props: Props): ReactNode {
|
||||||
const metadata = useCodeBlockMetadata(props);
|
const metadata = useCodeBlockMetadata(props);
|
||||||
return <CodeBlockLayout metadata={metadata} />;
|
const wordWrap = useCodeWordWrap();
|
||||||
|
return (
|
||||||
|
<CodeBlockContextProvider metadata={metadata} wordWrap={wordWrap}>
|
||||||
|
<CodeBlockLayout />
|
||||||
|
</CodeBlockContextProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* 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, type ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
|
||||||
|
import {usePrismTheme} from '@docusaurus/theme-common';
|
||||||
|
import {Highlight} from 'prism-react-renderer';
|
||||||
|
import type {Props} from '@theme/CodeBlock/Content';
|
||||||
|
import Line from '@theme/CodeBlock/Line';
|
||||||
|
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
// 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(props: ComponentProps<'code'>) {
|
||||||
|
const {metadata} = useCodeBlockContext();
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
{...props}
|
||||||
|
className={clsx(
|
||||||
|
props.className,
|
||||||
|
styles.codeBlockLines,
|
||||||
|
metadata.lineNumbersStart !== undefined &&
|
||||||
|
styles.codeBlockLinesWithNumbering,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
...props.style,
|
||||||
|
counterReset:
|
||||||
|
metadata.lineNumbersStart === undefined
|
||||||
|
? undefined
|
||||||
|
: `line-count ${metadata.lineNumbersStart - 1}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CodeBlockContent({
|
||||||
|
className: classNameProp,
|
||||||
|
}: Props): ReactNode {
|
||||||
|
const {metadata, wordWrap} = useCodeBlockContext();
|
||||||
|
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={clsx(classNameProp, className)}
|
||||||
|
style={style}>
|
||||||
|
<Code>
|
||||||
|
{lines.map((line, i) => (
|
||||||
|
<Line
|
||||||
|
key={i}
|
||||||
|
line={line}
|
||||||
|
getLineProps={getLineProps}
|
||||||
|
getTokenProps={getTokenProps}
|
||||||
|
classNames={lineClassNames[i]}
|
||||||
|
showLineNumbers={lineNumbersStart !== undefined}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Code>
|
||||||
|
</Pre>
|
||||||
|
)}
|
||||||
|
</Highlight>
|
||||||
|
);
|
||||||
|
}
|
|
@ -5,33 +5,12 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.codeBlockContent {
|
|
||||||
position: relative;
|
|
||||||
/* rtl:ignore */
|
|
||||||
direction: ltr;
|
|
||||||
border-radius: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeBlockTitle {
|
|
||||||
border-bottom: 1px solid var(--ifm-color-emphasis-300);
|
|
||||||
font-size: var(--ifm-code-font-size);
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 0.75rem var(--ifm-pre-padding);
|
|
||||||
border-top-left-radius: inherit;
|
|
||||||
border-top-right-radius: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeBlock {
|
.codeBlock {
|
||||||
--ifm-pre-background: var(--prism-background-color);
|
--ifm-pre-background: var(--prism-background-color);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeBlockTitle + .codeBlockContent .codeBlock {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.codeBlockStandalone {
|
.codeBlockStandalone {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -54,34 +33,3 @@
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonGroup {
|
|
||||||
display: flex;
|
|
||||||
column-gap: 0.2rem;
|
|
||||||
position: absolute;
|
|
||||||
/* rtl:ignore */
|
|
||||||
right: calc(var(--ifm-pre-padding) / 2);
|
|
||||||
top: calc(var(--ifm-pre-padding) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonGroup button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--prism-background-color);
|
|
||||||
color: var(--prism-color);
|
|
||||||
border: 1px solid var(--ifm-color-emphasis-300);
|
|
||||||
border-radius: var(--ifm-global-radius);
|
|
||||||
padding: 0.4rem;
|
|
||||||
line-height: 0;
|
|
||||||
transition: opacity var(--ifm-transition-fast) ease-in-out;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonGroup button:focus-visible,
|
|
||||||
.buttonGroup button:hover {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-code-block:hover) .buttonGroup button {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
|
||||||
|
import Container from '@theme/CodeBlock/Container';
|
||||||
|
import Title from '@theme/CodeBlock/Title';
|
||||||
|
import Content from '@theme/CodeBlock/Content';
|
||||||
|
import type {Props} from '@theme/CodeBlock/Layout';
|
||||||
|
import Buttons from '@theme/CodeBlock/Buttons';
|
||||||
|
|
||||||
|
import styles from './styles.module.css';
|
||||||
|
|
||||||
|
export default function CodeBlockLayout({className}: Props): ReactNode {
|
||||||
|
const {metadata} = useCodeBlockContext();
|
||||||
|
return (
|
||||||
|
<Container as="div" className={clsx(className, metadata.className)}>
|
||||||
|
{metadata.title && (
|
||||||
|
<div className={styles.codeBlockTitle}>
|
||||||
|
<Title>{metadata.title}</Title>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.codeBlockContent}>
|
||||||
|
<Content />
|
||||||
|
<Buttons />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.codeBlockContent {
|
||||||
|
position: relative;
|
||||||
|
/* rtl:ignore */
|
||||||
|
direction: ltr;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlockTitle {
|
||||||
|
border-bottom: 1px solid var(--ifm-color-emphasis-300);
|
||||||
|
font-size: var(--ifm-code-font-size);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.75rem var(--ifm-pre-padding);
|
||||||
|
border-top-left-radius: inherit;
|
||||||
|
border-top-right-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeBlockTitle + .codeBlockContent .codeBlock {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {type ReactNode} from 'react';
|
||||||
|
import type {Props} from '@theme/CodeBlock/Line/Token';
|
||||||
|
|
||||||
|
// Pass-through components that users can swizzle and customize
|
||||||
|
export default function CodeBlockLineToken({
|
||||||
|
line,
|
||||||
|
token,
|
||||||
|
...props
|
||||||
|
}: Props): ReactNode {
|
||||||
|
return <span {...props} />;
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import React, {type ReactNode} from 'react';
|
import React, {type ReactNode} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import LineToken from '@theme/CodeBlock/Line/Token';
|
||||||
import type {Props} from '@theme/CodeBlock/Line';
|
import type {Props} from '@theme/CodeBlock/Line';
|
||||||
|
|
||||||
import styles from './styles.module.css';
|
import styles from './styles.module.css';
|
||||||
|
@ -40,9 +41,14 @@ export default function CodeBlockLine({
|
||||||
className: clsx(classNames, showLineNumbers && styles.codeLine),
|
className: clsx(classNames, showLineNumbers && styles.codeLine),
|
||||||
});
|
});
|
||||||
|
|
||||||
const lineTokens = line.map((token, key) => (
|
const lineTokens = line.map((token, key) => {
|
||||||
<span key={key} {...getTokenProps({token})} />
|
const tokenProps = getTokenProps({token});
|
||||||
));
|
return (
|
||||||
|
<LineToken key={key} {...tokenProps} line={line} token={token}>
|
||||||
|
{tokenProps.children}
|
||||||
|
</LineToken>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...lineProps}>
|
<span {...lineProps}>
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* 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 type {ReactNode} from 'react';
|
||||||
|
|
||||||
|
import type {Props} from '@theme/CodeBlock/Title';
|
||||||
|
|
||||||
|
// Just a pass-through component that users can swizzle and customize
|
||||||
|
export default function CodeBlockTitle({children}: Props): ReactNode {
|
||||||
|
return children;
|
||||||
|
}
|
|
@ -52,12 +52,14 @@ function useTabBecameVisibleCallback(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCodeWordWrap(): {
|
export type WordWrap = {
|
||||||
readonly codeBlockRef: RefObject<HTMLPreElement>;
|
readonly codeBlockRef: RefObject<HTMLPreElement>;
|
||||||
readonly isEnabled: boolean;
|
readonly isEnabled: boolean;
|
||||||
readonly isCodeScrollable: boolean;
|
readonly isCodeScrollable: boolean;
|
||||||
readonly toggle: () => void;
|
readonly toggle: () => void;
|
||||||
} {
|
};
|
||||||
|
|
||||||
|
export function useCodeWordWrap(): WordWrap {
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
const [isCodeScrollable, setIsCodeScrollable] = useState<boolean>(false);
|
const [isCodeScrollable, setIsCodeScrollable] = useState<boolean>(false);
|
||||||
const codeBlockRef = useRef<HTMLPreElement>(null);
|
const codeBlockRef = useRef<HTMLPreElement>(null);
|
||||||
|
|
|
@ -37,6 +37,8 @@ export {
|
||||||
type CodeBlockMetadata,
|
type CodeBlockMetadata,
|
||||||
createCodeBlockMetadata,
|
createCodeBlockMetadata,
|
||||||
getPrismCssVariables,
|
getPrismCssVariables,
|
||||||
|
CodeBlockContextProvider,
|
||||||
|
useCodeBlockContext,
|
||||||
} from './utils/codeBlockUtils';
|
} from './utils/codeBlockUtils';
|
||||||
|
|
||||||
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||||
|
|
|
@ -6,9 +6,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {CSSProperties, ReactNode} from 'react';
|
import type {CSSProperties, ReactNode} from 'react';
|
||||||
|
import {createContext, useContext, useMemo} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import rangeParser from 'parse-numeric-range';
|
import rangeParser from 'parse-numeric-range';
|
||||||
|
import {ReactContextError} from './reactUtils';
|
||||||
import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer';
|
import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer';
|
||||||
|
import type {WordWrap} from '../hooks/useCodeWordWrap';
|
||||||
|
|
||||||
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
|
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
|
||||||
const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
|
const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
|
||||||
|
@ -322,9 +325,6 @@ export function parseLines(
|
||||||
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})
|
||||||
|
@ -460,3 +460,39 @@ export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
||||||
});
|
});
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CodeBlockContextValue = {
|
||||||
|
metadata: CodeBlockMetadata;
|
||||||
|
wordWrap: WordWrap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CodeBlockContext = createContext<CodeBlockContextValue | null>(null);
|
||||||
|
|
||||||
|
export function CodeBlockContextProvider({
|
||||||
|
metadata,
|
||||||
|
wordWrap,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
metadata: CodeBlockMetadata;
|
||||||
|
wordWrap: WordWrap;
|
||||||
|
children: ReactNode;
|
||||||
|
}): ReactNode {
|
||||||
|
// Should we optimize this in 2 contexts?
|
||||||
|
// Unlike metadata, wordWrap is stateful and likely to trigger re-renders
|
||||||
|
const value: CodeBlockContextValue = useMemo(() => {
|
||||||
|
return {metadata, wordWrap};
|
||||||
|
}, [metadata, wordWrap]);
|
||||||
|
return (
|
||||||
|
<CodeBlockContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</CodeBlockContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCodeBlockContext(): CodeBlockContextValue {
|
||||||
|
const value = useContext(CodeBlockContext);
|
||||||
|
if (value === null) {
|
||||||
|
throw new ReactContextError('CodeBlockContextProvider');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue