import { ArrowsPointingOutIcon } from "@heroicons/react/20/solid"; import { Clipboard, ClipboardCheck } from "lucide-react"; import type { Language, PrismTheme } from "prism-react-renderer"; import { Highlight, Prism } from "prism-react-renderer"; import { forwardRef, ReactNode, useCallback, useEffect, useState } from "react"; import { TextWrapIcon } from "~/assets/icons/TextWrapIcon"; import { cn } from "~/utils/cn"; import { Button } from "../primitives/Buttons"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/Dialog"; import { Paragraph } from "../primitives/Paragraph"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../primitives/Tooltip"; import { TextInlineIcon } from "~/assets/icons/TextInlineIcon"; //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 async function setup() { (typeof global !== "undefined" ? global : window).Prism = Prism; //@ts-ignore await import("prismjs/components/prism-json"); //@ts-ignore await import("prismjs/components/prism-typescript"); } setup(); type CodeBlockProps = { /** Code which will be highlighted */ code: string; /** Programming language that should be highlighted */ language?: Language; /** Show copy to clipboard button */ showCopyButton?: boolean; /** Show text wrapping button */ showTextWrapping?: 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; /** Whether to show the chrome, if you provide a string it will be used as the title, */ showChrome?: boolean; /** filename */ fileName?: string; /** title text for the Title row */ rowTitle?: ReactNode; /** Whether to show the open in modal button */ showOpenInModal?: boolean; }; const dimAmount = 0.5; const extraLinesWhenClipping = 0.35; const defaultTheme: PrismTheme = { plain: { color: "#9C9AF2", backgroundColor: "rgba(0, 0, 0, 0)", }, styles: [ { types: ["comment", "prolog", "doctype", "cdata"], style: { color: "#5F6570", }, }, { types: ["punctuation"], style: { color: "#878C99", }, }, { types: ["property", "tag", "constant", "symbol", "deleted"], style: { color: "#9B99FF", }, }, { types: ["boolean", "number"], style: { color: "#E5C07B", }, }, { types: ["selector", "attr-name", "string", "char", "builtin", "inserted"], style: { color: "#AFEC73", }, }, { types: ["operator", "entity", "url"], style: { color: "#D4D4D4", }, }, { types: ["variable"], style: { color: "#CCCBFF", }, }, { types: ["atrule", "attr-value", "keyword"], style: { color: "#E888F8", }, }, { types: ["function", "class-name"], style: { color: "#D9F07C", }, }, { types: ["regex"], style: { color: "#d16969", }, }, { types: ["important", "bold"], style: { fontWeight: "bold", }, }, { types: ["italic"], style: { fontStyle: "italic", }, }, { types: ["namespace"], style: { opacity: 0.7, }, }, { types: ["deleted"], style: { color: "#F85149", }, }, { types: ["char"], style: { color: "#b5cea8", }, }, { types: ["tag"], style: { color: "#D7BA7D", }, }, { types: ["keyword.operator"], style: { color: "#8271ED", }, }, { types: ["meta.template.expression"], style: { color: "#d4d4d4", }, }, ], }; export const CodeBlock = forwardRef( ( { showCopyButton = true, showTextWrapping = false, showLineNumbers = true, showOpenInModal = true, highlightedRanges, code, className, language = "typescript", theme = defaultTheme, maxLines, showChrome = false, fileName, rowTitle, ...props }: CodeBlockProps, ref ) => { const [mouseOver, setMouseOver] = useState(false); const [copied, setCopied] = useState(false); const [modalCopied, setModalCopied] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [isWrapped, setIsWrapped] = useState(false); const onCopied = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); navigator.clipboard.writeText(code); setCopied(true); setTimeout(() => { setCopied(false); }, 1500); }, [code] ); const onModalCopied = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); navigator.clipboard.writeText(code); setModalCopied(true); setTimeout(() => { setModalCopied(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) ); // if there are more than 1000 lines, don't highlight const shouldHighlight = lineCount <= 1000; return ( <>
{showChrome && } {rowTitle && }
{showTextWrapping && ( setIsWrapped(!isWrapped)} className="transition-colors focus-custom hover:cursor-pointer hover:text-text-bright" > {isWrapped ? ( ) : ( )} {isWrapped ? "Unwrap" : "Wrap"} )} {showCopyButton && ( setMouseOver(true)} onMouseLeave={() => setMouseOver(false)} className={cn( "transition-colors duration-100 focus-custom hover:cursor-pointer", copied ? "text-success" : "text-text-dimmed hover:text-text-bright" )} > {copied ? ( ) : ( )} {copied ? "Copied" : "Copy"} )} {showOpenInModal && ( setIsModalOpen(true)}> Expand )}
{shouldHighlight ? ( ) : (
                {code}
              
)}
{fileName && fileName} {rowTitle && rowTitle} {shouldHighlight ? ( ) : (
                  {code}
                
)}
); } ); CodeBlock.displayName = "CodeBlock"; function Chrome({ title }: { title?: string }) { return (
{title}
); } export function TitleRow({ title }: { title: ReactNode }) { return (
{title}
); } type HighlightCodeProps = { theme: PrismTheme; code: string; language: Language; showLineNumbers: boolean; highlightLines?: number[]; maxLineWidth?: number; className?: string; preClassName?: string; isWrapped: boolean; }; function HighlightCode({ theme, code, language, showLineNumbers, highlightLines, maxLineWidth, className, preClassName, isWrapped, }: HighlightCodeProps) { const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { Promise.all([ //@ts-ignore import("prismjs/components/prism-json"), //@ts-ignore import("prismjs/components/prism-typescript"), ]).then(() => setIsLoaded(true)); }, []); const containerClasses = cn( "px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600", !isWrapped && "overflow-x-auto", isWrapped && "overflow-y-auto", className ); const preClasses = cn( "relative mr-2 font-mono leading-relaxed", preClassName, isWrapped && "[&_span]:whitespace-pre-wrap [&_span]:break-words" ); if (!isLoaded) { return (
{code}
); } return ( {({ className: inheritedClassName, style: inheritedStyle, tokens, getLineProps, getTokenProps, }) => (
            {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 (
                  
{showLineNumbers && (
{lineNumber}
)}
{line.map((token, key) => { const tokenProps = getTokenProps({ token, key }); return ( ); })}
); }) .filter(Boolean)}
)}
); }