前占据顶部一行
return `${lang}
`
}
/**
* 块代码语法高亮:仅处理已知语言,未知/无语言兜底原样(确定性强,不做自动猜测)。
* marked 已对代码内容做 HTML 转义,先反转还原再交给 hljs(hljs 输出自带转义)。
* 同时补语言徽标:围栏有 lang 即显示(用原始标识如 ts,更贴近用户书写),无 lang 不显示。
*/
function highlightCodeBlocks(html: string): string {
return html.replace(/]*)>([\s\S]*?)<\/code><\/pre>/gi, (m, attrs: string, inner: string) => {
const langMatch = /class="language-([^"]+)"/i.exec(attrs)
const langRaw = langMatch ? langMatch[1] : ''
const lang = LANG_ALIASES[langRaw] ?? langRaw
const badge = langRaw ? codeLangBadge(langRaw) : ''
if (!lang || !hljs.getLanguage(lang)) {
// 未注册语言:不高亮但保留徽标(fence 有 lang 时),避免 ` ```未知lang ` 丢语言标签
return badge ? `${badge}${inner}
` : m
}
let value: string
try {
value = hljs.highlight(unescapeCode(inner), { language: lang, ignoreIllegals: true }).value
} catch {
return badge ? `${badge}${inner}
` : m
}
return `${badge}${inlineHljs(value)}
`
})
}
/**
* 对 marked.parse 产出的 HTML 注入 inline style。
*
* 设计取舍:pre(块代码)给深色背景; 不给背景(透明),仅等宽 + 橙色 —— 避免块代码内层 code
* 与 pre 背景冲突的双层色块(块代码内 code 透明继承 pre 深底,内联 code 橙色等宽在消息底上也可读);
* 块代码经 hljs 高亮后,关键字/字符串等 token 由内联色覆盖,未命中 token 的文本保留 code 基色。
*
* 正文颜色(真机确认):rich-text 只认节点 inline style,不继承页面 CSS `.msg text`
* (模板的 `.msg text{color:#e0e0e0}` 仅命中 `` 子元素)。marked 产出的 p/h1-6/li/td/th
* 等不带 style 时,文本在暗色气泡上呈近黑 → 暗底黑字。故给承载正文的块级元素统一注入颜色。
* strong/em/b 等行内元素继承父级(p/li)颜色,无需逐一注入。
*/
export function styleMarkdown(html: string): string {
let out = html
// 任务列表勾选框:rich-text 节点白名单不含 input,替换为字符符号(勾选/未勾选)
out = out.replace(/]*type="checkbox"[^>]*>/gi, (m) => {
const checked = /checked/i.test(m)
const color = checked ? '#6bd99a' : '#666666'
return `${checked ? '☑' : '☐'}`
})
// 块代码语法高亮(须在下方 pre/code styleTag 前执行,hljs 输出 token span 后再合并基样式)
out = highlightCodeBlocks(out)
// 正文着色:块级文本容器统一浅色(rich-text 样式继承链从容器传子文本)
// word-break:break-all 防长 URL/长单词撑开气泡(rich-text 内文本不继承外层 .msg 的
// word-break,须 inline 注入;对齐文件头"rich-text 只认 inline style")。
const BODY_STYLE = 'color:#e0e0e0;word-break:break-all'
for (const tag of ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'td']) {
out = styleTag(out, tag, BODY_STYLE)
}
// 块代码 :深色背景 + 等宽 + 圆角 + pre-wrap 换行防撑爆
out = styleTag(
out,
'pre',
'display:block;background-color:#1a1a1a;color:#d4d4d4;padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-all;font-family:monospace;font-size:12px;margin:6px 0',
)
// code(块内 + 内联):等宽橙色,透明背景(避与 pre 双层色块;块代码 token 由高亮色覆盖)
out = styleTag(
out,
'code',
'font-family:monospace;color:#e0a070;background-color:transparent;font-size:12px',
)
// 链接:蓝下划线(rich-text 不可点击,仅展示 + selectable 长按复制);word-break 防长 URL 撑开
out = styleTag(out, 'a', 'color:#4a9eff;text-decoration:underline;word-break:break-all')
// 图片:max-width 防超宽撑爆气泡;block 去基线间隙;圆角与气泡一致。
// 网络图显示依赖微信「downloadFile 合法域名」白名单(见文件头),非渲染问题。
out = styleTag(out, 'img', 'max-width:100%;display:block;border-radius:6px')
// 表格:显式 table 布局 + 等宽字列。注意不能用 display:block(会失去表格布局语义、
// 列宽错乱),故显式声明 display:table;rich-text 非滚动容器(见文件头注释),无法建横向
// 滚动区 → 不做滚动容器,靠 max-width:100% + word-break:break-all 防超宽撑爆气泡;
// border 提升可读。
out = styleTag(
out,
'table',
'display:table;border-collapse:collapse;width:100%;max-width:100%;font-size:12px;word-break:break-all',
)
// th/td:边框 + 内边距保留,字号与 table 对齐(font-size:12px)
out = styleTag(out, 'th', 'border:1px solid #333;padding:4px 6px;font-size:12px;background-color:#1a1a1a')
out = styleTag(out, 'td', 'border:1px solid #333;padding:4px 6px;font-size:12px')
return out
}