ratex_parser/mhchem/
mod.rs1mod actions;
7mod buffer;
8mod data;
9mod engine;
10mod error;
11mod json;
12mod patterns;
13mod texify;
14
15pub use data::data;
16pub use error::{MhchemError, MhchemResult};
17
18use crate::mhchem::data::MhchemData;
19use crate::stack_safety::{DepthBudget, MAX_INPUT_DEPTH};
20use serde_json::Value;
21
22pub struct ParserCtx<'a> {
32 pub data: &'a MhchemData,
33}
34
35impl ParserCtx<'_> {
36 pub fn go(&self, input: &str, machine: &str) -> MhchemResult<Vec<Value>> {
37 RuntimeCtx {
38 data: self.data,
39 depth_budget: DepthBudget::new(MAX_INPUT_DEPTH),
40 }
41 .go(input, machine)
42 }
43}
44
45pub(crate) struct RuntimeCtx<'a> {
48 pub(crate) data: &'a MhchemData,
49 pub depth_budget: DepthBudget,
50}
51
52impl RuntimeCtx<'_> {
53 pub(crate) fn go(&self, input: &str, machine: &str) -> MhchemResult<Vec<Value>> {
54 engine::go_machine(self, input, machine)
55 }
56}
57
58pub fn chem_parse_str(input: &str, mode: &str) -> MhchemResult<String> {
60 chem_parse_str_with_budget(input, mode, DepthBudget::new(MAX_INPUT_DEPTH))
61}
62
63pub(crate) fn chem_parse_str_with_budget(
64 input: &str,
65 mode: &str,
66 depth_budget: DepthBudget,
67) -> MhchemResult<String> {
68 let d = data();
69 let ctx = RuntimeCtx {
70 data: d,
71 depth_budget: depth_budget.clone(),
72 };
73 let sm = match mode {
74 "ce" => "ce",
75 "pu" => "pu",
76 _ => {
77 return Err(MhchemError::msg(format!(
78 "unknown mhchem mode (expected ce|pu): {mode}"
79 )));
80 }
81 };
82 let ast = ctx.go(input.trim(), sm)?;
83 texify::go_with_budget(&ast, false, &depth_budget)
84}
85
86pub fn mhchem_arg_tokens_to_string(tokens: &[ratex_lexer::token::Token]) -> String {
88 if tokens.is_empty() {
89 return String::new();
90 }
91 let mut expected_loc = tokens.last().unwrap().loc.start;
92 let mut out = String::new();
93 for i in (0..tokens.len()).rev() {
94 let t = &tokens[i];
95 if t.loc.start > expected_loc {
96 out.push(' ');
97 expected_loc = t.loc.start;
98 }
99 out.push_str(&t.text);
100 expected_loc += t.text.len();
101 }
102 out
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn h2o_ce() {
111 let t = chem_parse_str("H2O", "ce").expect("mhchem");
112 assert!(!t.is_empty());
113 assert!(t.contains('H'));
114 }
115
116 #[test]
117 fn reaction_arrow() {
118 let t = chem_parse_str("2H + O -> H2O", "ce").expect("mhchem");
119 assert!(t.contains("rightarrow") || t.contains("->"), "{}", t);
120 }
121
122 #[test]
123 fn pu_simple() {
124 let t = chem_parse_str("123 kJ/mol", "pu").expect("mhchem");
125 assert!(!t.is_empty());
126 }
127
128 #[test]
129 fn nested_submachines_have_a_depth_budget() {
130 let nested_ce = |depth: usize| format!("{}H{}", r"\ce{".repeat(depth), "}".repeat(depth));
131
132 assert!(chem_parse_str(&nested_ce(30), "ce").is_ok());
135 let error = chem_parse_str(&nested_ce(31), "ce").unwrap_err();
136 assert!(
137 error.to_string().contains("Recursion limit exceeded"),
138 "unexpected error: {error}"
139 );
140 }
141
142 #[test]
143 fn nested_submachine_engine_depth_is_bounded_before_texify() {
144 let nested_ce = |depth: usize| format!("{}H{}", r"\ce{".repeat(depth), "}".repeat(depth));
145 let nested_empty_ce =
146 |depth: usize| format!("{}{}", r"\ce{".repeat(depth), "}".repeat(depth));
147 let d = data();
148 let ctx = RuntimeCtx {
149 data: d,
150 depth_budget: DepthBudget::new(MAX_INPUT_DEPTH),
151 };
152
153 assert!(ctx.go(&nested_ce(30), "ce").is_ok());
154 let error = ctx.go(&nested_ce(31), "ce").unwrap_err();
155 assert!(
156 error.to_string().contains("Recursion limit exceeded"),
157 "unexpected error: {error}"
158 );
159
160 assert!(ctx.go(&nested_empty_ce(31), "ce").is_ok());
161 let error = ctx.go(&nested_empty_ce(32), "ce").unwrap_err();
162 assert!(
163 error.to_string().contains("Recursion limit exceeded"),
164 "unexpected error: {error}"
165 );
166 }
167
168 #[test]
169 fn pu_scientific_lowercase_e_cdot_uppercase_e_times() {
170 for src in ["1.2e3 kJ", "1,2e3 kJ"] {
171 let t = chem_parse_str(src, "pu").expect("mhchem");
172 assert!(
173 t.contains("\\cdot") && t.contains("10^{3}") && !t.contains("\\times"),
174 "expected \\cdot for lowercase e: {src:?} → {t:?}"
175 );
176 }
177 for src in ["1.2E3 kJ", "1,2E3 kJ"] {
178 let t = chem_parse_str(src, "pu").expect("mhchem");
179 assert!(
180 t.contains("\\times") && t.contains("10^{3}") && !t.contains("\\cdot"),
181 "expected \\times for uppercase E: {src:?} → {t:?}"
182 );
183 }
184 }
185
186 #[test]
187 fn dollar_underset_inner_ce_tex_is_valid_latex() {
188 let inner = r"$\underset{\mathrm{red}}{\ce{HgI2}}$";
189 let tex = chem_parse_str(inner, "ce").expect("mhchem");
190 crate::parser::parse(&tex).expect("mhchem TeX should parse");
191 }
192}