1#[derive(Debug, Clone)]
12pub struct MathPlaceholder {
13 pub index: usize,
14 pub source: String,
15 pub is_block: bool,
17}
18
19const PLACEHOLDER_PREFIX: &str = "MATH_PLACEHOLDER_";
20
21pub fn extract_math(source: &str) -> (String, Vec<MathPlaceholder>) {
26 let mut placeholders: Vec<MathPlaceholder> = Vec::new();
27 let mut output = String::with_capacity(source.len());
28 let chars: Vec<char> = source.chars().collect();
29 let len = chars.len();
30 let mut i = 0;
31
32 while i < len {
33 if i + 1 < len && chars[i] == '$' && chars[i + 1] == '$' {
35 if let Some(end) = find_closing(&chars, i + 2, "$$") {
36 let math_src: String = chars[i + 2..end].iter().collect();
37 let idx = placeholders.len();
38 placeholders.push(MathPlaceholder {
39 index: idx,
40 source: math_src,
41 is_block: true,
42 });
43 output.push_str(&format!("{PLACEHOLDER_PREFIX}{idx}"));
44 i = end + 2; continue;
46 }
47 }
48 if chars[i] == '$' {
50 if let Some(end) = find_closing(&chars, i + 1, "$") {
51 let math_src: String = chars[i + 1..end].iter().collect();
52 if !math_src.is_empty() && !math_src.contains('\n') {
54 let idx = placeholders.len();
55 placeholders.push(MathPlaceholder {
56 index: idx,
57 source: math_src,
58 is_block: false,
59 });
60 output.push_str(&format!("{PLACEHOLDER_PREFIX}{idx}"));
61 i = end + 1; continue;
63 }
64 }
65 }
66 output.push(chars[i]);
67 i += 1;
68 }
69
70 (output, placeholders)
71}
72
73fn find_closing(chars: &[char], start: usize, closing: &str) -> Option<usize> {
76 let closing_chars: Vec<char> = closing.chars().collect();
77 let clen = closing_chars.len();
78 let len = chars.len();
79 let mut i = start;
80 while i + clen <= len {
81 if chars[i..i + clen] == closing_chars[..] {
82 return Some(i);
83 }
84 i += 1;
85 }
86 None
87}
88
89pub fn inject_math(html: &str, placeholders: &[MathPlaceholder]) -> String {
91 let mut output = html.to_owned();
92 for ph in placeholders {
93 let token = format!("{PLACEHOLDER_PREFIX}{}", ph.index);
94 let escaped = html_escape(&ph.source);
95 let replacement = if ph.is_block {
96 format!(
97 r#"<div class="math-block" data-math="{escaped}">{source}</div>"#,
98 source = ph.source
99 )
100 } else {
101 format!(
102 r#"<span class="math-inline" data-math="{escaped}">{source}</span>"#,
103 source = ph.source
104 )
105 };
106 output = output.replace(&token, &replacement);
107 }
108 output
109}
110
111fn html_escape(s: &str) -> String {
113 s.replace('&', "&")
114 .replace('"', """)
115 .replace('<', "<")
116 .replace('>', ">")
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_math_inline_extracted() {
125 let (processed, phs) = extract_math("Here is $x^2$ inline.");
126 assert_eq!(phs.len(), 1);
127 assert!(!phs[0].is_block);
128 assert_eq!(phs[0].source, "x^2");
129 assert!(processed.contains("MATH_PLACEHOLDER_0"));
130
131 let html = inject_math(&processed, &phs);
132 assert!(html.contains(r#"class="math-inline""#));
133 assert!(html.contains("x^2"));
134 }
135
136 #[test]
137 fn test_math_block_extracted() {
138 let (processed, phs) = extract_math("$$\\int f$$");
139 assert_eq!(phs.len(), 1);
140 assert!(phs[0].is_block);
141 assert_eq!(phs[0].source, "\\int f");
142 assert!(processed.contains("MATH_PLACEHOLDER_0"));
143
144 let html = inject_math(&processed, &phs);
145 assert!(html.contains(r#"class="math-block""#));
146 assert!(html.contains("\\int f"));
147 }
148
149 #[test]
150 fn test_no_math_passthrough() {
151 let (processed, phs) = extract_math("No math here.");
152 assert!(phs.is_empty());
153 assert_eq!(processed, "No math here.");
154 }
155
156 #[test]
157 fn test_math_escape_in_attr() {
158 let (processed, phs) = extract_math(r#"$a < b$"#);
159 assert_eq!(phs.len(), 1);
160 let html = inject_math(&processed, &phs);
161 assert!(html.contains("<"));
162 }
163}