markdown_it/plugins/extra/
math.rs1use 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 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 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 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}