import MarkdownIt from 'markdown-it' import { DOCUMENT_DETAIL_HREF_PREFIX, extractTrustedHtmlBlocks, normalizeConversationText, restoreTrustedHtmlBlocks } from './conversationTrustedHtml.js' 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', '#application-submit': '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() if (normalizedHref.startsWith(DOCUMENT_DETAIL_HREF_PREFIX)) { return 'markdown-action-link-document' } 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) => ( `