1
0
Fork 1
mirror of https://github.com/thatmattlove/hyperglass.git synced 2026-01-17 08:48:05 +00:00
thatmattlove-hyperglass/docs/src/theme/CodeBlock/index.js
2020-07-17 00:54:35 -07:00

273 lines
7.9 KiB
JavaScript

/**
* 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 classnames from "classnames";
import Highlight, { defaultProps } from "prism-react-renderer";
import Prism from "prism-react-renderer/prism";
import darkTheme from "./dracula";
import lightTheme from "./github";
import Clipboard from "clipboard";
import rangeParser from "parse-numeric-range";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import useThemeContext from "@theme/hooks/useThemeContext";
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: "<!--",
end: "-->",
},
};
// 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 }) => {
(typeof global !== "undefined" ? global : window).Prism = Prism;
require("prismjs/components/prism-shell-session");
require("prismjs/components/prism-nginx");
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 { isDarkTheme } = useThemeContext();
const lightModeTheme = prism.theme || lightTheme || darkTheme;
const darkModeTheme = prism.darkTheme || darkTheme || lightTheme;
const prismTheme = isDarkTheme ? darkModeTheme : lightModeTheme;
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 (
<Highlight
{...defaultProps}
key={mounted}
theme={prismTheme}
code={code}
language={language}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<>
{codeBlockTitle && (
<div style={style} className={styles.codeBlockTitle}>
{codeBlockTitle}
</div>
)}
<div className={styles.codeBlockContent}>
{/* <button
ref={button}
type="button"
aria-label="Copy code to clipboard"
className={classnames(styles.copyButton, {
[styles.copyButtonWithTitle]: codeBlockTitle,
})}
onClick={handleCopyCode}>
{showCopied ? 'Copied' : 'Copy'}
</button> */}
<div
tabIndex="0"
className={classnames(className, styles.codeBlock, {
[styles.codeBlockWithTitle]: codeBlockTitle,
})}
>
<div ref={target} className={styles.codeBlockLines} style={style}>
{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 (
<div key={i} {...lineProps}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
);
})}
</div>
</div>
</div>
</>
)}
</Highlight>
);
};