| /* eslint-disable react/no-array-index-key */ |
| /* eslint-disable import/no-extraneous-dependencies */ |
| /* eslint-disable react/prop-types */ |
| /** |
| * 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 { FC } from 'react'; |
| import React, { useEffect, useState, useRef } from 'react'; |
| import clsx from 'clsx'; |
| import type { Language } from 'prism-react-renderer'; |
| import Highlight, { defaultProps } from 'prism-react-renderer'; |
| import copy from 'copy-text-to-clipboard'; |
| import rangeParser from 'parse-numeric-range'; |
| import usePrismTheme from '@theme/hooks/usePrismTheme'; |
| import type { Props } from '@theme/CodeBlock'; |
| import Translate, { translate } from '@docusaurus/Translate'; |
| |
| import { useThemeConfig, parseCodeBlockTitle } from '@docusaurus/theme-common'; |
| import styles from './styles.module.css'; |
| |
| const HighlightLinesRangeRegex = /{([\d,-]+)}/; |
| |
| const HighlightLanguages = ['js', 'jsBlock', 'jsx', 'python', 'html'] as const; |
| type HighlightLanguage = typeof HighlightLanguages[number]; |
| |
| // Supported types of highlight comments |
| const HighlightComments = { |
| js: { |
| start: '\\/\\/', |
| end: '', |
| }, |
| jsBlock: { |
| start: '\\/\\*', |
| end: '\\*\\/', |
| }, |
| jsx: { |
| start: '\\{\\s*\\/\\*', |
| end: '\\*\\/\\s*\\}', |
| }, |
| python: { |
| start: '#', |
| end: '', |
| }, |
| html: { |
| start: '<!--', |
| end: '-->', |
| }, |
| }; |
| |
| // Supported highlight directives |
| const HighlightDirectives = [ |
| 'highlight-next-line', |
| 'highlight-start', |
| 'highlight-end', |
| ]; |
| |
| const getHighlightDirectiveRegex = ( |
| languages: readonly HighlightLanguage[] = HighlightLanguages, |
| ) => { |
| // to be more reliable, the opening and closing comment must match |
| const commentPattern = languages |
| .map((lang) => { |
| const { start, end } = HighlightComments[lang]; |
| return `(?:${start}\\s*(${HighlightDirectives.join('|')})\\s*${end})`; |
| }) |
| .join('|'); |
| // white space is allowed, but otherwise it should be on it's own line |
| return new RegExp(`^\\s*(?:${commentPattern})\\s*$`); |
| }; |
| |
| // select comment styles based on language |
| const highlightDirectiveRegex = (lang: string) => { |
| switch (lang) { |
| case 'js': |
| case 'javascript': |
| case 'ts': |
| case 'typescript': |
| return getHighlightDirectiveRegex(['js', 'jsBlock']); |
| |
| case 'jsx': |
| case 'tsx': |
| return getHighlightDirectiveRegex(['js', 'jsBlock', 'jsx']); |
| |
| case 'html': |
| return getHighlightDirectiveRegex(['js', 'jsBlock', 'html']); |
| |
| case 'python': |
| case 'py': |
| return getHighlightDirectiveRegex(['python']); |
| |
| default: |
| // all comment types |
| return getHighlightDirectiveRegex(); |
| } |
| }; |
| |
| const CodeBlock: FC<Props> = ({ |
| children, |
| className: languageClassName, |
| metastring, |
| title, |
| }) => { |
| const { prism } = useThemeConfig(); |
| |
| const [showCopied, setShowCopied] = useState(false); |
| 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); |
| }, []); |
| |
| // TODO: the title is provided by MDX as props automatically |
| // so we probably don't need to parse the metastring |
| // (note: title="xyz" => title prop still has the quotes) |
| const codeBlockTitle = parseCodeBlockTitle(metastring) || title; |
| |
| const button = useRef(null); |
| let highlightLines: number[] = []; |
| |
| const prismTheme = usePrismTheme(); |
| |
| // In case interleaved Markdown (e.g. when using CodeBlock as standalone component). |
| const content = Array.isArray(children) |
| ? children.join('') |
| : (children as string); |
| |
| if (metastring && HighlightLinesRangeRegex.test(metastring)) { |
| // Tested above |
| const highlightLinesRange = metastring.match(HighlightLinesRangeRegex)![1]; |
| highlightLines = rangeParser(highlightLinesRange).filter((n) => n > 0); |
| } |
| |
| let language = languageClassName?.replace(/language-/, '') as Language; |
| |
| if (!language && prism.defaultLanguage) { |
| language = prism.defaultLanguage as Language; |
| } |
| |
| // only declaration OR directive highlight can be used for a block |
| let code = content.replace(/\n$/, ''); |
| if (highlightLines.length === 0 && language !== undefined) { |
| let range = ''; |
| const directiveRegex = highlightDirectiveRegex(language); |
| // go through line by line |
| const lines = content.replace(/\n$/, '').split('\n'); |
| let blockStart: number; |
| // loop through lines |
| for (let index = 0; index < lines.length;) { |
| const line = lines[index]; |
| // adjust for 0-index |
| const lineNumber = index + 1; |
| const match = line.match(directiveRegex); |
| if (match !== null) { |
| const directive = match |
| .slice(1) |
| .reduce( |
| (final: string | undefined, item) => final || item, |
| undefined, |
| ); |
| switch (directive) { |
| case 'highlight-next-line': |
| range += `${lineNumber},`; |
| break; |
| |
| case 'highlight-start': |
| blockStart = lineNumber; |
| break; |
| |
| case 'highlight-end': |
| range += `${blockStart!}-${lineNumber - 1},`; |
| break; |
| |
| default: |
| break; |
| } |
| lines.splice(index, 1); |
| } else { |
| // lines without directives are unchanged |
| index += 1; |
| } |
| } |
| highlightLines = rangeParser(range); |
| code = lines.join('\n'); |
| } |
| |
| const handleCopyCode = () => { |
| copy(code); |
| setShowCopied(true); |
| |
| setTimeout(() => setShowCopied(false), 2000); |
| }; |
| |
| return ( |
| <Highlight |
| {...defaultProps} |
| key={String(mounted)} |
| theme={prismTheme} |
| code={code} |
| language={language} |
| > |
| {({ |
| className, style, tokens, getLineProps, getTokenProps, |
| }) => ( |
| <div className={styles.codeBlockContainer}> |
| {codeBlockTitle && ( |
| <div style={style} className={styles.codeBlockTitle}> |
| {codeBlockTitle} |
| </div> |
| )} |
| <div className={clsx(styles.codeBlockContent, language)}> |
| <pre |
| /* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */ |
| tabIndex={0} |
| className={clsx(className, styles.codeBlock, 'thin-scrollbar')} |
| style={style} |
| > |
| <code className={styles.codeBlockLines}> |
| {tokens.map((line, i) => { |
| if (line.length === 1 && line[0].content === '\n') { |
| // eslint-disable-next-line no-param-reassign |
| line[0].content = ''; |
| } |
| |
| const lineProps = getLineProps({ line, key: i }); |
| |
| if (highlightLines.includes(i + 1)) { |
| lineProps.className += ' docusaurus-highlight-code-line'; |
| } |
| |
| return ( |
| <span key={i} {...lineProps}> |
| {line.map((token, key) => ( |
| <span key={key} {...getTokenProps({ token, key })} /> |
| ))} |
| <br /> |
| </span> |
| ); |
| })} |
| </code> |
| </pre> |
| |
| <button |
| ref={button} |
| type="button" |
| aria-label={translate({ |
| id: 'theme.CodeBlock.copyButtonAriaLabel', |
| message: 'Copy code to clipboard', |
| description: 'The ARIA label for copy code blocks button', |
| })} |
| className={clsx(styles.copyButton, 'clean-btn')} |
| onClick={handleCopyCode} |
| > |
| {showCopied ? ( |
| <Translate |
| id="theme.CodeBlock.copied" |
| description="The copied button label on code blocks" |
| > |
| Copied |
| </Translate> |
| ) : ( |
| <Translate |
| id="theme.CodeBlock.copy" |
| description="The copy button label on code blocks" |
| > |
| Copy |
| </Translate> |
| )} |
| </button> |
| </div> |
| </div> |
| )} |
| </Highlight> |
| ); |
| }; |
| |
| export default CodeBlock; |