Files
X-Financial/web/src/utils/markdown.js

131 lines
4.4 KiB
JavaScript
Raw Normal View History

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function renderRiskText(text) {
return escapeHtml(text).replace(/低风险|中风险|高风险/g, (label) => {
const className = RISK_TEXT_CLASS_BY_LABEL[label]
return className ? `<span class="${className}">${label}</span>` : 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) => (
`<div class="markdown-table-wrap">${defaultTableOpen ? defaultTableOpen(tokens, idx, options, env, self) : '<table>'}`
)
markdown.renderer.rules.table_close = (tokens, idx, options, env, self) => (
`${defaultTableClose ? defaultTableClose(tokens, idx, options, env, self) : '</table>'}</div>`
)
export function renderMarkdown(text = '') {
const { content, trustedHtmlBlocks } = extractTrustedHtmlBlocks(text)
const normalized = normalizeConversationText(content).trim()
return normalized ? restoreTrustedHtmlBlocks(markdown.render(normalized), trustedHtmlBlocks) : ''
}