import hljs from 'highlight.js' if (typeof window !== 'undefined') { const theme = document.documentElement.dataset.theme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') if (theme === 'dark') { import('highlight.js/styles/atom-one-dark.css') } else { import('highlight.js/styles/atom-one-light.css') } } import MarkdownIt from 'markdown-it' import { toast } from '../main' import { tiebaEmoji } from './tiebaEmoji' function mentionPlugin(md) { const mentionReg = /^@\[([^\]]+)\]/ function mention(state, silent) { const pos = state.pos if (state.src.charCodeAt(pos) !== 0x40) return false const match = mentionReg.exec(state.src.slice(pos)) if (!match) return false if (!silent) { const tokenOpen = state.push('link_open', 'a', 1) tokenOpen.attrs = [ ['href', `/users/${match[1]}`], ['target', '_blank'], ['class', 'mention-link'], ] const text = state.push('text', '', 0) text.content = `@${match[1]}` state.push('link_close', 'a', -1) } state.pos += match[0].length return true } md.inline.ruler.before('emphasis', 'mention', mention) } function tiebaEmojiPlugin(md) { md.renderer.rules['tieba-emoji'] = (tokens, idx) => { const name = tokens[idx].content const file = tiebaEmoji[name] return `${name}` } md.inline.ruler.before('emphasis', 'tieba-emoji', (state, silent) => { const pos = state.pos if (state.src.charCodeAt(pos) !== 0x3a) return false const match = state.src.slice(pos).match(/^:tieba(\d+):/) if (!match) return false const key = `tieba${match[1]}` if (!tiebaEmoji[key]) return false if (!silent) { const token = state.push('tieba-emoji', '', 0) token.content = key } state.pos += match[0].length return true }) } // 链接在新窗口打开 function linkPlugin(md) { const defaultRender = md.renderer.rules.link_open || function (tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options) } md.renderer.rules.link_open = function (tokens, idx, options, env, self) { const token = tokens[idx] const hrefIndex = token.attrIndex('href') if (hrefIndex >= 0) { const href = token.attrs[hrefIndex][1] // 如果是外部链接,添加 target="_blank" 和 rel="noopener noreferrer" if (href.startsWith('http://') || href.startsWith('https://')) { token.attrPush(['target', '_blank']) token.attrPush(['rel', 'noopener noreferrer']) } } return defaultRender(tokens, idx, options, env, self) } } const md = new MarkdownIt({ html: false, linkify: true, breaks: true, highlight: (str, lang) => { let code = '' if (lang && hljs.getLanguage(lang)) { code = hljs.highlight(str, { language: lang, ignoreIllegals: true }).value } else { code = hljs.highlightAuto(str).value } const lineNumbers = code .trim() .split('\n') .map(() => `
`) return `
${lineNumbers.join('')}
${code.trim()}
` }, }) md.use(mentionPlugin) md.use(tiebaEmojiPlugin) md.use(linkPlugin) // 添加链接插件 export function renderMarkdown(text) { return md.render(text || '') } export function handleMarkdownClick(e) { if (e.target.classList.contains('copy-code-btn')) { const pre = e.target.closest('pre') const codeEl = pre && pre.querySelector('code') if (codeEl) { navigator.clipboard.writeText(codeEl.innerText).then(() => { toast.success('已复制') }) } } } export function stripMarkdown(text) { const html = md.render(text || '') // 统一使用正则表达式方法,确保服务端和客户端行为一致 let plainText = html.replace(/<[^>]+>/g, '') // 标准化空白字符处理 plainText = plainText .replace(/\r\n/g, '\n') // Windows换行符转为Unix格式 .replace(/\r/g, '\n') // 旧Mac换行符转为Unix格式 .replace(/[ \t]+/g, ' ') // 合并空格和制表符为单个空格 .replace(/\n{3,}/g, '\n\n') // 最多保留两个连续换行(一个空行) .trim() return plainText } export function stripMarkdownLength(text, length) { const plain = stripMarkdown(text) if (!length || plain.length <= length) { return plain } // 截断并加省略号 return plain.slice(0, length) + '...' }