From 95fe9d862dacc13bcfaa7728f323841178ed8481 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 16 Jul 2026 14:11:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(markdown):=20=E6=96=87=E7=AB=A0=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=95=B0=E5=AD=A6=E5=85=AC=E5=BC=8F=20SSR=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit render_markdown_enhanced 事件循环新增 InlineMath/DisplayMath 分支: - $...$ 内联公式调 katex::render_inline 注入 span - $$...$$ 块级公式用

包裹 katex 输出 - 标题内公式按内联渲染,不产生块级

pulldown-cmark 的 Options::all() 已含 ENABLE_MATH,无需改 options。 新增 5 个端到端测试(内联/块级/标题内/坏公式不破坏全文)。 双 target 编译通过,16 个 markdown 测试全过。 --- src/api/markdown.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/api/markdown.rs b/src/api/markdown.rs index 7327361..db80d4e 100644 --- a/src/api/markdown.rs +++ b/src/api/markdown.rs @@ -211,6 +211,26 @@ pub fn render_markdown_enhanced(md: &str) -> RenderedContent { html.push_str(""); in_codeblock = false; } + Event::InlineMath(tex) => { + // 先刷出累积的普通事件,再注入 KaTeX 渲染结果。 + if !non_heading_events.is_empty() { + pulldown_cmark::html::push_html(&mut html, non_heading_events.into_iter()); + non_heading_events = Vec::new(); + } + // 标题内的公式按内联渲染(标题不应用块级公式)。 + html.push_str(&crate::api::katex::render_inline(&tex)); + } + Event::DisplayMath(tex) => { + if !non_heading_events.is_empty() { + pulldown_cmark::html::push_html(&mut html, non_heading_events.into_iter()); + non_heading_events = Vec::new(); + } + // 块级公式独占一行:用

包裹 KaTeX 输出。 + // KaTeX 自身已产出 .katex-display,外层

负责段间距与居中容器。 + html.push_str("

"); + html.push_str(&crate::api::katex::render_display(&tex)); + html.push_str("

"); + } _ => { if in_heading { // 标题内部只保留文本与行内代码,避免嵌套块级元素。 @@ -823,4 +843,68 @@ console.log(1) assert!(result.html.contains("未完成")); assert!(result.html.contains("已完成")); } + + #[test] + fn render_markdown_inline_math() { + // $...$ 内联公式:pulldown-cmark (ENABLE_MATH) 解析 → katex 渲染成 span。 + // sanitizer 放行 span 的 class/style,KaTeX 输出应原样保留。 + let result = render_markdown_enhanced("公式 $E = mc^2$ 很重要"); + assert!( + result.html.contains("katex"), + "内联公式应渲染为 katex span, got: {}", + result.html + ); + // 前后文本保留 + assert!(result.html.contains("公式")); + assert!(result.html.contains("很重要")); + } + + #[test] + fn render_markdown_display_math() { + // $$...$$ 块级公式:应产出

+ katex-display。 + let result = render_markdown_enhanced("$$\\frac{a}{b}$$"); + assert!( + result.html.contains("math-display"), + "块级公式应用 math-display 段落包裹, got: {}", + result.html + ); + assert!( + result.html.contains("katex-display"), + "KaTeX 块级输出应含 katex-display, got: {}", + result.html + ); + } + + #[test] + fn render_markdown_inline_math_in_heading() { + // 标题内的 $...$ 按内联公式渲染(不应崩,也不应产生块级

)。 + let result = render_markdown_enhanced("## 勾股 $a^2 + b^2 = c^2$ 定理"); + assert!( + result.html.contains("katex"), + "标题内联公式应渲染, got: {}", + result.html + ); + // 标题里不应出现块级公式的

+ assert!( + !result.html.contains("math-display"), + "标题内不应有块级公式包裹, got: {}", + result.html + ); + } + + #[test] + fn render_markdown_bad_math_does_not_break() { + // 坏 TeX 不应中断整篇渲染:throw_on_error=false 回退到错误 span。 + let result = render_markdown_enhanced("正常文本 $\\undefinedmacro{$ 后续文本"); + assert!( + result.html.contains("正常文本"), + "坏公式不应破坏前文, got: {}", + result.html + ); + assert!( + result.html.contains("后续文本"), + "坏公式不应破坏后文, got: {}", + result.html + ); + } }