From 5273a534d39e0e13f5ae9a8faf467686d808e6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lorber?= Date: Fri, 15 Apr 2022 11:58:12 +0200 Subject: [PATCH] refactor(theme-classic): split CodeBlock (#7175) * extract CodeBlockLine * stable refactor * stable refactor * stable refactor * add CodeBlockContainer * refactor * refactor * do the actual split Co-authored-by: Joshua Chen --- .../src/theme-classic.d.ts | 54 ++++- .../src/theme/CodeBlock/Container/index.tsx | 35 ++++ .../CodeBlock/Container/styles.module.css | 14 ++ .../src/theme/CodeBlock/Content/Element.tsx | 30 +++ .../src/theme/CodeBlock/Content/String.tsx | 94 +++++++++ .../CodeBlock/{ => Content}/styles.module.css | 36 ---- .../src/theme/CodeBlock/Line/index.tsx | 51 +++++ .../theme/CodeBlock/Line/styles.module.css | 34 ++++ .../src/theme/CodeBlock/index.tsx | 189 +++--------------- .../src/utils/codeBlockUtils.ts | 3 +- 10 files changed, 340 insertions(+), 200 deletions(-) create mode 100644 packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/styles.module.css create mode 100644 packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx rename packages/docusaurus-theme-classic/src/theme/CodeBlock/{ => Content}/styles.module.css (60%) create mode 100644 packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx create mode 100644 packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/styles.module.css diff --git a/packages/docusaurus-theme-classic/src/theme-classic.d.ts b/packages/docusaurus-theme-classic/src/theme-classic.d.ts index 7d3bda55d6a7..233f229c9f84 100644 --- a/packages/docusaurus-theme-classic/src/theme-classic.d.ts +++ b/packages/docusaurus-theme-classic/src/theme-classic.d.ts @@ -148,10 +148,10 @@ declare module '@theme/BlogLayout' { } declare module '@theme/CodeBlock' { - import type {ReactElement} from 'react'; + import type {ReactNode} from 'react'; export interface Props { - readonly children: string | ReactElement; + readonly children: ReactNode; readonly className?: string; readonly metastring?: string; readonly title?: string; @@ -170,6 +170,56 @@ declare module '@theme/CodeBlock/CopyButton' { export default function CopyButton(props: Props): JSX.Element; } +declare module '@theme/CodeBlock/Container' { + import type {ComponentProps} from 'react'; + + export default function CodeBlockContainer({ + as: As, + ...props + }: {as: T} & ComponentProps): JSX.Element; +} + +declare module '@theme/CodeBlock/Content/Element' { + import type {Props} from '@theme/CodeBlock'; + + export type {Props}; + + export default function CodeBlockElementContent(props: Props): JSX.Element; +} + +declare module '@theme/CodeBlock/Content/String' { + import type {Props as CodeBlockProps} from '@theme/CodeBlock'; + + export interface Props extends Omit { + readonly children: string; + } + + export default function CodeBlockStringContent(props: Props): JSX.Element; +} + +declare module '@theme/CodeBlock/Line' { + import type {ComponentProps} from 'react'; + import type Highlight from 'prism-react-renderer'; + + // Lib does not make this easy + type RenderProps = Parameters< + ComponentProps['children'] + >[0]; + type GetLineProps = RenderProps['getLineProps']; + type GetTokenProps = RenderProps['getTokenProps']; + type Token = RenderProps['tokens'][number][number]; + + export interface Props { + readonly line: Token[]; + readonly highlight: boolean; + readonly showLineNumbers: boolean; + readonly getLineProps: GetLineProps; + readonly getTokenProps: GetTokenProps; + } + + export default function CodeBlockLine(props: Props): JSX.Element; +} + declare module '@theme/DocCard' { import type {PropSidebarItem} from '@docusaurus/plugin-content-docs'; diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/index.tsx new file mode 100644 index 000000000000..293d700dad35 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/index.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, {type ComponentProps} from 'react'; +import clsx from 'clsx'; +import { + usePrismTheme, + getPrismCssVariables, + ThemeClassNames, +} from '@docusaurus/theme-common'; +import styles from './styles.module.css'; + +export default function CodeBlockContainer({ + as: As, + ...props +}: {as: T} & ComponentProps): JSX.Element { + const prismTheme = usePrismTheme(); + const prismCssVariables = getPrismCssVariables(prismTheme); + return ( + + ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/styles.module.css new file mode 100644 index 000000000000..27adece6bd9b --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Container/styles.module.css @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.codeBlockContainer { + background: var(--prism-background-color); + color: var(--prism-color); + margin-bottom: var(--ifm-leading); + box-shadow: var(--ifm-global-shadow-lw); + border-radius: var(--ifm-code-border-radius); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx new file mode 100644 index 000000000000..3f130e80e6d6 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/Element.tsx @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import Container from '@theme/CodeBlock/Container'; +import clsx from 'clsx'; +import type {Props} from '@theme/CodeBlock/Content/Element'; + +import styles from './styles.module.css'; + +//
 tags in markdown map to CodeBlocks. They may contain JSX children. When
+// the children is not a simple string, we just return a styled block without
+// actually highlighting.
+export default function CodeBlockJSX({
+  children,
+  className,
+}: Props): JSX.Element {
+  return (
+    
+      {children}
+    
+  );
+}
diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx
new file mode 100644
index 000000000000..d4e59b1718c5
--- /dev/null
+++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/String.tsx
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React from 'react';
+import {
+  useThemeConfig,
+  parseCodeBlockTitle,
+  parseLanguage,
+  parseLines,
+  containsLineNumbers,
+  usePrismTheme,
+} from '@docusaurus/theme-common';
+import clsx from 'clsx';
+import Highlight, {defaultProps, type Language} from 'prism-react-renderer';
+import Line from '@theme/CodeBlock/Line';
+import CopyButton from '@theme/CodeBlock/CopyButton';
+import Container from '@theme/CodeBlock/Container';
+import type {Props} from '@theme/CodeBlock/Content/String';
+
+import styles from './styles.module.css';
+
+export default function CodeBlockString({
+  children,
+  className: blockClassName = '',
+  metastring,
+  title: titleProp,
+  showLineNumbers: showLineNumbersProp,
+  language: languageProp,
+}: Props): JSX.Element {
+  const {
+    prism: {defaultLanguage},
+  } = useThemeConfig();
+  const language =
+    languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage;
+  const prismTheme = usePrismTheme();
+
+  // We still parse the metastring in case we want to support more syntax in the
+  // future. Note that MDX doesn't strip quotes when parsing metastring:
+  // "title=\"xyz\"" => title: "\"xyz\""
+  const title = parseCodeBlockTitle(metastring) || titleProp;
+
+  const {highlightLines, code} = parseLines(children, metastring, language);
+  const showLineNumbers =
+    showLineNumbersProp || containsLineNumbers(metastring);
+
+  return (
+    
+      {title && 
{title}
} +
+ + {({className, tokens, getLineProps, getTokenProps}) => ( +
+              
+                {tokens.map((line, i) => (
+                  
+                ))}
+              
+            
+ )} +
+ +
+
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/styles.module.css similarity index 60% rename from packages/docusaurus-theme-classic/src/theme/CodeBlock/styles.module.css rename to packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/styles.module.css index 970367a7e601..6f043e05c4da 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Content/styles.module.css @@ -5,14 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -.codeBlockContainer { - background: var(--prism-background-color); - color: var(--prism-color); - margin-bottom: var(--ifm-leading); - box-shadow: var(--ifm-global-shadow-lw); - border-radius: var(--ifm-code-border-radius); -} - .codeBlockContent { position: relative; /* rtl:ignore */ @@ -62,31 +54,3 @@ white-space: pre-wrap; } } - -.codeLine { - display: table-row; - counter-increment: line-count; -} - -.codeLineNumber { - display: table-cell; - text-align: right; - width: 1%; - position: sticky; - left: 0; - padding: 0 var(--ifm-pre-padding); - background: var(--ifm-pre-background); -} - -.codeLineNumber::before { - content: counter(line-count); - opacity: 0.4; -} - -:global(.docusaurus-highlight-code-line) .codeLineNumber::before { - opacity: 0.8; -} - -.codeLineContent { - padding-right: var(--ifm-pre-padding); -} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx new file mode 100644 index 000000000000..79505f42d756 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/index.tsx @@ -0,0 +1,51 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import type {Props} from '@theme/CodeBlock/Line'; +import styles from './styles.module.css'; + +export default function CodeBlockLine({ + line, + highlight, + showLineNumbers, + getLineProps, + getTokenProps, +}: Props): JSX.Element { + if (line.length === 1 && line[0]!.content === '\n') { + line[0]!.content = ''; + } + + const lineProps = getLineProps({ + line, + ...(showLineNumbers && {className: styles.codeLine}), + }); + + if (highlight) { + lineProps.className += ' docusaurus-highlight-code-line'; + } + + const lineTokens = line.map((token, key) => ( + + )); + + return ( + + {showLineNumbers ? ( + <> + + {lineTokens} + + ) : ( + <> + {lineTokens} +
+ + )} +
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/styles.module.css b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/styles.module.css new file mode 100644 index 000000000000..7597f007c5b2 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/Line/styles.module.css @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.codeLine { + display: table-row; + counter-increment: line-count; +} + +.codeLineNumber { + display: table-cell; + text-align: right; + width: 1%; + position: sticky; + left: 0; + padding: 0 var(--ifm-pre-padding); + background: var(--ifm-pre-background); +} + +.codeLineNumber::before { + content: counter(line-count); + opacity: 0.4; +} + +:global(.docusaurus-highlight-code-line) .codeLineNumber::before { + opacity: 0.8; +} + +.codeLineContent { + padding-right: var(--ifm-pre-padding); +} diff --git a/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx b/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx index f734ca0b15da..9565932fafcb 100644 --- a/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/CodeBlock/index.tsx @@ -5,172 +5,41 @@ * LICENSE file in the root directory of this source tree. */ -import React, {isValidElement, useEffect, useState} from 'react'; -import clsx from 'clsx'; -import Highlight, {defaultProps, type Language} from 'prism-react-renderer'; -import { - useThemeConfig, - parseCodeBlockTitle, - parseLanguage, - parseLines, - containsLineNumbers, - ThemeClassNames, - usePrismTheme, - getPrismCssVariables, -} from '@docusaurus/theme-common'; -import CopyButton from '@theme/CodeBlock/CopyButton'; +import React, {isValidElement, type ReactNode} from 'react'; +import useIsBrowser from '@docusaurus/useIsBrowser'; import type {Props} from '@theme/CodeBlock'; +import ElementContent from '@theme/CodeBlock/Content/Element'; +import StringContent from '@theme/CodeBlock/Content/String'; -import styles from './styles.module.css'; - -export default function CodeBlock({ - children, - className: blockClassName = '', - metastring, - title, - showLineNumbers, - language: languageProp, -}: Props): JSX.Element { - const {prism} = useThemeConfig(); - - const [mounted, setMounted] = useState(false); - // The Prism theme on SSR is always the default theme but the site theme - // can be in a different mode. React hydration doesn't update DOM styles - // that come from SSR. Hence force a re-render after mounting to apply the - // current relevant styles. There will be a flash seen of the original - // styles seen using this current approach but that's probably ok. Fixing - // the flash will require changing the theming approach and is not worth it - // at this point. - useEffect(() => { - setMounted(true); - }, []); - - // We still parse the metastring in case we want to support more syntax in the - // future. Note that MDX doesn't strip quotes when parsing metastring: - // "title=\"xyz\"" => title: "\"xyz\"" - const codeBlockTitle = parseCodeBlockTitle(metastring) || title; - const prismTheme = usePrismTheme(); - - const prismCssVariables = getPrismCssVariables(prismTheme); - - //
 tags in markdown map to CodeBlocks and they may contain JSX children.
-  // When the children is not a simple string, we just return a styled block
-  // without actually highlighting.
+/**
+ * Best attempt to make the children a plain string so it is copyable. If there
+ * are react elements, we will not be able to copy the content, and it will
+ * return `children` as-is; otherwise, it concatenates the string children
+ * together.
+ */
+function maybeStringifyChildren(children: ReactNode): ReactNode {
   if (React.Children.toArray(children).some((el) => isValidElement(el))) {
-    return (
-      
-        {({className}) => (
-          
-            {children}
-          
- )} -
- ); + return children; } - // The children is now guaranteed to be one/more plain strings - const content = Array.isArray(children) - ? children.join('') - : (children as string); - - const language = - languageProp ?? parseLanguage(blockClassName) ?? prism.defaultLanguage; - const {highlightLines, code} = parseLines(content, metastring, language); - const shouldShowLineNumbers = - showLineNumbers || containsLineNumbers(metastring); + return Array.isArray(children) ? children.join('') : (children as string); +} +export default function CodeBlock({ + children: rawChildren, + ...props +}: Props): JSX.Element { + // The Prism theme on SSR is always the default theme but the site theme can + // be in a different mode. React hydration doesn't update DOM styles that come + // from SSR. Hence force a re-render after mounting to apply the current + // relevant styles. + const isBrowser = useIsBrowser(); + const children = maybeStringifyChildren(rawChildren); + const CodeBlockComp = + typeof children === 'string' ? StringContent : ElementContent; return ( - - {({className, tokens, getLineProps, getTokenProps}) => ( -
- {codeBlockTitle && ( -
{codeBlockTitle}
- )} -
-
-              
-                {tokens.map((line, i) => {
-                  if (line.length === 1 && line[0]!.content === '\n') {
-                    line[0]!.content = '';
-                  }
-
-                  const lineProps = getLineProps({
-                    line,
-                    key: i,
-                    ...(shouldShowLineNumbers && {className: styles.codeLine}),
-                  });
-
-                  if (highlightLines.includes(i)) {
-                    lineProps.className += ' docusaurus-highlight-code-line';
-                  }
-
-                  const lineTokens = line.map((token, key) => (
-                    
-                  ));
-
-                  return (
-                    
-                      {shouldShowLineNumbers ? (
-                        <>
-                          
-                          
-                            {lineTokens}
-                          
-                        
-                      ) : (
-                        <>
-                          {lineTokens}
-                          
- - )} -
- ); - })} -
-
- - -
-
- )} -
+ + {children as string} + ); } diff --git a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts index 0dd49cfb2bde..8dd5108d93f1 100644 --- a/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts +++ b/packages/docusaurus-theme-common/src/utils/codeBlockUtils.ts @@ -182,11 +182,10 @@ export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties { backgroundColor: '--prism-background-color', }; - const properties: CSSProperties = {}; + const properties: {[key: string]: string} = {}; Object.entries(prismTheme.plain).forEach(([key, value]) => { const varName = mapping[key]; if (varName && typeof value === 'string') { - // @ts-expect-error: why css variables not in inline style type? properties[varName] = value; } });