1#![cfg(feature = "server")]
2
3use fancy_regex::Regex;
26use std::collections::HashMap;
27use std::sync::LazyLock;
28
29#[derive(Clone, Debug)]
35enum MVal {
36 S(String),
37 V(Vec<String>),
38}
39
40#[derive(Clone, Debug)]
42struct MMatch {
43 m: MVal,
44 remainder: String,
45}
46
47#[derive(Clone, Debug)]
49enum Field {
50 Str(String),
51 Nodes(Vec<Parsed>),
52}
53
54#[allow(clippy::large_enum_variant)] #[derive(Clone, Debug)]
57enum Parsed {
58 S(String),
59 N(NodeData),
60}
61
62#[derive(Clone, Debug, Default)]
63struct NodeData {
64 type_: String,
65 p1: Option<Field>,
66 p2: Option<Field>,
67 a: Option<Field>,
68 b: Option<Field>,
69 p: Option<Field>,
70 o: Option<Field>,
71 q: Option<Field>,
72 d: Option<Field>,
73 color: Option<String>,
74 color1: Option<String>,
75 color2: Option<Field>,
76 r: Option<String>,
77 rd: Option<Field>,
78 rq: Option<Field>,
79 d_type: Option<String>,
80 kind_: Option<String>,
81}
82
83#[derive(Clone, Default)]
85struct Buffer {
86 a: Option<String>,
87 b: Option<String>,
88 p: Option<String>,
89 o: Option<String>,
90 q: Option<String>,
91 d: Option<String>,
92 rm: Option<String>,
93 text_: Option<String>,
94 r: Option<String>,
95 rd: Option<String>,
96 rq: Option<String>,
97 rdt: Option<String>,
98 rqt: Option<String>,
99 d_type: Option<String>,
100 sb: bool,
101 begins_with_bond: bool,
102 parenthesis_level: i32,
103}
104
105impl Buffer {
106 fn clear(&mut self, keep: bool) {
109 let (pl, bb) = if keep {
110 (self.parenthesis_level, self.begins_with_bond)
111 } else {
112 (0, false)
113 };
114 *self = Buffer {
115 parenthesis_level: pl,
116 begins_with_bond: bb,
117 ..Buffer::default()
118 };
119 }
120}
121
122#[derive(Clone)]
127struct ActionRef {
128 type_: String,
129 option: Option<String>,
130}
131
132#[derive(Clone, Default)]
133struct Task {
134 actions: Vec<ActionRef>,
135 next_state: Option<String>,
136 revisit: bool,
137 to_continue: bool,
138}
139
140#[derive(Clone)]
141struct Transition {
142 pattern: String,
143 task: Task,
144}
145
146struct RawEntry {
148 patterns: &'static str,
149 states: &'static str,
150 task: Task,
151}
152
153fn build_transitions(raw: &[RawEntry]) -> HashMap<String, Vec<Transition>> {
156 let mut transitions: HashMap<String, Vec<Transition>> = HashMap::new();
157 for e in raw {
159 for state in e.states.split('|') {
160 transitions.entry(state.to_string()).or_default();
161 }
162 }
163 let all_states: Vec<String> = transitions.keys().cloned().collect();
165 for e in raw {
166 let state_list: Vec<&str> = e.states.split('|').collect();
167 for (idx, _state) in state_list.iter().enumerate() {
168 for pattern in e.patterns.split('|') {
169 let insert_states: Vec<String> = if state_list[idx] == "*" {
170 all_states.clone()
171 } else {
172 vec![state_list[idx].to_string()]
173 };
174 for s in insert_states {
175 transitions.entry(s).or_default().push(Transition {
176 pattern: pattern.to_string(),
177 task: e.task.clone(),
178 });
179 }
180 }
181 }
182 }
183 transitions
184}
185
186fn compile_or_log(pat: &str) -> Option<Regex> {
197 match Regex::new(pat) {
198 Ok(re) => Some(re),
199 Err(e) => {
200 tracing::error!(target: "yggdrasil::mhchem", pattern = pat, error = ?e, "mhchem 正则编译失败,该模式将降级为不匹配");
201 None
202 }
203 }
204}
205
206struct CompiledPat(Option<Regex>);
212
213impl CompiledPat {
214 #[inline]
220 fn captures_head<'s>(&'s self, input: &'s str) -> Option<fancy_regex::Captures<'s>> {
221 self.0.as_ref()?.captures(input).ok().flatten()
222 }
223
224 #[inline]
226 fn is_match(&self, input: &str) -> bool {
227 self.0
228 .as_ref()
229 .and_then(|r| r.is_match(input).ok())
230 .unwrap_or(false)
231 }
232}
233
234macro_rules! re {
236 ($pat:literal) => {{
237 static RE: LazyLock<CompiledPat> = LazyLock::new(|| CompiledPat(compile_or_log($pat)));
238 &*RE
239 }};
240}
241
242enum Pat {
244 Lit(&'static str),
245 Re(&'static CompiledPat),
246}
247
248fn pat_match_head(pat: &Pat, input: &str) -> Option<String> {
250 match pat {
251 Pat::Lit(s) => {
252 if input.starts_with(s) {
253 Some((*s).to_string())
254 } else {
255 None
256 }
257 }
258 Pat::Re(cp) => cp.captures_head(input).and_then(|c| {
259 let whole = c.get(0).map(|m| m.as_str())?;
260 if input.starts_with(whole) {
262 Some(whole.to_string())
263 } else {
264 None
265 }
266 }),
267 }
268}
269
270#[allow(clippy::too_many_arguments)]
275fn find_observe_groups(
276 input: &str,
277 beg_excl: Pat,
278 beg_incl: Pat,
279 end_incl: Pat,
280 end_excl: Pat,
281 beg2_excl: Option<Pat>,
282 beg2_incl: Option<Pat>,
283 end2_incl: Option<Pat>,
284 end2_excl: Option<Pat>,
285 combine: bool,
286) -> Option<MMatch> {
287 let m0 = pat_match_head(&beg_excl, input)?;
289 let rest0 = &input[m0.len()..];
290 let m1 = pat_match_head(&beg_incl, rest0)?;
291 let end_is_incl = !is_empty_pat(&end_incl);
293 let end_chars = if end_is_incl { &end_incl } else { &end_excl };
294 let e = find_observe_end(rest0, m1.len(), end_chars)?;
295 let body_end = if end_is_incl { e.end } else { e.begin };
296 let match1 = rest0[..body_end].to_string();
297 let after1 = &rest0[e.end..];
298 if beg2_excl.is_none() && beg2_incl.is_none() {
300 return Some(MMatch {
301 m: MVal::S(match1),
302 remainder: after1.to_string(),
303 });
304 }
305 let g2 = find_observe_groups(
307 after1,
308 beg2_excl.unwrap_or(Pat::Lit("")),
309 beg2_incl.unwrap_or(Pat::Lit("")),
310 end2_incl.unwrap_or(Pat::Lit("")),
311 end2_excl.unwrap_or(Pat::Lit("")),
312 None,
313 None,
314 None,
315 None,
316 false,
317 )?;
318 let mval = match g2.m {
319 MVal::S(s2) => {
320 if combine {
321 MVal::S(format!("{}{}", match1, s2))
322 } else {
323 MVal::V(vec![match1, s2])
324 }
325 }
326 MVal::V(_) => MVal::V(vec![match1]), };
328 Some(MMatch {
329 m: mval,
330 remainder: g2.remainder,
331 })
332}
333
334fn is_empty_pat(p: &Pat) -> bool {
335 matches!(p, Pat::Lit(""))
336}
337
338struct Span {
339 begin: usize,
340 end: usize,
341}
342
343fn find_observe_end(input: &str, start: usize, end_chars: &Pat) -> Option<Span> {
351 let mut braces = 0i32;
352 let start = input.get(start..).map(|_| start).unwrap_or(input.len());
355 for (i, a) in input.char_indices().skip_while(|(b, _)| *b < start) {
356 if braces == 0 {
358 if let Some(matched) = pat_match_head(end_chars, &input[i..]) {
359 return Some(Span {
360 begin: i,
361 end: i + matched.len(),
362 });
363 }
364 }
365 if a == '{' {
366 braces += 1;
367 } else if a == '}' {
368 if braces == 0 {
369 return None;
371 }
372 braces -= 1;
373 }
374 }
375 None
376}
377
378fn fancy_match(re: &Regex, input: &str) -> Option<MMatch> {
380 let caps = re.captures(input).ok()??;
381 let whole = caps.get(0)?.as_str().to_string();
382 if !input.starts_with(&whole) {
383 return None;
384 }
385 let n_groups = re.captures_len();
386 let mval = if n_groups >= 2 {
387 let mut v = Vec::with_capacity(n_groups);
388 for gi in 1..=n_groups {
389 v.push(
390 caps.get(gi)
391 .map(|m| m.as_str().to_string())
392 .unwrap_or_default(),
393 );
394 }
395 MVal::V(v)
396 } else {
397 let g1 = caps
399 .get(1)
400 .map(|m| m.as_str().to_string())
401 .filter(|s| !s.is_empty());
402 MVal::S(g1.unwrap_or(whole.clone()))
403 };
404 Some(MMatch {
405 m: mval,
406 remainder: input[whole.len()..].to_string(),
407 })
408}
409
410static GREEK_NAMES: &str = "alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega|Gamma|Delta|Theta|Lambda|Xi|Pi|Sigma|Upsilon|Phi|Psi|Omega";
412
413static LETTERS_RE: LazyLock<CompiledPat> = LazyLock::new(|| {
414 CompiledPat(compile_or_log(&format!(
415 "^(?:[a-zA-Z\u{03B1}-\u{03C9}\u{0391}-\u{03A9}?@]|(?:\\\\(?:{})(?:\\s+|\\{{\\}}|(?![a-zA-Z]))))+",
416 GREEK_NAMES
417 )))
418});
419
420static GREEK_RE: LazyLock<CompiledPat> = LazyLock::new(|| {
421 CompiledPat(compile_or_log(&format!(
422 "^\\\\(?:{})(?:\\s+|\\{{\\}}|(?![a-zA-Z]))",
423 GREEK_NAMES
424 )))
425});
426
427static ONE_GREEK_RE: LazyLock<CompiledPat> = LazyLock::new(|| {
428 CompiledPat(compile_or_log(
429 "^(?:\\$?[\u{03B1}-\u{03C9}]\\$?|\\$?\\\\(?:alpha|beta|gamma|delta|epsilon|zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|omicron|pi|rho|sigma|tau|upsilon|phi|chi|psi|omega)\\s*\\$?)(?:\\s+|\\{{\\}}|(?![a-zA-Z]))$"
430 ))
431});
432
433fn match_pattern(name: &str, input: &str) -> Option<MMatch> {
435 let re_match = |cp: &CompiledPat| cp.0.as_ref().and_then(|r| fancy_match(r, input));
439 match name {
440 "empty" => re_match(re!("^$")),
441 "else" | "else2" => re_match(re!("^.")),
442 "space" => re_match(re!("^\\s")),
443 "space A" => re_match(re!("^\\s(?=[A-Z\\\\$])")),
444 "space$" => re_match(re!("^\\s$")),
445 "a-z" => re_match(re!("^[a-z]")),
446 "x" => re_match(re!("^x")),
447 "x$" => re_match(re!("^x$")),
448 "i$" => re_match(re!("^i$")),
449 "letters" => re_match(&LETTERS_RE),
450 "\\greek" => re_match(&GREEK_RE),
451 "one lowercase latin letter $" => re_match(re!("^(?:([a-z])(?:$|[^a-zA-Z]))$")),
452 "$one lowercase latin letter$ $" => re_match(re!("^\\$(?:([a-z])(?:$|[^a-zA-Z]))\\$$")),
453 "one lowercase greek letter $" => re_match(&ONE_GREEK_RE),
454 "digits" => re_match(re!("^[0-9]+")),
455 "-9.,9" => re_match(re!("^[+\\-]?(?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))")),
456 "-9.,9 no missing 0" => re_match(re!("^[+\\-]?[0-9]+(?:[.,][0-9]+)?")),
457 "(-)(9)^(-9)" => re_match(re!("^(\\+\\-|\\+\\/\\-|\\+|\\-|\\\\pm\\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+)?)\\^([+\\-]?[0-9]+|\\{[+\\-]?[0-9]+\\})")),
458 "_{(state of aggregation)}$" => re_match(re!("^_\\{(\\([a-z]{1,3}\\))\\}")),
459 "{[(" => re_match(re!("^(?:\\\\\\{|\\[|\\()")),
460 ")]}" => re_match(re!("^(?:\\)|\\]|\\\\\\})")),
461 ", " => re_match(re!("^[,;]\\s*")),
462 "," => re_match(re!("^[,;]")),
463 "." => re_match(re!("^[.]")),
464 ". __* " => re_match(re!("^([.\u{22C5}\u{00B7}\u{2022}]|[*])\\s*")),
465 "..." => re_match(re!("^\\.\\.\\.(?=$|[^.])")),
466 "^a" => re_match(re!("^\\^([0-9]+|[^\\\\_])")),
467 "^\\x" => re_match(re!("^\\^(\\\\[a-zA-Z]+)\\s*")),
468 "^(-1)" => re_match(re!("^\\^(-?\\d+)")),
469 "'" => re_match(re!("^'")),
470 "_9" => re_match(re!("^_([+\\-]?[0-9]+|[^\\\\])")),
471 "_\\x" => re_match(re!("^_(\\\\[a-zA-Z]+)\\s*")),
472 "^_" => re_match(re!("^(?:\\^(?=_)|\\_(?=\\^)|[\\^_]$)")),
473 "{}^" => re_match(re!("^\\{\\}(?=\\^)")),
474 "{}" => re_match(re!("^\\{\\}")),
475 "=<>" => re_match(re!("^[=<>]")),
476 "#" => re_match(re!("^[#\u{2261}]")),
477 "+" => re_match(re!("^\\+")),
478 "-$" => re_match(re!("^-(?=[\\s_},;\\]/]|$|\\([a-z]+\\))")),
479 "-9" => re_match(re!("^-(?=[0-9])")),
480 "- orbital overlap" => re_match(re!("^-(?=(?:[spd]|sp)(?:$|[\\s,;\\)\\]\\}]))")),
481 "-" => re_match(re!("^-")),
482 "pm-operator" => re_match(re!("^(?:\\\\pm|\\$\\\\pm\\$|\\+-|\\+\\/-)")),
483 "operator" => re_match(re!("^(?:\\+|(?:[\\-=<>]|<<|>>|\\\\approx|\\$\\\\approx\\$)(?=\\s|$|-?[0-9]))")),
484 "arrowUpDown" => re_match(re!("^(?:v|\\(v\\)|\\^|\\(\\^\\))(?=$|[\\s,;\\)\\]\\}])")),
485 "->" => re_match(re!("^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u{2192}\u{27F6}\u{21CC}])")),
486 "CMT" => re_match(re!("^[CMT](?=\\[)")),
487 "1st-level escape" => re_match(re!("^(&|\\\\\\\\|\\\\hline)\\s*")),
488 "\\," => re_match(re!("^(?:\\\\[,\\ ;:])")),
489 "\\ca" => re_match(re!("^\\\\ca(?:\\s+|(?![a-zA-Z]))")),
490 "\\x" => re_match(re!("^(?:\\\\[a-zA-Z]+\\s*|\\\\[_&{}%])")),
491 "orbital" => re_match(re!("^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])")),
492 "others" => re_match(re!("^[/~|]")),
493 "oxidation$" => re_match(re!("^(?:[+-][IVX]+|(?:\\\\pm|\\$\\\\pm\\$|\\+-|\\+\\/-)\\s*0)$")),
494 "d-oxidation$" => re_match(re!("^(?:[+-]?[IVX]+|(?:\\\\pm|\\$\\\\pm\\$|\\+-|\\+\\/-)\\s*0)$")),
495 "1/2$" => re_match(re!("^[+\\-]?(?:[0-9]+|\\$[a-z]\\$|[a-z])\\/[0-9]+(?:\\$[a-z]\\$|[a-z])?$")),
496 "(KV letters)," => re_match(re!("^(?:[A-Z][a-z]{0,2}|i)(?=,)")),
497 "uprightEntities" => re_match(re!("^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])")),
498 "/" => re_match(re!("^\\s*(\\/)\\s*")),
499 "//" => re_match(re!("^\\s*(\\/\\/)\\s*")),
500 "*" => re_match(re!("^\\s*[*.]\\s*")),
501 "(-)(9.,9)(e)(99)" => pat_enumber(input),
503 "state of aggregation $" => pat_state_of_aggregation(input),
504 "^{(...)}" => fg(input, Pat::Lit("^{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false),
505 "^($...$)" => fg(input, Pat::Lit("^"), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false),
506 "^\\x{}{}" => fg(input, Pat::Lit("^"), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), Some(Pat::Lit("")), Some(Pat::Lit("{")), Some(Pat::Lit("}")), Some(Pat::Lit("")), true),
507 "^\\x{}" => fg(input, Pat::Lit("^"), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false),
508 "\\bond{(...)}" => fg(input, Pat::Lit("\\bond{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false),
509 "[(...)]" => fg(input, Pat::Lit("["), Pat::Lit(""), Pat::Lit(""), Pat::Lit("]"), None, None, None, None, false),
510 "\\x{}{}" => fg(input, Pat::Lit(""), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), Some(Pat::Lit("")), Some(Pat::Lit("{")), Some(Pat::Lit("}")), Some(Pat::Lit("")), true),
511 "\\x{}" => fg(input, Pat::Lit(""), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false),
512 "\\frac{(...)}" => fg(input, Pat::Lit("\\frac{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), Some(Pat::Lit("{")), Some(Pat::Lit("")), Some(Pat::Lit("")), Some(Pat::Lit("}")), false),
513 "\\overset{(...)}" => fg(input, Pat::Lit("\\overset{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), Some(Pat::Lit("{")), Some(Pat::Lit("")), Some(Pat::Lit("")), Some(Pat::Lit("}")), false),
514 "\\underset{(...)}" => fg(input, Pat::Lit("\\underset{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), Some(Pat::Lit("{")), Some(Pat::Lit("")), Some(Pat::Lit("")), Some(Pat::Lit("}")), false),
515 "\\underbrace{(...)}" => fg(input, Pat::Lit("\\underbrace{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), Some(Pat::Lit("_")), Some(Pat::Lit("{")), Some(Pat::Lit("")), Some(Pat::Lit("}")), false),
516 "\\color{(...)}" => fg(input, Pat::Lit("\\color{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false),
517 "\\color{(...)}{(...)}" => fg(input, Pat::Lit("\\color{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), Some(Pat::Lit("{")), Some(Pat::Lit("")), Some(Pat::Lit("")), Some(Pat::Lit("}")), false),
518 "\\ce{(...)}" => fg(input, Pat::Lit("\\ce{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false),
519 "\\pu{(...)}" => fg(input, Pat::Lit("\\pu{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false),
520 "_{(...)}" => fg(input, Pat::Lit("_{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false),
521 "_($...$)" => fg(input, Pat::Lit("_"), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false),
522 "_\\x{}{}" => fg(input, Pat::Lit("_"), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), Some(Pat::Lit("")), Some(Pat::Lit("{")), Some(Pat::Lit("}")), Some(Pat::Lit("")), true),
523 "_\\x{}" => fg(input, Pat::Lit("_"), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false),
524 "{...}" => fg(input, Pat::Lit(""), Pat::Lit("{"), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false),
525 "{(...)}" => fg(input, Pat::Lit("{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false),
526 "$...$" => fg(input, Pat::Lit(""), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false),
527 "${(...)}$__$(...)$" => fg(input, Pat::Lit("${"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}$"), None, None, None, None, false).or_else(|| fg(input, Pat::Lit("$"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("$"), None, None, None, None, false)),
528 "amount" | "amount2" => pat_amount(input),
529 "formula$" => pat_formula(input),
530 _ => {
531 None
533 }
534 }
535}
536
537#[allow(clippy::too_many_arguments)]
539fn fg(
540 input: &str,
541 beg_excl: Pat,
542 beg_incl: Pat,
543 end_incl: Pat,
544 end_excl: Pat,
545 beg2_excl: Option<Pat>,
546 beg2_incl: Option<Pat>,
547 end2_incl: Option<Pat>,
548 end2_excl: Option<Pat>,
549 combine: bool,
550) -> Option<MMatch> {
551 find_observe_groups(
552 input, beg_excl, beg_incl, end_incl, end_excl, beg2_excl, beg2_incl, end2_incl, end2_excl,
553 combine,
554 )
555}
556
557fn pat_enumber(input: &str) -> Option<MMatch> {
559 let re = re!("^(\\+\\-|\\+\\/\\-|\\+|\\-|\\\\pm\\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))?(\\((?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))\\))?(?:(?:([eE])|\\s*(\\*|x|\\\\times|\u{00D7})\\s*10\\^)([+\\-]?[0-9]+|\\{[+\\-]?[0-9]+\\}))?");
560 let caps = re.captures_head(input)?;
561 let whole = caps.get(0)?.as_str();
562 if whole.is_empty() || !input.starts_with(whole) {
563 return None;
564 }
565 let mut v = Vec::with_capacity(6);
566 for gi in 1..=6 {
567 v.push(
568 caps.get(gi)
569 .map(|m| m.as_str().to_string())
570 .unwrap_or_default(),
571 );
572 }
573 Some(MMatch {
574 m: MVal::V(v),
575 remainder: input[whole.len()..].to_string(),
576 })
577}
578
579fn pat_state_of_aggregation(input: &str) -> Option<MMatch> {
581 let a = fg(
582 input,
583 Pat::Lit(""),
584 Pat::Re(re!("^\\([a-z]{1,3}(?=[\\),])")),
585 Pat::Lit(""),
586 Pat::Lit(")"),
587 None,
588 None,
589 None,
590 None,
591 false,
592 )?;
593 if re!("^($|[\\s,;\\)\\]\\}])").is_match(a.remainder.as_str()) {
594 return Some(a);
595 }
596 let re2 = re!("^(?:\\((?:\\\\ca\\s?)?\\$[amothc]\\$\\))");
597 let caps = re2.captures_head(input)?;
598 let whole = caps.get(0)?.as_str().to_string();
599 Some(MMatch {
600 m: MVal::S(whole.clone()),
601 remainder: input[whole.len()..].to_string(),
602 })
603}
604
605fn pat_amount(input: &str) -> Option<MMatch> {
607 let re = re!("^(?:(?:(?:\\([+\\-]?[0-9]+\\/[0-9]+\\)|[+\\-]?(?:[0-9]+|\\$[a-z]\\$|[a-z])\\/[0-9]+|[+\\-]?[0-9]+[.,][0-9]+|[+\\-]?\\.[0-9]+|[+\\-]?[0-9]+)(?:[a-z](?=\\s*[A-Z]))?)|[+\\-]?[a-z](?=\\s*[A-Z])|\\+(?!\\s))");
608 if let Some(caps) = re.captures_head(input) {
609 let whole = caps.get(0)?.as_str();
610 if !whole.is_empty() {
611 return Some(MMatch {
612 m: MVal::S(whole.to_string()),
613 remainder: input[whole.len()..].to_string(),
614 });
615 }
616 }
617 let a = fg(
618 input,
619 Pat::Lit(""),
620 Pat::Lit("$"),
621 Pat::Lit("$"),
622 Pat::Lit(""),
623 None,
624 None,
625 None,
626 None,
627 false,
628 )?;
629 let re2 = re!("^\\$(?:\\(?[+\\-]?(?:[0-9]*[a-z]?[+\\-])?[0-9]*[a-z](?:[+\\-][0-9]*[a-z]?)?\\)?|\\+|-)\\$$");
630 let inner = mval_str(&a.m);
631 let inner_len = inner.len();
632 if re2.is_match(&inner) {
633 return Some(MMatch {
634 m: MVal::S(inner),
635 remainder: input[inner_len..].to_string(),
636 });
637 }
638 None
639}
640
641fn pat_formula(input: &str) -> Option<MMatch> {
643 if re!("^\\([a-z]+\\)$").is_match(input) {
644 return None;
645 }
646 let re = re!("^(?:[a-z]|(?:[0-9\\ +\\-\\,\\.\\(\\)]+[a-z])+[0-9\\ +\\-\\,\\.\\(\\)]*|(?:[a-z][0-9\\ +\\-\\,\\.\\(\\)]+)+[a-z]?)$");
647 let caps = re.captures_head(input)?;
648 let whole = caps.get(0)?.as_str().to_string();
649 Some(MMatch {
650 m: MVal::S(whole.clone()),
651 remainder: input[whole.len()..].to_string(),
652 })
653}
654
655fn mval_str(m: &MVal) -> String {
656 match m {
657 MVal::S(s) => s.clone(),
658 MVal::V(v) => v.first().cloned().unwrap_or_default(),
659 }
660}
661
662#[allow(clippy::large_enum_variant)] enum Out {
668 None,
669 One(Parsed),
670 Many(Vec<Parsed>),
671}
672
673fn concat(out: &mut Vec<Parsed>, o: Out) {
674 match o {
675 Out::None => {}
676 Out::One(p) => out.push(p),
677 Out::Many(v) => out.extend(v),
678 }
679}
680
681fn go(input: &str, machine: &str) -> Vec<Parsed> {
683 if input.is_empty() {
684 return Vec::new();
685 }
686 let mut input = input.replace('\n', " ");
688 input = input.replace(['\u{2212}', '\u{2013}', '\u{2014}', '\u{2010}'], "-");
689 input = input.replace('\u{2026}', "...");
690
691 let transitions = transitions_for(machine);
692 let mut state = String::from("0");
693 let mut buffer = Buffer {
694 parenthesis_level: 0,
695 ..Default::default()
696 };
697 let mut output: Vec<Parsed> = Vec::new();
698 let mut last_input: Option<String> = None;
699 let mut watchdog = 10i32;
700
701 loop {
702 if last_input.as_deref() != Some(input.as_str()) {
703 watchdog = 10;
704 last_input = Some(input.clone());
705 } else {
706 watchdog -= 1;
707 }
708 let t = transitions.get(&state).or_else(|| transitions.get("*"));
709 let t = match t {
710 Some(t) => t,
711 None => break,
712 };
713 let mut matched = false;
714 for tr in t {
715 if let Some(mres) = match_pattern(&tr.pattern, &input) {
716 matched = true;
717 for aref in &tr.task.actions {
719 let o = exec_action(machine, &mut buffer, &mres.m, &aref.option, &aref.type_);
720 concat(&mut output, o);
721 }
722 if let Some(ns) = &tr.task.next_state {
724 state = ns.clone();
725 }
726 if !input.is_empty() {
727 if !tr.task.revisit {
728 input = mres.remainder;
729 }
730 if !tr.task.to_continue {
731 break;
732 }
733 } else {
734 return output;
735 }
736 }
737 }
738 if !matched {
739 break;
740 }
741 if watchdog <= 0 {
742 break;
744 }
745 }
746 output
747}
748
749fn exec_action(
754 machine: &str,
755 buf: &mut Buffer,
756 m: &MVal,
757 opt: &Option<String>,
758 type_: &str,
759) -> Out {
760 if let Some(o) = machine_action(machine, buf, m, opt, type_) {
762 return o;
763 }
764 generic_action(buf, m, opt, type_)
765}
766
767fn generic_action(buf: &mut Buffer, m: &MVal, opt: &Option<String>, type_: &str) -> Out {
768 match type_ {
769 "a=" => {
770 append_field(&mut buf.a, m);
771 Out::None
772 }
773 "b=" => {
774 append_field(&mut buf.b, m);
775 Out::None
776 }
777 "p=" => {
778 append_field(&mut buf.p, m);
779 Out::None
780 }
781 "o=" => {
782 append_field(&mut buf.o, m);
783 Out::None
784 }
785 "o=+p1" => {
786 if let Some(a) = opt {
787 append_str(&mut buf.o, a);
788 }
789 Out::None
790 }
791 "q=" => {
792 append_field(&mut buf.q, m);
793 Out::None
794 }
795 "d=" => {
796 append_field(&mut buf.d, m);
797 Out::None
798 }
799 "rm=" => {
800 append_field(&mut buf.rm, m);
801 Out::None
802 }
803 "text=" => {
804 append_field(&mut buf.text_, m);
805 Out::None
806 }
807 "insert" => match opt {
808 Some(a) => Out::One(Parsed::N(NodeData {
809 type_: a.clone(),
810 ..Default::default()
811 })),
812 None => Out::None,
813 },
814 "insert+p1" => match opt {
815 Some(a) => Out::One(Parsed::N(NodeData {
816 type_: a.clone(),
817 p1: Some(Field::Str(mval_str(m))),
818 ..Default::default()
819 })),
820 None => Out::None,
821 },
822 "insert+p1+p2" => {
823 if let (Some(a), MVal::V(v)) = (opt, m) {
824 Out::One(Parsed::N(NodeData {
825 type_: a.clone(),
826 p1: Some(Field::Str(v.first().cloned().unwrap_or_default())),
827 p2: Some(Field::Str(v.get(1).cloned().unwrap_or_default())),
828 ..Default::default()
829 }))
830 } else {
831 Out::None
832 }
833 }
834 "copy" => mval_to_out(m),
835 "write" => match opt {
836 Some(a) => Out::One(Parsed::S(a.clone())),
837 None => Out::None,
838 },
839 "rm" => Out::One(Parsed::N(NodeData {
840 type_: "rm".into(),
841 p1: Some(Field::Str(mval_str(m))),
842 ..Default::default()
843 })),
844 "text" => Out::Many(go(&mval_str(m), "text")),
845 "tex-math" => Out::Many(go(&mval_str(m), "tex-math")),
846 "tex-math tight" => Out::Many(go(&mval_str(m), "tex-math tight")),
847 "bond" => {
848 let kind = opt
849 .clone()
850 .or_else(|| match m {
851 MVal::S(s) => Some(s.clone()),
852 _ => None,
853 })
854 .unwrap_or_default();
855 Out::One(Parsed::N(NodeData {
856 type_: "bond".into(),
857 kind_: Some(kind),
858 ..Default::default()
859 }))
860 }
861 "color0-output" => Out::One(Parsed::N(NodeData {
862 type_: "color0".into(),
863 color: Some(mval_str(m)),
864 ..Default::default()
865 })),
866 "ce" => Out::Many(go(&mval_str(m), "ce")),
867 "pu" => Out::Many(go(&mval_str(m), "pu")),
868 "9,9" => Out::Many(go(&mval_str(m), "9,9")),
869 "1/2" => {
870 let mut s = mval_str(m);
871 let mut ret: Vec<Parsed> = Vec::new();
872 if s.starts_with('+') || s.starts_with('-') {
873 ret.push(Parsed::S(s[..1].to_string()));
874 s = s[1..].to_string();
875 }
876 if let Some(caps) =
879 re!("^([0-9]+|\\$[a-z]\\$|[a-z])\\/([0-9]+)(\\$[a-z]\\$|[a-z])?$").captures_head(&s)
880 {
881 let mut n1 = caps
882 .get(1)
883 .map(|x| x.as_str().to_string())
884 .unwrap_or_default();
885 n1 = n1.replace('$', "");
886 let n2 = caps
887 .get(2)
888 .map(|x| x.as_str().to_string())
889 .unwrap_or_default();
890 ret.push(Parsed::N(NodeData {
891 type_: "frac".into(),
892 p1: Some(Field::Str(n1)),
893 p2: Some(Field::Str(n2)),
894 ..Default::default()
895 }));
896 if let Some(g3) = caps.get(3) {
897 let mut n3 = g3.as_str().replace('$', "");
898 ret.push(Parsed::N(NodeData {
899 type_: "tex-math".into(),
900 p1: Some(Field::Str(std::mem::take(&mut n3))),
901 ..Default::default()
902 }));
903 }
904 }
905 Out::Many(ret)
906 }
907 _ => Out::None,
908 }
909}
910
911fn append_field(field: &mut Option<String>, m: &MVal) {
912 let s = mval_str(m);
913 match field {
914 Some(existing) => existing.push_str(&s),
915 None => *field = Some(s),
916 }
917}
918
919fn append_str(field: &mut Option<String>, s: &str) {
920 match field {
921 Some(existing) => existing.push_str(s),
922 None => *field = Some(s.to_string()),
923 }
924}
925
926fn mval_to_out(m: &MVal) -> Out {
927 match m {
928 MVal::S(s) => Out::One(Parsed::S(s.clone())),
929 MVal::V(v) => Out::Many(v.iter().map(|s| Parsed::S(s.clone())).collect()),
930 }
931}
932
933fn texify_go(input: &[Parsed], add_outer_braces: bool) -> String {
938 if input.is_empty() {
939 return String::new();
940 }
941 let mut res = String::new();
942 let mut cee = false;
943 for p in input {
944 match p {
945 Parsed::S(s) => res.push_str(s),
946 Parsed::N(n) => {
947 res.push_str(&texify_go2(n));
948 if n.type_ == "1st-level escape" {
949 cee = true;
950 }
951 }
952 }
953 }
954 if add_outer_braces && !cee && !res.is_empty() {
955 res = format!("{{{}}}", res);
956 }
957 res
958}
959
960fn field_str(f: &Field) -> String {
961 match f {
962 Field::Str(s) => s.clone(),
963 Field::Nodes(v) => texify_go(v, false),
964 }
965}
966
967fn texify_go2(buf: &NodeData) -> String {
968 match buf.type_.as_str() {
969 "chemfive" => {
970 let mut res = String::new();
971 let a = buf.a.as_ref().map(field_str).unwrap_or_default();
972 let b = buf.b.as_ref().map(field_str).unwrap_or_default();
973 let p = buf.p.as_ref().map(field_str).unwrap_or_default();
974 let o = buf.o.as_ref().map(field_str).unwrap_or_default();
975 let q = buf.q.as_ref().map(field_str).unwrap_or_default();
976 let d = buf.d.as_ref().map(field_str).unwrap_or_default();
977 if !a.is_empty() {
979 let aa = if a.starts_with('+') || a.starts_with('-') {
980 format!("{{{}}}", a)
981 } else {
982 a.clone()
983 };
984 res.push_str(&aa);
985 res.push_str("\\,");
986 }
987 if !b.is_empty() || !p.is_empty() {
989 res.push_str("{\\vphantom{A}}");
990 res.push_str(&format!("^{{\\hphantom{{{}}}}}_{{\\hphantom{{{}}}}}", b, p));
991 res.push_str("\\mkern-1.5mu");
992 res.push_str("{\\vphantom{A}}");
993 res.push_str(&format!(
994 "^{{\\smash[t]{{\\vphantom{{2}}}}\\llap{{{}}}}}",
995 b
996 ));
997 res.push_str(&format!(
998 "_{{\\vphantom{{2}}\\llap{{\\smash[t]{{{}}}}}}}",
999 p
1000 ));
1001 }
1002 if !o.is_empty() {
1004 let oo = if o.starts_with('+') || o.starts_with('-') {
1005 format!("{{{}}}", o)
1006 } else {
1007 o.clone()
1008 };
1009 res.push_str(&oo);
1010 }
1011 match buf.d_type.as_deref() {
1013 Some("kv") => {
1014 if !d.is_empty() || !q.is_empty() {
1015 res.push_str("{\\vphantom{A}}");
1016 }
1017 if !d.is_empty() {
1018 res.push_str(&format!("^{{{}}}", d));
1019 }
1020 if !q.is_empty() {
1021 res.push_str(&format!("_{{\\smash[t]{{{}}}}}", q));
1022 }
1023 }
1024 Some("oxidation") => {
1025 if !d.is_empty() {
1026 res.push_str("{\\vphantom{A}}");
1027 res.push_str(&format!("^{{{}}}", d));
1028 }
1029 if !q.is_empty() {
1030 res.push_str("{\\vphantom{A}}");
1031 res.push_str(&format!("_{{\\smash[t]{{{}}}}}", q));
1032 }
1033 }
1034 _ => {
1035 if !q.is_empty() {
1036 res.push_str("{\\vphantom{A}}");
1037 res.push_str(&format!("_{{\\smash[t]{{{}}}}}", q));
1038 }
1039 if !d.is_empty() {
1040 res.push_str("{\\vphantom{A}}");
1041 res.push_str(&format!("^{{{}}}", d));
1042 }
1043 }
1044 }
1045 res
1046 }
1047 "rm" => format!(
1048 "\\mathrm{{{}}}",
1049 buf.p1.as_ref().map(field_str).unwrap_or_default()
1050 ),
1051 "text" => {
1052 let mut p1 = buf.p1.as_ref().map(field_str).unwrap_or_default();
1053 if p1.contains('^') || p1.contains('_') {
1054 p1 = p1.replace(' ', "~").replace('-', "\\text{-}");
1055 format!("\\mathrm{{{}}}", p1)
1056 } else {
1057 format!("\\text{{{}}}", p1)
1058 }
1059 }
1060 "roman numeral" => format!(
1061 "\\mathrm{{{}}}",
1062 buf.p1.as_ref().map(field_str).unwrap_or_default()
1063 ),
1064 "state of aggregation" => format!(
1065 "\\mskip2mu {}",
1066 buf.p1.as_ref().map(field_str).unwrap_or_default()
1067 ),
1068 "state of aggregation subscript" => format!(
1069 "\\mskip1mu {}",
1070 buf.p1.as_ref().map(field_str).unwrap_or_default()
1071 ),
1072 "bond" => get_bond(buf.kind_.as_deref().unwrap_or("")),
1073 "frac" => {
1074 let c = format!(
1075 "\\frac{{{}}}{{{}}}",
1076 buf.p1.as_ref().map(field_str).unwrap_or_default(),
1077 buf.p2.as_ref().map(field_str).unwrap_or_default()
1078 );
1079 format!("\\mathchoice{{\\textstyle{c}}}{{{c}}}{{{c}}}{{{c}}}")
1080 }
1081 "pu-frac" => {
1082 let d = format!(
1083 "\\frac{{{}}}{{{}}}",
1084 buf.p1.as_ref().map(field_str).unwrap_or_default(),
1085 buf.p2.as_ref().map(field_str).unwrap_or_default()
1086 );
1087 format!("\\mathchoice{{\\textstyle{d}}}{{{d}}}{{{d}}}{{{d}}}")
1088 }
1089 "tex-math" => format!("{} ", buf.p1.as_ref().map(field_str).unwrap_or_default()),
1090 "frac-ce" => format!(
1091 "\\frac{{{}}}{{{}}}",
1092 buf.p1.as_ref().map(field_str).unwrap_or_default(),
1093 buf.p2.as_ref().map(field_str).unwrap_or_default()
1094 ),
1095 "overset" => format!(
1096 "\\overset{{{}}}{{{}}}",
1097 buf.p1.as_ref().map(field_str).unwrap_or_default(),
1098 buf.p2.as_ref().map(field_str).unwrap_or_default()
1099 ),
1100 "underset" => format!(
1101 "\\underset{{{}}}{{{}}}",
1102 buf.p1.as_ref().map(field_str).unwrap_or_default(),
1103 buf.p2.as_ref().map(field_str).unwrap_or_default()
1104 ),
1105 "underbrace" => format!(
1106 "\\underbrace{{{}}}_{{{}}}",
1107 buf.p1.as_ref().map(field_str).unwrap_or_default(),
1108 buf.p2.as_ref().map(field_str).unwrap_or_default()
1109 ),
1110 "color" => format!(
1111 "{{\\color{{{}}}{{{}}}}}",
1112 buf.color1.as_deref().unwrap_or(""),
1113 buf.color2.as_ref().map(field_str).unwrap_or_default()
1114 ),
1115 "color0" => format!("\\color{{{}}}", buf.color.as_deref().unwrap_or("")),
1116 "arrow" => {
1117 let rd = buf.rd.as_ref().map(field_str).unwrap_or_default();
1118 let rq = buf.rq.as_ref().map(field_str).unwrap_or_default();
1119 let r = buf.r.as_deref().unwrap_or("");
1120 let mut arrow = get_arrow(r).to_string();
1121 if !rd.is_empty() || !rq.is_empty() {
1122 if matches!(r, "<=>" | "<=>>" | "<<=>" | "<-->") {
1123 arrow = format!("\\long{}", arrow);
1124 if !rd.is_empty() {
1125 arrow = format!("\\overset{{{}}}{{{}}}", rd, arrow);
1126 }
1127 if !rq.is_empty() {
1128 arrow = if r == "<-->" {
1129 format!("\\underset{{\\lower2mu{{{}}}}}{{{}}}", rq, arrow)
1130 } else {
1131 format!("\\underset{{\\lower6mu{{{}}}}}{{{}}}", rq, arrow)
1132 };
1133 }
1134 arrow = format!(" {{}}\\mathrel{{{}}}{{}} ", arrow);
1135 } else {
1136 if !rq.is_empty() {
1137 arrow.push_str(&format!("[{{{}}}]", rq));
1138 }
1139 arrow.push_str(&format!("{{{}}}", rd));
1140 arrow = format!(" {{}}\\mathrel{{\\x{}}}{{}} ", arrow);
1141 }
1142 } else {
1143 arrow = format!(" {{}}\\mathrel{{\\long{}}}{{}} ", arrow);
1144 }
1145 arrow
1146 }
1147 "operator" => get_operator(buf.kind_.as_deref().unwrap_or("")),
1148 "1st-level escape" => format!("{} ", buf.p1.as_ref().map(field_str).unwrap_or_default()),
1149 "space" => " ".to_string(),
1150 "tinySkip" => "\\mkern2mu".to_string(),
1151 "entitySkip" => "~".to_string(),
1152 "pu-space-1" => "~".to_string(),
1153 "pu-space-2" => "\\mkern3mu ".to_string(),
1154 "1000 separator" => "\\mkern2mu ".to_string(),
1155 "commaDecimal" => "{,}".to_string(),
1156 "comma enumeration L" => format!(
1157 "{{{}}}\\mkern6mu ",
1158 buf.p1.as_ref().map(field_str).unwrap_or_default()
1159 ),
1160 "comma enumeration M" => format!(
1161 "{{{}}}\\mkern3mu ",
1162 buf.p1.as_ref().map(field_str).unwrap_or_default()
1163 ),
1164 "comma enumeration S" => format!(
1165 "{{{}}}\\mkern1mu ",
1166 buf.p1.as_ref().map(field_str).unwrap_or_default()
1167 ),
1168 "hyphen" => "\\text{-}".to_string(),
1169 "addition compound" => "\\,{\\cdot}\\,".to_string(),
1170 "electron dot" => "\\mkern1mu \\bullet\\mkern1mu ".to_string(),
1171 "KV x" => "{\\times}".to_string(),
1172 "prime" => "\\prime ".to_string(),
1173 "cdot" => "\\cdot ".to_string(),
1174 "tight cdot" => "\\mkern1mu{\\cdot}\\mkern1mu ".to_string(),
1175 "times" => "\\times ".to_string(),
1176 "circa" => "{\\sim}".to_string(),
1177 "^" => "uparrow".to_string(),
1178 "v" => "downarrow".to_string(),
1179 "ellipsis" => "\\ldots ".to_string(),
1180 "/" => "/".to_string(),
1181 " / " => "\\,/\\,".to_string(),
1182 _ => String::new(),
1183 }
1184}
1185
1186fn get_arrow(a: &str) -> &'static str {
1187 match a {
1188 "->" | "\u{2192}" | "\u{27F6}" => "rightarrow",
1189 "<-" => "leftarrow",
1190 "<->" => "leftrightarrow",
1191 "<-->" => "leftrightarrows",
1192 "<=>" | "\u{21CC}" => "rightleftharpoons",
1193 "<=>>" => "Rightleftharpoons",
1194 "<<=>" => "Leftrightharpoons",
1195 _ => "rightarrow",
1196 }
1197}
1198
1199fn get_bond(a: &str) -> String {
1200 match a {
1201 "-" | "1" => "{-}".to_string(),
1202 "=" | "2" => "{=}".to_string(),
1203 "#" | "3" => "{\\equiv}".to_string(),
1204 "~" => "{\\tripledash}".to_string(),
1205 "~-" => "{\\rlap{\\lower.1em{-}}\\raise.1em{\\tripledash}}".to_string(),
1206 "~=" | "~--" => "{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{\\tripledash}}-}".to_string(),
1207 "-~-" => "{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{-}}\\tripledash}".to_string(),
1208 "..." => "{{\\cdot}{\\cdot}{\\cdot}}".to_string(),
1209 "...." => "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}".to_string(),
1210 "->" => "{\\rightarrow}".to_string(),
1211 "<-" => "{\\leftarrow}".to_string(),
1212 "<" => "{<}".to_string(),
1213 ">" => "{>}".to_string(),
1214 _ => format!("{{{}}}", a),
1215 }
1216}
1217
1218fn get_operator(a: &str) -> String {
1219 match a {
1220 "+" => " {}+{} ".to_string(),
1221 "-" => " {}-{} ".to_string(),
1222 "=" => " {}={} ".to_string(),
1223 "<" => " {}<{} ".to_string(),
1224 ">" => " {}>{} ".to_string(),
1225 "<<" => " {}\\ll{} ".to_string(),
1226 ">>" => " {}\\gg{} ".to_string(),
1227 "\\pm" => " {}\\pm{} ".to_string(),
1228 "\\approx" | "$\\approx$" => " {}\\approx{} ".to_string(),
1229 "v" | "(v)" => " \\downarrow{} ".to_string(),
1230 "^" | "(^)" => " \\uparrow{} ".to_string(),
1231 _ => format!(" {{{}}} ", a),
1232 }
1233}
1234
1235pub fn ce(input: &str) -> String {
1249 to_tex(input, "ce")
1250}
1251
1252pub fn pu(input: &str) -> String {
1254 to_tex(input, "pu")
1255}
1256
1257fn to_tex(input: &str, kind: &str) -> String {
1258 let result = std::panic::catch_unwind(|| {
1261 let parsed = go(input, kind);
1262 texify_go(&parsed, true)
1264 });
1265 result.unwrap_or_else(|_| input.to_string())
1266}
1267
1268include!("mhchem_tables.rs");
1270
1271#[cfg(test)]
1272mod tests {
1273 use super::*;
1274
1275 #[test]
1276 fn ce_water_produces_mathrm() {
1277 let tex = ce("H2O");
1278 assert!(tex.contains(r"\mathrm{H}"), "H 应为直立体: {tex}");
1279 assert!(tex.contains(r"\mathrm{O}"), "O 应为直立体: {tex}");
1280 }
1281
1282 #[test]
1283 fn ce_reaction_has_arrow() {
1284 let tex = ce("2H2 + O2 -> 2H2O");
1285 assert!(tex.contains("rightarrow"), "应含反应箭头: {tex}");
1286 }
1287
1288 #[test]
1289 fn ce_empty_is_empty() {
1290 assert_eq!(ce(""), "");
1291 }
1292
1293 #[test]
1294 fn ce_does_not_panic_on_garbage() {
1295 let _ = ce("}}}{{{]][[");
1297 let _ = ce("\\frac{");
1298 let _ = ce("<<<<>>>>");
1299 }
1300
1301 #[test]
1302 fn ce_gas_arrow_becomes_uparrow() {
1303 let tex = ce("CaCO3 ->[\\Delta] CaO + CO2 ^");
1305 assert!(
1306 tex.contains("uparrow") || tex.contains("rightarrow"),
1307 "气体/反应符号: {tex}"
1308 );
1309 }
1310
1311 #[test]
1312 fn pu_unit_has_mathrm() {
1313 let tex = pu("9.8 m/s^2");
1314 assert!(tex.contains(r"\mathrm"), "单位应有直立体: {tex}");
1315 }
1316
1317 #[test]
1318 fn pu_empty_is_empty() {
1319 assert_eq!(pu(""), "");
1320 }
1321
1322 #[test]
1323 fn ce_charge_superscript() {
1324 let tex = ce("SO4^2-");
1325 assert!(!tex.is_empty(), "离子应产出非空: {tex}");
1327 }
1328
1329 #[test]
1335 fn ce_dot_bullet_bond_inputs_dont_panic() {
1336 for s in [
1337 ".",
1338 "·",
1339 "•",
1340 "⋅",
1341 "*",
1342 ". ",
1343 "••",
1344 "· ",
1345 "H·OH",
1346 "CaCO3 · H2O",
1347 ] {
1348 let tex = ce(s);
1349 assert!(!tex.is_empty(), "ce({s:?}) 不应为空");
1350 }
1351 }
1352
1353 #[test]
1359 fn compile_or_log_degrades_bad_regex_without_panic() {
1360 assert!(compile_or_log("^a").is_some(), "合法正则应编译成功");
1362 let bad = "^([abc|[*])";
1364 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| compile_or_log(bad)));
1365 assert!(r.is_ok(), "compile_or_log 对坏正则不应 panic");
1366 assert!(r.unwrap().is_none(), "坏正则应返回 None");
1367 }
1368
1369 #[test]
1375 fn ce_multibyte_char_in_braces_does_not_panic() {
1376 for s in [
1377 "{浓}",
1378 "浓H2SO4",
1379 "{中文}",
1380 "H{浓}O",
1381 "\\frac{浓}{稀}",
1382 "[浓]",
1383 ] {
1384 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| ce(s)));
1386 assert!(r.is_ok(), "ce({s:?}) PANICKED (char boundary)");
1387 }
1388 }
1389
1390 #[test]
1393 fn ce_arbitrary_multibyte_does_not_panic() {
1394 let inputs = [
1395 "🔥",
1396 "café",
1397 "naïve",
1398 "Σ",
1399 "αβγ",
1400 "ΔH",
1401 "你好世界",
1402 "안녕",
1403 "こんにちは",
1404 "{🧪}",
1405 "H₂O",
1406 "[α]",
1407 "\\frac{β}{γ}",
1408 "${日本}$",
1409 "A·B•C⋅D",
1410 "naïve H2O",
1411 "{β-Gal}",
1412 "😀😂",
1413 "\u{1F9EA}", ];
1415 for s in inputs {
1416 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| ce(s)));
1417 assert!(r.is_ok(), "ce({s:?}) PANICKED");
1418 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| pu(s)));
1419 assert!(r.is_ok(), "pu({s:?}) PANICKED");
1420 }
1421 }
1422
1423 #[test]
1427 fn all_match_patterns_compile() {
1428 for name in [
1429 "empty",
1430 "else",
1431 "else2",
1432 "space",
1433 "space A",
1434 "space$",
1435 "a-z",
1436 "x",
1437 "x$",
1438 "i$",
1439 "letters",
1440 "\\greek",
1441 "one lowercase latin letter $",
1442 "$one lowercase latin letter$ $",
1443 "one lowercase greek letter $",
1444 "digits",
1445 "-9.,9",
1446 "-9.,9 no missing 0",
1447 "(-)(9)^(-9)",
1448 "_{(state of aggregation)}$",
1449 "{[(",
1450 ")]}",
1451 ", ",
1452 ",",
1453 ".",
1454 ". __* ",
1455 "...",
1456 "^a",
1457 "^\\x",
1458 "^(-1)",
1459 "'",
1460 "_9",
1461 "_\\x",
1462 "^_",
1463 "{}^",
1464 "{}",
1465 "=<>",
1466 "#",
1467 "+",
1468 "-$",
1469 "-9",
1470 "- orbital overlap",
1471 "-",
1472 "pm-operator",
1473 "operator",
1474 "arrowUpDown",
1475 "->",
1476 "CMT",
1477 "1st-level escape",
1478 "\\,",
1479 "\\ca",
1480 "\\x",
1481 "orbital",
1482 "others",
1483 "oxidation$",
1484 "d-oxidation$",
1485 "1/2$",
1486 "(KV letters),",
1487 "uprightEntities",
1488 "/",
1489 "//",
1490 "*",
1491 ] {
1492 let _ = match_pattern(name, "");
1494 }
1495 }
1496}