Files
triggerdotdev--trigger.dev/apps/webapp/app/components/code/CodeBlock.tsx
T
2023-05-11 10:50:57 +01:00

345 lines
9.2 KiB
TypeScript

import type { Language, PrismTheme } from "prism-react-renderer";
import Highlight, { defaultProps } from "prism-react-renderer";
import { forwardRef, useCallback, useState } from "react";
import { cn } from "~/utils/cn";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../primitives/Tooltip";
import {
ClipboardDocumentCheckIcon,
ClipboardIcon,
} from "@heroicons/react/24/solid";
//This is a fork of https://github.com/mantinedev/mantine/blob/master/src/mantine-prism/src/Prism/Prism.tsx
//it didn't support highlighting lines by dimming the rest of the code, or animations on the highlighting
type CodeBlockProps = {
/** Code which will be highlighted */
code: string;
/** Programming language that should be highlighted */
language?: Language;
/** Show copy to clipboard button */
showCopyButton?: boolean;
/** Display line numbers */
showLineNumbers?: boolean;
/** Highlight line at given line number with color from theme.colors */
highlightedRanges?: [number, number][];
/** Add/override classes on the overall element */
className?: string;
/** Add/override code theme */
theme?: PrismTheme;
/** Max lines */
maxLines?: number;
};
const dimAmount = 0.5;
const extraLinesWhenClipping = 0.35;
const defaultTheme: PrismTheme = {
plain: {
color: "#9CDCFE",
backgroundColor: "#0e1521",
},
styles: [
{
types: ["prolog"],
style: {
color: "rgb(0, 0, 128)",
},
},
{
types: ["comment"],
style: {
color: "rgb(106, 153, 85)",
},
},
{
types: ["builtin", "changed", "keyword", "interpolation-punctuation"],
style: {
color: "rgb(86, 156, 214)",
},
},
{
types: ["number", "inserted"],
style: {
color: "rgb(181, 206, 168)",
},
},
{
types: ["constant"],
style: {
color: "rgb(100, 102, 149)",
},
},
{
types: ["attr-name", "variable"],
style: {
color: "rgb(156, 220, 254)",
},
},
{
types: ["deleted", "string", "attr-value", "template-punctuation"],
style: {
color: "rgb(206, 145, 120)",
},
},
{
types: ["selector"],
style: {
color: "rgb(215, 186, 125)",
},
},
{
// Fix tag color
types: ["tag"],
style: {
color: "rgb(78, 201, 176)",
},
},
{
// Fix tag color for HTML
types: ["tag"],
languages: ["markup"],
style: {
color: "rgb(86, 156, 214)",
},
},
{
types: ["punctuation", "operator"],
style: {
color: "rgb(212, 212, 212)",
},
},
{
// Fix punctuation color for HTML
types: ["punctuation"],
languages: ["markup"],
style: {
color: "#808080",
},
},
{
types: ["function"],
style: {
color: "rgb(220, 220, 170)",
},
},
{
types: ["class-name"],
style: {
color: "rgb(78, 201, 176)",
},
},
{
types: ["char"],
style: {
color: "rgb(209, 105, 105)",
},
},
],
};
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
(
{
showCopyButton = true,
showLineNumbers = true,
highlightedRanges,
code,
className,
language = "typescript",
theme = defaultTheme,
maxLines,
...props
}: CodeBlockProps,
ref
) => {
const [mouseOver, setMouseOver] = useState(false);
const [copied, setCopied] = useState(false);
const onCopied = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1500);
},
[code]
);
code = code.trim();
const lineCount = code.split("\n").length;
const maxLineWidth = lineCount.toString().length;
let maxHeight: string | undefined = undefined;
if (maxLines && lineCount > maxLines) {
maxHeight = `calc(${
(maxLines + extraLinesWhenClipping) * 0.75 * 1.625
}rem + 1.5rem )`;
}
const highlightLines = highlightedRanges?.flatMap(([start, end]) =>
Array.from({ length: end - start + 1 }, (_, i) => start + i)
);
return (
<div
className={cn(
"relative overflow-hidden rounded-md border border-slate-800",
className
)}
style={{
backgroundColor: theme.plain.backgroundColor,
}}
ref={ref}
{...props}
translate="no"
>
{showCopyButton && (
<TooltipProvider>
<Tooltip open={copied || mouseOver}>
<TooltipTrigger
onClick={onCopied}
onMouseEnter={() => setMouseOver(true)}
onMouseLeave={() => setMouseOver(false)}
className={cn(
"absolute top-3 right-3 z-50 transition-colors duration-100 hover:cursor-pointer",
copied
? "text-emerald-500"
: "text-slate-500 hover:text-slate-300"
)}
>
{copied ? (
<ClipboardDocumentCheckIcon className="h-5 w-5" />
) : (
<ClipboardIcon className="h-5 w-5" />
)}
</TooltipTrigger>
<TooltipContent side="left" className="text-xs">
{copied ? "Copied" : "Copy"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Highlight
{...defaultProps}
theme={theme}
code={code}
language={language}
>
{({
className: inheritedClassName,
style: inheritedStyle,
tokens,
getLineProps,
getTokenProps,
}) => (
<div
dir="ltr"
className="overflow-auto py-3 px-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700"
style={{
maxHeight,
}}
>
<pre
className={cn(
"relative mr-2 font-mono text-xs leading-relaxed",
inheritedClassName
)}
style={inheritedStyle}
dir="ltr"
>
{tokens
.map((line, index) => {
if (
index === tokens.length - 1 &&
line.length === 1 &&
line[0].content === "\n"
) {
return null;
}
const lineNumber = index + 1;
const lineProps = getLineProps({ line, key: index });
let hasAnyHighlights = highlightLines
? highlightLines.length > 0
: false;
let shouldDim = hasAnyHighlights;
if (
hasAnyHighlights &&
highlightLines?.includes(lineNumber)
) {
shouldDim = false;
}
return (
<div
key={lineProps.key}
{...lineProps}
className={cn(
"flex w-full justify-start transition-opacity duration-500",
lineProps.className
)}
style={{
opacity: shouldDim ? dimAmount : undefined,
...lineProps.style,
}}
>
{showLineNumbers && (
<div
className={
"mr-2 flex-none select-none text-right text-slate-500 transition-opacity duration-500"
}
style={{
width: `calc(8 * ${maxLineWidth / 16}rem)`,
}}
>
{lineNumber}
</div>
)}
<div className="flex-1">
{line.map((token, key) => {
const tokenProps = getTokenProps({ token, key });
return (
<span
key={tokenProps.key}
{...tokenProps}
style={{
color: tokenProps?.style?.color as string,
...tokenProps.style,
}}
/>
);
})}
</div>
<div className="w-4 flex-none" />
</div>
);
})
.filter(Boolean)}
</pre>
</div>
)}
</Highlight>
</div>
);
}
);
CodeBlock.displayName = "CodeBlock";