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