#![cfg(feature = "server")] //! mhchem 化学公式转译器:把 `\ce{...}` / `\pu{...}` 预转译为标准 LaTeX, //! 再交给 katex 渲染。 //! //! 这是 [mhchemParser](https://github.com/mhchem/mhchemParser) 4.2.2(Apache-2.0) //! 的**机械移植**:状态机 + texify 输出。mhchemParser 是纯字符串→字符串转译器, //! `ce("H2O")` → `\mathrm{H}\sb{2}\mathrm{O}` 之类,无需嵌入 katex 内部解析树。 //! //! 移植要点(TS→Rust): //! - 动态 `buffer` 对象 → [`Buffer`] 结构体(`Option` 字段 + `clear`)。 //! - 动态 `Parsed = string | object` → [`Parsed`] 枚举;节点字段用 [`Field`](字符串或节点向量)。 //! - 正则模式(含 lookahead `(?=)`/`(?!)`,Rust `regex` 不支持)→ `fancy-regex`(已被 //! syntect 间接引入,此处显式声明)。 //! - `findObserveGroups` 花括号配对扫描 → [`find_observe_groups`]。 //! - 状态机转移表 → [`build_transitions`] 展开(对应 `_mhchemCreateTransitions`)。 //! //! 公开 API:[`ce`] / [`pu`]。任意内部异常都回退为原样输出(绝不 panic,与 //! katex.rs 的容错哲学一致)。 //! //! ----------------------------------------------------------------------- //! Copyright 2015-2023 Martin Hensel(mhchemParser 原作者)。Apache-2.0。 //! ----------------------------------------------------------------------- use fancy_regex::Regex; use std::collections::HashMap; use std::sync::LazyLock; // ========================================================================= // 数据模型 // ========================================================================= /// 模式匹配值:单字符串或捕获组数组(正则 >1 个捕获组时为数组)。 #[derive(Clone, Debug)] enum MVal { S(String), V(Vec), } /// 模式匹配结果。 #[derive(Clone, Debug)] struct MMatch { m: MVal, remainder: String, } /// 解析中间表示的字段值:字符串或子表达式节点向量。 #[derive(Clone, Debug)] enum Field { Str(String), Nodes(Vec), } /// 解析中间表示:字面字符串或节点。 #[allow(clippy::large_enum_variant)] // 机械移植:镜像 mhchemParser 上游结构,Parsed 经 Vec 进堆,非栈热路径 #[derive(Clone, Debug)] enum Parsed { S(String), N(NodeData), } #[derive(Clone, Debug, Default)] struct NodeData { type_: String, p1: Option, p2: Option, a: Option, b: Option, p: Option, o: Option, q: Option, d: Option, color: Option, color1: Option, color2: Option, r: Option, rd: Option, rq: Option, d_type: Option, kind_: Option, } /// 状态机缓冲区(对应 TS 的动态 `buffer` 对象)。 #[derive(Clone, Default)] struct Buffer { a: Option, b: Option, p: Option, o: Option, q: Option, d: Option, rm: Option, text_: Option, r: Option, rd: Option, rq: Option, rdt: Option, rqt: Option, d_type: Option, sb: bool, begins_with_bond: bool, parenthesis_level: i32, } impl Buffer { /// 清空内容字段。`keep` 为真时保留 `parenthesis_level` 与 `begins_with_bond` /// (对应 ce 的 output:`delete buffer[p]` 跳过这两项)。 fn clear(&mut self, keep: bool) { let (pl, bb) = if keep { (self.parenthesis_level, self.begins_with_bond) } else { (0, false) }; *self = Buffer { parenthesis_level: pl, begins_with_bond: bb, ..Buffer::default() }; } } // ========================================================================= // 转移表 // ========================================================================= #[derive(Clone)] struct ActionRef { type_: String, option: Option, } #[derive(Clone, Default)] struct Task { actions: Vec, next_state: Option, revisit: bool, to_continue: bool, } #[derive(Clone)] struct Transition { pattern: String, task: Task, } /// 原始转移条目(对应 TS 字面量,`patterns`/`states` 均为 `|` 分隔的合并名)。 struct RawEntry { patterns: &'static str, states: &'static str, task: Task, } /// 展开 `{pattern: {state: task}}` 为 `{state => [(pattern, task)]}`。 /// 对应 `_mhchemCreateTransitions`:拆分 `|`、`'*'` 插入所有状态。 fn build_transitions(raw: &[RawEntry]) -> HashMap> { let mut transitions: HashMap> = HashMap::new(); // 1. 收集所有状态(拆分 states 的 `|`) for e in raw { for state in e.states.split('|') { transitions.entry(state.to_string()).or_default(); } } // 2. 填充(拆分 patterns 与 states 的 `|`,`'*'` 插入所有已收集状态) let all_states: Vec = transitions.keys().cloned().collect(); for e in raw { let state_list: Vec<&str> = e.states.split('|').collect(); for (idx, _state) in state_list.iter().enumerate() { for pattern in e.patterns.split('|') { let insert_states: Vec = if state_list[idx] == "*" { all_states.clone() } else { vec![state_list[idx].to_string()] }; for s in insert_states { transitions.entry(s).or_default().push(Transition { pattern: pattern.to_string(), task: e.task.clone(), }); } } } } transitions } // ========================================================================= // 模式匹配 // ========================================================================= macro_rules! re { ($pat:literal) => {{ static RE: LazyLock = LazyLock::new(|| Regex::new($pat).unwrap()); &*RE }}; } /// 字面量或正则模式(`findObserveGroups` 的参数)。 enum Pat { Lit(&'static str), Re(&'static Regex), } /// 在 `input` 起始处匹配 `pat`,返回匹配到的文本(不含 remainder)。 fn pat_match_head(pat: &Pat, input: &str) -> Option { match pat { Pat::Lit(s) => { if input.starts_with(s) { Some((*s).to_string()) } else { None } } Pat::Re(re) => re.captures(input).ok().flatten().and_then(|c| { let whole = c.get(0).map(|m| m.as_str())?; // 模式均锚定 ^,确认从 0 开始 if input.starts_with(whole) { Some(whole.to_string()) } else { None } }), } } /// `findObserveGroups`:花括号感知的定界符配对扫描。 /// /// 移植自 TS `findObserveGroups`。`beg*_excl/incl`/`end*_incl/excl` 为定界符, /// `combine` 控制第二组结果是否拼接为字符串。 #[allow(clippy::too_many_arguments)] fn find_observe_groups( input: &str, beg_excl: Pat, beg_incl: Pat, end_incl: Pat, end_excl: Pat, beg2_excl: Option, beg2_incl: Option, end2_incl: Option, end2_excl: Option, combine: bool, ) -> Option { // 第一组 let m0 = pat_match_head(&beg_excl, input)?; let rest0 = &input[m0.len()..]; let m1 = pat_match_head(&beg_incl, rest0)?; // 结束定界符:endIncl || endExcl(endIncl 为空字面量时取 endExcl) let end_is_incl = !is_empty_pat(&end_incl); let end_chars = if end_is_incl { &end_incl } else { &end_excl }; let e = find_observe_end(rest0, m1.len(), end_chars)?; let body_end = if end_is_incl { e.end } else { e.begin }; let match1 = rest0[..body_end].to_string(); let after1 = &rest0[e.end..]; // 无第二组 if beg2_excl.is_none() && beg2_incl.is_none() { return Some(MMatch { m: MVal::S(match1), remainder: after1.to_string(), }); } // 第二组(递归) let g2 = find_observe_groups( after1, beg2_excl.unwrap_or(Pat::Lit("")), beg2_incl.unwrap_or(Pat::Lit("")), end2_incl.unwrap_or(Pat::Lit("")), end2_excl.unwrap_or(Pat::Lit("")), None, None, None, None, false, )?; let mval = match g2.m { MVal::S(s2) => { if combine { MVal::S(format!("{}{}", match1, s2)) } else { MVal::V(vec![match1, s2]) } } MVal::V(_) => MVal::V(vec![match1]), // 不应发生(第二组无二级) }; Some(MMatch { m: mval, remainder: g2.remainder, }) } fn is_empty_pat(p: &Pat) -> bool { matches!(p, Pat::Lit("")) } struct Span { begin: usize, end: usize, } /// 从 `rest0` 的 `start` 位置扫描,跟踪花括号深度,在深度 0 遇到 `end_chars` 时返回其区间。 fn find_observe_end(input: &str, start: usize, end_chars: &Pat) -> Option { let bytes = input.as_bytes(); let mut i = start; let mut braces = 0i32; while i < input.len() { // 结束定界符匹配(仅在花括号平衡时) if braces == 0 { if let Some(matched) = pat_match_head(end_chars, &input[i..]) { return Some(Span { begin: i, end: i + matched.len(), }); } } let a = bytes[i] as char; if a == '{' { braces += 1; } else if a == '}' { if braces == 0 { // ExtraCloseMissingOpen —— 原实现抛错,这里返回 None 容错 return None; } braces -= 1; } i += 1; } None } /// 把 fancy-regex 的捕获结果转为 [`MMatch`](锚定 `^`,whole 从 0 起)。 fn fancy_match(re: &Regex, input: &str) -> Option { let caps = re.captures(input).ok()??; let whole = caps.get(0)?.as_str().to_string(); if !input.starts_with(&whole) { return None; } let n_groups = re.captures_len(); let mval = if n_groups >= 2 { let mut v = Vec::with_capacity(n_groups); for gi in 1..=n_groups { v.push( caps.get(gi) .map(|m| m.as_str().to_string()) .unwrap_or_default(), ); } MVal::V(v) } else { // n_groups == 1 或 0:match[1] || match[0](空串视作未匹配) let g1 = caps .get(1) .map(|m| m.as_str().to_string()) .filter(|s| !s.is_empty()); MVal::S(g1.unwrap_or(whole.clone())) }; Some(MMatch { m: mval, remainder: input[whole.len()..].to_string(), }) } /// 希腊字母宏集合(`letters` / `\greek` 等模式用到)。 static 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"; static LETTERS_RE: LazyLock = LazyLock::new(|| { Regex::new(&format!( "^(?:[a-zA-Z\u{03B1}-\u{03C9}\u{0391}-\u{03A9}?@]|(?:\\\\(?:{})(?:\\s+|\\{{\\}}|(?![a-zA-Z]))))+", GREEK_NAMES )) .unwrap() }); static GREEK_RE: LazyLock = LazyLock::new(|| { Regex::new(&format!( "^\\\\(?:{})(?:\\s+|\\{{\\}}|(?![a-zA-Z]))", GREEK_NAMES )) .unwrap() }); static ONE_GREEK_RE: LazyLock = LazyLock::new(|| { Regex::new( "^(?:\\$?[\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]))$" ) .unwrap() }); /// 按名匹配模式(对应 `_mhchemParser.patterns.match_`)。 fn match_pattern(name: &str, input: &str) -> Option { // ── 正则模式 ── let re_match = |re: &Regex| fancy_match(re, input); match name { "empty" => re_match(re!("^$")), "else" | "else2" => re_match(re!("^.")), "space" => re_match(re!("^\\s")), "space A" => re_match(re!("^\\s(?=[A-Z\\\\$])")), "space$" => re_match(re!("^\\s$")), "a-z" => re_match(re!("^[a-z]")), "x" => re_match(re!("^x")), "x$" => re_match(re!("^x$")), "i$" => re_match(re!("^i$")), "letters" => re_match(&LETTERS_RE), "\\greek" => re_match(&GREEK_RE), "one lowercase latin letter $" => re_match(re!("^(?:([a-z])(?:$|[^a-zA-Z]))$")), "$one lowercase latin letter$ $" => re_match(re!("^\\$(?:([a-z])(?:$|[^a-zA-Z]))\\$$")), "one lowercase greek letter $" => re_match(&ONE_GREEK_RE), "digits" => re_match(re!("^[0-9]+")), "-9.,9" => re_match(re!("^[+\\-]?(?:[0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+))")), "-9.,9 no missing 0" => re_match(re!("^[+\\-]?[0-9]+(?:[.,][0-9]+)?")), "(-)(9)^(-9)" => re_match(re!("^(\\+\\-|\\+\\/\\-|\\+|\\-|\\\\pm\\s?)?([0-9]+(?:[,.][0-9]+)?|[0-9]*(?:\\.[0-9]+)?)\\^([+\\-]?[0-9]+|\\{[+\\-]?[0-9]+\\})")), "_{(state of aggregation)}$" => re_match(re!("^_\\{(\\([a-z]{1,3}\\))\\}")), "{[(" => re_match(re!("^(?:\\\\\\{|\\[|\\()")), ")]}" => re_match(re!("^(?:\\)|\\]|\\\\\\})")), ", " => re_match(re!("^[,;]\\s*")), "," => re_match(re!("^[,;]")), "." => re_match(re!("^[.]")), ". __* " => re_match(re!("^([.\u{22C5}\u{00B7}\u{2022}|[*])\\s*")), "..." => re_match(re!("^\\.\\.\\.(?=$|[^.])")), "^a" => re_match(re!("^\\^([0-9]+|[^\\\\_])")), "^\\x" => re_match(re!("^\\^(\\\\[a-zA-Z]+)\\s*")), "^(-1)" => re_match(re!("^\\^(-?\\d+)")), "'" => re_match(re!("^'")), "_9" => re_match(re!("^_([+\\-]?[0-9]+|[^\\\\])")), "_\\x" => re_match(re!("^_(\\\\[a-zA-Z]+)\\s*")), "^_" => re_match(re!("^(?:\\^(?=_)|\\_(?=\\^)|[\\^_]$)")), "{}^" => re_match(re!("^\\{\\}(?=\\^)")), "{}" => re_match(re!("^\\{\\}")), "=<>" => re_match(re!("^[=<>]")), "#" => re_match(re!("^[#\u{2261}]")), "+" => re_match(re!("^\\+")), "-$" => re_match(re!("^-(?=[\\s_},;\\]/]|$|\\([a-z]+\\))")), "-9" => re_match(re!("^-(?=[0-9])")), "- orbital overlap" => re_match(re!("^-(?=(?:[spd]|sp)(?:$|[\\s,;\\)\\]\\}]))")), "-" => re_match(re!("^-")), "pm-operator" => re_match(re!("^(?:\\\\pm|\\$\\\\pm\\$|\\+-|\\+\\/-)")), "operator" => re_match(re!("^(?:\\+|(?:[\\-=<>]|<<|>>|\\\\approx|\\$\\\\approx\\$)(?=\\s|$|-?[0-9]))")), "arrowUpDown" => re_match(re!("^(?:v|\\(v\\)|\\^|\\(\\^\\))(?=$|[\\s,;\\)\\]\\}])")), "->" => re_match(re!("^(?:<->|<-->|->|<-|<=>>|<<=>|<=>|[\u{2192}\u{27F6}\u{21CC}])")), "CMT" => re_match(re!("^[CMT](?=\\[)")), "1st-level escape" => re_match(re!("^(&|\\\\\\\\|\\\\hline)\\s*")), "\\," => re_match(re!("^(?:\\\\[,\\ ;:])")), "\\ca" => re_match(re!("^\\\\ca(?:\\s+|(?![a-zA-Z]))")), "\\x" => re_match(re!("^(?:\\\\[a-zA-Z]+\\s*|\\\\[_&{}%])")), "orbital" => re_match(re!("^(?:[0-9]{1,2}[spdfgh]|[0-9]{0,2}sp)(?=$|[^a-zA-Z])")), "others" => re_match(re!("^[/~|]")), "oxidation$" => re_match(re!("^(?:[+-][IVX]+|(?:\\\\pm|\\$\\\\pm\\$|\\+-|\\+\\/-)\\s*0)$")), "d-oxidation$" => re_match(re!("^(?:[+-]?[IVX]+|(?:\\\\pm|\\$\\\\pm\\$|\\+-|\\+\\/-)\\s*0)$")), "1/2$" => re_match(re!("^[+\\-]?(?:[0-9]+|\\$[a-z]\\$|[a-z])\\/[0-9]+(?:\\$[a-z]\\$|[a-z])?$")), "(KV letters)," => re_match(re!("^(?:[A-Z][a-z]{0,2}|i)(?=,)")), "uprightEntities" => re_match(re!("^(?:pH|pOH|pC|pK|iPr|iBu)(?=$|[^a-zA-Z])")), "/" => re_match(re!("^\\s*(\\/)\\s*")), "//" => re_match(re!("^\\s*(\\/\\/)\\s*")), "*" => re_match(re!("^\\s*[*.]\\s*")), // ── 函数模式(findObserveGroups / 自定义)── "(-)(9.,9)(e)(99)" => pat_enumber(input), "state of aggregation $" => pat_state_of_aggregation(input), "^{(...)}" => fg(input, Pat::Lit("^{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false), "^($...$)" => fg(input, Pat::Lit("^"), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false), "^\\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), "^\\x{}" => fg(input, Pat::Lit("^"), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false), "\\bond{(...)}" => fg(input, Pat::Lit("\\bond{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false), "[(...)]" => fg(input, Pat::Lit("["), Pat::Lit(""), Pat::Lit(""), Pat::Lit("]"), None, None, None, None, false), "\\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), "\\x{}" => fg(input, Pat::Lit(""), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false), "\\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), "\\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), "\\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), "\\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), "\\color{(...)}" => fg(input, Pat::Lit("\\color{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false), "\\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), "\\ce{(...)}" => fg(input, Pat::Lit("\\ce{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false), "\\pu{(...)}" => fg(input, Pat::Lit("\\pu{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false), "_{(...)}" => fg(input, Pat::Lit("_{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false), "_($...$)" => fg(input, Pat::Lit("_"), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false), "_\\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), "_\\x{}" => fg(input, Pat::Lit("_"), Pat::Re(re!("^\\\\[a-zA-Z]+\\{")), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false), "{...}" => fg(input, Pat::Lit(""), Pat::Lit("{"), Pat::Lit("}"), Pat::Lit(""), None, None, None, None, false), "{(...)}" => fg(input, Pat::Lit("{"), Pat::Lit(""), Pat::Lit(""), Pat::Lit("}"), None, None, None, None, false), "$...$" => fg(input, Pat::Lit(""), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false), "${(...)}$__$(...)$" => 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)), "amount" | "amount2" => pat_amount(input), "formula$" => pat_formula(input), _ => { // 未知模式:容错返回 None(原实现抛 MhchemBugP) None } } } /// `find_observe_groups` 的简写封装。 #[allow(clippy::too_many_arguments)] fn fg( input: &str, beg_excl: Pat, beg_incl: Pat, end_incl: Pat, end_excl: Pat, beg2_excl: Option, beg2_incl: Option, end2_incl: Option, end2_excl: Option, combine: bool, ) -> Option { find_observe_groups( input, beg_excl, beg_incl, end_incl, end_excl, beg2_excl, beg2_incl, end2_incl, end2_excl, combine, ) } /// `(-)(9.,9)(e)(99)` 模式。 fn pat_enumber(input: &str) -> Option { 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]+\\}))?"); let caps = re.captures(input).ok()??; let whole = caps.get(0)?.as_str(); if whole.is_empty() || !input.starts_with(whole) { return None; } let mut v = Vec::with_capacity(6); for gi in 1..=6 { v.push( caps.get(gi) .map(|m| m.as_str().to_string()) .unwrap_or_default(), ); } Some(MMatch { m: MVal::V(v), remainder: input[whole.len()..].to_string(), }) } /// `state of aggregation $` 模式。 fn pat_state_of_aggregation(input: &str) -> Option { let a = fg( input, Pat::Lit(""), Pat::Re(re!("^\\([a-z]{1,3}(?=[\\),])")), Pat::Lit(""), Pat::Lit(")"), None, None, None, None, false, )?; if re!("^($|[\\s,;\\)\\]\\}])") .is_match(a.remainder.as_str()) .ok()? { return Some(a); } let re2 = re!("^(?:\\((?:\\\\ca\\s?)?\\$[amothc]\\$\\))"); let caps = re2.captures(input).ok()??; let whole = caps.get(0)?.as_str().to_string(); Some(MMatch { m: MVal::S(whole.clone()), remainder: input[whole.len()..].to_string(), }) } /// `amount` 模式。 fn pat_amount(input: &str) -> Option { 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))"); if let Some(caps) = re.captures(input).ok().flatten() { let whole = caps.get(0)?.as_str(); if !whole.is_empty() { return Some(MMatch { m: MVal::S(whole.to_string()), remainder: input[whole.len()..].to_string(), }); } } let a = fg( input, Pat::Lit(""), Pat::Lit("$"), Pat::Lit("$"), Pat::Lit(""), None, None, None, None, false, )?; let re2 = re!("^\\$(?:\\(?[+\\-]?(?:[0-9]*[a-z]?[+\\-])?[0-9]*[a-z](?:[+\\-][0-9]*[a-z]?)?\\)?|\\+|-)\\$$"); let inner = mval_str(&a.m); let inner_len = inner.len(); if re2.is_match(&inner).ok()? { return Some(MMatch { m: MVal::S(inner), remainder: input[inner_len..].to_string(), }); } None } /// `formula$` 模式。 fn pat_formula(input: &str) -> Option { if re!("^\\([a-z]+\\)$").is_match(input).ok()? { return None; } let re = re!("^(?:[a-z]|(?:[0-9\\ +\\-\\,\\.\\(\\)]+[a-z])+[0-9\\ +\\-\\,\\.\\(\\)]*|(?:[a-z][0-9\\ +\\-\\,\\.\\(\\)]+)+[a-z]?)$"); let caps = re.captures(input).ok()??; let whole = caps.get(0)?.as_str().to_string(); Some(MMatch { m: MVal::S(whole.clone()), remainder: input[whole.len()..].to_string(), }) } fn mval_str(m: &MVal) -> String { match m { MVal::S(s) => s.clone(), MVal::V(v) => v.first().cloned().unwrap_or_default(), } } // ========================================================================= // 状态机主循环(对应 `_mhchemParser.go`) // ========================================================================= #[allow(clippy::large_enum_variant)] // 同上:Out 为动作返回值,瞬态使用 enum Out { None, One(Parsed), Many(Vec), } fn concat(out: &mut Vec, o: Out) { match o { Out::None => {} Out::One(p) => out.push(p), Out::Many(v) => out.extend(v), } } /// 主解析循环。 fn go(input: &str, machine: &str) -> Vec { if input.is_empty() { return Vec::new(); } // 输入预处理 let mut input = input.replace('\n', " "); input = input.replace(['\u{2212}', '\u{2013}', '\u{2014}', '\u{2010}'], "-"); input = input.replace('\u{2026}', "..."); let transitions = transitions_for(machine); let mut state = String::from("0"); let mut buffer = Buffer { parenthesis_level: 0, ..Default::default() }; let mut output: Vec = Vec::new(); let mut last_input: Option = None; let mut watchdog = 10i32; loop { if last_input.as_deref() != Some(input.as_str()) { watchdog = 10; last_input = Some(input.clone()); } else { watchdog -= 1; } let t = transitions.get(&state).or_else(|| transitions.get("*")); let t = match t { Some(t) => t, None => break, }; let mut matched = false; for tr in t { if let Some(mres) = match_pattern(&tr.pattern, &input) { matched = true; // 执行动作链 for aref in &tr.task.actions { let o = exec_action(machine, &mut buffer, &mres.m, &aref.option, &aref.type_); concat(&mut output, o); } // 设置下一状态 if let Some(ns) = &tr.task.next_state { state = ns.clone(); } if !input.is_empty() { if !tr.task.revisit { input = mres.remainder; } if !tr.task.to_continue { break; } } else { return output; } } } if !matched { break; } if watchdog <= 0 { // 防死循环:容错返回当前输出(原实现抛 MhchemBugU) break; } } output } // ========================================================================= // 动作分发 // ========================================================================= fn exec_action( machine: &str, buf: &mut Buffer, m: &MVal, opt: &Option, type_: &str, ) -> Out { // 优先机器局部动作,再查通用动作 if let Some(o) = machine_action(machine, buf, m, opt, type_) { return o; } generic_action(buf, m, opt, type_) } fn generic_action(buf: &mut Buffer, m: &MVal, opt: &Option, type_: &str) -> Out { match type_ { "a=" => { append_field(&mut buf.a, m); Out::None } "b=" => { append_field(&mut buf.b, m); Out::None } "p=" => { append_field(&mut buf.p, m); Out::None } "o=" => { append_field(&mut buf.o, m); Out::None } "o=+p1" => { if let Some(a) = opt { append_str(&mut buf.o, a); } Out::None } "q=" => { append_field(&mut buf.q, m); Out::None } "d=" => { append_field(&mut buf.d, m); Out::None } "rm=" => { append_field(&mut buf.rm, m); Out::None } "text=" => { append_field(&mut buf.text_, m); Out::None } "insert" => match opt { Some(a) => Out::One(Parsed::N(NodeData { type_: a.clone(), ..Default::default() })), None => Out::None, }, "insert+p1" => match opt { Some(a) => Out::One(Parsed::N(NodeData { type_: a.clone(), p1: Some(Field::Str(mval_str(m))), ..Default::default() })), None => Out::None, }, "insert+p1+p2" => { if let (Some(a), MVal::V(v)) = (opt, m) { Out::One(Parsed::N(NodeData { type_: a.clone(), p1: Some(Field::Str(v.first().cloned().unwrap_or_default())), p2: Some(Field::Str(v.get(1).cloned().unwrap_or_default())), ..Default::default() })) } else { Out::None } } "copy" => mval_to_out(m), "write" => match opt { Some(a) => Out::One(Parsed::S(a.clone())), None => Out::None, }, "rm" => Out::One(Parsed::N(NodeData { type_: "rm".into(), p1: Some(Field::Str(mval_str(m))), ..Default::default() })), "text" => Out::Many(go(&mval_str(m), "text")), "tex-math" => Out::Many(go(&mval_str(m), "tex-math")), "tex-math tight" => Out::Many(go(&mval_str(m), "tex-math tight")), "bond" => { let kind = opt .clone() .or_else(|| match m { MVal::S(s) => Some(s.clone()), _ => None, }) .unwrap_or_default(); Out::One(Parsed::N(NodeData { type_: "bond".into(), kind_: Some(kind), ..Default::default() })) } "color0-output" => Out::One(Parsed::N(NodeData { type_: "color0".into(), color: Some(mval_str(m)), ..Default::default() })), "ce" => Out::Many(go(&mval_str(m), "ce")), "pu" => Out::Many(go(&mval_str(m), "pu")), "9,9" => Out::Many(go(&mval_str(m), "9,9")), "1/2" => { let mut s = mval_str(m); let mut ret: Vec = Vec::new(); if s.starts_with('+') || s.starts_with('-') { ret.push(Parsed::S(s[..1].to_string())); s = s[1..].to_string(); } if let Some(caps) = re!("^([0-9]+|\\$[a-z]\\$|[a-z])\\/([0-9]+)(\\$[a-z]\\$|[a-z])?$") .captures(&s) .ok() .flatten() { let mut n1 = caps .get(1) .map(|x| x.as_str().to_string()) .unwrap_or_default(); n1 = n1.replace('$', ""); let n2 = caps .get(2) .map(|x| x.as_str().to_string()) .unwrap_or_default(); ret.push(Parsed::N(NodeData { type_: "frac".into(), p1: Some(Field::Str(n1)), p2: Some(Field::Str(n2)), ..Default::default() })); if let Some(g3) = caps.get(3) { let mut n3 = g3.as_str().replace('$', ""); ret.push(Parsed::N(NodeData { type_: "tex-math".into(), p1: Some(Field::Str(std::mem::take(&mut n3))), ..Default::default() })); } } Out::Many(ret) } _ => Out::None, } } fn append_field(field: &mut Option, m: &MVal) { let s = mval_str(m); match field { Some(existing) => existing.push_str(&s), None => *field = Some(s), } } fn append_str(field: &mut Option, s: &str) { match field { Some(existing) => existing.push_str(s), None => *field = Some(s.to_string()), } } fn mval_to_out(m: &MVal) -> Out { match m { MVal::S(s) => Out::One(Parsed::S(s.clone())), MVal::V(v) => Out::Many(v.iter().map(|s| Parsed::S(s.clone())).collect()), } } // ========================================================================= // texify 输出(对应 `_mhchemTexify`) // ========================================================================= fn texify_go(input: &[Parsed], add_outer_braces: bool) -> String { if input.is_empty() { return String::new(); } let mut res = String::new(); let mut cee = false; for p in input { match p { Parsed::S(s) => res.push_str(s), Parsed::N(n) => { res.push_str(&texify_go2(n)); if n.type_ == "1st-level escape" { cee = true; } } } } if add_outer_braces && !cee && !res.is_empty() { res = format!("{{{}}}", res); } res } fn go_inner(f: &Field) -> String { match f { Field::Str(s) => s.clone(), Field::Nodes(v) => texify_go(v, false), } } fn field_str(f: &Field) -> String { match f { Field::Str(s) => s.clone(), Field::Nodes(v) => texify_go(v, false), } } fn texify_go2(buf: &NodeData) -> String { match buf.type_.as_str() { "chemfive" => { let mut res = String::new(); let a = buf.a.as_ref().map(field_str).unwrap_or_default(); let b = buf.b.as_ref().map(field_str).unwrap_or_default(); let p = buf.p.as_ref().map(field_str).unwrap_or_default(); let o = buf.o.as_ref().map(field_str).unwrap_or_default(); let q = buf.q.as_ref().map(field_str).unwrap_or_default(); let d = buf.d.as_ref().map(field_str).unwrap_or_default(); // a if !a.is_empty() { let aa = if a.starts_with('+') || a.starts_with('-') { format!("{{{}}}", a) } else { a.clone() }; res.push_str(&aa); res.push_str("\\,"); } // b and p if !b.is_empty() || !p.is_empty() { res.push_str("{\\vphantom{A}}"); res.push_str(&format!("^{{\\hphantom{{{}}}}}_{{\\hphantom{{{}}}}}", b, p)); res.push_str("\\mkern-1.5mu"); res.push_str("{\\vphantom{A}}"); res.push_str(&format!( "^{{\\smash[t]{{\\vphantom{{2}}}}\\llap{{{}}}}}", b )); res.push_str(&format!( "_{{\\vphantom{{2}}\\llap{{\\smash[t]{{{}}}}}}}", p )); } // o if !o.is_empty() { let oo = if o.starts_with('+') || o.starts_with('-') { format!("{{{}}}", o) } else { o.clone() }; res.push_str(&oo); } // q and d match buf.d_type.as_deref() { Some("kv") => { if !d.is_empty() || !q.is_empty() { res.push_str("{\\vphantom{A}}"); } if !d.is_empty() { res.push_str(&format!("^{{{}}}", d)); } if !q.is_empty() { res.push_str(&format!("_{{\\smash[t]{{{}}}}}", q)); } } Some("oxidation") => { if !d.is_empty() { res.push_str("{\\vphantom{A}}"); res.push_str(&format!("^{{{}}}", d)); } if !q.is_empty() { res.push_str("{\\vphantom{A}}"); res.push_str(&format!("_{{\\smash[t]{{{}}}}}", q)); } } _ => { if !q.is_empty() { res.push_str("{\\vphantom{A}}"); res.push_str(&format!("_{{\\smash[t]{{{}}}}}", q)); } if !d.is_empty() { res.push_str("{\\vphantom{A}}"); res.push_str(&format!("^{{{}}}", d)); } } } res } "rm" => format!( "\\mathrm{{{}}}", buf.p1.as_ref().map(field_str).unwrap_or_default() ), "text" => { let mut p1 = buf.p1.as_ref().map(field_str).unwrap_or_default(); if p1.contains('^') || p1.contains('_') { p1 = p1.replace(' ', "~").replace('-', "\\text{-}"); format!("\\mathrm{{{}}}", p1) } else { format!("\\text{{{}}}", p1) } } "roman numeral" => format!( "\\mathrm{{{}}}", buf.p1.as_ref().map(field_str).unwrap_or_default() ), "state of aggregation" => format!( "\\mskip2mu {}", buf.p1.as_ref().map(go_inner).unwrap_or_default() ), "state of aggregation subscript" => format!( "\\mskip1mu {}", buf.p1.as_ref().map(go_inner).unwrap_or_default() ), "bond" => get_bond(buf.kind_.as_deref().unwrap_or("")), "frac" => { let c = format!( "\\frac{{{}}}{{{}}}", buf.p1.as_ref().map(field_str).unwrap_or_default(), buf.p2.as_ref().map(field_str).unwrap_or_default() ); format!("\\mathchoice{{\\textstyle{c}}}{{{c}}}{{{c}}}{{{c}}}") } "pu-frac" => { let d = format!( "\\frac{{{}}}{{{}}}", buf.p1.as_ref().map(go_inner).unwrap_or_default(), buf.p2.as_ref().map(go_inner).unwrap_or_default() ); format!("\\mathchoice{{\\textstyle{d}}}{{{d}}}{{{d}}}{{{d}}}") } "tex-math" => format!("{} ", buf.p1.as_ref().map(field_str).unwrap_or_default()), "frac-ce" => format!( "\\frac{{{}}}{{{}}}", buf.p1.as_ref().map(go_inner).unwrap_or_default(), buf.p2.as_ref().map(go_inner).unwrap_or_default() ), "overset" => format!( "\\overset{{{}}}{{{}}}", buf.p1.as_ref().map(go_inner).unwrap_or_default(), buf.p2.as_ref().map(go_inner).unwrap_or_default() ), "underset" => format!( "\\underset{{{}}}{{{}}}", buf.p1.as_ref().map(go_inner).unwrap_or_default(), buf.p2.as_ref().map(go_inner).unwrap_or_default() ), "underbrace" => format!( "\\underbrace{{{}}}_{{{}}}", buf.p1.as_ref().map(go_inner).unwrap_or_default(), buf.p2.as_ref().map(go_inner).unwrap_or_default() ), "color" => format!( "{{\\color{{{}}}{{{}}}}}", buf.color1.as_deref().unwrap_or(""), buf.color2.as_ref().map(go_inner).unwrap_or_default() ), "color0" => format!("\\color{{{}}}", buf.color.as_deref().unwrap_or("")), "arrow" => { let rd = buf.rd.as_ref().map(go_inner).unwrap_or_default(); let rq = buf.rq.as_ref().map(go_inner).unwrap_or_default(); let r = buf.r.as_deref().unwrap_or(""); let mut arrow = get_arrow(r).to_string(); if !rd.is_empty() || !rq.is_empty() { if matches!(r, "<=>" | "<=>>" | "<<=>" | "<-->") { arrow = format!("\\long{}", arrow); if !rd.is_empty() { arrow = format!("\\overset{{{}}}{{{}}}", rd, arrow); } if !rq.is_empty() { arrow = if r == "<-->" { format!("\\underset{{\\lower2mu{{{}}}}}{{{}}}", rq, arrow) } else { format!("\\underset{{\\lower6mu{{{}}}}}{{{}}}", rq, arrow) }; } arrow = format!(" {{}}\\mathrel{{{}}}{{}} ", arrow); } else { if !rq.is_empty() { arrow.push_str(&format!("[{{{}}}]", rq)); } arrow.push_str(&format!("{{{}}}", rd)); arrow = format!(" {{}}\\mathrel{{\\x{}}}{{}} ", arrow); } } else { arrow = format!(" {{}}\\mathrel{{\\long{}}}{{}} ", arrow); } arrow } "operator" => get_operator(buf.kind_.as_deref().unwrap_or("")), "1st-level escape" => format!("{} ", buf.p1.as_ref().map(field_str).unwrap_or_default()), "space" => " ".to_string(), "tinySkip" => "\\mkern2mu".to_string(), "entitySkip" => "~".to_string(), "pu-space-1" => "~".to_string(), "pu-space-2" => "\\mkern3mu ".to_string(), "1000 separator" => "\\mkern2mu ".to_string(), "commaDecimal" => "{,}".to_string(), "comma enumeration L" => format!( "{{{}}}\\mkern6mu ", buf.p1.as_ref().map(field_str).unwrap_or_default() ), "comma enumeration M" => format!( "{{{}}}\\mkern3mu ", buf.p1.as_ref().map(field_str).unwrap_or_default() ), "comma enumeration S" => format!( "{{{}}}\\mkern1mu ", buf.p1.as_ref().map(field_str).unwrap_or_default() ), "hyphen" => "\\text{-}".to_string(), "addition compound" => "\\,{\\cdot}\\,".to_string(), "electron dot" => "\\mkern1mu \\bullet\\mkern1mu ".to_string(), "KV x" => "{\\times}".to_string(), "prime" => "\\prime ".to_string(), "cdot" => "\\cdot ".to_string(), "tight cdot" => "\\mkern1mu{\\cdot}\\mkern1mu ".to_string(), "times" => "\\times ".to_string(), "circa" => "{\\sim}".to_string(), "^" => "uparrow".to_string(), "v" => "downarrow".to_string(), "ellipsis" => "\\ldots ".to_string(), "/" => "/".to_string(), " / " => "\\,/\\,".to_string(), _ => String::new(), } } fn get_arrow(a: &str) -> &'static str { match a { "->" | "\u{2192}" | "\u{27F6}" => "rightarrow", "<-" => "leftarrow", "<->" => "leftrightarrow", "<-->" => "leftrightarrows", "<=>" | "\u{21CC}" => "rightleftharpoons", "<=>>" => "Rightleftharpoons", "<<=>" => "Leftrightharpoons", _ => "rightarrow", } } fn get_bond(a: &str) -> String { match a { "-" | "1" => "{-}".to_string(), "=" | "2" => "{=}".to_string(), "#" | "3" => "{\\equiv}".to_string(), "~" => "{\\tripledash}".to_string(), "~-" => "{\\rlap{\\lower.1em{-}}\\raise.1em{\\tripledash}}".to_string(), "~=" | "~--" => "{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{\\tripledash}}-}".to_string(), "-~-" => "{\\rlap{\\lower.2em{-}}\\rlap{\\raise.2em{-}}\\tripledash}".to_string(), "..." => "{{\\cdot}{\\cdot}{\\cdot}}".to_string(), "...." => "{{\\cdot}{\\cdot}{\\cdot}{\\cdot}}".to_string(), "->" => "{\\rightarrow}".to_string(), "<-" => "{\\leftarrow}".to_string(), "<" => "{<}".to_string(), ">" => "{>}".to_string(), _ => format!("{{{}}}", a), } } fn get_operator(a: &str) -> String { match a { "+" => " {}+{} ".to_string(), "-" => " {}-{} ".to_string(), "=" => " {}={} ".to_string(), "<" => " {}<{} ".to_string(), ">" => " {}>{} ".to_string(), "<<" => " {}\\ll{} ".to_string(), ">>" => " {}\\gg{} ".to_string(), "\\pm" => " {}\\pm{} ".to_string(), "\\approx" | "$\\approx$" => " {}\\approx{} ".to_string(), "v" | "(v)" => " \\downarrow{} ".to_string(), "^" | "(^)" => " \\uparrow{} ".to_string(), _ => format!(" {{{}}} ", a), } } // ========================================================================= // 公开 API // ========================================================================= /// 把 `\ce{...}` 内容转译为 LaTeX。任何内部异常回退为原样输出。 pub fn ce(input: &str) -> String { to_tex(input, "ce") } /// 把 `\pu{...}` 内容转译为 LaTeX。任何内部异常回退为原样输出。 pub fn pu(input: &str) -> String { to_tex(input, "pu") } fn to_tex(input: &str, kind: &str) -> String { let result = std::panic::catch_unwind(|| { let parsed = go(input, kind); texify_go(&parsed, kind != "tex") }); result.unwrap_or_else(|_| input.to_string()) } // 状态机转移表 + 局部动作在 mhchem_tables.rs(同模块,含大量常量数据)。 include!("mhchem_tables.rs"); #[cfg(test)] mod tests { use super::*; #[test] fn ce_water_produces_mathrm() { let tex = ce("H2O"); assert!(tex.contains(r"\mathrm{H}"), "H 应为直立体: {tex}"); assert!(tex.contains(r"\mathrm{O}"), "O 应为直立体: {tex}"); } #[test] fn ce_reaction_has_arrow() { let tex = ce("2H2 + O2 -> 2H2O"); assert!(tex.contains("rightarrow"), "应含反应箭头: {tex}"); } #[test] fn ce_empty_is_empty() { assert_eq!(ce(""), ""); } #[test] fn ce_does_not_panic_on_garbage() { // 任意乱码不应 panic(容错回退为原样)。 let _ = ce("}}}{{{]][["); let _ = ce("\\frac{"); let _ = ce("<<<<>>>>"); } #[test] fn ce_gas_arrow_becomes_uparrow() { // 行尾 ^ 气体符号转译后应含 uparrow(消解原行尾 ^ 解析错误)。 let tex = ce("CaCO3 ->[\\Delta] CaO + CO2 ^"); assert!( tex.contains("uparrow") || tex.contains("rightarrow"), "气体/反应符号: {tex}" ); } #[test] fn pu_unit_has_mathrm() { let tex = pu("9.8 m/s^2"); assert!(tex.contains(r"\mathrm"), "单位应有直立体: {tex}"); } #[test] fn pu_empty_is_empty() { assert_eq!(pu(""), ""); } #[test] fn ce_charge_superscript() { let tex = ce("SO4^2-"); // 应含上标(^...)且无 panic。 assert!(!tex.is_empty(), "离子应产出非空: {tex}"); } }