docs: add comments to the src/components directory
This commit is contained in:
@@ -1,15 +1,31 @@
|
||||
import { memo } from 'react'
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:14:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:14:59
|
||||
*/
|
||||
/**
|
||||
* AudioBlock Component
|
||||
*
|
||||
* Renders audio elements from markdown nodes.
|
||||
* Extracts audio source URLs and creates HTML audio players with controls.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import type { FC } from 'react'
|
||||
import { memo, type FC } from 'react'
|
||||
|
||||
/** Props interface for AudioBlock component */
|
||||
interface AudioBlockProps {
|
||||
node: {
|
||||
children: { properties: { src: string } }[]
|
||||
}
|
||||
}
|
||||
|
||||
/** Audio block component that renders audio elements from markdown nodes */
|
||||
const AudioBlock: FC<AudioBlockProps> = (props) => {
|
||||
// console.log('AudioBlock', props)
|
||||
const { children } = props.node;
|
||||
/** Extract audio source URLs from node children and filter out empty values */
|
||||
const srcs = children.map(item => item.properties?.src).filter(item => item)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,22 +1,45 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:15:05
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:15:05
|
||||
*/
|
||||
/**
|
||||
* Code Component
|
||||
*
|
||||
* A versatile code rendering component that supports:
|
||||
* - Syntax-highlighted code blocks
|
||||
* - ECharts visualizations
|
||||
* - SVG rendering
|
||||
* - Mermaid diagrams
|
||||
* - Inline code snippets
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { type FC, useMemo } from 'react'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
import CopyBtn from './CopyBtn';
|
||||
import ReactEcharts from 'echarts-for-react';
|
||||
|
||||
import CopyBtn from './CopyBtn';
|
||||
import Svg from './Svg'
|
||||
import MermaidChart from './MermaidChart'
|
||||
|
||||
|
||||
/** Props interface for Code component */
|
||||
type ICodeProps = {
|
||||
children: string;
|
||||
className: string;
|
||||
}
|
||||
|
||||
/** Code block component that renders syntax-highlighted code or special visualizations */
|
||||
const Code: FC<ICodeProps> = (props) => {
|
||||
const { children, className } = props;
|
||||
/** Extract language from className (e.g., 'language-javascript' -> 'javascript') */
|
||||
const language = className?.split('-')[1]
|
||||
console.log('Code', props)
|
||||
|
||||
// Parse ECharts configuration from code content
|
||||
const charData = useMemo(() => {
|
||||
if (language !== 'echarts') return null;
|
||||
try {
|
||||
@@ -27,6 +50,7 @@ const Code: FC<ICodeProps> = (props) => {
|
||||
}
|
||||
}, [language, children])
|
||||
|
||||
// Render ECharts visualization
|
||||
if (language === 'echarts') {
|
||||
return (
|
||||
<ReactEcharts
|
||||
@@ -39,6 +63,7 @@ const Code: FC<ICodeProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Render SVG content
|
||||
if (language === 'svg') {
|
||||
return (
|
||||
<Svg
|
||||
@@ -46,6 +71,7 @@ const Code: FC<ICodeProps> = (props) => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
// Render Mermaid diagram
|
||||
if (language === 'mermaid') {
|
||||
return (
|
||||
<MermaidChart
|
||||
@@ -54,6 +80,7 @@ const Code: FC<ICodeProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
// Render syntax-highlighted code block with copy button
|
||||
if (className) {
|
||||
return (
|
||||
<div className="rb:relative">
|
||||
@@ -81,6 +108,7 @@ const Code: FC<ICodeProps> = (props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Render inline code
|
||||
return <code className="rb:bg-[#F0F3F8] rb:px-1 rb:py-0.5 rb:rounded rb:text-sm rb:font-mono rb:whitespace-break-spaces">{children}</code>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:15:11
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:15:11
|
||||
*/
|
||||
/**
|
||||
* CodeBlock Component
|
||||
*
|
||||
* A standalone code block component for displaying formatted code with:
|
||||
* - Syntax highlighting
|
||||
* - Optional copy functionality
|
||||
* - Configurable size and line numbers
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { type FC } from 'react'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
|
||||
import CopyBtn from './CopyBtn';
|
||||
|
||||
|
||||
/** Props interface for CodeBlock component */
|
||||
type ICodeBlockProps = {
|
||||
value: string;
|
||||
needCopy?: boolean;
|
||||
@@ -11,12 +29,7 @@ type ICodeBlockProps = {
|
||||
showLineNumbers?: boolean;
|
||||
}
|
||||
|
||||
// enum languageType {
|
||||
// echarts = 'echarts',
|
||||
// mermaid = 'mermaid',
|
||||
// svg = 'svg',
|
||||
// }
|
||||
|
||||
/** Code block component for displaying formatted code with optional copy functionality */
|
||||
const CodeBlock: FC<ICodeBlockProps> = ({
|
||||
value,
|
||||
needCopy = true,
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:15:21
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:15:21
|
||||
*/
|
||||
/**
|
||||
* CopyBtn Component
|
||||
*
|
||||
* A button component that copies text to clipboard and displays a success message.
|
||||
* Uses the copy-to-clipboard library for cross-browser compatibility.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { Button, App } from 'antd'
|
||||
|
||||
|
||||
/** Props interface for CopyBtn component */
|
||||
type ICopyBtnProps = {
|
||||
value: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/** Copy button component that copies text to clipboard and shows success message */
|
||||
const CopyBtn: FC<ICopyBtnProps> = ({
|
||||
value,
|
||||
className,
|
||||
@@ -18,6 +34,7 @@ const CopyBtn: FC<ICopyBtnProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const { message } = App.useApp()
|
||||
|
||||
/** Copy value to clipboard and show success message */
|
||||
const handleCopy = () => {
|
||||
copy(value)
|
||||
message.success(t('common.copySuccess'))
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import { memo } from 'react'
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:15:55
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:15:55
|
||||
*/
|
||||
/**
|
||||
* Link Component
|
||||
*
|
||||
* A secure link component that opens URLs in a new tab.
|
||||
* Includes security attributes (noopener, noreferrer) to prevent security vulnerabilities.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { memo } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
/** Props interface for Link component */
|
||||
interface LinkProps {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/** Link component that opens in a new tab with security attributes */
|
||||
const Link: FC<LinkProps> = (props) => {
|
||||
// console.log('Link', props)
|
||||
const { children, href } = props;
|
||||
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:16:01
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:16:01
|
||||
*/
|
||||
/**
|
||||
* MermaidChart Component
|
||||
*
|
||||
* Renders Mermaid diagrams as images.
|
||||
* - Converts Mermaid syntax to SVG
|
||||
* - Converts SVG to base64 data URL for display
|
||||
* - Generates unique IDs based on content hash
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { useRef, useEffect, useState, type FC } from 'react'
|
||||
import mermaid from 'mermaid'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { Image } from 'antd'
|
||||
|
||||
/** Initialize Mermaid with default configuration */
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
@@ -12,6 +30,7 @@ mermaid.initialize({
|
||||
},
|
||||
})
|
||||
|
||||
/** Convert SVG string to base64 data URL for image display */
|
||||
const svgToBase64 = (svgGraph: string) => {
|
||||
const svgBytes = new TextEncoder().encode(svgGraph)
|
||||
const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
|
||||
@@ -22,8 +41,11 @@ const svgToBase64 = (svgGraph: string) => {
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
/** Mermaid chart component that renders Mermaid diagrams as images */
|
||||
const MermaidChart: FC<{ content: string }> = ({ content }) => {
|
||||
const [chartSvg, setChartSvg] = useState<string>('')
|
||||
/** Generate unique ID based on content hash to avoid conflicts */
|
||||
const id = useRef(`mermaidchart_${CryptoJS.MD5(content).toString()}`)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,6 +55,7 @@ const MermaidChart: FC<{ content: string }> = ({ content }) => {
|
||||
drawDiagram()
|
||||
}, [content])
|
||||
|
||||
/** Render Mermaid diagram and convert to base64 image */
|
||||
const drawDiagram = async function () {
|
||||
const { svg } = await mermaid.render(id.current, content);
|
||||
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import { memo } from 'react'
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:16:06
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:16:06
|
||||
*/
|
||||
/**
|
||||
* Paragraph Component
|
||||
*
|
||||
* A simple paragraph component for rendering markdown paragraphs.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { memo } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
|
||||
/** Props interface for Paragraph component */
|
||||
interface ParagraphProps {
|
||||
node: {
|
||||
children: ReactNode;
|
||||
};
|
||||
children: string[]
|
||||
}
|
||||
|
||||
/** Paragraph component for rendering markdown paragraphs */
|
||||
const Paragraph: FC<ParagraphProps> = (props) => {
|
||||
// console.log('Paragraph', props)
|
||||
const { children } = props
|
||||
|
||||
return <p>{children}</p>
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:16:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:16:10
|
||||
*/
|
||||
/**
|
||||
* RbButton Component
|
||||
*
|
||||
* A button component for rendering buttons in markdown content.
|
||||
* Wraps Ant Design Button component.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { memo } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { Button } from 'antd'
|
||||
|
||||
/** Props interface for RbButton component */
|
||||
interface RbButtonProps {
|
||||
node: {
|
||||
children: ReactNode;
|
||||
};
|
||||
children: string[]
|
||||
}
|
||||
|
||||
/** Button component for rendering buttons in markdown */
|
||||
const RbButton: FC<RbButtonProps> = (props) => {
|
||||
console.log('RbButton', props)
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:16:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:16:14
|
||||
*/
|
||||
/**
|
||||
* Svg Component
|
||||
*
|
||||
* Renders SVG content from string using dangerouslySetInnerHTML.
|
||||
* Used for displaying SVG code blocks in markdown.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
/** Props interface for Svg component */
|
||||
interface SvgProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染SVG内容的组件
|
||||
*/
|
||||
/** Component for rendering SVG content from string */
|
||||
function Svg(props: SvgProps): JSX.Element {
|
||||
const { content } = props;
|
||||
// console.log('Svg', props)
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import { memo } from 'react'
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:16:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:54:55
|
||||
*/
|
||||
/**
|
||||
* VideoBlock Component
|
||||
*
|
||||
* Renders video elements from markdown nodes.
|
||||
* Extracts video source URLs and creates HTML video players with controls.
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { memo } from 'react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
/** Props interface for VideoBlock component */
|
||||
interface VideoBlockProps {
|
||||
node: {
|
||||
children: { properties: { src: string } }[]
|
||||
}
|
||||
}
|
||||
|
||||
/** Video block component that renders video elements from markdown nodes */
|
||||
const VideoBlock: FC<VideoBlockProps> = (props) => {
|
||||
// console.log('VideoBlock', props)
|
||||
const { children } = props.node;
|
||||
/** Extract video source URLs from node children and filter out empty values */
|
||||
const srcs = children.map(item => item.properties?.src).filter(item => item)
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,3 +1,28 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:17:31
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-02 15:17:31
|
||||
*/
|
||||
/**
|
||||
* RbMarkdown Component
|
||||
*
|
||||
* A comprehensive markdown renderer with support for:
|
||||
* - Standard markdown syntax (headings, lists, tables, etc.)
|
||||
* - Code syntax highlighting
|
||||
* - Math equations (KaTeX)
|
||||
* - Mermaid diagrams
|
||||
* - ECharts visualizations
|
||||
* - SVG rendering
|
||||
* - Audio/video embedding
|
||||
* - Interactive form elements
|
||||
* - HTML comments visibility toggle
|
||||
* - Editable mode with live preview
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { useState, useRef, useEffect, type FC } from 'react'
|
||||
import { Image, Input, Select, Form, Checkbox, Radio, ColorPicker, DatePicker, TimePicker, InputNumber, Slider } from 'antd'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import RemarkGfm from 'remark-gfm'
|
||||
@@ -5,8 +30,6 @@ import RemarkMath from 'remark-math'
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RehypeKatex from 'rehype-katex'
|
||||
import RehypeRaw from 'rehype-raw'
|
||||
import type { FC } from 'react'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
import Code from './Code'
|
||||
import VideoBlock from './VideoBlock'
|
||||
@@ -14,14 +37,21 @@ import AudioBlock from './AudioBlock'
|
||||
import Link from './Link'
|
||||
import RbButton from './RbButton'
|
||||
|
||||
/** Props interface for RbMarkdown component */
|
||||
interface RbMarkdownProps {
|
||||
/** Markdown content to render */
|
||||
content: string;
|
||||
showHtmlComments?: boolean; // 是否显示 HTML 注释,默认为 false(隐藏)
|
||||
editable?: boolean; // 是否可编辑,默认为 false
|
||||
onContentChange?: (content: string) => void; // 内容变化回调
|
||||
/** Whether to display HTML comments (default: false) */
|
||||
showHtmlComments?: boolean;
|
||||
/** Whether the content is editable (default: false) */
|
||||
editable?: boolean;
|
||||
/** Callback fired when content changes in edit mode */
|
||||
onContentChange?: (content: string) => void;
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Custom component mappings for markdown elements */
|
||||
const components = {
|
||||
h1: ({ children, ...props }: any) => <h1 className="rb:text-2xl rb:font-bold rb:mb-2" {...props}>{children}</h1>,
|
||||
h2: ({ children, ...props }: any) => <h2 className="rb:text-xl rb:font-bold rb:mb-2" {...props}>{children}</h2>,
|
||||
@@ -38,7 +68,7 @@ const components = {
|
||||
em: ({ children, ...props }: any) => <em className="rb:italic" {...props}>{children}</em>,
|
||||
del: ({ children, ...props }: any) => <del className="rb:line-through" {...props}>{children}</del>,
|
||||
span: ({ children, style, ...restProps }: any) => {
|
||||
// 如果是 HTML 注释的 span,应用特殊样式
|
||||
// Apply special styling for HTML comment spans
|
||||
if (style?.color === '#999') {
|
||||
return <span style={{ color: '#999', fontSize: '0.9em' }}>{children}</span>
|
||||
}
|
||||
@@ -104,30 +134,33 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||
const [editContent, setEditContent] = useState(content)
|
||||
const textareaRef = useRef<any>(null)
|
||||
|
||||
// 当外部 content 变化时,同步更新编辑内容
|
||||
/** Sync edit content when external content changes */
|
||||
useEffect(() => {
|
||||
setEditContent(content)
|
||||
}, [content])
|
||||
|
||||
// 处理 textarea 内容变化
|
||||
/** Handle textarea content changes and trigger callback */
|
||||
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newContent = e.target.value
|
||||
setEditContent(newContent)
|
||||
// 实时回调内容变化
|
||||
/** Trigger real-time content change callback */
|
||||
onContentChange?.(newContent)
|
||||
}
|
||||
|
||||
// 根据参数决定是否将 HTML 注释转换为可见文本
|
||||
// 使用特殊的 markdown 语法来显示注释,避免被 rehype-raw 过滤
|
||||
/**
|
||||
* Process content based on showHtmlComments flag
|
||||
* Converts HTML comments to visible text when showHtmlComments is true
|
||||
* Uses special span markup to display comments with styling
|
||||
*/
|
||||
const processedContent = showHtmlComments
|
||||
? (editable ? editContent : content).replace(/<!--([\s\S]*?)-->/g, (_match, commentContent) => {
|
||||
// 转换为带样式的文本,使用 <span class="html-comment"> 标记
|
||||
/** Convert to styled text using span with html-comment class */
|
||||
const escaped = commentContent.trim().replace(/</g, '<').replace(/>/g, '>')
|
||||
return `<span class="html-comment"><!-- ${escaped} --></span>`
|
||||
})
|
||||
: (editable ? editContent : content)
|
||||
|
||||
// 如果是编辑模式,显示 textarea
|
||||
/** Render textarea in edit mode */
|
||||
if (editable) {
|
||||
return (
|
||||
<div className="rb:relative">
|
||||
@@ -138,21 +171,21 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* 编辑区域 */}
|
||||
{/* Edit area with textarea */}
|
||||
<Input.TextArea
|
||||
ref={textareaRef}
|
||||
value={editContent}
|
||||
onChange={handleTextareaChange}
|
||||
rows={10}
|
||||
className="rb:font-mono rb:text-sm"
|
||||
placeholder="请输入 Markdown 内容..."
|
||||
placeholder="Enter Markdown content..."
|
||||
style={{ resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 处理键盘快捷键
|
||||
/** Handle keyboard shortcuts (e.g., Ctrl+C for copy) */
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
|
||||
const selection = window.getSelection()
|
||||
@@ -162,7 +195,7 @@ const RbMarkdown: FC<RbMarkdownProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 预览模式
|
||||
/** Render markdown preview mode */
|
||||
return (
|
||||
<div className={`rb:relative ${className || ''}`} onKeyDown={handleKeyDown} tabIndex={0}>
|
||||
<style>{`
|
||||
|
||||
Reference in New Issue
Block a user