feat(markdown): 评论支持数学公式渲染

- comments/markdown.rs: 开启 ENABLE_MATH,InlineMath/DisplayMath 事件
  渲染成 katex HTML 注入事件流(评论不用块级 <p> 包裹,保持紧凑)
- sanitizer.rs: 评论路径 span 白名单加 style(KaTeX 内联定位 style 需保留,
  与文章正文 sanitizer.rs:382 对齐)
- 新增 3 个评论公式测试(内联/块级/style 保留)

双 target 编译通过,24 个评论测试 + 36 个 sanitizer 测试全过。
This commit is contained in:
xfy 2026-07-16 14:12:20 +08:00
parent 95fe9d862d
commit 84f6223c68
2 changed files with 51 additions and 2 deletions

View File

@ -22,7 +22,7 @@ pub fn clean_comment_html(input: &str) -> String {
pub fn render_comment_markdown(md: &str) -> String { pub fn render_comment_markdown(md: &str) -> String {
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, TagEnd}; use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, TagEnd};
let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH; let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_MATH;
let parser = pulldown_cmark::Parser::new_ext(md, opts); let parser = pulldown_cmark::Parser::new_ext(md, opts);
let mut events: Vec<Event> = Vec::new(); let mut events: Vec<Event> = Vec::new();
@ -33,6 +33,17 @@ pub fn render_comment_markdown(md: &str) -> String {
// 逐事件处理 Markdown AST转换标题并收集代码块内容。 // 逐事件处理 Markdown AST转换标题并收集代码块内容。
for event in parser { for event in parser {
match event { match event {
Event::InlineMath(tex) => {
// 内联公式直接渲染成 HTML 注入事件流(评论不使用块级段落包裹)。
let html = crate::api::katex::render_inline(&tex);
events.push(Event::Html(html.into()));
}
Event::DisplayMath(tex) => {
// 评论里的块级公式KaTeX 输出本身已含 .katex-display 居中样式,
// 无需额外 <p>(评论渲染较紧凑,避免引入多余段间距)。
let html = crate::api::katex::render_display(&tex);
events.push(Event::Html(html.into()));
}
Event::Start(Tag::Heading { .. }) => { Event::Start(Tag::Heading { .. }) => {
// 评论中不保留标题层级,统一加粗。 // 评论中不保留标题层级,统一加粗。
events.push(Event::Start(Tag::Strong)); events.push(Event::Start(Tag::Strong));
@ -233,4 +244,40 @@ mod tests {
assert!(result.contains("<code>foo()</code>")); assert!(result.contains("<code>foo()</code>"));
assert!(!result.contains("<h2>")); assert!(!result.contains("<h2>"));
} }
#[test]
fn render_comment_inline_math() {
// 评论里的 $...$ 内联公式ENABLE_MATH 解析 → katex 渲染 → sanitizer 放行 span。
let result = render_comment_markdown("方程 $a^2 + b^2 = c^2$ 是勾股定理");
assert!(
result.contains("katex"),
"评论内联公式应渲染为 katex span, got: {}",
result
);
assert!(result.contains("方程"));
assert!(result.contains("勾股定理"));
}
#[test]
fn render_comment_display_math() {
// 评论里的 $$...$$ 块级公式。
let result = render_comment_markdown("$$\\int_0^1 x\\,dx$$");
assert!(
result.contains("katex-display"),
"评论块级公式应含 katex-display, got: {}",
result
);
}
#[test]
fn render_comment_math_span_style_preserved() {
// KaTeX 内联 style垂直对齐必须经 clean_comment_html 保留,
// 否则公式在评论里排版错位。验证 span 的 style 属性未被剥离。
let result = render_comment_markdown("$x^2$");
assert!(
result.contains("style=\""),
"评论 katex span 的 style 应保留, got: {}",
result
);
}
} }

View File

@ -410,7 +410,9 @@ pub fn clean_comment_html(input: &str) -> String {
], ],
extra_tag_attrs: vec![ extra_tag_attrs: vec![
("a", vec!["class", "aria-hidden", "aria-label"]), ("a", vec!["class", "aria-hidden", "aria-label"]),
("span", vec!["class"]), // span 的 styleKaTeX 服务端渲染产出的内联 style元素垂直对齐/定位)
// 需保留否则公式排版错位。与文章正文路径sanitizer.rs:382对齐。
("span", vec!["class", "style"]),
], ],
allowed_schemes: &DEFAULT_ALLOWED_SCHEMES, allowed_schemes: &DEFAULT_ALLOWED_SCHEMES,
allow_data_uri: false, allow_data_uri: false,