diff --git a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts index 60363ec484..a91f953fe4 100644 --- a/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts +++ b/packages/docusaurus-theme-classic/src/getSwizzleConfig.ts @@ -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.', diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index a815adb2fc..d005b05806 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -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): 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' { diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/Button/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/Button/index.tsx new file mode 100644 index 0000000000..167bd3395d --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/Button/index.tsx @@ -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 ( + + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/CopyButton/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/CopyButton/styles.module.css similarity index 100% rename from packages/docusaurus-theme-classic/src/theme/CodeBlock/CopyButton/styles.module.css rename to packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/CopyButton/styles.module.css diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/WordWrapButton/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/WordWrapButton/index.tsx similarity index 59% rename from packages/docusaurus-theme-classic/src/theme/CodeBlock/WordWrapButton/index.tsx rename to packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/WordWrapButton/index.tsx index eddb120c9b..1ca6519f36 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/WordWrapButton/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/WordWrapButton/index.tsx @@ -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 ( - + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/WordWrapButton/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/WordWrapButton/styles.module.css similarity index 100% rename from packages/docusaurus-theme-classic/src/theme/CodeBlock/WordWrapButton/styles.module.css rename to packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/WordWrapButton/styles.module.css diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/index.tsx new file mode 100644 index 0000000000..5372dc8410 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/index.tsx @@ -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 ( + + {() => ( +
+ + +
+ )} +
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/styles.module.css new file mode 100644 index 0000000000..20acffe8fe --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Buttons/styles.module.css @@ -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; +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx index a856aee703..e0a952bcb3 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx @@ -12,8 +12,10 @@ import type {Props} from '@theme/CodeBlock/Content/Element'; import styles from './styles.module.css'; -//
 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: 
in MDX +//
 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 (
diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx
index 5a6b0a07a1..8e43172134 100644
--- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx
+++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx
@@ -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;
-
-function CodeBlockTitle({children}: {children: ReactNode}): ReactNode {
-  // Just a pass-through for now
-  return children;
-}
-
-// TODO Docusaurus v4: remove useless forwardRef
-const Pre = React.forwardRef>(
-  (props, ref) => {
-    return (
-      
-    );
-  },
-);
-
-function Code({
-  metadata,
-  ...props
-}: {metadata: CodeBlockMetadata} & ComponentProps<'code'>) {
-  return (
-    
-  );
-}
-
-function CodeBlockContent({
-  metadata,
-  wordWrap,
-}: {
-  metadata: CodeBlockMetadata;
-  wordWrap: WordWrap;
-}): ReactNode {
-  const prismTheme = usePrismTheme();
-  const {code, language, lineNumbersStart, lineClassNames} = metadata;
-  return (
-    
-      {({className, style, tokens: lines, getLineProps, getTokenProps}) => (
-        
-          
-            {lines.map((line, i) => (
-              
-            ))}
-          
-        
- )} -
- ); -} - -function CodeBlockButtons({ - metadata, - wordWrap, -}: { - metadata: CodeBlockMetadata; - wordWrap: WordWrap; -}): ReactNode { - return ( -
- {(wordWrap.isEnabled || wordWrap.isCodeScrollable) && ( - wordWrap.toggle()} - isEnabled={wordWrap.isEnabled} - /> - )} - -
- ); -} - -function CodeBlockLayout({metadata}: {metadata: CodeBlockMetadata}): ReactNode { - const isBrowser = useIsBrowser(); - const wordWrap = useCodeWordWrap(); - return ( - - {metadata.title && ( -
- {metadata.title} -
- )} -
- - {isBrowser && ( - - )} -
-
- ); -} +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 ; + const wordWrap = useCodeWordWrap(); + return ( + + + + ); } diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/index.tsx new file mode 100644 index 0000000000..dec2ddd8f2 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/index.tsx @@ -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>( + (props, ref) => { + return ( +
+    );
+  },
+);
+
+function Code(props: ComponentProps<'code'>) {
+  const {metadata} = useCodeBlockContext();
+  return (
+    
+  );
+}
+
+export default function CodeBlockContent({
+  className: classNameProp,
+}: Props): ReactNode {
+  const {metadata, wordWrap} = useCodeBlockContext();
+  const prismTheme = usePrismTheme();
+  const {code, language, lineNumbersStart, lineClassNames} = metadata;
+  return (
+    
+      {({className, style, tokens: lines, getLineProps, getTokenProps}) => (
+        
+          
+            {lines.map((line, i) => (
+              
+            ))}
+          
+        
+ )} +
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/styles.module.css index 3760c530c4..48030df78a 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/styles.module.css @@ -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; -} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Layout/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Layout/index.tsx new file mode 100644 index 0000000000..100532faca --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Layout/index.tsx @@ -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 ( + + {metadata.title && ( +
+ {metadata.title} +
+ )} +
+ + +
+
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Layout/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Layout/styles.module.css new file mode 100644 index 0000000000..74cdb53870 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Layout/styles.module.css @@ -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; +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/Token/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/Token/index.tsx new file mode 100644 index 0000000000..cdbfcb88b4 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/Token/index.tsx @@ -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 ; +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx index e56bba27ed..80000b80d3 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx @@ -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) => ( - - )); + const lineTokens = line.map((token, key) => { + const tokenProps = getTokenProps({token}); + return ( + + {tokenProps.children} + + ); + }); return ( diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Title/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Title/index.tsx new file mode 100644 index 0000000000..4b1559e0b1 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Title/index.tsx @@ -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; +} diff --git a/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts b/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts index 12cf2eff4c..11cd158965 100644 --- a/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts +++ b/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts @@ -52,12 +52,14 @@ function useTabBecameVisibleCallback( ); } -export function useCodeWordWrap(): { +export type WordWrap = { readonly codeBlockRef: RefObject; readonly isEnabled: boolean; readonly isCodeScrollable: boolean; readonly toggle: () => void; -} { +}; + +export function useCodeWordWrap(): WordWrap { const [isEnabled, setIsEnabled] = useState(false); const [isCodeScrollable, setIsCodeScrollable] = useState(false); const codeBlockRef = useRef(null); diff --git a/packages/docusaurus-theme-common/src/internal.ts b/packages/docusaurus-theme-common/src/internal.ts index d889a2f7a3..ca12647f23 100644 --- a/packages/docusaurus-theme-common/src/internal.ts +++ b/packages/docusaurus-theme-common/src/internal.ts @@ -37,6 +37,8 @@ export { type CodeBlockMetadata, createCodeBlockMetadata, getPrismCssVariables, + CodeBlockContextProvider, + useCodeBlockContext, } from './utils/codeBlockUtils'; export {DEFAULT_SEARCH_TAG} from './utils/searchUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.tsx similarity index 92% rename from packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts rename to packages/docusaurus-theme-common/src/utils/codeBlockUtils.tsx index 6665a8f2ba..914fc5de35 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.tsx @@ -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=(?["'])(?.*?)\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; +}