Skip to main content

nargo_document/plugin/
katex.rs

1//! KaTeX 数学公式渲染插件
2//!
3//! 提供对 Markdown 中 LaTeX 数学公式的支持,包括行内公式 `$...$` 和块级公式 `$$...$$`
4
5use crate::plugin::{DocumentPlugin, PluginContext, PluginMeta};
6use lazy_static::lazy_static;
7use regex::Regex;
8
9lazy_static! {
10    /// 匹配块级公式 `$$...$$` 的正则表达式
11    static ref BLOCK_MATH_RE: Regex = Regex::new(r"\$\$([\s\S]*?)\$\$").unwrap();
12    /// 匹配行内公式 `$...$` 的正则表达式(简化版本,不使用 look-around)
13    static ref INLINE_MATH_RE: Regex = Regex::new(r"\$([^$\n]+?)\$").unwrap();
14}
15
16/// KaTeX 数学公式渲染插件
17pub struct KaTeXPlugin {
18    /// 插件元数据
19    meta: PluginMeta,
20}
21
22impl KaTeXPlugin {
23    /// 创建新的 KaTeX 插件实例
24    pub fn new() -> Self {
25        Self { meta: PluginMeta::new("nargo-document-plugin-katex".to_string(), "0.1.0".to_string(), "KaTeX 数学公式渲染插件,支持行内公式和块级公式".to_string()) }
26    }
27
28    /// 处理块级公式,将 `$$...$$` 替换为 `<div class="katex-block">...</div>`
29    ///
30    /// # Arguments
31    ///
32    /// * `content` - 包含数学公式的文本内容
33    ///
34    /// # Returns
35    ///
36    /// 替换后的文本内容
37    fn process_block_math(&self, content: &str) -> String {
38        BLOCK_MATH_RE
39            .replace_all(content, |caps: &regex::Captures| {
40                let math = &caps[1];
41                format!("<div class=\"katex-block\">{}</div>", math.trim())
42            })
43            .to_string()
44    }
45
46    /// 处理行内公式,将 `$...$` 替换为 `<span class="katex-inline">...</span>`
47    ///
48    /// # Arguments
49    ///
50    /// * `content` - 包含数学公式的文本内容
51    ///
52    /// # Returns
53    ///
54    /// 替换后的文本内容
55    fn process_inline_math(&self, content: &str) -> String {
56        let mut result = String::new();
57        let mut chars = content.chars().peekable();
58        let mut in_math = false;
59        let mut current_math = String::new();
60
61        while let Some(c) = chars.next() {
62            if c == '$' {
63                if let Some(&next) = chars.peek() {
64                    if next == '$' {
65                        chars.next();
66                        if in_math {
67                            result.push_str("$$");
68                            result.push_str(&current_math);
69                            result.push_str("$$");
70                            in_math = false;
71                            current_math.clear();
72                        }
73                        else {
74                            result.push_str("$$");
75                        }
76                        continue;
77                    }
78                }
79
80                if in_math {
81                    result.push_str(&format!("<span class=\"katex-inline\">{}</span>", current_math));
82                    in_math = false;
83                    current_math.clear();
84                }
85                else {
86                    in_math = true;
87                }
88            }
89            else {
90                if in_math {
91                    current_math.push(c);
92                }
93                else {
94                    result.push(c);
95                }
96            }
97        }
98
99        if in_math {
100            result.push('$');
101            result.push_str(&current_math);
102        }
103
104        result
105    }
106}
107
108impl Default for KaTeXPlugin {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114impl DocumentPlugin for KaTeXPlugin {
115    /// 获取插件元数据
116    fn meta(&self) -> &PluginMeta {
117        &self.meta
118    }
119
120    /// 渲染前钩子,在 Markdown 解析后、HTML 渲染前处理数学公式
121    ///
122    /// # Arguments
123    ///
124    /// * `context` - 插件上下文,包含文档内容等信息
125    ///
126    /// # Returns
127    ///
128    /// 处理后的插件上下文
129    fn before_render(&self, context: PluginContext) -> PluginContext {
130        let mut content = context.content;
131
132        content = self.process_block_math(&content);
133        content = self.process_inline_math(&content);
134
135        PluginContext { content, frontmatter: context.frontmatter, path: context.path }
136    }
137}