import React, { type ComponentProps, useEffect, useMemo } from 'react'; import { Button, Tabs, Typography } from 'antd'; import { createStyles } from 'antd-style'; import toReactElement from 'jsonml-to-react-element'; import JsonML from 'jsonml.js/lib/utils'; import Prism from 'prismjs'; import LiveCode from './LiveCode'; const useStyle = createStyles(({ token, css }) => { const { colorIcon, colorBgTextHover, antCls } = token; return { code: css` position: relative; margin-top: -16px; `, copyButton: css` color: ${colorIcon}; position: absolute; z-index: 2; top: 16px; inset-inline-end: 16px; width: 32px; text-align: center; background: ${colorBgTextHover}; padding: 0; `, copyIcon: css` ${antCls}-typography-copy { position: relative; margin-inline-start: 0; // expand clickable area &::before { content: ''; display: block; position: absolute; top: -5px; left: -9px; bottom: -5px; right: -9px; } } ${antCls}-typography-copy:not(${antCls}-typography-copy-success) { color: ${colorIcon}; &:hover { color: ${colorIcon}; } } `, }; }); const LANGS = { tsx: 'TypeScript', jsx: 'JavaScript', style: 'CSS', }; interface CodePreviewProps extends Omit, 'initialValue' | 'lang'> { sourceCode?: string; jsxCode?: string; styleCode?: string; entryName: string; onCodeTypeChange?: (activeKey: string) => void; onSourceTranspile?: (source: Record) => void; } function toReactComponent(jsonML: any[]) { return toReactElement(jsonML, [ [ (node: any) => JsonML.isElement(node) && JsonML.getTagName(node) === 'pre', (node: any, index: number) => { const attr = JsonML.getAttributes(node); return (
            
          
); }, ], ]); } const CodePreview: React.FC = ({ sourceCode = '', jsxCode = '', styleCode = '', entryName, onCodeTypeChange, onSourceTranspile, }) => { // 避免 Tabs 数量不稳定的闪动问题 const initialCodes = {} as Record<'tsx' | 'jsx' | 'style', string>; if (sourceCode) { initialCodes.tsx = ''; } if (jsxCode) { initialCodes.jsx = ''; } if (styleCode) { initialCodes.style = ''; } const [highlightedCodes, setHighlightedCodes] = React.useState(initialCodes); const sourceCodes = { // omit trailing line break tsx: sourceCode?.trim(), jsx: jsxCode?.trim(), style: styleCode?.trim(), } as Record<'tsx' | 'jsx' | 'style', string>; useEffect(() => { const codes = { tsx: Prism.highlight(sourceCode, Prism.languages.javascript, 'jsx'), jsx: Prism.highlight(jsxCode, Prism.languages.javascript, 'jsx'), style: Prism.highlight(styleCode, Prism.languages.css, 'css'), }; // 去掉空的代码类型 Object.keys(codes).forEach((key: keyof typeof codes) => { if (!codes[key]) { delete codes[key]; } }); setHighlightedCodes(codes); }, [jsxCode, sourceCode, styleCode]); const langList = Object.keys(highlightedCodes) as ('tsx' | 'jsx' | 'style')[]; const { styles } = useStyle(); const items = useMemo( () => langList.map((lang: keyof typeof LANGS) => ({ label: LANGS[lang], key: lang, children: (
{lang === 'tsx' ? ( { onSourceTranspile?.({ [entryName]: code }); }} /> ) : ( toReactComponent(['pre', { lang, highlighted: highlightedCodes[lang] }]) )}
), })), [JSON.stringify(highlightedCodes)], ); if (!langList.length) { return null; } if (langList.length === 1) { return ( { onSourceTranspile?.({ [entryName]: code }); }} /> ); } return ; }; export default CodePreview;