Skip to main content

markdown_it/plugins/extra/
math.rs

1// reference to exist CodeFence & CodeSpan rule in the code base
2
3use crate::{
4    parser::{
5        block::{BlockRule, BlockState},
6        inline::{InlineRule, InlineState},
7    },
8    MarkdownIt, Node, NodeValue, Renderer,
9};
10
11#[derive(Debug)]
12struct MathBlock {
13    pub content: String,
14}
15
16impl NodeValue for MathBlock {
17    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
18        #[cfg(not(feature = "katex"))]
19        {
20            let mut attrs = node.attrs.clone();
21            attrs.push(("class", "math-block".into()));
22
23            fmt.cr();
24            fmt.open("div", &attrs);
25            fmt.text(&self.content);
26            fmt.close("div");
27            fmt.cr();
28        }
29
30        #[cfg(feature = "katex")]
31        {
32            let mut attrs = node.attrs.clone();
33            attrs.push(("class", "math-block".into()));
34            fmt.cr();
35            fmt.open("div", &attrs);
36
37            // render katex
38            let ctx = katex::KatexContext::default();
39            let setting = katex::Settings::default();
40            match katex::render_to_string(&ctx, &self.content, &setting) {
41                Ok(html) => fmt.text_raw(&html),
42                Err(_) => fmt.text(&self.content),
43            }
44
45            fmt.close("div");
46            fmt.cr();
47        }
48    }
49}
50
51#[doc(hidden)]
52pub struct MathBlockScanner;
53
54impl MathBlockScanner {
55    fn get_header<'a>(state: &'a mut BlockState) -> Option<&'a str> {
56        if state.line_indent(state.line) >= state.md.max_indent {
57            return None;
58        }
59
60        let line = state.get_line(state.line);
61        let trimmed = line.trim_end();
62        if trimmed != "$$" {
63            return None;
64        }
65
66        Some(trimmed)
67    }
68}
69
70impl BlockRule for MathBlockScanner {
71    fn check(state: &mut BlockState) -> Option<()> {
72        Self::get_header(state).map(|_| ())
73    }
74
75    fn run(state: &mut BlockState) -> Option<(Node, usize)> {
76        Self::get_header(state)?;
77
78        let mut next_line = state.line;
79        let mut have_end_marker = false;
80
81        loop {
82            next_line += 1;
83            if next_line >= state.line_max {
84                break;
85            }
86
87            let line = state.get_line(next_line);
88            let trimmed = line.trim();
89            if !line.is_empty() && state.line_indent(next_line) < 0 {
90                break;
91            }
92            if trimmed == "$$" {
93                have_end_marker = true;
94                break;
95            }
96        }
97
98        let indent = state.line_offsets[state.line].indent_nonspace;
99        let (content, _) = state.get_lines(state.line + 1, next_line, indent as usize, false);
100
101        Some((
102            Node::new(MathBlock {
103                content: content.trim().to_owned(),
104            }),
105            next_line - state.line + if have_end_marker { 1 } else { 0 },
106        ))
107    }
108}
109
110#[derive(Debug)]
111struct MathInline {
112    pub content: String,
113}
114
115impl NodeValue for MathInline {
116    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
117        #[cfg(not(feature = "katex"))]
118        {
119            let mut attrs = node.attrs.clone();
120            attrs.push(("class", "math-inline".into()));
121            fmt.open("span", &attrs);
122            fmt.text(&self.content);
123            fmt.close("span");
124        }
125
126        #[cfg(feature = "katex")]
127        {
128            let mut attrs = node.attrs.clone();
129            attrs.push(("class", "math-inline".into()));
130            fmt.open("span", &attrs);
131
132            let ctx = katex::KatexContext::default();
133            let mut setting = katex::Settings::default();
134            setting.display_mode = false;
135            match katex::render_to_string(&ctx, &self.content, &setting) {
136                Ok(html) => fmt.text_raw(&html),
137                Err(_) => fmt.text(&self.content),
138            }
139
140            fmt.close("span");
141        }
142    }
143}
144
145#[doc(hidden)]
146pub struct MathInlineScanner;
147
148impl InlineRule for MathInlineScanner {
149    const MARKER: char = '$';
150
151    fn run(state: &mut InlineState) -> Option<(Node, usize)> {
152        let mut char = state.src[state.pos..state.pos_max].chars();
153        if char.next()? != '$' {
154            return None;
155        }
156
157        let mut pos = state.pos + 1;
158        while pos < state.pos_max {
159            if state.src.as_bytes()[pos] == b'$' {
160                if state.src.as_bytes()[pos - 1] == b'\\' {
161                    pos += 1;
162                    continue;
163                }
164
165                let content = &state.src[state.pos + 1..pos];
166                if content.is_empty() {
167                    pos += 1;
168                    continue;
169                }
170
171                // $ something$ or $something $
172                if content.starts_with(|c: char| c.is_whitespace())
173                    || content.ends_with(|c: char| c.is_whitespace())
174                {
175                    pos += 1;
176                    continue;
177                }
178
179                // $20
180                if pos + 1 < state.pos_max && state.src.as_bytes()[pos + 1].is_ascii_digit() {
181                    pos += 1;
182                    continue;
183                }
184
185                let mut node = Node::new(MathInline {
186                    content: content.to_owned(),
187                });
188                node.srcmap = state.get_map(state.pos, pos + 1);
189                return Some((node, pos - state.pos + 1));
190            }
191
192            pos += 1;
193        }
194
195        None
196    }
197}
198
199pub fn add(md: &mut MarkdownIt) {
200    md.block.add_rule::<MathBlockScanner>();
201    md.inline.add_rule::<MathInlineScanner>();
202}