mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +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': {
|
||||
actions: {
|
||||
eject: 'unsafe',
|
||||
wrap: 'forbidden',
|
||||
wrap: 'unsafe',
|
||||
},
|
||||
description:
|
||||
'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;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/CopyButton' {
|
||||
declare module '@theme/CodeBlock/Provider' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {ComponentProps} from 'react';
|
||||
|
@ -447,13 +506,23 @@ declare module '@theme/CodeBlock/Container' {
|
|||
}: {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' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Props} from '@theme/CodeBlock';
|
||||
|
||||
export type {Props};
|
||||
|
||||
export default function CodeBlockElementContent(props: Props): ReactNode;
|
||||
export default function CodeBlockContentElement(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Content/String' {
|
||||
|
@ -464,7 +533,7 @@ declare module '@theme/CodeBlock/Content/String' {
|
|||
readonly children: string;
|
||||
}
|
||||
|
||||
export default function CodeBlockStringContent(props: Props): ReactNode;
|
||||
export default function CodeBlockContentString(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Line' {
|
||||
|
@ -488,16 +557,16 @@ declare module '@theme/CodeBlock/Line' {
|
|||
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 {Token, TokenOutputProps} from 'prism-react-renderer';
|
||||
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
readonly onClick: React.MouseEventHandler;
|
||||
readonly isEnabled: boolean;
|
||||
export interface Props extends TokenOutputProps {
|
||||
readonly token: Token;
|
||||
readonly line: Token[];
|
||||
}
|
||||
|
||||
export default function WordWrapButton(props: Props): ReactNode;
|
||||
export default function CodeBlockLine(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
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 copy from 'copy-text-to-clipboard';
|
||||
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 IconSuccess from '@theme/Icon/Success';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function CopyButton({code, className}: Props): ReactNode {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyTimeout = useRef<number | undefined>(undefined);
|
||||
const handleCopyCode = useCallback(() => {
|
||||
copy(code);
|
||||
setIsCopied(true);
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 1000);
|
||||
}, [code]);
|
||||
function title() {
|
||||
return translate({
|
||||
id: 'theme.CodeBlock.copy',
|
||||
message: 'Copy',
|
||||
description: 'The copy button label on code blocks',
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
isCopied
|
||||
function ariaLabel(isCopied: boolean) {
|
||||
return isCopied
|
||||
? translate({
|
||||
id: 'theme.CodeBlock.copied',
|
||||
message: 'Copied',
|
||||
|
@ -48,24 +42,46 @@ export default function CopyButton({code, className}: Props): ReactNode {
|
|||
id: 'theme.CodeBlock.copyButtonAriaLabel',
|
||||
message: 'Copy code to clipboard',
|
||||
description: 'The ARIA label for copy code blocks button',
|
||||
})
|
||||
});
|
||||
}
|
||||
title={translate({
|
||||
id: 'theme.CodeBlock.copy',
|
||||
message: 'Copy',
|
||||
description: 'The copy button label on code blocks',
|
||||
})}
|
||||
|
||||
function useCopyButton() {
|
||||
const {
|
||||
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(
|
||||
'clean-btn',
|
||||
className,
|
||||
styles.copyButton,
|
||||
isCopied && styles.copyButtonCopied,
|
||||
)}
|
||||
onClick={handleCopyCode}>
|
||||
onClick={copyCode}>
|
||||
<span className={styles.copyButtonIcons} aria-hidden="true">
|
||||
<IconCopy className={styles.copyButtonIcon} />
|
||||
<IconSuccess className={styles.copyButtonSuccessIcon} />
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -8,16 +8,21 @@
|
|||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
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 styles from './styles.module.css';
|
||||
|
||||
export default function WordWrapButton({
|
||||
className,
|
||||
onClick,
|
||||
isEnabled,
|
||||
}: Props): ReactNode {
|
||||
export default function WordWrapButton({className}: Props): ReactNode {
|
||||
const {wordWrap} = useCodeBlockContext();
|
||||
|
||||
const canShowButton = wordWrap.isEnabled || wordWrap.isCodeScrollable;
|
||||
if (!canShowButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = translate({
|
||||
id: 'theme.CodeBlock.wordWrapToggle',
|
||||
message: 'Toggle word wrap',
|
||||
|
@ -26,17 +31,15 @@ export default function WordWrapButton({
|
|||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
<Button
|
||||
onClick={() => wordWrap.toggle()}
|
||||
className={clsx(
|
||||
'clean-btn',
|
||||
className,
|
||||
isEnabled && styles.wordWrapButtonEnabled,
|
||||
wordWrap.isEnabled && styles.wordWrapButtonEnabled,
|
||||
)}
|
||||
aria-label={title}
|
||||
title={title}>
|
||||
<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';
|
||||
|
||||
// <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
|
||||
// TODO Docusaurus v4: move this component at the root?
|
||||
// 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.
|
||||
export default function CodeBlockJSX({children, className}: Props): ReactNode {
|
||||
return (
|
||||
|
|
|
@ -5,141 +5,16 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ComponentProps, type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {useThemeConfig, usePrismTheme} from '@docusaurus/theme-common';
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
||||
import {
|
||||
useCodeWordWrap,
|
||||
createCodeBlockMetadata,
|
||||
CodeBlockContextProvider,
|
||||
type CodeBlockMetadata,
|
||||
createCodeBlockMetadata,
|
||||
useCodeWordWrap,
|
||||
} 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 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>
|
||||
);
|
||||
}
|
||||
import CodeBlockLayout from '@theme/CodeBlock/Layout';
|
||||
|
||||
function useCodeBlockMetadata(props: Props): CodeBlockMetadata {
|
||||
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 {
|
||||
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.
|
||||
*/
|
||||
|
||||
.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 {
|
||||
--ifm-pre-background: var(--prism-background-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.codeBlockTitle + .codeBlockContent .codeBlock {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.codeBlockStandalone {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -54,34 +33,3 @@
|
|||
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 clsx from 'clsx';
|
||||
import LineToken from '@theme/CodeBlock/Line/Token';
|
||||
import type {Props} from '@theme/CodeBlock/Line';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
@ -40,9 +41,14 @@ export default function CodeBlockLine({
|
|||
className: clsx(classNames, showLineNumbers && styles.codeLine),
|
||||
});
|
||||
|
||||
const lineTokens = line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({token})} />
|
||||
));
|
||||
const lineTokens = line.map((token, key) => {
|
||||
const tokenProps = getTokenProps({token});
|
||||
return (
|
||||
<LineToken key={key} {...tokenProps} line={line} token={token}>
|
||||
{tokenProps.children}
|
||||
</LineToken>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<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 isEnabled: boolean;
|
||||
readonly isCodeScrollable: boolean;
|
||||
readonly toggle: () => void;
|
||||
} {
|
||||
};
|
||||
|
||||
export function useCodeWordWrap(): WordWrap {
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [isCodeScrollable, setIsCodeScrollable] = useState<boolean>(false);
|
||||
const codeBlockRef = useRef<HTMLPreElement>(null);
|
||||
|
|
|
@ -37,6 +37,8 @@ export {
|
|||
type CodeBlockMetadata,
|
||||
createCodeBlockMetadata,
|
||||
getPrismCssVariables,
|
||||
CodeBlockContextProvider,
|
||||
useCodeBlockContext,
|
||||
} from './utils/codeBlockUtils';
|
||||
|
||||
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||
|
|
|
@ -6,9 +6,12 @@
|
|||
*/
|
||||
|
||||
import type {CSSProperties, ReactNode} from 'react';
|
||||
import {createContext, useContext, useMemo} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import rangeParser from 'parse-numeric-range';
|
||||
import {ReactContextError} from './reactUtils';
|
||||
import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer';
|
||||
import type {WordWrap} from '../hooks/useCodeWordWrap';
|
||||
|
||||
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
|
||||
const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
|
||||
|
@ -322,9 +325,6 @@ export function parseLines(
|
|||
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})
|
||||
|
@ -460,3 +460,39 @@ export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
|||
});
|
||||
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