import MarkdownIt from 'markdown-it' const markdown = new MarkdownIt({ html: false, linkify: true, breaks: true }) const defaultTableOpen = markdown.renderer.rules.table_open const defaultTableClose = markdown.renderer.rules.table_close const defaultParagraphOpen = markdown.renderer.rules.paragraph_open const defaultLinkOpen = markdown.renderer.rules.link_open const defaultBlockquoteOpen = markdown.renderer.rules.blockquote_open const RISK_TEXT_CLASS_BY_LABEL = { 低风险: 'markdown-risk-text-low', 中风险: 'markdown-risk-text-medium', 高风险: 'markdown-risk-text-high' } const ACTION_LINK_CLASS_BY_HREF = { '#confirm-attachment-association': 'markdown-action-link-confirm', '#review-next-step': 'markdown-action-link-next', '#review-quick-edit': 'markdown-action-link-edit', '#review-risk-panel': 'markdown-action-link-risk' } function escapeHtml(text) { return String(text || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } function renderRiskText(text) { return escapeHtml(text).replace(/低风险|中风险|高风险/g, (label) => { const className = RISK_TEXT_CLASS_BY_LABEL[label] return className ? `${label}` : label }) } function resolveActionLinkClass(href) { const normalizedHref = String(href || '').trim() return ACTION_LINK_CLASS_BY_HREF[normalizedHref] || '' } function inlineTokenHasActionLink(token) { const children = Array.isArray(token?.children) ? token.children : [] return children.some((child) => ( child?.type === 'link_open' && resolveActionLinkClass(child.attrGet?.('href')) )) } function resolveInlineTokenPlainText(token) { const children = Array.isArray(token?.children) ? token.children : [] const childText = children .filter((child) => ['text', 'code_inline'].includes(String(child?.type || ''))) .map((child) => String(child?.content || '')) .join('') .trim() return childText || String(token?.content || '').replace(/[*_`]+/g, '').trim() } function blockquoteHasAttachmentHeading(tokens, idx) { for (let i = idx + 1; i < tokens.length; i += 1) { const token = tokens[i] if (token?.type === 'blockquote_close') { return false } if (token?.type === 'inline') { return /^附件\s*\d+\s*[::]/.test(resolveInlineTokenPlainText(token)) } } return false } markdown.renderer.rules.paragraph_open = (tokens, idx, options, env, self) => { if (inlineTokenHasActionLink(tokens[idx + 1])) { tokens[idx].attrJoin('class', 'markdown-action-paragraph') } return defaultParagraphOpen ? defaultParagraphOpen(tokens, idx, options, env, self) : self.renderToken(tokens, idx, options) } markdown.renderer.rules.link_open = (tokens, idx, options, env, self) => { const actionClass = resolveActionLinkClass(tokens[idx].attrGet('href')) if (actionClass) { tokens[idx].attrJoin('class', `markdown-action-link ${actionClass}`) } return defaultLinkOpen ? defaultLinkOpen(tokens, idx, options, env, self) : self.renderToken(tokens, idx, options) } markdown.renderer.rules.text = (tokens, idx) => renderRiskText(tokens[idx]?.content) markdown.renderer.rules.blockquote_open = (tokens, idx, options, env, self) => { if (blockquoteHasAttachmentHeading(tokens, idx)) { tokens[idx].attrJoin('class', 'markdown-attachment-card') } return defaultBlockquoteOpen ? defaultBlockquoteOpen(tokens, idx, options, env, self) : self.renderToken(tokens, idx, options) } markdown.renderer.rules.table_open = (tokens, idx, options, env, self) => ( `
${defaultTableOpen ? defaultTableOpen(tokens, idx, options, env, self) : ''}` ) markdown.renderer.rules.table_close = (tokens, idx, options, env, self) => ( `${defaultTableClose ? defaultTableClose(tokens, idx, options, env, self) : '
'}
` ) const ALLOWED_COLON_HEADING_TITLES = new Set([ '基础信息识别结果', '报销测算参考', '补充信息' ]) function splitColonHeadingLine(line) { const rawLine = String(line || '') const trimmed = rawLine.trim() if (!trimmed || trimmed.startsWith('|') || /^#{1,6}\s/.test(trimmed)) { return [rawLine] } const chineseColonIndex = trimmed.indexOf(':') const asciiColonIndex = trimmed.indexOf(':') const colonIndexes = [chineseColonIndex, asciiColonIndex].filter((index) => index > 0) if (!colonIndexes.length) { return [rawLine] } const colonIndex = Math.min(...colonIndexes) const title = trimmed.slice(0, colonIndex + 1) const titleText = title.slice(0, -1) const body = trimmed.slice(colonIndex + 1).trim() if (!ALLOWED_COLON_HEADING_TITLES.has(titleText)) { return [rawLine] } return body ? [`### ${title}`, '', body] : [`### ${title}`] } function normalizeColonHeadings(text) { const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n') const normalizedLines = [] let inFence = false lines.forEach((line) => { if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence normalizedLines.push(line) return } if (inFence) { normalizedLines.push(line) return } const nextLines = splitColonHeadingLine(line) if (nextLines[0]?.startsWith('### ') && normalizedLines.length) { const previousLine = normalizedLines[normalizedLines.length - 1] if (String(previousLine || '').trim()) { normalizedLines.push('') } } normalizedLines.push(...nextLines) }) return normalizedLines.join('\n').replace(/\n{3,}/g, '\n\n') } export function renderMarkdown(text = '') { const normalized = normalizeColonHeadings(text).trim() return normalized ? markdown.render(normalized) : '' }