UNPKG

@ant-design/x-markdown

Version:

placeholder for @ant-design/x-markdown

304 lines (297 loc) 10.8 kB
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } import { CopyOutlined, DownloadOutlined, ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons'; import useXComponentConfig from '@ant-design/x/es/_util/hooks/use-x-component-config'; import useLocale from '@ant-design/x/es/locale/useLocale'; import useXProviderContext from '@ant-design/x/es/x-provider/hooks/use-x-provider-context'; import locale_EN from '@ant-design/x/locale/en_US'; import { Button, message, Segmented, Space, Tooltip } from 'antd'; import classnames from 'classnames'; import throttle from 'lodash.throttle'; import mermaid from 'mermaid'; import React, { useEffect, useRef, useState } from 'react'; import SyntaxHighlighter from 'react-syntax-highlighter'; import useStyle from "./style"; var RenderType = /*#__PURE__*/function (RenderType) { RenderType["Code"] = "code"; RenderType["Image"] = "image"; return RenderType; }(RenderType || {}); mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default', fontFamily: 'monospace' }); let uuid = 0; const Mermaid = /*#__PURE__*/React.memo(props => { const { prefixCls: customizePrefixCls, className, style, classNames = {}, styles = {}, header, children, highlightProps } = props; const [renderType, setRenderType] = useState(RenderType.Image); const [scale, setScale] = useState(1); const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 }); const containerRef = useRef(null); const id = `mermaid-${uuid++}-${children?.length || 0}`; const [messageApi, contextHolder] = message.useMessage(); // ============================ locale ============================ const [contextLocale] = useLocale('Mermaid', locale_EN.Mermaid); // ============================ Prefix ============================ const { getPrefixCls, direction } = useXProviderContext(); const prefixCls = getPrefixCls('mermaid', customizePrefixCls); const [hashId, cssVarCls] = useStyle(prefixCls); // ===================== Component Config ========================= const contextConfig = useXComponentConfig('mermaid'); // ============================ style ============================ const mergedCls = classnames(prefixCls, contextConfig.className, contextConfig.classNames.root, className, classNames.root, hashId, cssVarCls, { [`${prefixCls}-rtl`]: direction === 'rtl' }); // ============================ render mermaid ============================ const renderDiagram = throttle(async () => { if (!children || !containerRef.current || renderType === RenderType.Code) return; try { const isValid = await mermaid.parse(children, { suppressErrors: true }); if (!isValid) throw new Error('Invalid Mermaid syntax'); const newText = children.replace(/[`\s]+$/g, ''); const { svg } = await mermaid.render(id, newText, containerRef.current); containerRef.current.innerHTML = svg; } catch (error) { console.warn(`Mermaid render failed: ${error}`); } }, 100); useEffect(() => { if (renderType === RenderType.Code && containerRef.current) { // 清理图表内容,避免在代码视图下出现渲染错误 containerRef.current.innerHTML = ''; } else { renderDiagram(); } }, [children, renderType]); useEffect(() => { const container = containerRef.current; if (!container || renderType !== RenderType.Image) return; let lastTime = 0; const wheelHandler = e => { e.preventDefault(); e.stopPropagation(); const now = Date.now(); if (now - lastTime < 16) return; lastTime = now; const delta = e.deltaY > 0 ? -0.1 : 0.1; setScale(prev => Math.max(0.5, Math.min(3, prev + delta))); }; container.addEventListener('wheel', wheelHandler, { passive: false }); return () => { container.removeEventListener('wheel', wheelHandler); }; }, [renderType]); useEffect(() => { if (containerRef.current && renderType === RenderType.Image) { const svg = containerRef.current.querySelector('svg'); if (svg) { svg.style.transform = `scale(${scale}) translate(${position.x}px, ${position.y}px)`; svg.style.transformOrigin = 'center'; svg.style.transition = isDragging ? 'none' : 'transform 0.1s ease-out'; svg.style.cursor = isDragging ? 'grabbing' : 'grab'; } } }, [scale, position, renderType, isDragging]); // 鼠标拖动事件处理 const handleMouseDown = e => { if (renderType !== RenderType.Image) return; e.preventDefault(); setIsDragging(true); setLastMousePos({ x: e.clientX, y: e.clientY }); }; const handleMouseMove = e => { if (!isDragging || renderType !== RenderType.Image) return; e.preventDefault(); const deltaX = e.clientX - lastMousePos.x; const deltaY = e.clientY - lastMousePos.y; setPosition(prev => ({ x: prev.x + deltaX / scale, y: prev.y + deltaY / scale })); setLastMousePos({ x: e.clientX, y: e.clientY }); }; const handleMouseUp = () => { setIsDragging(false); }; const handleReset = () => { setScale(1); setPosition({ x: 0, y: 0 }); }; // ============================ render content ============================ if (!children) { return null; } const handleDownload = async () => { const svgElement = containerRef.current?.querySelector('svg'); if (!svgElement) return; const svgString = new XMLSerializer().serializeToString(svgElement); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return; const { width, height } = svgElement.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = width * dpr; canvas.height = height * dpr; canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; ctx.scale(dpr, dpr); const img = new Image(); img.onload = () => { ctx.drawImage(img, 0, 0, width, height); const link = document.createElement('a'); link.download = `${Date.now()}.png`; link.href = canvas.toDataURL('image/png', 1); link.click(); }; img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`; }; const handleZoomIn = () => { setScale(prev => Math.min(prev + 0.2, 3)); }; const handleZoomOut = () => { setScale(prev => Math.max(prev - 0.2, 0.5)); }; const handleCopyCode = async () => { if (!children) return; try { await navigator.clipboard.writeText(children.trim()); messageApi.open({ type: 'success', content: contextLocale.copySuccess }); } catch (error) { console.error('Failed to copy code:', error); } }; const renderHeader = () => { if (header === null) return null; if (header) return header; return /*#__PURE__*/React.createElement("div", { className: classnames(`${prefixCls}-header`, contextConfig.classNames.header, classNames?.header), style: { ...contextConfig.styles.header, ...styles.header } }, contextHolder, /*#__PURE__*/React.createElement(Segmented, { options: [{ label: contextLocale.image, value: RenderType.Image }, { label: contextLocale.code, value: RenderType.Code }], value: renderType, onChange: setRenderType }), /*#__PURE__*/React.createElement(Space, null, /*#__PURE__*/React.createElement(Tooltip, { title: contextLocale.copy }, /*#__PURE__*/React.createElement(Button, { type: "text", size: "small", icon: /*#__PURE__*/React.createElement(CopyOutlined, null), onClick: handleCopyCode })), renderType === RenderType.Image ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Tooltip, { title: contextLocale.zoomOut }, /*#__PURE__*/React.createElement(Button, { type: "text", size: "small", icon: /*#__PURE__*/React.createElement(ZoomInOutlined, null), onClick: handleZoomIn })), /*#__PURE__*/React.createElement(Tooltip, { title: contextLocale.zoomIn }, /*#__PURE__*/React.createElement(Button, { type: "text", size: "small", icon: /*#__PURE__*/React.createElement(ZoomOutOutlined, null), onClick: handleZoomOut })), /*#__PURE__*/React.createElement(Tooltip, { title: contextLocale.zoomReset }, /*#__PURE__*/React.createElement(Button, { type: "text", size: "small", onClick: handleReset }, contextLocale.zoomReset)), /*#__PURE__*/React.createElement(Tooltip, { title: contextLocale.download }, /*#__PURE__*/React.createElement(Button, { type: "text", size: "small", icon: /*#__PURE__*/React.createElement(DownloadOutlined, null), onClick: handleDownload }))) : null)); }; const renderContent = () => { return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", { className: classnames(`${prefixCls}-graph`, contextConfig.classNames.graph, renderType === RenderType.Code && `${prefixCls}-graph-hidden`, classNames?.graph), style: { ...contextConfig.styles.graph, ...styles.graph }, ref: containerRef, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp }), renderType === RenderType.Code ? /*#__PURE__*/React.createElement("div", { className: classnames(`${prefixCls}-code`, contextConfig.classNames.code, classNames.code), style: { ...contextConfig.styles.code, ...styles.code } }, /*#__PURE__*/React.createElement(SyntaxHighlighter, _extends({ customStyle: { padding: 0, background: 'transparent' }, language: "mermaid", wrapLines: true }, highlightProps), children.replace(/\n$/, ''))) : null); }; return /*#__PURE__*/React.createElement("div", { className: mergedCls, style: { ...style, ...contextConfig.style, ...contextConfig.styles.root, ...styles.root } }, renderHeader(), renderContent()); }); export default Mermaid;