/** * 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. */ /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ import React, {useEffect, useState, useRef} from 'react'; import clsx from 'clsx'; import Highlight, {defaultProps} from 'prism-react-renderer'; import Clipboard from 'clipboard'; import rangeParser from 'parse-numeric-range'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import usePrismTheme from '@theme/hooks/usePrismTheme'; import styles from './styles.module.css'; const highlightLinesRangeRegex = /{([\d,-]+)}/; const getHighlightDirectiveRegex = ( languages = ['js', 'jsBlock', 'jsx', 'python', 'html'], ) => { // supported types of comments const comments = { js: { start: '\\/\\/', end: '', }, jsBlock: { start: '\\/\\*', end: '\\*\\/', }, jsx: { start: '\\{\\s*\\/\\*', end: '\\*\\/\\s*\\}', }, python: { start: '#', end: '', }, html: { start: '', }, }; // supported directives const directives = [ 'highlight-next-line', 'highlight-start', 'highlight-end', ].join('|'); // to be more reliable, the opening and closing comment must match const commentPattern = languages .map( (lang) => `(?:${comments[lang].start}\\s*(${directives})\\s*${comments[lang].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) => { 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 codeBlockTitleRegex = /title=".*"/; export default ({children, className: languageClassName, metastring}) => { const { siteConfig: { themeConfig: {prism = {}}, }, } = useDocusaurusContext(); 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); }, []); const target = useRef(null); const button = useRef(null); let highlightLines = []; let codeBlockTitle = ''; const prismTheme = usePrismTheme(); if (metastring && highlightLinesRangeRegex.test(metastring)) { const highlightLinesRange = metastring.match(highlightLinesRangeRegex)[1]; highlightLines = rangeParser .parse(highlightLinesRange) .filter((n) => n > 0); } if (metastring && codeBlockTitleRegex.test(metastring)) { codeBlockTitle = metastring .match(codeBlockTitleRegex)[0] .split('title=')[1] .replace(/"+/g, ''); } useEffect(() => { let clipboard; if (button.current) { clipboard = new Clipboard(button.current, { target: () => target.current, }); } return () => { if (clipboard) { clipboard.destroy(); } }; }, [button.current, target.current]); let language = languageClassName && languageClassName.replace(/language-/, ''); if (!language && prism.defaultLanguage) { language = prism.defaultLanguage; } // only declaration OR directive highlight can be used for a block let code = children.replace(/\n$/, ''); if (highlightLines.length === 0 && language !== undefined) { let range = ''; const directiveRegex = highlightDirectiveRegex(language); // go through line by line const lines = children.replace(/\n$/, '').split('\n'); let blockStart; // 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, 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.parse(range); code = lines.join('\n'); } const handleCopyCode = () => { window.getSelection().empty(); setShowCopied(true); setTimeout(() => setShowCopied(false), 2000); }; return ( {({className, style, tokens, getLineProps, getTokenProps}) => ( <> {codeBlockTitle && (
{codeBlockTitle}
)}
{tokens.map((line, i) => { if (line.length === 1 && line[0].content === '') { line[0].content = '\n'; // eslint-disable-line no-param-reassign } const lineProps = getLineProps({line, key: i}); if (highlightLines.includes(i + 1)) { lineProps.className = `${lineProps.className} docusaurus-highlight-code-line`; } return (
{line.map((token, key) => ( ))}
); })}
)}
); };