yggdrasil/src/api/mhchem_tables.rs
xfy bcd13958ac
Some checks failed
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
feat(katex): 移植 mhchem 化学公式转译器(\ce/\pu → LaTeX)
katex-rs 默认无 mhchem 解析器,\ce/\pu 化学公式渲染为红字(实测正确页面 648
的 137 个公式中 32 处化学公式坏掉,含 \ce×24 + \pu×8)。

从 mhchemParser 4.2.2(Apache-2.0)机械移植状态机转译器为纯 Rust:
- src/api/mhchem.rs:数据模型 + 模式匹配 + go() 主循环 + texify 输出 + 公开 API
- src/api/mhchem_tables.rs:14 个状态机转移表 + 机器局部动作(ce/pu/...)
- 模式含 lookahead (?=)/(?!),Rust regex crate 不支持,故用 fancy-regex
  (已被 syntect 间接引入,此处显式声明为直接 server 依赖)
- ce/pu 包裹 catch_unwind,任何内部异常回退原样输出,绝不 panic
- katex.rs 新增 expand_chem:渲染前嵌套花括号配对扫描 \ce/\pu 预转译
- 行尾 ^ 气体符号转译为 \uparrow,消解原 mhchem 解析错误
- 8 个 mhchem 单测 + 5 个 katex 集成测试(水/反应箭头/气体箭头/单位/络离子)
2026-07-23 12:23:53 +08:00

