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:
Sébastien Lorber 2025-04-04 20:11:40 +02:00 committed by GitHub
parent d28210d35b
commit 31b279fea6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 474 additions and 243 deletions

View file

@ -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.',

View file

@ -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' {

View file

@ -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)} />
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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 (

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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;
}

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.
*/
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>
);
}

View file

@ -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;
}

View file

@ -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} />;
}

View file

@ -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}>

View file

@ -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;
}

View file

@ -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);

View file

@ -37,6 +37,8 @@ export {
type CodeBlockMetadata,
createCodeBlockMetadata,
getPrismCssVariables,
CodeBlockContextProvider,
useCodeBlockContext,
} from './utils/codeBlockUtils';
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';

View file

@ -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;
}