1279 lines
46 KiB
Rust
Executable File
1279 lines
46 KiB
Rust
Executable File
#![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<String>),
|
||
}
|
||
|
||
/// 模式匹配结果。
|
||
#[derive(Clone, Debug)]
|
||
struct MMatch {
|
||
m: MVal,
|
||
remainder: String,
|
||
}
|
||
|
||
/// 解析中间表示的字段值:字符串或子表达式节点向量。
|
||
#[derive(Clone, Debug)]
|
||
enum Field {
|
||
Str(String),
|
||
Nodes(Vec<Parsed>),
|
||
}
|
||
|
||
/// 解析中间表示:字面字符串或节点。
|
||
#[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<Field>,
|
||
p2: Option<Field>,
|
||
a: Option<Field>,
|
||
b: Option<Field>,
|
||
p: Option<Field>,
|
||
o: Option<Field>,
|
||
q: Option<Field>,
|
||
d: Option<Field>,
|
||
color: Option<String>,
|
||
color1: Option<String>,
|
||
color2: Option<Field>,
|
||
r: Option<String>,
|
||
rd: Option<Field>,
|
||
rq: Option<Field>,
|
||
d_type: Option<String>,
|
||
kind_: Option<String>,
|
||
}
|
||
|
||
/// 状态机缓冲区(对应 TS 的动态 `buffer` 对象)。
|
||
#[derive(Clone, Default)]
|
||
struct Buffer {
|
||
a: Option<String>,
|
||
b: Option<String>,
|
||
p: Option<String>,
|
||
o: Option<String>,
|
||
q: Option<String>,
|
||
d: Option<String>,
|
||
rm: Option<String>,
|
||
text_: Option<String>,
|
||
r: Option<String>,
|
||
rd: Option<String>,
|
||
rq: Option<String>,
|
||
rdt: Option<String>,
|
||
rqt: Option<String>,
|
||
d_type: Option<String>,
|
||
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<String>,
|
||
}
|
||
|
||
#[derive(Clone, Default)]
|
||
struct Task {
|
||
actions: Vec<ActionRef>,
|
||
next_state: Option<String>,
|
||
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<String, Vec<Transition>> {
|
||
let mut transitions: HashMap<String, Vec<Transition>> = 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<String> = 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<String> = 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<Regex> = 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<String> {
|
||
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<Pat>,
|
||
beg2_incl: Option<Pat>,
|
||
end2_incl: Option<Pat>,
|
||
end2_excl: Option<Pat>,
|
||
combine: bool,
|
||
) -> Option<MMatch> {
|
||
// 第一组
|
||
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<Span> {
|
||
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<MMatch> {
|
||
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<Regex> = 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<Regex> = LazyLock::new(|| {
|
||
Regex::new(&format!(
|
||
"^\\\\(?:{})(?:\\s+|\\{{\\}}|(?![a-zA-Z]))",
|
||
GREEK_NAMES
|
||
))
|
||
.unwrap()
|
||
});
|
||
|
||
static ONE_GREEK_RE: LazyLock<Regex> = 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<MMatch> {
|
||
// ── 正则模式 ──
|
||
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<Pat>,
|
||
beg2_incl: Option<Pat>,
|
||
end2_incl: Option<Pat>,
|
||
end2_excl: Option<Pat>,
|
||
combine: bool,
|
||
) -> Option<MMatch> {
|
||
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<MMatch> {
|
||
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<MMatch> {
|
||
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<MMatch> {
|
||
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<MMatch> {
|
||
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<Parsed>),
|
||
}
|
||
|
||
fn concat(out: &mut Vec<Parsed>, 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<Parsed> {
|
||
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<Parsed> = Vec::new();
|
||
let mut last_input: Option<String> = 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<String>,
|
||
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<String>, 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<Parsed> = 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<String>, 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<String>, 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}");
|
||
}
|
||
}
|