import { Paragraph, Title } from '@stash/ui' import type { ReactNode } from 'react' /** * 极简 Markdown 渲染:只覆盖文章正文实际用到的子集。 * * 支持: * - `## 小标题`(1~6 级井号都渲染成同一级小标题,标题层级由版式决定) * - 空行分段 * - 单独一行的 `![alt](url)` 作为配图 * - 行内 `**粗体**`、`` `代码` ``、`[文字](链接)` * * 不支持列表、表格、引用块——这里渲染的是文章正文,不是通用 Markdown 编辑器。 */ type Block = | { kind: 'heading'; text: string } | { kind: 'image'; alt: string; src: string } | { kind: 'paragraph'; text: string } /** 行内语法:粗体 / 代码 / 链接;其余按纯文本处理 */ const INLINE_PATTERN = /(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))/g /** 把 Markdown 文本切成块;规则尽量简单,宁可少解析也不误伤正文 */ function parseBlocks(markdown: string): Block[] { const blocks: Block[] = [] const lines = markdown.replace(/\r\n/g, '\n').split('\n') let paragraph: string[] = [] // 空行或遇到标题/图片时,把攒着的行收成一个段落 const flush = () => { if (paragraph.length === 0) return blocks.push({ kind: 'paragraph', text: paragraph.join(' ') }) paragraph = [] } for (const raw of lines) { const line = raw.trim() if (line === '') { flush() continue } const heading = /^#{1,6}\s+(.*)$/.exec(line) if (heading !== null) { flush() blocks.push({ kind: 'heading', text: heading[1] }) continue } const image = /^!\[([^\]]*)\]\(([^)\s]+)\)$/.exec(line) if (image !== null) { flush() blocks.push({ kind: 'image', alt: image[1], src: image[2] }) continue } paragraph.push(line) } flush() return blocks } /** 渲染一行里的行内语法 */ function renderInline(text: string): ReactNode[] { return text .split(INLINE_PATTERN) .filter((part) => part !== '') .map((part, index) => { if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)} } if (part.startsWith('`') && part.endsWith('`')) { return {part.slice(1, -1)} } const link = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part) if (link !== null) { return ( {link[1]} ) } return {part} }) } type ArticleBodyProps = { /** Markdown 正文;为空时组件渲染空内容 */ markdown?: string } export function ArticleBody({ markdown }: ArticleBodyProps) { if (markdown === undefined || markdown.trim() === '') return null return ( <> {parseBlocks(markdown).map((block, index) => { if (block.kind === 'heading') { return ( {renderInline(block.text)} ) } if (block.kind === 'image') { return ( {block.alt} ) } return ( {renderInline(block.text)} ) })} ) }