import * as React from 'react'; import { useShallow } from 'zustand/react/shallow'; import type { SxProps } from '@mui/joy/styles/types'; import { Box, ButtonGroup, Sheet, Tooltip, Typography } from '@mui/joy'; import ChangeHistoryTwoToneIcon from '@mui/icons-material/ChangeHistoryTwoTone'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import FitScreenIcon from '@mui/icons-material/FitScreen'; import HtmlIcon from '@mui/icons-material/Html'; import NumbersRoundedIcon from '@mui/icons-material/NumbersRounded'; import SchemaIcon from '@mui/icons-material/Schema'; import WrapTextIcon from '@mui/icons-material/WrapText'; import { copyToClipboard } from '~/common/util/clipboardUtils'; import { useUIPreferencesStore } from '~/common/state/store-ui'; import type { CodeBlock } from '../blocks.types'; import { ButtonCodePen, isCodePenSupported } from './ButtonCodePen'; import { ButtonJsFiddle, isJSFiddleSupported } from './ButtonJSFiddle'; import { ButtonStackBlitz, isStackBlitzSupported } from './ButtonStackBlitz'; import { OverlayButton, overlayButtonsActiveSx, overlayButtonsClassName, overlayButtonsSx } from '../OverlayButton'; import { RenderCodeHtmlIFrame } from './RenderCodeHtmlIFrame'; import { RenderCodeMermaid } from './RenderCodeMermaid'; import { RenderCodeSVG } from './RenderCodeSVG'; import { RenderCodeSyntax } from './RenderCodeSyntax'; import { heuristicIsBlockPlantUML, RenderCodePlantUML, usePlantUmlSvg } from './RenderCodePlantUML'; import { heuristicIsBlockPureHTML } from '../html/RenderHtmlResponse'; // style for line-numbers import './RenderCode.css'; // configuration const ALWAYS_SHOW_OVERLAY = false; // RenderCode export const renderCodeMemoOrNot = (memo: boolean) => memo ? RenderCodeMemo : RenderCode; const RenderCodeMemo = React.memo(RenderCode); interface RenderCodeBaseProps { codeBlock: CodeBlock, fitScreen?: boolean, initialShowHTML?: boolean, noCopyButton?: boolean, optimizeLightweight?: boolean, sx?: SxProps, } function RenderCode(props: RenderCodeBaseProps) { return ( }> <_DynamicPrism {...props} /> ); } // Lazy loader of the heavy prism functions const _DynamicPrism = React.lazy(async () => { // Dynamically import the code highlight functions const { highlightCode, inferCodeLanguage } = await import('./codePrism'); return { default: (props: RenderCodeBaseProps) => , }; }); // Actual implemetation of the code rendering const renderCodecontainerSx: SxProps = { // position the overlay buttons - this has to be one level up from the code, otherwise the buttons will h-scroll with the code position: 'relative', // fade in children buttons [`&:hover > .${overlayButtonsClassName}`]: overlayButtonsActiveSx, }; function RenderCodeImpl(props: RenderCodeBaseProps & { highlightCode: (inferredCodeLanguage: string | null, blockCode: string, addLineNumbers: boolean) => string, inferCodeLanguage: (blockTitle: string, code: string) => string | null, }) { // state const [isHovering, setIsHovering] = React.useState(false); const [fitScreen, setFitScreen] = React.useState(!!props.fitScreen); const [showHTML, setShowHTML] = React.useState(props.initialShowHTML === true); const [showMermaid, setShowMermaid] = React.useState(true); const [showPlantUML, setShowPlantUML] = React.useState(true); const [showSVG, setShowSVG] = React.useState(true); const { showLineNumbers, showSoftWrap, setShowLineNumbers, setShowSoftWrap } = useUIPreferencesStore(useShallow(state => ({ showLineNumbers: state.renderCodeLineNumbers, showSoftWrap: state.renderCodeSoftWrap, setShowLineNumbers: state.setRenderCodeLineNumbers, setShowSoftWrap: state.setRenderCodeSoftWrap, }))); // derived props const { codeBlock: { blockTitle, blockCode, complete: blockComplete }, highlightCode, inferCodeLanguage, optimizeLightweight, } = props; // handlers const handleMouseOverEnter = React.useCallback(() => setIsHovering(true), []); const handleMouseOverLeave = React.useCallback(() => setIsHovering(false), []); const handleCopyToClipboard = React.useCallback((e: React.MouseEvent) => { e.stopPropagation(); copyToClipboard(blockCode, 'Code'); }, [blockCode]); // heuristics for specialized rendering const isHTMLCode = heuristicIsBlockPureHTML(blockCode); const renderHTML = isHTMLCode && showHTML; const isMermaidCode = blockTitle === 'mermaid' && blockComplete; const renderMermaid = isMermaidCode && showMermaid; const isPlantUMLCode = heuristicIsBlockPlantUML(blockCode); let renderPlantUML = isPlantUMLCode && showPlantUML; const { data: plantUmlSvgData, error: plantUmlError } = usePlantUmlSvg(renderPlantUML, blockCode); renderPlantUML = renderPlantUML && (!!plantUmlSvgData || !!plantUmlError); const isSVGCode = (blockCode.startsWith('\n'); const renderSVG = isSVGCode && showSVG; const canScaleSVG = renderSVG && blockCode.includes('viewBox="'); const renderSyntaxHighlight = !renderHTML && !renderMermaid && !renderPlantUML && !renderSVG; const cannotRenderLineNumbers = !renderSyntaxHighlight || showSoftWrap; const renderLineNumbers = showLineNumbers && !cannotRenderLineNumbers; // Language & Highlight const { highlightedCode, inferredCodeLanguage } = React.useMemo(() => { const inferredCodeLanguage = inferCodeLanguage(blockTitle, blockCode); const highlightedCode = !renderSyntaxHighlight ? null : highlightCode(inferredCodeLanguage, blockCode, renderLineNumbers); return { highlightedCode, inferredCodeLanguage }; }, [blockCode, blockTitle, highlightCode, inferCodeLanguage, renderLineNumbers, renderSyntaxHighlight]); // Title let showBlockTitle = (blockTitle != inferredCodeLanguage) && (blockTitle.includes('.') || blockTitle.includes('://')); // Beautify: hide the block title when rendering HTML if (renderHTML) showBlockTitle = false; const isBorderless = (renderHTML || renderSVG) && !showBlockTitle; const canCodePen = blockComplete && isCodePenSupported(inferredCodeLanguage, isSVGCode); const canJSFiddle = blockComplete && isJSFiddleSupported(inferredCodeLanguage, blockCode); const canStackBlitz = blockComplete && isStackBlitzSupported(inferredCodeLanguage); const canOpenExternally = canCodePen || canJSFiddle || canStackBlitz; return ( {/* Markdown Title (File/Type) */} {showBlockTitle && ( {blockTitle} {/*{inferredCodeLanguage}*/} )} {/* Renders HTML, or inline SVG, inline plantUML rendered, or highlighted code */} {renderHTML ? : renderMermaid ? : renderSVG ? : (renderPlantUML && plantUmlSvgData) ? : } {/* [overlay] Buttons (Code blocks (SVG, diagrams, HTML, syntax, ...)) */} {(ALWAYS_SHOW_OVERLAY || isHovering) && ( {/* Show HTML */} {isHTMLCode && ( setShowHTML(!showHTML)}> )} {/* Show SVG */} {isSVGCode && ( setShowSVG(!showSVG)}> )} {/* Show Diagrams */} {(isMermaidCode || isPlantUMLCode) && ( {/* Toggle rendering */} { if (isMermaidCode) setShowMermaid(on => !on); if (isPlantUMLCode) setShowPlantUML(on => !on); }}> {/* Fit-To-Screen */} {((isMermaidCode && showMermaid) || (isPlantUMLCode && showPlantUML && !plantUmlError) || (isSVGCode && showSVG && canScaleSVG)) && ( setFitScreen(on => !on)}> )} )} {/* Group: Open Externally */} {canOpenExternally && ( {canJSFiddle && } {canCodePen && } {canStackBlitz && } )} {/* Group: Text Options */} {/* Soft Wrap toggle */} {renderSyntaxHighlight && ( setShowSoftWrap(!showSoftWrap)}> )} {/* Line Numbers toggle */} {renderSyntaxHighlight && ( setShowLineNumbers(!showLineNumbers)}> )} {/* Copy */} {props.noCopyButton !== true && ( )} )} ); }