1548 lines
53 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// mhchem 状态机转移表 + 机器局部动作(由 mhchem.rs `include!`)。
// 数据机械移植自 mhchemParser 4.2.2 的 `stateMachines`。
// (被 include 进 mhchem.rs 中部,故不能用 `//!` 内部文档注释。)
#[allow(clippy::needless_pass_by_value)]
use super::*;
// ── Task 构造器 ──────────────────────────────────────────────────────────
fn task(acts: &[(&str, Option<&str>)], next: Option<&str>, revisit: bool, cont: bool) -> Task {
Task {
actions: acts
.iter()
.map(|(t, o)| ActionRef {
type_: (*t).to_string(),
option: o.map(|s| s.to_string()),
})
.collect(),
next_state: next.map(|s| s.to_string()),
revisit,
to_continue: cont,
}
}
fn raw(patterns: &'static str, states: &'static str, task: Task) -> RawEntry {
RawEntry {
patterns,
states,
task,
}
}
// ── 转移表取用 ────────────────────────────────────────────────────────────
fn transitions_for(machine: &str) -> &'static HashMap<String, Vec<Transition>> {
match machine {
"tex" => &TEX,
"ce" => &CE,
"a" => &A,
"o" => &O,
"text" => &TEXT,
"pq" => &PQ,
"bd" => &BD,
"oxidation" => &OXIDATION,
"tex-math" => &TEX_MATH,
"tex-math tight" => &TEX_MATH_TIGHT,
"9,9" => &NUM99,
"pu" => &PU,
"pu-2" => &PU2,
"pu-9,9" => &PU99,
_ => &CE,
}
}
// =========================================================================
// tex 状态机
// =========================================================================
static TEX: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("copy", None)], None, false, false)),
raw(
"\\ce{(...)}",
"0",
task(
&[
("write", Some("{")),
("ce", None),
("write", Some("}")),
],
None,
false,
false,
),
),
raw(
"\\pu{(...)}",
"0",
task(
&[
("write", Some("{")),
("pu", None),
("write", Some("}")),
],
None,
false,
false,
),
),
raw("else", "0", task(&[("copy", None)], None, false, false)),
])
});
// =========================================================================
// ce 状态机(主解析器)
// =========================================================================
static CE: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("output", None)], None, false, false)),
raw(
"else",
"0|1|2",
task(&[("beginsWithBond=false", None)], None, true, true),
),
raw(
"oxidation$",
"0",
task(&[("oxidation-output", None)], None, false, false),
),
raw(
"CMT",
"r",
task(&[("rdt=", None)], Some("rt"), false, false),
),
raw(
"CMT",
"rd",
task(&[("rqt=", None)], Some("rdt"), false, false),
),
raw(
"arrowUpDown",
"0|1|2|as",
task(&[("sb=false", None), ("output", None), ("operator", None)], Some("1"), false, false),
),
raw(
"uprightEntities",
"0|1|2",
task(&[("o=", None), ("output", None)], Some("1"), false, false),
),
raw("orbital", "0|1|2|3", task(&[("o=", None)], Some("o"), false, false)),
raw("->", "0|1|2|3", task(&[("r=", None)], Some("r"), false, false)),
raw(
"->",
"a|as",
task(&[("output", None), ("r=", None)], Some("r"), false, false),
),
raw(
"->",
"*",
task(&[("output", None), ("r=", None)], Some("r"), false, false),
),
raw("+", "o", task(&[("d= kv", None)], Some("d"), false, false)),
raw("+", "d|D", task(&[("d=", None)], Some("d"), false, false)),
raw("+", "q", task(&[("d=", None)], Some("qd"), false, false)),
raw("+", "qd|qD", task(&[("d=", None)], Some("qd"), false, false)),
raw(
"+",
"dq",
task(&[("output", None), ("d=", None)], Some("d"), false, false),
),
raw(
"+",
"3",
task(&[("sb=false", None), ("output", None), ("operator", None)], Some("0"), false, false),
),
raw("amount", "0|2", task(&[("a=", None)], Some("a"), false, false)),
raw(
"pm-operator",
"0|1|2|a|as",
task(&[("sb=false", None), ("output", None), ("operator", Some("\\pm"))], Some("0"), false, false),
),
raw(
"operator",
"0|1|2|a|as",
task(&[("sb=false", None), ("output", None), ("operator", None)], Some("0"), false, false),
),
raw(
"-$",
"o|q",
task(&[("charge or bond", None), ("output", None)], Some("qd"), false, false),
),
raw("-$", "d", task(&[("d=", None)], Some("d"), false, false)),
raw(
"-$",
"D",
task(&[("output", None), ("bond", Some("-"))], Some("3"), false, false),
),
raw("-$", "q", task(&[("d=", None)], Some("qd"), false, false)),
raw("-$", "qd", task(&[("d=", None)], Some("qd"), false, false)),
raw(
"-$",
"qD|dq",
task(&[("output", None), ("bond", Some("-"))], Some("3"), false, false),
),
raw(
"-9",
"3|o",
task(&[("output", None), ("insert", Some("hyphen"))], Some("3"), false, false),
),
raw(
"- orbital overlap",
"o",
task(&[("output", None), ("insert", Some("hyphen"))], Some("2"), false, false),
),
raw(
"- orbital overlap",
"d",
task(&[("output", None), ("insert", Some("hyphen"))], Some("2"), false, false),
),
raw(
"-",
"0|1|2",
task(&[("output", Some("1")), ("beginsWithBond=true", None), ("bond", Some("-"))], Some("3"), false, false),
),
raw("-", "3", task(&[("bond", Some("-"))], None, false, false)),
raw(
"-",
"a",
task(&[("output", None), ("insert", Some("hyphen"))], Some("2"), false, false),
),
raw(
"-",
"as",
task(&[("output", Some("2")), ("bond", Some("-"))], Some("3"), false, false),
),
raw("-", "b", task(&[("b=", None)], None, false, false)),
raw(
"-",
"o",
task(&[("- after o/d", Some("false"))], Some("2"), false, false),
),
raw(
"-",
"q",
task(&[("- after o/d", Some("false"))], Some("2"), false, false),
),
raw(
"-",
"d|qd|dq",
task(&[("- after o/d", Some("true"))], Some("2"), false, false),
),
raw(
"-",
"D|qD|p",
task(&[("output", None), ("bond", Some("-"))], Some("3"), false, false),
),
raw("amount2", "1|3", task(&[("a=", None)], Some("a"), false, false)),
raw(
"letters",
"0|1|2|3|a|as|b|p|bp|o",
task(&[("o=", None)], Some("o"), false, false),
),
raw(
"letters",
"q|dq",
task(&[("output", None), ("o=", None)], Some("o"), false, false),
),
raw(
"letters",
"d|D|qd|qD",
task(&[("o after d", None)], Some("o"), false, false),
),
raw("digits", "o", task(&[("q=", None)], Some("q"), false, false)),
raw("digits", "d|D", task(&[("q=", None)], Some("dq"), false, false)),
raw(
"digits",
"q",
task(&[("output", None), ("o=", None)], Some("o"), false, false),
),
raw("digits", "a", task(&[("o=", None)], Some("o"), false, false)),
raw("space A", "b|p|bp", task(&[], None, false, false)),
raw("space", "a", task(&[], Some("as"), false, false)),
raw("space", "0", task(&[("sb=false", None)], None, false, false)),
raw("space", "1|2", task(&[("sb=true", None)], None, false, false)),
raw(
"space",
"r|rt|rd|rdt|rdq",
task(&[("output", None)], Some("0"), false, false),
),
raw(
"space",
"*",
task(&[("output", None), ("sb=true", None)], Some("1"), false, false),
),
raw(
"1st-level escape",
"1|2",
task(&[("output", None), ("insert+p1", Some("1st-level escape"))], None, false, false),
),
raw(
"1st-level escape",
"*",
task(&[("output", None), ("insert+p1", Some("1st-level escape"))], Some("0"), false, false),
),
raw("[(...)]", "r|rt", task(&[("rd=", None)], Some("rd"), false, false)),
raw(
"[(...)]",
"rd|rdt",
task(&[("rq=", None)], Some("rdq"), false, false),
),
raw(
"...",
"o|d|D|dq|qd|qD",
task(&[("output", None), ("bond", Some("..."))], Some("3"), false, false),
),
raw(
"...",
"*",
task(&[("output", Some("1")), ("insert", Some("ellipsis"))], Some("1"), false, false),
),
raw(
". __* ",
"*",
task(&[("output", None), ("insert", Some("addition compound"))], Some("1"), false, false),
),
raw(
"state of aggregation $",
"*",
task(&[("output", None), ("state of aggregation", None)], Some("1"), false, false),
),
raw(
"{[(",
"a|as|o",
task(&[("o=", None), ("output", None), ("parenthesisLevel++", None)], Some("2"), false, false),
),
raw(
"{[(",
"0|1|2|3",
task(&[("o=", None), ("output", None), ("parenthesisLevel++", None)], Some("2"), false, false),
),
raw(
"{[(",
"*",
task(&[("output", None), ("o=", None), ("output", None), ("parenthesisLevel++", None)], Some("2"), false, false),
),
raw(
")]}",
"0|1|2|3|b|p|bp|o",
task(&[("o=", None), ("parenthesisLevel--", None)], Some("o"), false, false),
),
raw(
")]}",
"a|as|d|D|q|qd|qD|dq",
task(&[("output", None), ("o=", None), ("parenthesisLevel--", None)], Some("o"), false, false),
),
raw(
", ",
"*",
task(&[("output", None), ("comma", None)], Some("0"), false, false),
),
raw("^_", "*", task(&[], None, false, false)),
raw(
"^{(...)}|^($...$)",
"0|1|2|as",
task(&[("b=", None)], Some("b"), false, false),
),
raw(
"^{(...)}|^($...$)",
"p",
task(&[("b=", None)], Some("bp"), false, false),
),
raw(
"^{(...)}|^($...$)",
"3|o",
task(&[("d= kv", None)], Some("D"), false, false),
),
raw(
"^{(...)}|^($...$)",
"q",
task(&[("d=", None)], Some("qD"), false, false),
),
raw(
"^{(...)}|^($...$)",
"d|D|qd|qD|dq",
task(&[("output", None), ("d=", None)], Some("D"), false, false),
),
raw(
"^a|^\\x{}{}|^\\x{}|^\\x|'",
"0|1|2|as",
task(&[("b=", None)], Some("b"), false, false),
),
raw(
"^a|^\\x{}{}|^\\x{}|^\\x|'",
"p",
task(&[("b=", None)], Some("bp"), false, false),
),
raw(
"^a|^\\x{}{}|^\\x{}|^\\x|'",
"3|o",
task(&[("d= kv", None)], Some("d"), false, false),
),
raw(
"^a|^\\x{}{}|^\\x{}|^\\x|'",
"q",
task(&[("d=", None)], Some("qd"), false, false),
),
raw(
"^a|^\\x{}{}|^\\x{}|^\\x|'",
"d|qd|D|qD",
task(&[("d=", None)], None, false, false),
),
raw(
"^a|^\\x{}{}|^\\x{}|^\\x|'",
"dq",
task(&[("output", None), ("d=", None)], Some("d"), false, false),
),
raw(
"_{(state of aggregation)}$",
"d|D|q|qd|qD|dq",
task(&[("output", None), ("q=", None)], Some("q"), false, false),
),
raw(
"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x",
"0|1|2|as",
task(&[("p=", None)], Some("p"), false, false),
),
raw(
"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x",
"b",
task(&[("p=", None)], Some("bp"), false, false),
),
raw(
"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x",
"3|o",
task(&[("q=", None)], Some("q"), false, false),
),
raw(
"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x",
"d|D",
task(&[("q=", None)], Some("dq"), false, false),
),
raw(
"_{(...)}|_($...$)|_9|_\\x{}{}|_\\x{}|_\\x",
"q|qd|qD|dq",
task(&[("output", None), ("q=", None)], Some("q"), false, false),
),
raw(
"=<>",
"0|1|2|3|a|as|o|q|d|D|qd|qD|dq",
task(&[("output", Some("2")), ("bond", None)], Some("3"), false, false),
),
raw(
"#",
"0|1|2|3|a|as|o",
task(&[("output", Some("2")), ("bond", Some("#"))], Some("3"), false, false),
),
raw(
"{}^",
"*",
task(&[("output", Some("1")), ("insert", Some("tinySkip"))], Some("1"), false, false),
),
raw("{}", "*", task(&[("output", Some("1"))], Some("1"), false, false)),
raw(
"{...}",
"0|1|2|3|a|as|b|p|bp",
task(&[("o=", None)], Some("o"), false, false),
),
raw(
"{...}",
"o|d|D|q|qd|qD|dq",
task(&[("output", None), ("o=", None)], Some("o"), false, false),
),
raw("$...$", "a", task(&[("a=", None)], None, false, false)),
raw(
"$...$",
"0|1|2|3|as|b|p|bp|o",
task(&[("o=", None)], Some("o"), false, false),
),
raw("$...$", "as|o", task(&[("o=", None)], None, false, false)),
raw(
"$...$",
"q|d|D|qd|qD|dq",
task(&[("output", None), ("o=", None)], Some("o"), false, false),
),
raw(
"\\bond{(...)}",
"*",
task(&[("output", Some("2")), ("bond", None)], Some("3"), false, false),
),
raw(
"\\frac{(...)}",
"*",
task(&[("output", Some("1")), ("frac-output", None)], Some("3"), false, false),
),
raw(
"\\overset{(...)}",
"*",
task(&[("output", Some("2")), ("overset-output", None)], Some("3"), false, false),
),
raw(
"\\underset{(...)}",
"*",
task(&[("output", Some("2")), ("underset-output", None)], Some("3"), false, false),
),
raw(
"\\underbrace{(...)}",
"*",
task(&[("output", Some("2")), ("underbrace-output", None)], Some("3"), false, false),
),
raw(
"\\color{(...)}{(...)}",
"*",
task(&[("output", Some("2")), ("color-output", None)], Some("3"), false, false),
),
raw(
"\\color{(...)}",
"*",
task(&[("output", Some("2")), ("color0-output", None)], None, false, false),
),
raw(
"\\ce{(...)}",
"*",
task(&[("output", Some("2")), ("ce", None)], Some("3"), false, false),
),
raw(
"\\,",
"*",
task(&[("output", Some("1")), ("copy", None)], Some("1"), false, false),
),
raw(
"\\pu{(...)}",
"*",
task(&[("output", None), ("write", Some("{")), ("pu", None), ("write", Some("}"))], Some("3"), false, false),
),
raw(
"\\x{}{}|\\x{}|\\x",
"0|1|2|3|a|as|b|p|bp|o|c0",
task(&[("o=", None), ("output", None)], Some("3"), false, false),
),
raw(
"\\x{}{}|\\x{}|\\x",
"*",
task(&[("output", None), ("o=", None), ("output", None)], Some("3"), false, false),
),
raw(
"others",
"*",
task(&[("output", Some("1")), ("copy", None)], Some("3"), false, false),
),
raw(
"else2",
"a",
task(&[("a to o", None)], Some("o"), true, false),
),
raw(
"else2",
"as",
task(&[("output", None), ("sb=true", None)], Some("1"), true, false),
),
raw(
"else2",
"r|rt|rd|rdt|rdq",
task(&[("output", None)], Some("0"), true, false),
),
raw(
"else2",
"*",
task(&[("output", None), ("copy", None)], Some("3"), true, false),
),
])
});
// =========================================================================
// a / o / text / pq / bd / oxidation / tex-math / tex-math tight / 9,9
// =========================================================================
static A: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[], None, false, false)),
raw("1/2$", "0", task(&[("1/2", None)], None, false, false)),
raw("else", "0", task(&[], Some("1"), true, false)),
raw(
"${(...)}$__$(...)$",
"*",
task(&[("tex-math tight", None)], Some("1"), false, false),
),
raw(
",",
"*",
task(&[("insert", Some("commaDecimal"))], None, false, false),
),
raw("else2", "*", task(&[("copy", None)], None, false, false)),
])
});
static O: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[], None, false, false)),
raw("1/2$", "0", task(&[("1/2", None)], None, false, false)),
raw("else", "0", task(&[], Some("1"), true, false)),
raw("letters", "*", task(&[("rm", None)], None, false, false)),
raw(
"\\ca",
"*",
task(&[("insert", Some("circa"))], None, false, false),
),
raw(
"\\pu{(...)}",
"*",
task(&[("write", Some("{")), ("pu", None), ("write", Some("}"))], None, false, false),
),
raw("\\x{}{}|\\x{}|\\x", "*", task(&[("copy", None)], None, false, false)),
raw(
"${(...)}$__$(...)$",
"*",
task(&[("tex-math", None)], None, false, false),
),
raw(
"{(...)}",
"*",
task(&[("write", Some("{")), ("text", None), ("write", Some("}"))], None, false, false),
),
raw("else2", "*", task(&[("copy", None)], None, false, false)),
])
});
static TEXT: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("output", None)], None, false, false)),
raw("{...}", "*", task(&[("text=", None)], None, false, false)),
raw(
"${(...)}$__$(...)$",
"*",
task(&[("tex-math", None)], None, false, false),
),
raw(
"\\greek",
"*",
task(&[("output", None), ("rm", None)], None, false, false),
),
raw(
"\\pu{(...)}",
"*",
task(&[("output", None), ("write", Some("{")), ("pu", None), ("write", Some("}"))], None, false, false),
),
raw(
"\\,|\\x{}{}|\\x{}|\\x",
"*",
task(&[("output", None), ("copy", None)], None, false, false),
),
raw("else", "*", task(&[("text=", None)], None, false, false)),
])
});
static PQ: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[], None, false, false)),
raw(
"state of aggregation $",
"*",
task(&[("state of aggregation", None)], None, false, false),
),
raw("i$", "0", task(&[], Some("!f"), true, false)),
raw("(KV letters),", "0", task(&[("rm", None)], Some("0"), false, false)),
raw("formula$", "0", task(&[], Some("f"), true, false)),
raw("1/2$", "0", task(&[("1/2", None)], None, false, false)),
raw("else", "0", task(&[], Some("!f"), true, false)),
raw(
"${(...)}$__$(...)$",
"*",
task(&[("tex-math", None)], None, false, false),
),
raw("{(...)}", "*", task(&[("text", None)], None, false, false)),
raw("a-z", "f", task(&[("tex-math", None)], None, false, false)),
raw("letters", "*", task(&[("rm", None)], None, false, false)),
raw("-9.,9", "*", task(&[("9,9", None)], None, false, false)),
raw(
",",
"*",
task(&[("insert+p1", Some("comma enumeration S"))], None, false, false),
),
raw(
"\\color{(...)}{(...)}",
"*",
task(&[("color-output", None)], None, false, false),
),
raw(
"\\color{(...)}",
"*",
task(&[("color0-output", None)], None, false, false),
),
raw("\\ce{(...)}", "*", task(&[("ce", None)], None, false, false)),
raw(
"\\pu{(...)}",
"*",
task(&[("write", Some("{")), ("pu", None), ("write", Some("}"))], None, false, false),
),
raw("\\,|\\x{}{}|\\x{}|\\x", "*", task(&[("copy", None)], None, false, false)),
raw("else2", "*", task(&[("copy", None)], None, false, false)),
])
});
static BD: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[], None, false, false)),
raw("x$", "0", task(&[], Some("!f"), true, false)),
raw("formula$", "0", task(&[], Some("f"), true, false)),
raw("else", "0", task(&[], Some("!f"), true, false)),
raw("-9.,9 no missing 0", "*", task(&[("9,9", None)], None, false, false)),
raw(
".",
"*",
task(&[("insert", Some("electron dot"))], None, false, false),
),
raw("a-z", "f", task(&[("tex-math", None)], None, false, false)),
raw("x", "*", task(&[("insert", Some("KV x"))], None, false, false)),
raw("letters", "*", task(&[("rm", None)], None, false, false)),
raw("'", "*", task(&[("insert", Some("prime"))], None, false, false)),
raw(
"${(...)}$__$(...)$",
"*",
task(&[("tex-math", None)], None, false, false),
),
raw("{(...)}", "*", task(&[("text", None)], None, false, false)),
raw(
"\\color{(...)}{(...)}",
"*",
task(&[("color-output", None)], None, false, false),
),
raw(
"\\color{(...)}",
"*",
task(&[("color0-output", None)], None, false, false),
),
raw("\\ce{(...)}", "*", task(&[("ce", None)], None, false, false)),
raw(
"\\pu{(...)}",
"*",
task(&[("write", Some("{")), ("pu", None), ("write", Some("}"))], None, false, false),
),
raw("\\,|\\x{}{}|\\x{}|\\x", "*", task(&[("copy", None)], None, false, false)),
raw("else2", "*", task(&[("copy", None)], None, false, false)),
])
});
static OXIDATION: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("roman-numeral", None)], None, false, false)),
raw(
"pm-operator",
"*",
task(&[("o=+p1", Some("\\pm"))], None, false, false),
),
raw("else", "*", task(&[("o=", None)], None, false, false)),
])
});
static TEX_MATH: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("output", None)], None, false, false)),
raw(
"\\ce{(...)}",
"*",
task(&[("output", None), ("ce", None)], None, false, false),
),
raw(
"\\pu{(...)}",
"*",
task(&[("output", None), ("write", Some("{")), ("pu", None), ("write", Some("}"))], None, false, false),
),
raw("{...}|\\,|\\x{}{}|\\x{}|\\x", "*", task(&[("o=", None)], None, false, false)),
raw("else", "*", task(&[("o=", None)], None, false, false)),
])
});
static TEX_MATH_TIGHT: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("output", None)], None, false, false)),
raw(
"\\ce{(...)}",
"*",
task(&[("output", None), ("ce", None)], None, false, false),
),
raw(
"\\pu{(...)}",
"*",
task(&[("output", None), ("write", Some("{")), ("pu", None), ("write", Some("}"))], None, false, false),
),
raw("{...}|\\,|\\x{}{}|\\x{}|\\x", "*", task(&[("o=", None)], None, false, false)),
raw("-|+", "*", task(&[("tight operator", None)], None, false, false)),
raw("else", "*", task(&[("o=", None)], None, false, false)),
])
});
static NUM99: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[], None, false, false)),
raw(",", "*", task(&[("comma", None)], None, false, false)),
raw("else", "*", task(&[("copy", None)], None, false, false)),
])
});
// =========================================================================
// pu / pu-2 / pu-9,9 状态机
// =========================================================================
static PU: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("output", None)], None, false, false)),
raw(
"space$",
"*",
task(&[("output", None), ("space", None)], None, false, false),
),
raw("{[(|)]}", "0|a", task(&[("copy", None)], None, false, false)),
raw(
"(-)(9)^(-9)",
"0",
task(&[("number^", None)], Some("a"), false, false),
),
raw(
"(-)(9.,9)(e)(99)",
"0",
task(&[("enumber", None)], Some("a"), false, false),
),
raw("space", "0|a", task(&[], None, false, false)),
raw(
"pm-operator",
"0|a",
task(&[("operator", Some("\\pm"))], Some("0"), false, false),
),
raw("operator", "0|a", task(&[("copy", None)], Some("0"), false, false)),
raw("//", "d", task(&[("o=", None)], Some("/"), false, false)),
raw("/", "d", task(&[("o=", None)], Some("/"), false, false)),
raw(
"{...}|else",
"0|d",
task(&[("d=", None)], Some("d"), false, false),
),
raw(
"{...}|else",
"a",
task(&[("space", None), ("d=", None)], Some("d"), false, false),
),
raw(
"{...}|else",
"/|q",
task(&[("q=", None)], Some("q"), false, false),
),
])
});
static PU2: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "*", task(&[("output", None)], None, false, false)),
raw(
"*",
"*",
task(&[("output", None), ("cdot", None)], Some("0"), false, false),
),
raw("\\x", "*", task(&[("rm=", None)], None, false, false)),
raw(
"space",
"*",
task(&[("output", None), ("space", None)], Some("0"), false, false),
),
raw(
"^{(...)}|^(-1)",
"1",
task(&[("^(-1)", None)], None, false, false),
),
raw("-9.,9", "0", task(&[("rm=", None)], Some("0"), false, false)),
raw(
"-9.,9",
"1",
task(&[("^(-1)", None)], Some("0"), false, false),
),
raw("{...}|else", "*", task(&[("rm=", None)], Some("1"), false, false)),
])
});
static PU99: LazyLock<HashMap<String, Vec<Transition>>> = LazyLock::new(|| {
build_transitions(&[
raw("empty", "0", task(&[("output-0", None)], None, false, false)),
raw("empty", "o", task(&[("output-o", None)], None, false, false)),
raw(
",",
"0",
task(&[("output-0", None), ("comma", None)], Some("o"), false, false),
),
raw(
".",
"0",
task(&[("output-0", None), ("copy", None)], Some("o"), false, false),
),
raw("else", "*", task(&[("text=", None)], None, false, false)),
])
});
// =========================================================================
// 机器局部动作分发
// =========================================================================
fn machine_action(
machine: &str,
buf: &mut Buffer,
m: &MVal,
opt: &Option<String>,
type_: &str,
) -> Option<Out> {
match machine {
"ce" => ce_action(buf, m, opt, type_),
"text" => text_action(buf, type_),
"pq" => pq_action(buf, m, type_),
"bd" => bd_action(buf, m, type_),
"oxidation" => oxidation_action(buf, type_),
"tex-math" | "tex-math tight" => texmath_action(machine, buf, m, type_),
"9,9" => Some(num99_action(type_)),
"pu" => pu_action(buf, m, opt, type_),
"pu-2" => pu2_action(buf, m, type_),
"pu-9,9" => pu99_action(buf, type_),
_ => None,
}
}
fn gof(opt_str: &Option<String>, m: &str) -> Field {
Field::Nodes(go(opt_str.as_deref().unwrap_or(""), m))
}
fn arrow_sub(buf_field: &Option<String>, rdt: &Option<String>, ce_m: &str) -> Field {
match rdt.as_deref() {
Some("M") => Field::Nodes(go(buf_field.as_deref().unwrap_or(""), "tex-math")),
Some("T") => Field::Nodes(vec![Parsed::N(NodeData {
type_: "text".into(),
p1: Some(Field::Str(buf_field.clone().unwrap_or_default())),
..Default::default()
})]),
_ => Field::Nodes(go(buf_field.as_deref().unwrap_or(""), ce_m)),
}
}
fn ce_action(buf: &mut Buffer, m: &MVal, opt: &Option<String>, type_: &str) -> Option<Out> {
let out = match type_ {
"o after d" => {
let mut ret: Vec<Parsed> = Vec::new();
let d_is_int = buf
.d
.as_ref()
.map(|d| re!("^[1-9][0-9]*$").is_match(d).unwrap_or(false))
.unwrap_or(false);
if d_is_int {
let tmp = buf.d.take();
concat(&mut ret, ce_action(buf, m, &None, "output").unwrap_or(Out::None));
ret.push(Parsed::N(NodeData {
type_: "tinySkip".into(),
..Default::default()
}));
buf.b = tmp;
} else {
concat(&mut ret, ce_action(buf, m, &None, "output").unwrap_or(Out::None));
}
append_field(&mut buf.o, m);
Out::Many(ret)
}
"d= kv" => {
buf.d = Some(mval_str(m));
buf.d_type = Some("kv".into());
Out::None
}
"charge or bond" => {
if buf.begins_with_bond {
let mut ret: Vec<Parsed> = Vec::new();
concat(&mut ret, ce_action(buf, m, &None, "output").unwrap_or(Out::None));
ret.push(Parsed::N(NodeData {
type_: "bond".into(),
kind_: Some("-".into()),
..Default::default()
}));
Out::Many(ret)
} else {
buf.d = Some(mval_str(m));
Out::None
}
}
"- after o/d" => {
let is_after_d = opt.as_deref() == Some("true");
let o_str = buf.o.clone().unwrap_or_default();
let c1 = match_pattern("orbital", &o_str);
let c2 = match_pattern("one lowercase greek letter $", &o_str);
let c3 = match_pattern("one lowercase latin letter $", &o_str);
let c4 = match_pattern("$one lowercase latin letter$ $", &o_str);
let m_str = mval_str(m);
let hyphen_follows = m_str == "-"
&& ((c1.as_ref().map(|x| x.remainder.is_empty()).unwrap_or(false))
|| c2.is_some()
|| c3.is_some()
|| c4.is_some());
if hyphen_follows
&& buf.a.is_none()
&& buf.b.is_none()
&& buf.p.is_none()
&& buf.d.is_none()
&& buf.q.is_none()
&& c1.is_none()
&& c3.is_some()
{
let ov = buf.o.take().unwrap_or_default();
buf.o = Some(format!("${}$", ov));
}
let mut ret: Vec<Parsed> = Vec::new();
if hyphen_follows {
concat(&mut ret, ce_action(buf, m, &None, "output").unwrap_or(Out::None));
ret.push(Parsed::N(NodeData {
type_: "hyphen".into(),
..Default::default()
}));
} else {
let d_str = buf.d.clone().unwrap_or_default();
let c1d = match_pattern("digits", &d_str);
if is_after_d && c1d.as_ref().map(|x| x.remainder.is_empty()).unwrap_or(false) {
append_field(&mut buf.d, m);
concat(&mut ret, ce_action(buf, m, &None, "output").unwrap_or(Out::None));
} else {
concat(&mut ret, ce_action(buf, m, &None, "output").unwrap_or(Out::None));
ret.push(Parsed::N(NodeData {
type_: "bond".into(),
kind_: Some("-".into()),
..Default::default()
}));
}
}
Out::Many(ret)
}
"a to o" => {
buf.o = buf.a.take();
Out::None
}
"sb=true" => {
buf.sb = true;
Out::None
}
"sb=false" => {
buf.sb = false;
Out::None
}
"beginsWithBond=true" => {
buf.begins_with_bond = true;
Out::None
}
"beginsWithBond=false" => {
buf.begins_with_bond = false;
Out::None
}
"parenthesisLevel++" => {
buf.parenthesis_level += 1;
Out::None
}
"parenthesisLevel--" => {
buf.parenthesis_level -= 1;
Out::None
}
"state of aggregation" => Out::One(Parsed::N(NodeData {
type_: "state of aggregation".into(),
p1: Some(Field::Nodes(go(&mval_str(m), "o"))),
..Default::default()
})),
"comma" => {
let raw = mval_str(m);
let a = raw.trim_end();
let with_space = a != raw;
let kind = if with_space && buf.parenthesis_level == 0 {
"comma enumeration L"
} else {
"comma enumeration M"
};
Out::One(Parsed::N(NodeData {
type_: kind.into(),
p1: Some(Field::Str(a.to_string())),
..Default::default()
}))
}
"output" => {
let entity_follows = match opt.as_deref() {
Some("1") => 1,
Some("2") => 2,
_ => 0,
};
let ret: Vec<Parsed> = if buf.r.is_none() {
let mut ret = Vec::new();
let empty = buf.a.is_none()
&& buf.b.is_none()
&& buf.p.is_none()
&& buf.o.is_none()
&& buf.q.is_none()
&& buf.d.is_none()
&& entity_follows == 0;
if !empty {
if buf.sb {
ret.push(Parsed::N(NodeData {
type_: "entitySkip".into(),
..Default::default()
}));
}
let o_none = buf.o.is_none();
let q_none = buf.q.is_none();
let d_none = buf.d.is_none();
let b_none = buf.b.is_none();
let p_none = buf.p.is_none();
if o_none && q_none && d_none && b_none && p_none && entity_follows != 2 {
buf.o = buf.a.take();
} else if o_none && q_none && d_none && (buf.b.is_some() || buf.p.is_some()) {
buf.o = buf.a.take();
buf.d = buf.b.take();
buf.q = buf.p.take();
} else if buf.o.is_some()
&& buf.d_type.as_deref() == Some("kv")
&& match_pattern("d-oxidation$", &buf.d.clone().unwrap_or_default()).is_some()
{
buf.d_type = Some("oxidation".into());
} else if buf.o.is_some() && buf.d_type.as_deref() == Some("kv") && buf.q.is_none() {
buf.d_type = None;
}
let d_machine = if buf.d_type.as_deref() == Some("oxidation") {
"oxidation"
} else {
"bd"
};
ret.push(Parsed::N(NodeData {
type_: "chemfive".into(),
a: Some(gof(&buf.a.clone(), "a")),
b: Some(gof(&buf.b.clone(), "bd")),
p: Some(gof(&buf.p.clone(), "pq")),
o: Some(gof(&buf.o.clone(), "o")),
q: Some(gof(&buf.q.clone(), "pq")),
d: Some(gof(&buf.d.clone(), d_machine)),
d_type: buf.d_type.clone(),
..Default::default()
}));
}
ret
} else {
let rd = arrow_sub(&buf.rd.clone(), &buf.rdt.clone(), "ce");
let rq = arrow_sub(&buf.rq.clone(), &buf.rqt.clone(), "ce");
vec![Parsed::N(NodeData {
type_: "arrow".into(),
r: buf.r.clone(),
rd: Some(rd),
rq: Some(rq),
..Default::default()
})]
};
buf.clear(true);
Out::Many(ret)
}
"oxidation-output" => {
let mut ret: Vec<Parsed> = vec![Parsed::S("{".into())];
ret.extend(go(&mval_str(m), "oxidation"));
ret.push(Parsed::S("}".into()));
Out::Many(ret)
}
"frac-output" | "overset-output" | "underset-output" | "underbrace-output" => {
let (node_type, p1m, p2m) = match type_ {
"frac-output" => ("frac-ce", 0usize, 1usize),
"overset-output" => ("overset", 0, 1),
"underset-output" => ("underset", 0, 1),
_ => ("underbrace", 0, 1),
};
let (g1, g2) = mval_pair(m, p1m, p2m);
Out::One(Parsed::N(NodeData {
type_: node_type.into(),
p1: Some(Field::Nodes(go(&g1, "ce"))),
p2: Some(Field::Nodes(go(&g2, "ce"))),
..Default::default()
}))
}
"color-output" => {
let (g1, g2) = mval_pair(m, 0, 1);
Out::One(Parsed::N(NodeData {
type_: "color".into(),
color1: Some(g1),
color2: Some(Field::Nodes(go(&g2, "ce"))),
..Default::default()
}))
}
"r=" => {
buf.r = Some(mval_str(m));
Out::None
}
"rdt=" => {
buf.rdt = Some(mval_str(m));
Out::None
}
"rd=" => {
buf.rd = Some(mval_str(m));
Out::None
}
"rqt=" => {
buf.rqt = Some(mval_str(m));
Out::None
}
"rq=" => {
buf.rq = Some(mval_str(m));
Out::None
}
"operator" => Out::One(Parsed::N(NodeData {
type_: "operator".into(),
kind_: Some(opt.clone().unwrap_or_else(|| mval_str(m))),
..Default::default()
})),
_ => return None,
};
Some(out)
}
fn mval_pair(m: &MVal, i: usize, j: usize) -> (String, String) {
match m {
MVal::V(v) => (
v.get(i).cloned().unwrap_or_default(),
v.get(j).cloned().unwrap_or_default(),
),
MVal::S(s) => (s.clone(), String::new()),
}
}
fn text_action(buf: &mut Buffer, type_: &str) -> Option<Out> {
if type_ == "output" {
let ret = buf.text_.take().map(|t| {
Out::One(Parsed::N(NodeData {
type_: "text".into(),
p1: Some(Field::Str(t)),
..Default::default()
}))
});
buf.clear(false);
Some(ret.unwrap_or(Out::None))
} else {
None
}
}
fn pq_action(buf: &mut Buffer, m: &MVal, type_: &str) -> Option<Out> {
match type_ {
"state of aggregation" => Some(Out::One(Parsed::N(NodeData {
type_: "state of aggregation subscript".into(),
p1: Some(Field::Nodes(go(&mval_str(m), "o"))),
..Default::default()
}))),
"color-output" => {
let (g1, g2) = mval_pair(m, 0, 1);
Some(Out::One(Parsed::N(NodeData {
type_: "color".into(),
color1: Some(g1),
color2: Some(Field::Nodes(go(&g2, "pq"))),
..Default::default()
})))
}
_ => None,
}
}
fn bd_action(_buf: &mut Buffer, m: &MVal, type_: &str) -> Option<Out> {
if type_ == "color-output" {
let (g1, g2) = mval_pair(m, 0, 1);
Some(Out::One(Parsed::N(NodeData {
type_: "color".into(),
color1: Some(g1),
color2: Some(Field::Nodes(go(&g2, "bd"))),
..Default::default()
})))
} else {
None
}
}
fn oxidation_action(buf: &mut Buffer, type_: &str) -> Option<Out> {
if type_ == "roman-numeral" {
Some(Out::One(Parsed::N(NodeData {
type_: "roman numeral".into(),
p1: Some(Field::Str(buf.o.clone().unwrap_or_default())),
..Default::default()
})))
} else {
None
}
}
fn texmath_action(machine: &str, buf: &mut Buffer, m: &MVal, type_: &str) -> Option<Out> {
match type_ {
"tight operator" => {
append_str(&mut buf.o, &format!("{{{}}}", mval_str(m)));
Some(Out::None)
}
"output" => {
let ret = buf.o.take().map(|o| {
Out::One(Parsed::N(NodeData {
type_: "tex-math".into(),
p1: Some(Field::Str(o)),
..Default::default()
}))
});
buf.clear(false);
let _ = machine;
Some(ret.unwrap_or(Out::None))
}
_ => None,
}
}
fn num99_action(type_: &str) -> Out {
if type_ == "comma" {
Out::One(Parsed::N(NodeData {
type_: "commaDecimal".into(),
..Default::default()
}))
} else {
Out::None
}
}
fn pu_action(buf: &mut Buffer, m: &MVal, opt: &Option<String>, type_: &str) -> Option<Out> {
let out = match type_ {
"enumber" => {
let v = match m {
MVal::V(v) => v.clone(),
MVal::S(s) => vec![s.clone()],
};
let mut ret: Vec<Parsed> = Vec::new();
let g0 = v.get(0).map(|s| s.as_str()).unwrap_or("");
if g0 == "+-" || g0 == "+/-" {
ret.push(Parsed::S("\\pm ".into()));
} else if !g0.is_empty() {
ret.push(Parsed::S(g0.to_string()));
}
let g1 = v.get(1).cloned().unwrap_or_default();
if !g1.is_empty() {
ret.extend(go(&g1, "pu-9,9"));
let g2 = v.get(2).cloned().unwrap_or_default();
if !g2.is_empty() {
if g2.contains(',') || g2.contains('.') {
ret.extend(go(&g2, "pu-9,9"));
} else {
ret.push(Parsed::S(g2));
}
}
let g3 = v.get(3).map(|s| s.as_str()).unwrap_or("");
let g4 = v.get(4).map(|s| s.as_str()).unwrap_or("");
if !g3.is_empty() || !g4.is_empty() {
if g3 == "e" || g4 == "*" {
ret.push(Parsed::N(NodeData {
type_: "cdot".into(),
..Default::default()
}));
} else {
ret.push(Parsed::N(NodeData {
type_: "times".into(),
..Default::default()
}));
}
}
}
let g5 = v.get(5).cloned().unwrap_or_default();
if !g5.is_empty() {
ret.push(Parsed::S(format!("10^{{{}}}", g5)));
}
Out::Many(ret)
}
"number^" => {
let v = match m {
MVal::V(v) => v.clone(),
MVal::S(s) => vec![s.clone()],
};
let mut ret: Vec<Parsed> = Vec::new();
let g0 = v.get(0).map(|s| s.as_str()).unwrap_or("");
if g0 == "+-" || g0 == "+/-" {
ret.push(Parsed::S("\\pm ".into()));
} else if !g0.is_empty() {
ret.push(Parsed::S(g0.to_string()));
}
let g1 = v.get(1).cloned().unwrap_or_default();
ret.extend(go(&g1, "pu-9,9"));
let g2 = v.get(2).cloned().unwrap_or_default();
ret.push(Parsed::S(format!("^{{{}}}", g2)));
Out::Many(ret)
}
"operator" => Out::One(Parsed::N(NodeData {
type_: "operator".into(),
kind_: Some(opt.clone().unwrap_or_else(|| mval_str(m))),
..Default::default()
})),
"space" => Out::One(Parsed::N(NodeData {
type_: "pu-space-1".into(),
..Default::default()
})),
"output" => {
// {(...)} 解包
if let Some(d) = &buf.d {
if let Some(md) = match_pattern("{(...)}", d) {
if md.remainder.is_empty() {
buf.d = Some(mval_str(&md.m));
}
}
}
if let Some(q) = &buf.q {
if let Some(mq) = match_pattern("{(...)}", q) {
if mq.remainder.is_empty() {
buf.q = Some(mval_str(&mq.m));
}
}
}
fn rep_c_f(s: &str) -> String {
s.replace("\u{00B0}C", "{}^{\\circ}C")
.replace("^oC", "{}^{\\circ}C")
.replace("^{o}C", "{}^{\\circ}C")
.replace("\u{00B0}F", "{}^{\\circ}F")
.replace("^oF", "{}^{\\circ}F")
.replace("^{o}F", "{}^{\\circ}F")
}
if let Some(d) = buf.d.as_mut() {
*d = rep_c_f(d);
}
let ret: Vec<Parsed> = if let Some(q) = buf.q.as_mut() {
*q = rep_c_f(q);
let bd = go(&buf.d.clone().unwrap_or_default(), "pu");
let bq = go(&buf.q.clone().unwrap_or_default(), "pu");
if buf.o.as_deref() == Some("//") {
vec![Parsed::N(NodeData {
type_: "pu-frac".into(),
p1: Some(Field::Nodes(bd)),
p2: Some(Field::Nodes(bq)),
..Default::default()
})]
} else {
let mut ret = bd;
if ret.len() > 1 || bq.len() > 1 {
ret.push(Parsed::N(NodeData {
type_: " / ".into(),
..Default::default()
}));
} else {
ret.push(Parsed::N(NodeData {
type_: "/".into(),
..Default::default()
}));
}
ret.extend(bq);
ret
}
} else {
go(&buf.d.clone().unwrap_or_default(), "pu-2")
};
buf.clear(false);
Out::Many(ret)
}
_ => return None,
};
Some(out)
}
fn pu2_action(buf: &mut Buffer, m: &MVal, type_: &str) -> Option<Out> {
let out = match type_ {
"cdot" => Out::One(Parsed::N(NodeData {
type_: "tight cdot".into(),
..Default::default()
})),
"^(-1)" => {
if let Some(rm) = buf.rm.as_mut() {
rm.push_str(&format!("^{{{}}}", mval_str(m)));
}
Out::None
}
"space" => Out::One(Parsed::N(NodeData {
type_: "pu-space-2".into(),
..Default::default()
})),
"output" => {
let ret: Vec<Parsed> = if let Some(rm) = &buf.rm {
if let Some(mrm) = match_pattern("{(...)}", rm) {
if mrm.remainder.is_empty() {
go(&mval_str(&mrm.m), "pu")
} else {
vec![Parsed::N(NodeData {
type_: "rm".into(),
p1: Some(Field::Str(rm.clone())),
..Default::default()
})]
}
} else {
vec![Parsed::N(NodeData {
type_: "rm".into(),
p1: Some(Field::Str(rm.clone())),
..Default::default()
})]
}
} else {
Vec::new()
};
buf.clear(false);
Out::Many(ret)
}
_ => return None,
};
Some(out)
}
fn pu99_action(buf: &mut Buffer, type_: &str) -> Option<Out> {
let out = match type_ {
"comma" => Out::One(Parsed::N(NodeData {
type_: "commaDecimal".into(),
..Default::default()
})),
"output-0" => {
let text = buf.text_.clone().unwrap_or_default();
let ret = thousands_split(&text, true);
buf.clear(false);
Out::Many(ret)
}
"output-o" => {
let text = buf.text_.clone().unwrap_or_default();
let ret = thousands_split(&text, false);
buf.clear(false);
Out::Many(ret)
}
_ => return None,
};
Some(out)
}
/// 千分位拆分pu-9,9 的 output-0 / output-o
fn thousands_split(text: &str, reverse: bool) -> Vec<Parsed> {
let mut ret: Vec<Parsed> = Vec::new();
if text.len() > 4 {
if reverse {
let mut a = text.len() % 3;
if a == 0 {
a = 3;
}
let mut i = text.len().saturating_sub(3);
while i > 0 {
ret.push(Parsed::S(text[i..i + 3].to_string()));
ret.push(Parsed::N(NodeData {
type_: "1000 separator".into(),
..Default::default()
}));
i = i.saturating_sub(3);
}
ret.push(Parsed::S(text[..a].to_string()));
ret.reverse();
} else {
let a = text.len() - 3;
let mut i = 0;
while i < a {
ret.push(Parsed::S(text[i..i + 3].to_string()));
ret.push(Parsed::N(NodeData {
type_: "1000 separator".into(),
..Default::default()
}));
i += 3;
}
ret.push(Parsed::S(text[i..].to_string()));
}
} else {
ret.push(Parsed::S(text.to_string()));
}
ret
}