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