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