From c87877cd43f25c80fa4acbc4b07594ce176baf4c Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 12 Nov 2026 00:51:55 +0000 Subject: [PATCH] craft-wordmodel: the math modules, symbol_fonts and Rgb are craft-math's math, math_latex, math_linear, math_symbols, math_tree and symbol_fonts are re-exported from craft-math at the paths they had, and props::Rgb is craft-math's Rgb (TextColor stays), so the serde forms and the app's imports do not change. Their tests are in craft-math. --- crates/wordmodel/Cargo.toml | 3 + crates/wordmodel/src/lib.rs | 7 +- crates/wordmodel/src/math.rs | 1138 ----------------------- crates/wordmodel/src/math_latex.rs | 1242 -------------------------- crates/wordmodel/src/math_linear.rs | 762 ---------------- crates/wordmodel/src/math_symbols.rs | 390 -------- crates/wordmodel/src/math_tree.rs | 104 --- crates/wordmodel/src/props/color.rs | 134 +-- crates/wordmodel/src/symbol_fonts.rs | 316 ------- 9 files changed, 6 insertions(+), 4090 deletions(-) delete mode 100644 crates/wordmodel/src/math.rs delete mode 100644 crates/wordmodel/src/math_latex.rs delete mode 100644 crates/wordmodel/src/math_linear.rs delete mode 100644 crates/wordmodel/src/math_symbols.rs delete mode 100644 crates/wordmodel/src/math_tree.rs delete mode 100644 crates/wordmodel/src/symbol_fonts.rs diff --git a/crates/wordmodel/Cargo.toml b/crates/wordmodel/Cargo.toml index 7426a738..200cd165 100644 --- a/crates/wordmodel/Cargo.toml +++ b/crates/wordmodel/Cargo.toml @@ -15,6 +15,9 @@ workspace = true test-support = [] [dependencies] +# The equation tree and its forms (math, math_latex, math_linear, math_symbols, math_tree), with +# symbol_fonts and Rgb, re-exported here under the paths they had. +craft-math = { workspace = true } base64 = "0.23" log = "0.4" serde = { version = "1", features = ["derive", "rc"] } diff --git a/crates/wordmodel/src/lib.rs b/crates/wordmodel/src/lib.rs index 9034bd8a..5fb2d14d 100644 --- a/crates/wordmodel/src/lib.rs +++ b/crates/wordmodel/src/lib.rs @@ -37,11 +37,7 @@ pub mod legacy_form; pub mod line_dash; pub mod link; pub mod locale_number; -pub mod math; -pub mod math_latex; -pub mod math_linear; -pub mod math_symbols; -pub mod math_tree; +pub use craft_math::{math, math_latex, math_linear, math_symbols, math_tree, symbol_fonts}; pub mod numbering; pub mod para; pub mod picfx; @@ -50,7 +46,6 @@ pub mod resolve; pub mod section; pub mod smart_art; pub mod styles; -pub mod symbol_fonts; pub mod table; pub mod walk; pub mod wordart; diff --git a/crates/wordmodel/src/math.rs b/crates/wordmodel/src/math.rs deleted file mode 100644 index db9875fd..00000000 --- a/crates/wordmodel/src/math.rs +++ /dev/null @@ -1,1138 +0,0 @@ -//! Equations: the Office Math object model (OMML, ECMA-376 Part 1 §22.1) as a tree, its linear -//! format (UnicodeMath, the plain text Word shows for an equation) and a parser for that format. -//! -//! An equation is a list of [`MNode`]s (an *argument*); structures hold arguments of their own -//! (a fraction's numerator and denominator, a radical's degree and base…). Layout reads the tree; -//! DOCX import builds it from OMML and keeps the source XML so a saved file carries the equation -//! exactly as Word wrote it. - -use serde::{Deserialize, Serialize}; - -use crate::props::Rgb; - -/// A sequence of math nodes (OMML's `m:e`, `m:num`, `m:sub`…). -pub type Arg = Vec; - -/// Deepest structure nesting kept (parsers stop here; layout draws nothing deeper). -pub const MAX_DEPTH: usize = 64; - -/// Most matrix columns kept (writers pad every row to the widest, so this bounds a row's cost). -pub const MAX_MATRIX_COLS: usize = 64; - -/// Most matrix rows kept. -pub const MAX_MATRIX_ROWS: usize = 1000; - -/// An equation's structure, plus what's needed to write it back. -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct Math { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub nodes: Arg, - /// The source `m:oMath` element (DOCX import), written back unchanged on save. Empty for - /// equations made here. - #[serde(default, skip_serializing_if = "String::is_empty")] - pub omml: String, - /// Justification of a display equation. - #[serde(default, skip_serializing_if = "is_default")] - pub jc: MathJc, - /// Shown as its linear-format text (Equation › Convert › Linear) instead of built up. - #[serde(default, skip_serializing_if = "is_default")] - pub linear: bool, -} - -fn is_default(v: &T) -> bool { - *v == T::default() -} - -impl Math { - pub fn is_empty(&self) -> bool { - self.nodes.is_empty() - } - - /// Rough heap bytes a clone copies: every node, its argument lists and its run text. - /// Iterative, so it costs at most the size of the structure. - pub fn heap_bytes(&self) -> usize { - let mut n = self.omml.len(); - let mut todo: Vec<&MNode> = self.nodes.iter().collect(); - while let Some(node) = todo.pop() { - n = n.saturating_add(std::mem::size_of::()); - if let MNode::Run(r) = node { - n = n.saturating_add(r.text.len()).saturating_add(r.font.as_ref().map_or(0, String::len)); - } - each_arg(node, &mut |a| { - n = n.saturating_add(std::mem::size_of::()); - todo.extend(a); - }); - } - n - } -} - -/// Display equation justification (`m:oMathParaPr/m:jc`). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum MathJc { - #[default] - CenterGroup, - Center, - Left, - Right, -} - -/// Where a display equation too wide for its line breaks at a binary operator (`m:brkBin`). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum BrkBin { - /// The operator starts the next line. - #[default] - Before, - /// The operator ends the line. - After, - /// The operator ends the line and starts the next one again. - Repeat, -} - -/// How a minus repeats across a break when [`BrkBin::Repeat`] (`m:brkBinSub`): the operator -/// ending the line, then the one starting the next. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum BrkBinSub { - /// `--` - #[default] - MinusMinus, - /// `-+` - MinusPlus, - /// `+-` - PlusMinus, -} - -/// Document-wide equation options (`m:mathPr` in the settings part) that layout uses: how long -/// display equations wrap. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] -pub struct MathProps { - pub brk_bin: BrkBin, - pub brk_bin_sub: BrkBinSub, - /// Indent of a wrapped equation's continuation lines, points (`m:wrapIndent`). - pub wrap_indent: f32, - /// Continuation lines align right instead (`m:wrapRight`). - pub wrap_right: bool, -} - -/// `m:wrapIndent`'s default: 1440 twips, one inch. -pub const DEFAULT_WRAP_INDENT: f32 = 72.0; - -impl Default for MathProps { - fn default() -> Self { - MathProps { brk_bin: BrkBin::Before, brk_bin_sub: BrkBinSub::MinusMinus, wrap_indent: DEFAULT_WRAP_INDENT, wrap_right: false } - } -} - -/// Math run style (`m:sty`): plain, bold, italic, bold italic. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum MSty { - Plain, - Bold, - Italic, - BoldItalic, -} - -/// Math alphabet (`m:scr`). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum MScr { - #[default] - Roman, - Script, - Fraktur, - DoubleStruck, - SansSerif, - Monospace, -} - -/// A run of math text (`m:r`). -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct MRun { - pub text: String, - /// `None`: math default (letters italic, the rest upright). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sty: Option, - #[serde(default, skip_serializing_if = "is_default")] - pub scr: MScr, - /// Normal (non-math) text. - #[serde(default, skip_serializing_if = "is_default")] - pub nor: bool, - /// Literal: no automatic operator handling. - #[serde(default, skip_serializing_if = "is_default")] - pub lit: bool, - /// Font size, points. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub size: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub color: Option, - /// Font for normal text. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub font: Option, - /// A manual line break before this run in a display equation (`m:brk`): `Some(0)` wraps as - /// usual, `Some(n)` aligns the new line at the `n`th operator of the first line (`m:alnAt`). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub brk: Option, -} - -impl MRun { - pub fn new(text: impl Into) -> MRun { - MRun { text: text.into(), ..Default::default() } - } - pub fn plain(text: impl Into) -> MRun { - MRun { text: text.into(), sty: Some(MSty::Plain), ..Default::default() } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum FracKind { - /// Stacked with a rule. - #[default] - Bar, - /// Skewed (num ⁄ den). - Skewed, - /// Linear (num/den). - Linear, - /// Stacked without a rule. - NoBar, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum ScriptKind { - Sup, - Sub, - SubSup, - /// Pre-scripts (`m:sPre`). - Pre, -} - -/// Where an n-ary operator's limits go. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum LimLoc { - /// Under and over the operator. - UndOvr, - /// As sub- and superscripts. - SubSup, -} - -/// Column justification in a matrix. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum ColJc { - #[default] - Center, - Left, - Right, -} - -/// A math node. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(tag = "t", rename_all = "camelCase")] -pub enum MNode { - Run(MRun), - /// `m:f`. - Frac { - #[serde(default)] - kind: FracKind, - num: Arg, - den: Arg, - }, - /// `m:sSup`, `m:sSub`, `m:sSubSup`, `m:sPre`. - Script { - kind: ScriptKind, - base: Arg, - #[serde(default)] - sub: Arg, - #[serde(default)] - sup: Arg, - }, - /// `m:rad`. - Rad { - #[serde(default)] - deg: Arg, - #[serde(default)] - deg_hide: bool, - e: Arg, - }, - /// `m:nary`: ∑ ∫ ∏ … with limits and an operand. - Nary { - chr: char, - /// `None`: Word's default (integrals take sub/sup limits, the rest under/over). - #[serde(default)] - lim_loc: Option, - #[serde(default)] - grow: bool, - #[serde(default)] - sub_hide: bool, - #[serde(default)] - sup_hide: bool, - #[serde(default)] - sub: Arg, - #[serde(default)] - sup: Arg, - e: Arg, - }, - /// `m:d`: delimiters around elements separated by `sep`. - Delim { - /// `None` = no delimiter. - beg: Option, - end: Option, - #[serde(default)] - sep: Option, - /// Delimiters grow to the content (Word's default). - #[serde(default = "yes")] - grow: bool, - /// Grow to match the content exactly rather than centred on the math axis. - #[serde(default)] - shp_match: bool, - elems: Vec, - }, - /// `m:func`: function name applied to an argument (sin x). - Func { - name: Arg, - e: Arg, - }, - /// `m:limLow` / `m:limUpp`. - Lim { - upper: bool, - e: Arg, - lim: Arg, - }, - /// `m:acc`: accent over the base. - Acc { - chr: char, - e: Arg, - }, - /// `m:bar`: overbar or underbar. - Bar { - top: bool, - e: Arg, - }, - /// `m:borderBox`. `hide`: top, bottom, left, right. `strike`: horizontal, vertical, - /// bottom-left→top-right, top-left→bottom-right. - BorderBox { - #[serde(default)] - hide: [bool; 4], - #[serde(default)] - strike: [bool; 4], - e: Arg, - }, - /// `m:box`. - Boxed { - e: Arg, - }, - /// `m:groupChr`: a stretched character (brace, arrow) under or over the base. - GroupChr { - chr: char, - top: bool, - e: Arg, - }, - /// `m:eqArr`: rows aligned at `&`. - EqArr { - rows: Vec, - }, - /// `m:m`. - Matrix { - rows: Vec>, - #[serde(default)] - col_jc: Vec, - }, - /// `m:phant`. - Phant { - #[serde(default = "yes")] - show: bool, - #[serde(default)] - zero_wid: bool, - #[serde(default)] - zero_asc: bool, - #[serde(default)] - zero_desc: bool, - e: Arg, - }, -} - -fn yes() -> bool { - true -} - -/// A row holds an equation-number mark (`#`) in a top-level run. -pub fn has_number_mark(row: &[MNode]) -> bool { - row.iter().any(|n| matches!(n, MNode::Run(r) if !r.lit && !r.nor && r.text.contains('#'))) -} - -/// Integral signs (their limits default to sub/sup positions). -pub fn is_integral(c: char) -> bool { - matches!(c, '∫' | '∬' | '∭' | '∮' | '∯' | '∰' | '∱' | '∲' | '∳' | '⨌') -} - -/// Characters that take n-ary operator form in the linear format. -pub fn is_nary_char(c: char) -> bool { - is_integral(c) || matches!(c, '∑' | '∏' | '∐' | '⋃' | '⋂' | '⋁' | '⋀' | '⨁' | '⨂' | '⨀' | '⨄' | '⨆') -} - -/// Mathematical alphanumeric for `c` in a style and alphabet (Unicode block U+1D400). -pub fn math_alnum(c: char, bold: bool, italic: bool, scr: MScr) -> char { - let off = |base: u32, k: u32| char::from_u32(base + k).unwrap_or(c); - let upper = c.is_ascii_uppercase(); - let lower = c.is_ascii_lowercase(); - if upper || lower { - let k = if upper { c as u32 - 'A' as u32 } else { c as u32 - 'a' as u32 }; - // Letters that live in the Letterlike Symbols block. - let hole = |table: &[(char, char)]| table.iter().find(|(f, _)| *f == c).map(|(_, t)| *t); - let (base_u, base_l) = match (scr, bold, italic) { - (MScr::Roman, false, false) => return c, - (MScr::Roman, true, false) => (0x1D400, 0x1D41A), - (MScr::Roman, false, true) => { - if c == 'h' { - return '\u{210E}'; - } - (0x1D434, 0x1D44E) - } - (MScr::Roman, true, true) => (0x1D468, 0x1D482), - (MScr::Script, false, _) => { - if let Some(t) = hole(&[ - ('B', 'ℬ'), - ('E', 'ℰ'), - ('F', 'ℱ'), - ('H', 'ℋ'), - ('I', 'ℐ'), - ('L', 'ℒ'), - ('M', 'ℳ'), - ('R', 'ℛ'), - ('e', 'ℯ'), - ('g', 'ℊ'), - ('o', 'ℴ'), - ]) { - return t; - } - (0x1D49C, 0x1D4B6) - } - (MScr::Script, true, _) => (0x1D4D0, 0x1D4EA), - (MScr::Fraktur, false, _) => { - if let Some(t) = hole(&[('C', 'ℭ'), ('H', 'ℌ'), ('I', 'ℑ'), ('R', 'ℜ'), ('Z', 'ℨ')]) { - return t; - } - (0x1D504, 0x1D51E) - } - (MScr::Fraktur, true, _) => (0x1D56C, 0x1D586), - (MScr::DoubleStruck, _, _) => { - if let Some(t) = hole(&[('C', 'ℂ'), ('H', 'ℍ'), ('N', 'ℕ'), ('P', 'ℙ'), ('Q', 'ℚ'), ('R', 'ℝ'), ('Z', 'ℤ')]) { - return t; - } - (0x1D538, 0x1D552) - } - (MScr::SansSerif, false, false) => (0x1D5A0, 0x1D5BA), - (MScr::SansSerif, true, false) => (0x1D5D4, 0x1D5EE), - (MScr::SansSerif, false, true) => (0x1D608, 0x1D622), - (MScr::SansSerif, true, true) => (0x1D63C, 0x1D656), - (MScr::Monospace, _, _) => (0x1D670, 0x1D68A), - }; - return off(if upper { base_u } else { base_l }, k); - } - if c.is_ascii_digit() { - let k = c as u32 - '0' as u32; - return match (scr, bold) { - (MScr::Roman, true) => off(0x1D7CE, k), - (MScr::DoubleStruck, _) => off(0x1D7D8, k), - (MScr::SansSerif, false) => off(0x1D7E2, k), - (MScr::SansSerif, true) => off(0x1D7EC, k), - (MScr::Monospace, _) => off(0x1D7F6, k), - _ => c, - }; - } - // Greek (roman alphabet only). - if scr == MScr::Roman && (bold || italic) { - let (cap, small) = match (bold, italic) { - (true, false) => (0x1D6A8, 0x1D6C2), - (false, true) => (0x1D6E2, 0x1D6FC), - _ => (0x1D71C, 0x1D736), - }; - let u = c as u32; - if (0x391..=0x3A9).contains(&u) && u != 0x3A2 { - return off(cap, u - 0x391); - } - if (0x3B1..=0x3C9).contains(&u) { - return off(small, u - 0x3B1); - } - let extra = match c { - '∂' => Some(25), - 'ϵ' => Some(26), - 'ϑ' => Some(27), - 'ϰ' => Some(28), - 'ϕ' => Some(29), - 'ϱ' => Some(30), - 'ϖ' => Some(31), - _ => None, - }; - if let Some(e) = extra - && italic - { - return off(small, e); - } - } - c -} - -// --------------------------------------------------------------------------------------------- -// Linear format out. - -/// The equation as linear-format text (`x=(-b±√(b^2-4ac))/2a`). -pub fn to_linear(nodes: &[MNode]) -> String { - let mut s = String::new(); - lin_arg(nodes, &mut s, 0); - s -} - -fn lin_arg(a: &[MNode], s: &mut String, depth: usize) { - if depth > MAX_DEPTH { - return; - } - for (i, n) in a.iter().enumerate() { - lin_node(n, s, depth + 1); - // A bare script or limit ends at a space: a letter, digit or period after it is its own item. - if matches!(n, MNode::Script { kind, .. } if *kind != ScriptKind::Pre) || matches!(n, MNode::Lim { .. }) { - let ends_bare = s.chars().next_back().is_some_and(|c| c.is_alphanumeric() || c == '.'); - if ends_bare && a.get(i + 1).and_then(first_lin_char).is_some_and(|c| c.is_alphanumeric() || c == '.') { - s.push(' '); - } - } - } -} - -/// The first character `n` is written with, when that is cheap to tell (letters and digits). -fn first_lin_char(n: &MNode) -> Option { - match n { - MNode::Run(r) => r.text.chars().next(), - MNode::Func { name, .. } => name.first().and_then(first_lin_char), - MNode::Script { kind, base, .. } if *kind != ScriptKind::Pre => base.first().and_then(first_lin_char), - MNode::Lim { e, .. } => e.first().and_then(first_lin_char), - MNode::Frac { num, .. } => num.first().and_then(first_lin_char), - _ => None, - } -} - -/// An argument as an operand: bare when it's a single simple atom, parenthesised otherwise. -fn lin_operand(a: &[MNode], s: &mut String, depth: usize) { - let mut inner = String::new(); - lin_arg(a, &mut inner, depth); - let simple = match a { - [MNode::Run(r)] => { - let mut cs = r.text.chars(); - match (cs.next(), cs.next()) { - (Some(c), None) => !"+-−=/^_ ".contains(c), - _ => r.text.chars().all(|c| c.is_alphanumeric() || c == '.'), - } - } - [MNode::Delim { .. }] => true, - _ => false, - }; - if simple && !inner.is_empty() { - s.push_str(&inner); - } else { - s.push('('); - s.push_str(&inner); - s.push(')'); - } -} - -/// A script or limit operand. The parser reads a run of letters and digits up to the next space or -/// operator, so `V_dd` is written bare as Word's Linear format shows it (`V_(dd)` reads back the same). -fn lin_script(a: &[MNode], s: &mut String, depth: usize) { - // Bare only when the parser reads the whole run back, in the same style. - if let [MNode::Run(r)] = a - && r.text.chars().nth(1).is_some() - { - let upright_name = r.sty == Some(MSty::Plain) && crate::math_linear::FUNCTION_NAMES.contains(&r.text.as_str()); - let number = r.text.starts_with(|c: char| c.is_ascii_digit()) && r.text.chars().all(|c| c.is_alphanumeric() || c == '.'); - let word = r.text.starts_with(char::is_alphabetic) && r.text.chars().all(char::is_alphanumeric); - let upright_other = r.sty == Some(MSty::Plain) && !upright_name; - if upright_other || (!number && !word) { - s.push('('); - s.push_str(&r.text); - s.push(')'); - return; - } - } - lin_operand(a, s, depth); -} - -fn lin_node(n: &MNode, s: &mut String, depth: usize) { - match n { - MNode::Run(r) => s.push_str(&r.text), - MNode::Frac { kind, num, den } => { - lin_operand(num, s, depth); - s.push(match kind { - FracKind::Skewed => '⁄', - FracKind::Linear => '∕', - FracKind::NoBar => '¦', - FracKind::Bar => '/', - }); - lin_operand(den, s, depth); - } - MNode::Script { kind, base, sub, sup } => { - if *kind == ScriptKind::Pre { - s.push('_'); - lin_script(sub, s, depth); - s.push('^'); - lin_script(sup, s, depth); - s.push('▒'); - lin_operand(base, s, depth); - return; - } - lin_operand(base, s, depth); - if matches!(kind, ScriptKind::Sub | ScriptKind::SubSup) { - s.push('_'); - lin_script(sub, s, depth); - } - if matches!(kind, ScriptKind::Sup | ScriptKind::SubSup) { - s.push('^'); - lin_script(sup, s, depth); - } - } - MNode::Rad { deg, deg_hide, e } => { - if *deg_hide || deg.is_empty() { - s.push('√'); - lin_operand(e, s, depth); - } else { - s.push_str("√("); - lin_arg(deg, s, depth); - s.push('&'); - lin_arg(e, s, depth); - s.push(')'); - } - } - MNode::Nary { chr, sub_hide, sup_hide, sub, sup, e, .. } => { - s.push(*chr); - if !sub_hide && !sub.is_empty() { - s.push('_'); - lin_script(sub, s, depth); - } - if !sup_hide && !sup.is_empty() { - s.push('^'); - lin_script(sup, s, depth); - } - s.push('▒'); - lin_operand(e, s, depth); - } - MNode::Delim { beg, end, elems, .. } => { - // A standard pair writes as itself; anything else uses explicit `├X … Y┤`. - let pair = matches!((beg, end), (Some(b), Some(e)) if crate::math_linear::closer(*b) == Some(*e)); - let invisible = beg.is_none() && end.is_none(); - if pair { - s.extend(*beg); - } else if invisible { - s.push('〖'); - } else { - s.push('├'); - s.extend(*beg); - } - for (i, e) in elems.iter().enumerate() { - if i > 0 { - s.push('│'); - } - lin_arg(e, s, depth); - } - if pair { - s.extend(*end); - } else if invisible { - s.push('〗'); - } else { - s.extend(*end); - s.push('┤'); - } - } - MNode::Func { name, e } => { - lin_arg(name, s, depth); - s.push('\u{2061}'); - lin_operand(e, s, depth); - } - MNode::Lim { upper, e, lim } => { - lin_operand(e, s, depth); - s.push(if *upper { '┴' } else { '┬' }); - lin_script(lim, s, depth); - } - MNode::Acc { chr, e } => { - lin_operand(e, s, depth); - s.push(*chr); - } - MNode::Bar { top, e } => { - s.push(if *top { '¯' } else { '▁' }); - lin_operand(e, s, depth); - } - MNode::BorderBox { e, .. } => { - s.push('▭'); - lin_operand(e, s, depth); - } - MNode::Boxed { e } => { - s.push('□'); - lin_operand(e, s, depth); - } - MNode::GroupChr { chr, e, .. } => { - s.push(*chr); - lin_operand(e, s, depth); - } - // A numbered equation (`E=mc^2#(1)`) is written as its one row. - MNode::EqArr { rows } if rows.len() == 1 && rows.first().is_some_and(|r| has_number_mark(r)) => { - if let Some(r) = rows.first() { - lin_arg(r, s, depth); - } - } - MNode::EqArr { rows } => { - s.push_str("█("); - for (i, r) in rows.iter().enumerate() { - if i > 0 { - s.push('@'); - } - lin_arg(r, s, depth); - } - s.push(')'); - } - MNode::Matrix { rows, .. } => { - s.push_str("■("); - for (i, r) in rows.iter().enumerate() { - if i > 0 { - s.push('@'); - } - for (j, c) in r.iter().enumerate() { - if j > 0 { - s.push('&'); - } - lin_arg(c, s, depth); - } - } - s.push(')'); - } - MNode::Phant { e, .. } => { - s.push('⟡'); - lin_operand(e, s, depth); - } - } -} - -// --------------------------------------------------------------------------------------------- -// Linear format in. - -/// Parse linear-format text into nodes ([`crate::math_linear`]). Never fails: what isn't -/// understood stays as text. -pub fn parse_linear(s: &str) -> Arg { - crate::math_linear::parse(s) -} - -pub use crate::math_linear::FUNCTION_NAMES; - -/// Spacing class of a math character (TeX's atom classes). -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -pub enum MClass { - #[default] - Ord, - Op, - Bin, - Rel, - Open, - Close, - Punct, - Inner, -} - -/// The spacing class of a character in an equation. -pub fn math_class(c: char) -> MClass { - match c { - '+' | '−' | '-' | '±' | '∓' | '×' | '÷' | '·' | '∙' | '∗' | '*' | '∘' | '⊕' | '⊖' | '⊗' | '⊘' | '⊙' | '⊚' | '⊛' | '∪' | '∩' | '∧' | '∨' - | '∖' | '⋅' | '⋆' | '⊎' | '⊓' | '⊔' | '⋄' | '⊞' | '⊟' | '⊠' | '⊡' | '⋉' | '⋊' | '⋋' | '⋌' | '⋎' | '⋏' | '⋒' | '⋓' | '∔' | '∸' | '≀' | '⨿' - | '⊼' | '⊻' | '⊽' => MClass::Bin, - '=' | '<' | '>' | '≤' | '≥' | '≠' | '≈' | '≡' | '≢' | '∼' | '≃' | '≅' | '≐' | '∝' | '≪' | '≫' | '≺' | '≻' | '≼' | '≽' | '⊂' | '⊃' | '⊆' - | '⊇' | '⊄' | '⊅' | '⊈' | '⊉' | '⊊' | '⊋' | '∈' | '∉' | '∋' | '∌' | '⊥' | '∥' | '∦' | '∣' | '∤' | '≔' | '≕' | '≝' | '≜' | '≞' | '≟' | '→' - | '←' | '↔' | '⇒' | '⇐' | '⇔' | '↦' | '⟼' | '⟶' | '⟵' | '⟷' | '⟹' | '⟸' | '⟺' | '↑' | '↓' | '↕' | '⇑' | '⇓' | '⇕' | '↗' | '↘' | '↙' | '↖' - | '⊢' | '⊣' | '⊨' | '⊩' | '⊪' | '⊬' | '⊭' | '⊮' | '⊯' | '≲' | '≳' | '≍' | '⊏' | '⊐' | '⊑' | '⊒' | '⋢' | '⋣' | '≶' | '≷' | '≮' | '≯' | '≰' - | '≱' | '≦' | '≧' | '⩽' | '⩾' | ':' | '∶' | '∷' | '∺' | '∻' | '≗' | '≙' | '≚' | '≑' | '≒' | '≓' | '≖' | '⇌' | '⇋' | '⇄' | '⇆' | '⇇' | '⇉' - | '⇈' | '⇊' | '↪' | '↩' | '↼' | '↽' | '⇀' | '⇁' | '↿' | '↾' | '⇃' | '⇂' | '↶' | '↷' | '↺' | '↻' | '⊸' | '↜' | '↝' | '↞' | '↠' | '↢' | '↣' - | '↫' | '↬' | '↭' | '⇚' | '⇛' | '↰' | '↱' | '⇝' | '≁' | '≄' | '≉' | '≇' | '≭' | '≨' | '≩' | '⊀' | '⊁' | '⋠' | '⋡' | '⋦' | '⋧' | '⋨' | '⋩' - | '⋪' | '⋫' | '⋬' | '⋭' | '⋈' | '∴' | '∵' | '≏' | '≎' | '⋘' | '⋙' | '⊲' | '⊳' | '⊴' | '⊵' | '⋐' | '⋑' => { - MClass::Rel - } - '(' | '[' | '{' | '⟨' | '⌈' | '⌊' | '⟦' | '〈' => MClass::Open, - ')' | ']' | '}' | '⟩' | '⌉' | '⌋' | '⟧' | '〉' | '!' => MClass::Close, - ',' | ';' => MClass::Punct, - c if is_nary_char(c) => MClass::Op, - _ => MClass::Ord, - } -} - -/// Merge adjacent runs with the same formatting; recurse into every structure. -pub fn merge_runs(a: Arg) -> Arg { - let mut out: Arg = Vec::with_capacity(a.len()); - for n in a { - let n = match n { - MNode::Run(r) => { - if let Some(MNode::Run(prev)) = out.last_mut() - && prev.sty == r.sty - && prev.scr == r.scr - && prev.nor == r.nor - && prev.lit == r.lit - && prev.size == r.size - && prev.color == r.color - && prev.font == r.font - && r.brk.is_none() - { - prev.text.push_str(&r.text); - continue; - } - MNode::Run(r) - } - other => crate::math_linear::map_args(other, &merge_runs), - }; - out.push(n); - } - out -} - -/// Deepest node (a leaf counts 1) the parsers wrap further. Half of [`MAX_DEPTH`]: serde_json -/// stops reading at 128 nested containers and a node costs two (object and array). -pub const WRAP_DEPTH: usize = MAX_DEPTH / 2; - -/// Calls `f` with every argument of `n`. -fn each_arg<'a>(n: &'a MNode, f: &mut impl FnMut(&'a [MNode])) { - match n { - MNode::Run(_) => {} - MNode::Frac { num, den, .. } => { - f(num); - f(den); - } - MNode::Script { base, sub, sup, .. } => { - f(base); - f(sub); - f(sup); - } - MNode::Rad { deg, e, .. } => { - f(deg); - f(e); - } - MNode::Nary { sub, sup, e, .. } => { - f(sub); - f(sup); - f(e); - } - MNode::Delim { elems, .. } => elems.iter().for_each(|a| f(a)), - MNode::Func { name, e } => { - f(name); - f(e); - } - MNode::Lim { e, lim, .. } => { - f(e); - f(lim); - } - MNode::Acc { e, .. } - | MNode::Bar { e, .. } - | MNode::BorderBox { e, .. } - | MNode::Boxed { e } - | MNode::GroupChr { e, .. } - | MNode::Phant { e, .. } => f(e), - MNode::EqArr { rows } => rows.iter().for_each(|a| f(a)), - MNode::Matrix { rows, .. } => rows.iter().flatten().for_each(|a| f(a)), - } -} - -/// `n` is already nested too deeply to be wrapped in another structure. The parsers use this so -/// `x^1^1^1…` stays shallow (a left-nested tree 10 000 deep overflows the stack in `Clone`, -/// `Drop`, `PartialEq` and serde). Iterative, stops at the first too-deep path; costs at most the -/// size of `n`, which the parsers' input caps bound. -pub fn too_deep(n: &MNode) -> bool { - let mut stack: Vec<(&MNode, usize)> = vec![(n, 1)]; - while let Some((n, d)) = stack.pop() { - if d >= WRAP_DEPTH { - return true; - } - each_arg(n, &mut |a| stack.extend(a.iter().map(|c| (c, d + 1)))); - } - false -} - -/// Any of `a` is too deep to be wrapped ([`too_deep`]). -pub fn any_too_deep(a: &[MNode]) -> bool { - a.iter().any(too_deep) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn run(t: &str) -> MNode { - MNode::Run(MRun::new(t)) - } - - #[test] - fn quadratic_formula_parses_into_structures() { - let n = parse_linear("x=(-b±√(b^2-4ac))/2a"); - assert_eq!(n.len(), 2, "{n:#?}"); - assert_eq!(n[0], run("x=")); - let MNode::Frac { num, den, .. } = &n[1] else { panic!("{n:#?}") }; - assert_eq!(den, &vec![run("2a")]); - assert!(num.iter().any(|m| matches!(m, MNode::Rad { .. })), "{num:#?}"); - let MNode::Rad { e, .. } = num.iter().find(|m| matches!(m, MNode::Rad { .. })).unwrap() else { panic!() }; - assert!(matches!(e.first(), Some(MNode::Script { kind: ScriptKind::Sup, .. })), "{e:#?}"); - } - - #[test] - fn scripts_and_nary() { - let n = parse_linear("∑_(i=1)^n▒i^2"); - let MNode::Nary { chr, sub, sup, e, .. } = &n[0] else { panic!("{n:#?}") }; - assert_eq!(*chr, '∑'); - assert_eq!(sub, &vec![run("i=1")]); - assert_eq!(sup, &vec![run("n")]); - assert!(matches!(e.first(), Some(MNode::Script { .. }))); - let n = parse_linear("x_i^2"); - assert!(matches!(&n[0], MNode::Script { kind: ScriptKind::SubSup, .. }), "{n:#?}"); - } - - #[test] - fn functions_and_matrices() { - let n = parse_linear("sin x"); - assert!(matches!(&n[0], MNode::Func { .. }), "{n:#?}"); - let n = parse_linear("■(a&b@c&d)"); - let MNode::Matrix { rows, .. } = &n[0] else { panic!("{n:#?}") }; - assert_eq!(rows.len(), 2); - assert_eq!(rows[0].len(), 2); - } - - #[test] - fn linear_round_trip_keeps_structure() { - for s in [ - "x=(-b±√(b^2-4ac))/2a", - "∑_(i=1)^n▒i", - "a/b+c", - "E=mc^2", - "√(3&x)", - "├]a[┤", - "{█(x@y)┤", - "|x|+‖v‖", - "⏞(a+b)┴k", - "_1^n▒Y", - "sin^(−1)⁡x", - "lim┬(n→∞)⁡a_n", - "A⃗+x̂", - "▭(a)+¯(b)+▁(c)", - "(a│b)", - "E=mc^2#(1)", - ] { - let n = parse_linear(s); - let back = parse_linear(&to_linear(&n)); - assert_eq!(n, back, "{s} → {}", to_linear(&n)); - } - } - - //T1 - /// Deepest nesting of `a` (a leaf counts 1), without recursion. - fn depth_of(a: &[MNode]) -> usize { - let mut best = 0; - let mut stack: Vec<(&MNode, usize)> = a.iter().map(|n| (n, 1)).collect(); - while let Some((n, d)) = stack.pop() { - best = best.max(d); - each_arg(n, &mut |a| stack.extend(a.iter().map(|c| (c, d + 1)))); - } - best - } - - /// Run `f` on a thread with a stack far below the 8 MB of a main thread (#1456). - fn on_small_stack(f: impl FnOnce() + Send + 'static) { - std::thread::Builder::new().stack_size(512 * 1024).spawn(f).unwrap().join().unwrap(); - } - - #[test] - fn deeply_chained_input_stays_shallow() { - on_small_stack(|| { - let cases: Vec<(&str, Arg)> = vec![ - ("x^1^1…", parse_linear(&format!("x{}", "^1".repeat(9999)))), - ("a/a/a…", parse_linear(&format!("a{}", "/a".repeat(9999)))), - ("accents", parse_linear(&format!("x{}", "\u{302}".repeat(9999)))), - ("limits", parse_linear(&format!("x{}", "┬a".repeat(9000)))), - ("(a b)^1^1…", parse_linear(&format!("(a b){}", "^1".repeat(9000)))), - ("latex x^1^1…", crate::math_latex::parse_latex(&format!("x{}", "^1".repeat(9999)))), - ("latex \\sum…", crate::math_latex::parse_latex(&"\\sum".repeat(5000))), - ("latex \\sin x^1…", crate::math_latex::parse_latex(&format!("\\sin x{}", "^1".repeat(9000)))), - ]; - for (name, n) in cases { - assert!(!n.is_empty(), "{name}"); - assert!(depth_of(&n) <= 70, "{name}: depth {}", depth_of(&n)); - let copy = n.clone(); - assert_eq!(copy, n, "{name}"); - let _ = to_linear(&n); - let _ = crate::math_latex::to_latex(&n); - let json = serde_json::to_string(&n).unwrap(); - let back: Arg = serde_json::from_str(&json).unwrap(); - assert_eq!(back, n, "{name}"); - } - }); - } - - #[test] - fn ordinary_nesting_is_still_built() { - // Well below the cap: scripts, fractions and operators still wrap. - let n = parse_linear(&format!("x{}", "^1".repeat(10))); - assert_eq!(depth_of(&n), 11, "{n:#?}"); - let n = crate::math_latex::parse_latex(r"\sum_{i}\sum_{j}a_{ij}"); - assert!(matches!(&n[0], MNode::Nary { e, .. } if matches!(e.first(), Some(MNode::Nary { .. }))), "{n:#?}"); - } - - //T2 - #[test] - fn latex_text_escapes_what_latex_reads_as_markup() { - for text in ["a}b\\c", "{x}", "50% of #1", "a_b&c$d~e^f", "\\", "\\alpha", "\\{"] { - let n = vec![MNode::Run(MRun { text: text.into(), nor: true, sty: Some(MSty::Plain), ..Default::default() })]; - let tex = crate::math_latex::to_latex(&n); - assert_eq!(crate::math_latex::parse_latex(&tex), n, "{text:?} → {tex}"); - } - let n = vec![MNode::Run(MRun::new("a\\b"))]; - assert_eq!(crate::math_latex::parse_latex(&crate::math_latex::to_latex(&n)), n); - } - - //T3 - #[test] - fn latex_runs_merge_inside_every_structure() { - use crate::math_latex::parse_latex; - for (tex, lin) in [(r"\overline{ab}", "¯(ab)"), (r"\underline{ab}", "▁(ab)"), (r"\boxed{ab}", "▭(ab)"), (r"\phantom{ab}", "⟡(ab)")] { - assert_eq!(parse_latex(tex), parse_linear(lin), "{tex}"); - } - let n = parse_latex(r"\begin{matrix}a b&c d\\e f&g\end{matrix}"); - let MNode::Matrix { rows, .. } = &n[0] else { panic!("{n:#?}") }; - assert!(rows.iter().flatten().all(|c| c.len() == 1), "{rows:#?}"); - let n = parse_latex(r"\hat{ab}\underbrace{ab}\underset{ab}{cd}"); - let runs = |a: &Arg| a.iter().filter(|n| matches!(n, MNode::Run(_))).count(); - assert!(matches!(&n[0], MNode::Acc { e, .. } if runs(e) == 1), "{n:#?}"); - assert!(matches!(&n[1], MNode::GroupChr { e, .. } if runs(e) == 1), "{n:#?}"); - assert!(matches!(&n[2], MNode::Lim { e, lim, .. } if runs(e) == 1 && runs(lim) == 1), "{n:#?}"); - } - - #[test] - fn multi_letter_scripts_round_trip() { - // Word for Mac 16.113.4 (#1609): a script is the whole run up to a space or operator, and Word's - // Linear format shows it bare (`V_dd`); the parenthesised form reads back identically. - let script = |kind, sub: &str, sup: &str| MNode::Script { kind, base: vec![run("V")], sub: parse_linear(sub), sup: parse_linear(sup) }; - for n in [ - vec![script(ScriptKind::Sub, "dd", "")], - vec![script(ScriptKind::Sup, "", "ab")], - vec![script(ScriptKind::SubSup, "in", "2")], - vec![MNode::Nary { - chr: '∑', - lim_loc: None, - grow: false, - sub_hide: false, - sup_hide: false, - sub: vec![run("ij")], - sup: vec![run("n")], - e: vec![run("x")], - }], - vec![MNode::Lim { upper: false, e: vec![run("⟶")], lim: vec![run("yields")] }], - ] { - assert_eq!(parse_linear(&to_linear(&n)), n, "{}", to_linear(&n)); - } - // Numbers and single letters stay bare. - assert_eq!(to_linear(&[script(ScriptKind::SubSup, "10", "2.5")]), "V_10^2.5"); - assert_eq!(to_linear(&[script(ScriptKind::Sub, "d", "")]), "V_d"); - assert_eq!(to_linear(&[script(ScriptKind::Sub, "dd", "")]), "V_dd"); - assert_eq!(to_linear(&[script(ScriptKind::Sup, "", "ab")]), "V^ab"); - } - - #[test] - fn multi_letter_script_operands_follow_word() { - // #1609, Word for Mac 16.113.4: `V_dd` and `V_(dd)` build up the same; `dd` is all subscript. - let sub_dd = MNode::Script { kind: ScriptKind::Sub, base: vec![run("V")], sub: vec![run("dd")], sup: vec![] }; - assert_eq!(parse_linear("V_dd"), vec![sub_dd.clone()]); - assert_eq!(parse_linear("V_(dd)"), vec![sub_dd]); - let sup_ab = MNode::Script { kind: ScriptKind::Sup, base: vec![run("x")], sub: vec![], sup: vec![run("ab")] }; - assert_eq!(parse_linear("x^ab"), vec![sup_ab.clone()]); - assert_eq!(parse_linear("x^(ab)"), vec![sup_ab]); - // Delimiters still end the operand. - let both = MNode::Script { kind: ScriptKind::SubSup, base: vec![run("x")], sub: vec![run("i")], sup: vec![run("2")] }; - assert_eq!(parse_linear("x_i^2"), vec![both]); - let a_i = MNode::Script { kind: ScriptKind::Sub, base: vec![run("a")], sub: vec![run("i")], sup: vec![] }; - assert_eq!(parse_linear("a_i b"), vec![a_i, run("b")]); - let both2 = MNode::Script { kind: ScriptKind::SubSup, base: vec![run("x")], sub: vec![run("ij")], sup: vec![run("kl")] }; - assert_eq!(parse_linear("x_ij^kl"), vec![both2]); - // Round trip is Word's bare form. - assert_eq!(to_linear(&parse_linear("V_(dd)")), "V_dd"); - } - - #[test] - fn a_bare_script_does_not_absorb_what_follows() { - // #1609 review: `x_i` then `y` must not read back as x with subscript `iy`. - let sc = - |kind, base: &str, sub: &str, sup: &str| MNode::Script { kind, base: vec![run(base)], sub: parse_linear(sub), sup: parse_linear(sup) }; - let sin_x = MNode::Func { name: vec![MNode::Run(MRun::plain("sin"))], e: vec![run("x")] }; - let lim = MNode::Lim { upper: false, e: vec![run("a")], lim: vec![run("n")] }; - for n in [ - vec![sc(ScriptKind::Sub, "x", "i", ""), run("y")], - vec![sc(ScriptKind::Sub, "a", "i", ""), sc(ScriptKind::Sub, "b", "j", "")], - vec![sc(ScriptKind::Sup, "x", "", "2"), run("y")], - vec![sc(ScriptKind::Sup, "x", "", "2"), run("3")], - vec![sc(ScriptKind::Sub, "x", "i", ""), sin_x], - vec![lim.clone(), run("y")], - vec![sc(ScriptKind::Sub, "x", "ab", ""), run("y")], - vec![sc(ScriptKind::Sub, "x", "i", ""), run(".5")], - ] { - let lin = to_linear(&n); - assert_eq!(parse_linear(&lin), n, "{lin}"); - } - assert_eq!(to_linear(&[sc(ScriptKind::Sub, "x", "i", ""), run("y")]), "x_i y"); - // Nothing extra when the script already ends in a bracket or a symbol follows. - assert_eq!(to_linear(&[sc(ScriptKind::Sub, "x", "i", ""), run("+")]), "x_i+"); - } - - #[test] - fn a_function_name_in_a_script_stays_upright() { - // Word for Mac (#1609): `x_max` and `x_(max)` both give an upright function name. - let up = vec![MNode::Script { kind: ScriptKind::Sub, base: vec![run("x")], sub: vec![MNode::Run(MRun::plain("max"))], sup: vec![] }]; - assert_eq!(parse_linear("x_max"), up); - assert_eq!(parse_linear("x_(max)"), up); - assert_eq!(parse_linear(&to_linear(&up)), up, "{}", to_linear(&up)); - // Other upright text is not a function name: it keeps its parentheses. - let other = vec![MNode::Script { kind: ScriptKind::Sub, base: vec![run("x")], sub: vec![MNode::Run(MRun::plain("ab"))], sup: vec![] }]; - assert_eq!(to_linear(&other), "x_(ab)"); - } - - #[test] - fn digits_then_letters_continue_a_script() { - // Word for Mac (#1609): a script runs to a space or operator, so `x_2a` is subscript "2a". - let sub = |t: &str| vec![MNode::Script { kind: ScriptKind::Sub, base: vec![run("x")], sub: vec![run(t)], sup: vec![] }]; - assert_eq!(parse_linear("x_2a"), sub("2a")); - assert_eq!(parse_linear("x_a2"), sub("a2")); - assert_eq!(parse_linear("x_αβ"), sub("αβ")); - assert_eq!(parse_linear("x_10"), sub("10")); - assert_eq!(parse_linear("x_2.5"), sub("2.5")); - assert_eq!(to_linear(&sub("2a")), "x_2a"); - assert_eq!(parse_linear(&to_linear(&sub("2a"))), sub("2a")); - } - - #[test] - fn hostile_linear_never_panics() { - for s in ["", "/", "^", "_", "√", "∑", "((((", "))))", "■(", "█(@@&&", "a/", "/b", "^^^__", "√(&)", "∫_^▒", "lim_", "sin"] { - let _ = to_linear(&parse_linear(s)); - } - let deep = "(".repeat(5000) + &")".repeat(5000); - let _ = parse_linear(&deep); - let deep = "√".repeat(3000); - let _ = parse_linear(&deep); - } - - #[test] - fn a_long_run_of_letters_parses_in_linear_time() { - // Each letter that doesn't start a function name used to rescan (and copy) the rest of - // the run, so a 20,000-letter equation did 200 million steps. (No wall-clock assert: a - // loaded runner would fail it; a regression shows as a slow test.) - for _ in 0..5 { - let e = parse_linear(&"x".repeat(30_000)); - assert!(matches!(e.as_slice(), [MNode::Run(r)] if r.text.len() == 20_000), "{:?}", e.len()); - } - // Function names are still found at the start of a run, and only as whole runs. - assert!(matches!(parse_linear("sin x").as_slice(), [MNode::Func { .. }])); - assert!(matches!(parse_linear("arcsin x").as_slice(), [MNode::Func { .. }])); - assert!(matches!(parse_linear("sinx").as_slice(), [MNode::Run(_)])); - assert!(matches!(parse_linear("arcsinhx").as_slice(), [MNode::Run(_)])); - } -} diff --git a/crates/wordmodel/src/math_latex.rs b/crates/wordmodel/src/math_latex.rs deleted file mode 100644 index 92979534..00000000 --- a/crates/wordmodel/src/math_latex.rs +++ /dev/null @@ -1,1242 +0,0 @@ -//! LaTeX ↔ math tree (Word's LaTeX equation input and conversion). -//! -//! Input covers the commands people type in equations: `\frac`, `\sqrt[n]{}`, scripts, big -//! operators with limits, `\left…\middle…\right`, matrix and `cases`/`aligned` environments, -//! accents, `\overbrace`/`\underbrace`, `\text`/`\mathrm`/`\mathbf`/`\mathbb`…, function names, -//! spacing and every named symbol in [`crate::math_symbols`]. Unknown commands stay as text. - -use crate::math::{ - Arg, ColJc, FracKind, MAX_DEPTH, MNode, MRun, MScr, MSty, ScriptKind, WRAP_DEPTH, any_too_deep, is_nary_char, math_class, merge_runs, too_deep, -}; -use crate::math_linear::{FUNCTION_NAMES, map_args}; -use crate::math_symbols::{name_of, symbol}; - -#[derive(Clone, Debug, PartialEq)] -enum Tok { - Cmd(String), - Open, - Close, - Sup, - Sub, - Amp, - Row, - Ch(char), -} - -/// Commands whose `{…}` argument is text, where spaces are part of the content. -const TEXT_COMMANDS: &[&str] = &["text", "textrm", "mbox", "textnormal", "textbf", "textit"]; - -fn tokenize(s: &str) -> Vec { - let cs: Vec = s.chars().take(20_000).collect(); - let mut out = Vec::new(); - let mut i = 0; - // A text command was just read: its `{` starts text, where spaces count (#448). - let mut text_next = false; - // Brace depth inside a text argument. - let mut text_depth: Option = None; - while let Some(&c) = cs.get(i) { - i += 1; - if text_depth.is_some() && matches!(c, ' ' | '\t' | '\n' | '\r') { - // A run of white space is one space, as in LaTeX. - if out.last() != Some(&Tok::Ch(' ')) { - out.push(Tok::Ch(' ')); - } - continue; - } - let was_text_next = std::mem::take(&mut text_next); - match c { - '\\' => { - let start = i; - while cs.get(i).is_some_and(char::is_ascii_alphabetic) { - i += 1; - } - if i == start { - match cs.get(i) { - Some('\\') => { - i += 1; - out.push(Tok::Row); - } - Some(&c) => { - i += 1; - out.push(Tok::Cmd(c.to_string())); - } - None => {} - } - } else { - let name: String = cs.get(start..i).unwrap_or(&[]).iter().collect(); - text_next = text_depth.is_none() && TEXT_COMMANDS.contains(&name.as_str()); - out.push(Tok::Cmd(name)); - } - } - '{' => { - text_depth = match text_depth { - Some(d) => Some(d.saturating_add(1)), - None if was_text_next => Some(0), - None => None, - }; - out.push(Tok::Open); - } - '}' => { - text_depth = match text_depth { - Some(0) | None => None, - Some(d) => Some(d.saturating_sub(1)), - }; - out.push(Tok::Close); - } - '^' => out.push(Tok::Sup), - '_' => out.push(Tok::Sub), - '&' => out.push(Tok::Amp), - // White space between a text command and its `{` doesn't end the wait. - ' ' | '\t' | '\n' | '\r' => text_next = was_text_next, - '~' => out.push(Tok::Ch('\u{A0}')), - c => out.push(Tok::Ch(c)), - } - } - out -} - -/// Parse LaTeX math into nodes. Never fails. -pub fn parse_latex(s: &str) -> Arg { - let mut p = Lp { t: tokenize(s), i: 0, depth: 0 }; - let mut out = Vec::new(); - while p.i < p.t.len() { - out.extend(p.seq(false)); - // A stray `}` or `&`/`\\` at the top level. - match p.t.get(p.i) { - Some(Tok::Amp) => out.push(run("&")), - Some(Tok::Row) => {} - Some(Tok::Close) => {} - _ => {} - } - p.i += 1; - } - let out = merge_runs(out); - if out.iter().any(|n| matches!(n, MNode::Run(r) if !r.nor && r.text.contains('#'))) { - return vec![MNode::EqArr { rows: vec![out] }]; - } - out -} - -fn run(t: &str) -> MNode { - MNode::Run(MRun::new(t)) -} - -struct Lp { - t: Vec, - i: usize, - depth: usize, -} - -impl Lp { - fn peek(&self) -> Option<&Tok> { - self.t.get(self.i) - } - - /// A sequence up to `}` (in a group), `&`, `\\`, `\right`, `\middle` or `\end`. - fn seq(&mut self, in_group: bool) -> Arg { - self.depth += 1; - let mut out: Arg = Vec::new(); - if self.depth > MAX_DEPTH { - self.i = self.t.len(); - self.depth -= 1; - return out; - } - while let Some(tok) = self.peek().cloned() { - match tok { - Tok::Close if in_group => break, - Tok::Close | Tok::Amp | Tok::Row => break, - Tok::Cmd(ref c) if matches!(c.as_str(), "right" | "middle" | "end") => break, - Tok::Sup | Tok::Sub => { - self.i += 1; - let s = self.arg(); - if out.last().is_some_and(too_deep) { - // Too deep to wrap (`x^1^1^1…`): the operator is dropped, the script kept. - out.extend(s); - } else { - let base = out.pop(); - out.push(script(base, tok == Tok::Sup, s)); - } - } - Tok::Cmd(ref c) if c == "limits" || c == "nolimits" || c == "displaystyle" || c == "textstyle" => { - self.i += 1; - if c == "nolimits" - && let Some(MNode::Nary { lim_loc, .. }) = out.last_mut() - { - *lim_loc = Some(crate::math::LimLoc::SubSup); - } - if c == "limits" - && let Some(MNode::Nary { lim_loc, .. }) = out.last_mut() - { - *lim_loc = Some(crate::math::LimLoc::UndOvr); - } - } - _ => { - let a = self.atom(); - out.extend(a); - } - } - } - self.depth -= 1; - // Big operators take the following term as their operand. - nary_operands(out, self.depth) - } - - /// A command argument: a `{group}` or one token. - fn arg(&mut self) -> Arg { - match self.peek() { - Some(Tok::Open) => { - self.i += 1; - let g = self.seq(true); - if self.peek() == Some(&Tok::Close) { - self.i += 1; - } - g - } - Some(_) => self.atom(), - None => Vec::new(), - } - } - - /// `[optional]` argument. - fn opt(&mut self) -> Option { - if self.peek() != Some(&Tok::Ch('[')) { - return None; - } - self.i += 1; - let mut toks = Vec::new(); - let mut level = 0i32; - while let Some(t) = self.peek().cloned() { - self.i += 1; - match t { - Tok::Ch(']') if level == 0 => break, - Tok::Open => level += 1, - Tok::Close => level -= 1, - _ => {} - } - toks.push(t); - } - let mut p = Lp { t: toks, i: 0, depth: self.depth }; - Some(p.seq(false)) - } - - /// The text of a `{…}` argument, verbatim. - fn raw_arg(&mut self) -> String { - let mut s = String::new(); - if self.peek() != Some(&Tok::Open) { - if let Some(Tok::Ch(c)) = self.peek().cloned() { - self.i += 1; - s.push(c); - } - return s; - } - self.i += 1; - let mut level = 0i32; - // The previous token was a control word (`\backslash{}`: the empty braces end its name). - let mut after_word = false; - while let Some(t) = self.peek().cloned() { - self.i += 1; - let was_word = std::mem::replace(&mut after_word, matches!(&t, Tok::Cmd(c) if c.chars().all(|c| c.is_ascii_alphabetic()))); - match t { - Tok::Close if level == 0 => break, - Tok::Open if was_word && self.peek() == Some(&Tok::Close) => self.i += 1, - Tok::Open => { - level += 1; - s.push('{'); - } - Tok::Close => { - level -= 1; - s.push('}'); - } - Tok::Ch(c) => s.push(c), - Tok::Cmd(c) => { - if c.chars().all(|c| c.is_ascii_alphabetic()) { - match symbol(&c) { - Some(sym) => s.push(sym), - None => s.push_str(&c), - } - } else { - s.push_str(&c); - } - if c == " " { - s.push(' '); - } - } - Tok::Sup => s.push('^'), - Tok::Sub => s.push('_'), - Tok::Amp => s.push('&'), - Tok::Row => s.push('\n'), - } - } - s - } - - /// A delimiter after `\left`, `\right`, `\middle`, `\big…`. - fn delim(&mut self) -> Option { - let t = self.peek().cloned()?; - self.i += 1; - match t { - Tok::Ch('.') => None, - Tok::Ch(c) => Some(c), - Tok::Cmd(c) => match c.as_str() { - "{" => Some('{'), - "}" => Some('}'), - "|" => Some('‖'), - other => symbol(other), - }, - _ => None, - } - } - - fn atom(&mut self) -> Arg { - if self.depth > MAX_DEPTH { - self.i = self.t.len(); - return Vec::new(); - } - self.depth += 1; - let out = self.atom_inner(); - self.depth -= 1; - out - } - - fn atom_inner(&mut self) -> Arg { - let Some(t) = self.peek().cloned() else { return Vec::new() }; - self.i += 1; - match t { - Tok::Open => { - let g = self.seq(true); - if self.peek() == Some(&Tok::Close) { - self.i += 1; - } - g - } - Tok::Ch(c) => vec![run(&c.to_string())], - Tok::Close | Tok::Amp | Tok::Row | Tok::Sup | Tok::Sub => Vec::new(), - Tok::Cmd(c) => self.command(&c), - } - } - - fn command(&mut self, c: &str) -> Arg { - let styled = |a: Arg, sty: Option, scr: MScr, nor: bool| -> Arg { restyle(a, sty, scr, nor) }; - let one = |n: MNode| vec![n]; - match c { - "frac" | "dfrac" | "tfrac" | "cfrac" => { - let num = self.arg(); - let den = self.arg(); - one(MNode::Frac { kind: FracKind::Bar, num, den }) - } - "genfrac" => { - let beg = delim_arg(&self.arg()); - let end = delim_arg(&self.arg()); - let thickness = self.arg(); - let _style = self.arg(); - let num = self.arg(); - let den = self.arg(); - let kind = if is_zero_length(&thickness) { FracKind::NoBar } else { FracKind::Bar }; - let frac = MNode::Frac { kind, num, den }; - if beg.is_none() && end.is_none() { - one(frac) - } else { - one(MNode::Delim { beg, end, sep: None, grow: true, shp_match: false, elems: vec![vec![frac]] }) - } - } - "binom" | "dbinom" | "tbinom" => { - let n = self.arg(); - let k = self.arg(); - one(MNode::Delim { - beg: Some('('), - end: Some(')'), - sep: None, - grow: true, - shp_match: false, - elems: vec![vec![MNode::Frac { kind: FracKind::NoBar, num: n, den: k }]], - }) - } - "sqrt" => { - let deg = self.opt().unwrap_or_default(); - let e = self.arg(); - one(MNode::Rad { deg_hide: deg.is_empty(), deg, e }) - } - "left" => { - let beg = self.delim(); - let mut elems = vec![self.seq(false)]; - let mut sep = None; - let mut end = None; - while let Some(Tok::Cmd(k)) = self.peek().cloned() { - self.i += 1; - if k == "middle" { - sep = self.delim().or(Some('|')); - elems.push(self.seq(false)); - } else { - if k == "right" { - end = self.delim(); - } - break; - } - } - one(MNode::Delim { beg, end, sep, grow: true, shp_match: false, elems }) - } - "bigl" | "Bigl" | "biggl" | "Biggl" | "bigr" | "Bigr" | "biggr" | "Biggr" | "big" | "Big" | "bigg" | "Bigg" => { - self.delim().map(|d| vec![run(&d.to_string())]).unwrap_or_default() - } - "begin" => { - let env = self.raw_arg(); - self.environment(env.trim()) - } - "text" | "textrm" | "mbox" | "textnormal" => { - let t = self.raw_arg(); - one(MNode::Run(MRun { text: t, nor: true, sty: Some(MSty::Plain), ..Default::default() })) - } - "textbf" => { - let t = self.raw_arg(); - one(MNode::Run(MRun { text: t, nor: true, sty: Some(MSty::Bold), ..Default::default() })) - } - "textit" => { - let t = self.raw_arg(); - one(MNode::Run(MRun { text: t, nor: true, sty: Some(MSty::Italic), ..Default::default() })) - } - "operatorname" => { - let starred = self.peek() == Some(&Tok::Ch('*')); - if starred { - self.i += 1; - } - let t = self.raw_arg(); - let name = vec![MNode::Run(MRun::plain(t))]; - if starred && self.peek() == Some(&Tok::Sub) { - self.i += 1; - let lim = self.arg(); - vec![MNode::Lim { upper: false, e: name, lim }] - } else { - self.apply(name) - } - } - "tag" => { - let starred = self.peek() == Some(&Tok::Ch('*')); - if starred { - self.i += 1; - } - let t = self.raw_arg(); - let num_str = if starred { format!("#{t}") } else { format!("# ({t})") }; - let mut r = MRun::new(num_str); - r.lit = false; - vec![MNode::Run(r)] - } - "mathrm" | "rm" => { - let a = self.arg(); - styled(a, Some(MSty::Plain), MScr::Roman, false) - } - "mathbf" | "bf" => { - let a = self.arg(); - styled(a, Some(MSty::Bold), MScr::Roman, false) - } - "mathit" => { - let a = self.arg(); - styled(a, Some(MSty::Italic), MScr::Roman, false) - } - "boldsymbol" | "bm" => { - let a = self.arg(); - styled(a, Some(MSty::BoldItalic), MScr::Roman, false) - } - "mathbb" => { - let a = self.arg(); - styled(a, None, MScr::DoubleStruck, false) - } - "mathcal" | "mathscr" => { - let a = self.arg(); - styled(a, None, MScr::Script, false) - } - "mathfrak" => { - let a = self.arg(); - styled(a, None, MScr::Fraktur, false) - } - "mathsf" => { - let a = self.arg(); - styled(a, None, MScr::SansSerif, false) - } - "mathtt" => { - let a = self.arg(); - styled(a, None, MScr::Monospace, false) - } - "overline" => one(MNode::Bar { top: true, e: self.arg() }), - "underline" => one(MNode::Bar { top: false, e: self.arg() }), - "boxed" | "fbox" => one(MNode::BorderBox { hide: [false; 4], strike: [false; 4], e: self.arg() }), - "phantom" => one(MNode::Phant { show: false, zero_wid: false, zero_asc: false, zero_desc: false, e: self.arg() }), - "overbrace" | "underbrace" | "overparen" | "underparen" | "overbracket" | "underbracket" => { - let top = c.starts_with("over"); - let chr = match c { - "overbrace" => '⏞', - "underbrace" => '⏟', - "overparen" => '⏜', - "underparen" => '⏝', - "overbracket" => '⎴', - _ => '⎵', - }; - let e = self.arg(); - let g = MNode::GroupChr { chr, top, e }; - let want = if top { Tok::Sup } else { Tok::Sub }; - if self.peek() == Some(&want) { - self.i += 1; - let lim = self.arg(); - one(MNode::Lim { upper: top, e: vec![g], lim }) - } else { - one(g) - } - } - "stackrel" | "overset" => { - let over = self.arg(); - let base = self.arg(); - one(MNode::Lim { upper: true, e: base, lim: over }) - } - "underset" => { - let under = self.arg(); - let base = self.arg(); - one(MNode::Lim { upper: false, e: base, lim: under }) - } - "hat" | "widehat" | "tilde" | "widetilde" | "dot" | "ddot" | "dddot" | "check" | "breve" | "acute" | "grave" | "vec" | "bar" - | "overrightarrow" | "overleftarrow" | "overleftrightarrow" | "mathring" => { - let chr = match c { - "hat" | "widehat" => '\u{302}', - "tilde" | "widetilde" => '\u{303}', - "dot" => '\u{307}', - "ddot" => '\u{308}', - "dddot" => '\u{20DB}', - "check" => '\u{30C}', - "breve" => '\u{306}', - "acute" => '\u{301}', - "grave" => '\u{300}', - "bar" => '\u{305}', - "overleftarrow" => '\u{20D6}', - "overleftrightarrow" => '\u{20E1}', - "mathring" => '\u{30A}', - _ => '\u{20D7}', - }; - one(MNode::Acc { chr, e: self.arg() }) - } - "," | "thinspace" => vec![run("\u{2009}")], - ":" | ">" | "medspace" => vec![run("\u{205F}")], - ";" | "thickspace" => vec![run("\u{2005}")], - "!" | "negthinspace" => Vec::new(), - " " => vec![run(" ")], - "qquad" => vec![run("\u{2003}\u{2003}")], - "{" | "}" | "%" | "$" | "#" | "&" | "_" => { - // Escaped characters (a literal `#` must not number the equation). - let mut r = MRun::new(c); - r.lit = c == "#"; - vec![MNode::Run(r)] - } - "|" => vec![run("‖")], - name if FUNCTION_NAMES.contains(&name) => { - let plain = vec![MNode::Run(MRun::plain(name))]; - let limits = matches!(name, "lim" | "liminf" | "limsup" | "max" | "min" | "sup" | "inf" | "det" | "gcd" | "Pr"); - let named = if limits && self.peek() == Some(&Tok::Sub) { - self.i += 1; - let lim = self.arg(); - vec![MNode::Lim { upper: false, e: plain, lim }] - } else if matches!(self.peek(), Some(Tok::Sup) | Some(Tok::Sub)) { - let sup = self.peek() == Some(&Tok::Sup); - self.i += 1; - let s = self.arg(); - vec![script(Some(MNode::Run(MRun::plain(name))), sup, s)] - } else { - plain - }; - self.apply(named) - } - name => match symbol(name) { - Some(ch) if is_nary_char(ch) => vec![MNode::Nary { - chr: ch, - lim_loc: None, - grow: false, - sub_hide: true, - sup_hide: true, - sub: Vec::new(), - sup: Vec::new(), - e: Vec::new(), - }], - Some(ch) => vec![run(&ch.to_string())], - None => vec![run(&format!("\\{name}"))], - }, - } - } - - /// A function name applied to the next atom (with its scripts). - fn apply(&mut self, name: Arg) -> Arg { - let mut e = self.atom(); - while let Some(t @ (Tok::Sup | Tok::Sub)) = self.peek().cloned() { - self.i += 1; - let s = self.arg(); - if any_too_deep(&e) { - e.extend(s); - continue; - } - let base = if e.len() == 1 { e.pop() } else { Some(group(std::mem::take(&mut e))) }; - e.push(script(base, t == Tok::Sup, s)); - } - if e.is_empty() { - return name; - } - vec![MNode::Func { name, e }] - } - - fn environment(&mut self, env: &str) -> Arg { - let base = env.trim_end_matches('*'); - let mut col_jc = Vec::new(); - if base == "array" || base == "tabular" { - let _ = self.opt(); - let spec = self.raw_arg(); - for ch in spec.chars() { - match ch { - 'l' | 'L' => col_jc.push(ColJc::Left), - 'r' | 'R' => col_jc.push(ColJc::Right), - 'c' | 'C' => col_jc.push(ColJc::Center), - _ => {} - } - } - } else if base == "aligned" { - let _ = self.opt(); - } - // Rows of `&`-separated cells up to `\end{…}`. - let mut rows: Vec> = vec![vec![]]; - loop { - let cell = self.seq(false); - if let Some(r) = rows.last_mut() { - r.push(cell); - } - match self.peek().cloned() { - Some(Tok::Amp) => self.i += 1, - Some(Tok::Row) => { - self.i += 1; - rows.push(vec![]); - } - Some(Tok::Cmd(k)) if k == "end" => { - self.i += 1; - let _ = self.raw_arg(); - break; - } - Some(Tok::Close) => self.i += 1, - Some(_) => self.i += 1, - None => break, - } - } - // A trailing `\\` leaves an empty last row. - if rows.len() > 1 && rows.last().is_some_and(|r| r.iter().all(|c| c.is_empty())) { - rows.pop(); - } - let delims = match base { - "pmatrix" => Some((Some('('), Some(')'))), - "bmatrix" => Some((Some('['), Some(']'))), - "Bmatrix" => Some((Some('{'), Some('}'))), - "vmatrix" => Some((Some('|'), Some('|'))), - "Vmatrix" => Some((Some('‖'), Some('‖'))), - _ => None, - }; - let wrap = |inner: MNode, beg: Option, end: Option| MNode::Delim { - beg, - end, - sep: None, - grow: true, - shp_match: false, - elems: vec![vec![inner]], - }; - match base { - "matrix" | "pmatrix" | "bmatrix" | "Bmatrix" | "vmatrix" | "Vmatrix" | "smallmatrix" | "array" | "tabular" => { - let m = MNode::Matrix { rows, col_jc }; - match delims { - Some((b, e)) => vec![wrap(m, b, e)], - None => vec![m], - } - } - "cases" | "dcases" => { - let eq = MNode::EqArr { rows: rows.into_iter().map(join_cells).collect() }; - vec![wrap(eq, Some('{'), None)] - } - "rcases" => { - let eq = MNode::EqArr { rows: rows.into_iter().map(join_cells).collect() }; - vec![wrap(eq, None, Some('}'))] - } - _ => vec![MNode::EqArr { rows: rows.into_iter().map(join_cells).collect() }], - } - } -} - -/// Cells of an aligned row joined with `&` alignment marks. -fn join_cells(cells: Vec) -> Arg { - let mut out = Vec::new(); - for (i, c) in cells.into_iter().enumerate() { - if i > 0 { - out.push(run("&")); - } - out.extend(c); - } - merge_runs(out) -} - -fn group(a: Arg) -> MNode { - MNode::Delim { beg: None, end: None, sep: None, grow: true, shp_match: false, elems: vec![a] } -} - -fn script(base: Option, sup: bool, s: Arg) -> MNode { - match base { - Some(MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub, sup: sp, e }) if e.is_empty() => { - if sup { - MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide: false, sub, sup: s, e } - } else { - MNode::Nary { chr, lim_loc, grow, sub_hide: false, sup_hide, sub: s, sup: sp, e } - } - } - Some(MNode::Script { kind: ScriptKind::Sub, base, sub, .. }) if sup => MNode::Script { kind: ScriptKind::SubSup, base, sub, sup: s }, - Some(MNode::Script { kind: ScriptKind::Sup, base, sup: sp, .. }) if !sup => MNode::Script { kind: ScriptKind::SubSup, base, sub: s, sup: sp }, - b => { - let base = b.map(|b| vec![b]).unwrap_or_default(); - if sup { - MNode::Script { kind: ScriptKind::Sup, base, sub: Vec::new(), sup: s } - } else { - MNode::Script { kind: ScriptKind::Sub, base, sub: s, sup: Vec::new() } - } - } - } -} - -/// Big operators without an operand take the atoms after them up to a relation or binary operator. -/// `depth` is how deep the operators already sit; past [`WRAP_DEPTH`] they stay without operand -/// (`\sum\sum\sum…` must not nest once per operator). -fn nary_operands(a: Arg, depth: usize) -> Arg { - let mut out: Arg = Vec::with_capacity(a.len()); - let mut it = a.into_iter().peekable(); - while let Some(n) = it.next() { - match n { - MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub, sup, e } if e.is_empty() && depth < WRAP_DEPTH => { - let mut body = Vec::new(); - while let Some(next) = it.peek() { - let stop = match next { - MNode::Run(r) => r.text.chars().next().is_some_and(|c| { - matches!(math_class(c), crate::math::MClass::Rel | crate::math::MClass::Bin | crate::math::MClass::Punct) || c == '&' - }), - _ => false, - }; - if stop { - break; - } - if let Some(x) = it.next() { - body.push(x); - } - } - let body = merge_runs(nary_operands(body, depth + 1)); - if any_too_deep(&body) { - // No room to wrap the operand: it follows the operator. - out.push(MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub, sup, e }); - out.extend(body); - } else { - out.push(MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub, sup, e: body }); - } - } - other => out.push(other), - } - } - out -} - -/// Set a style on every run inside. -fn restyle(a: Arg, sty: Option, scr: MScr, nor: bool) -> Arg { - a.into_iter() - .map(|n| match n { - MNode::Run(mut r) => { - if sty.is_some() { - r.sty = sty; - } - if scr != MScr::Roman { - r.scr = scr; - } - r.nor |= nor; - MNode::Run(r) - } - other => map_args(other, &|a| restyle(a, sty, scr, nor)), - }) - .collect() -} - -// --------------------------------------------------------------------------------------------- -// Out. - -/// The equation as LaTeX. -pub fn to_latex(nodes: &[MNode]) -> String { - let mut s = String::new(); - arg(nodes, &mut s, 0); - s.trim().to_string() -} - -fn arg(a: &[MNode], s: &mut String, depth: usize) { - if depth > MAX_DEPTH { - return; - } - for n in a { - node(n, s, depth + 1); - } -} - -fn braced(a: &[MNode], s: &mut String, depth: usize) { - s.push('{'); - arg(a, s, depth); - s.push('}'); -} - -fn chars(text: &str, s: &mut String) { - for c in text.chars() { - match c { - '{' | '}' | '%' | '$' | '#' | '&' | '_' => { - s.push('\\'); - s.push(c); - } - '\\' => s.push_str("\\backslash "), - '−' => s.push('-'), - c if c.is_ascii() => s.push(c), - c => match name_of(c) { - Some(n) if !matches!(n, "sqrt" | "matrix" | "of" | "above" | "below" | "box" | "rect") => { - s.push('\\'); - s.push_str(n); - s.push(' '); - } - _ => s.push(c), - }, - } - } -} - -/// Text inside `\text{…}`: only what LaTeX would read as markup is escaped. -fn text_chars(text: &str, s: &mut String) { - for c in text.chars() { - match c { - '{' | '}' | '%' | '$' | '#' | '&' | '_' | '~' => { - s.push('\\'); - s.push(c); - } - // `{}` ends the command name without adding a space (spaces count in text). - '\\' => s.push_str("\\backslash{}"), - c => s.push(c), - } - } -} - -/// The delimiter of a `\genfrac` argument: one character, or none for `{}` and anything else. -fn delim_arg(a: &[MNode]) -> Option { - let [MNode::Run(r)] = a else { return None }; - let mut cs = r.text.chars(); - match (cs.next(), cs.next()) { - (Some(c), None) if c != '.' => Some(c), - _ => None, - } -} - -/// Whether a `\genfrac` thickness is a zero length such as `0pt`, `0.0pt` or `0 em`. An empty -/// thickness means the default rule and anything that is not a plain number and unit is not zero. -fn is_zero_length(a: &[MNode]) -> bool { - let mut text = String::new(); - for n in a { - let MNode::Run(r) = n else { return false }; - text.extend(r.text.chars().filter(|c| !c.is_whitespace() && !matches!(c, '\u{2009}' | '\u{205F}' | '\u{2005}'))); - } - let number = text.trim_end_matches(|c: char| c.is_ascii_alphabetic()); - let unit = &text[number.len()..]; - !number.is_empty() - && matches!(unit, "pt" | "em" | "ex" | "mm" | "cm" | "in" | "bp" | "pc" | "sp" | "mu") - && number.parse::().is_ok_and(|v| v == 0.0) -} - -fn delim_tex(c: Option) -> String { - match c { - None => ".".into(), - Some('{') => "\\{".into(), - Some('}') => "\\}".into(), - Some('‖') => "\\|".into(), - Some(c) if c.is_ascii() => c.to_string(), - Some(c) => name_of(c).map(|n| format!("\\{n} ")).unwrap_or_else(|| c.to_string()), - } -} - -fn group_chr_tex(chr: char, top: bool) -> &'static str { - match chr { - '⏜' => "\\overparen", - '⏝' => "\\underparen", - '⎴' => "\\overbracket", - '⎵' => "\\underbracket", - _ if top => "\\overbrace", - _ => "\\underbrace", - } -} - -fn node(node: &MNode, output: &mut String, depth: usize) { - match node { - MNode::Run(run) => run_node(run, output), - MNode::Frac { kind, num, den } => fraction_node(*kind, num, den, output, depth), - MNode::Script { kind, base, sub, sup } => script_node(*kind, base, sub, sup, output, depth), - MNode::Rad { deg, deg_hide, e } => radical_node(deg, *deg_hide, e, output, depth), - MNode::Nary { .. } => nary_node(node, output, depth), - MNode::Delim { beg, end, sep, elems, .. } => delimiter_node(*beg, *end, *sep, elems, output, depth), - MNode::Func { name, e } => { - arg(name, output, depth); - braced(e, output, depth); - } - MNode::Lim { upper, e, lim } => limit_node(*upper, e, lim, output, depth), - MNode::Acc { chr, e } => accent_node(*chr, e, output, depth), - MNode::Bar { top, e } => { - output.push_str(if *top { "\\overline" } else { "\\underline" }); - braced(e, output, depth); - } - MNode::BorderBox { e, .. } => { - output.push_str("\\boxed"); - braced(e, output, depth); - } - MNode::Boxed { e } => braced(e, output, depth), - MNode::GroupChr { chr, top, e } => { - output.push_str(group_chr_tex(*chr, *top)); - braced(e, output, depth); - } - MNode::EqArr { rows } => equation_array_node(rows, output, depth), - MNode::Matrix { rows, .. } => matrix_node(rows, output, depth), - MNode::Phant { e, .. } => { - output.push_str("\\phantom"); - braced(e, output, depth); - } - } -} - -fn run_node(run: &MRun, output: &mut String) { - let (open, close) = if run.nor { - ("\\text{", "}") - } else { - match (run.sty, run.scr) { - (_, MScr::DoubleStruck) => ("\\mathbb{", "}"), - (_, MScr::Script) => ("\\mathcal{", "}"), - (_, MScr::Fraktur) => ("\\mathfrak{", "}"), - (_, MScr::SansSerif) => ("\\mathsf{", "}"), - (_, MScr::Monospace) => ("\\mathtt{", "}"), - (Some(MSty::Bold), _) => ("\\mathbf{", "}"), - (Some(MSty::BoldItalic), _) => ("\\boldsymbol{", "}"), - (Some(MSty::Plain), _) if run.text.chars().any(char::is_alphabetic) => { - if FUNCTION_NAMES.contains(&run.text.as_str()) { - output.push('\\'); - output.push_str(&run.text); - output.push(' '); - return; - } - ("\\mathrm{", "}") - } - _ => ("", ""), - } - }; - output.push_str(open); - if run.nor { - text_chars(&run.text, output) - } else { - chars(&run.text, output) - } - output.push_str(close); -} - -fn fraction_node(kind: FracKind, numerator: &[MNode], denominator: &[MNode], output: &mut String, depth: usize) { - match kind { - FracKind::NoBar => { - output.push_str("\\genfrac{}{}{0pt}{}"); - braced(numerator, output, depth); - braced(denominator, output, depth); - } - FracKind::Linear | FracKind::Skewed => { - braced(numerator, output, depth); - output.push('/'); - braced(denominator, output, depth); - } - FracKind::Bar => { - output.push_str("\\frac"); - braced(numerator, output, depth); - braced(denominator, output, depth); - } - } -} - -fn script_node(kind: ScriptKind, base: &[MNode], sub: &[MNode], sup: &[MNode], output: &mut String, depth: usize) { - if kind == ScriptKind::Pre { - output.push_str("{}_"); - braced(sub, output, depth); - output.push('^'); - braced(sup, output, depth); - braced(base, output, depth); - return; - } - braced(base, output, depth); - if matches!(kind, ScriptKind::Sub | ScriptKind::SubSup) { - output.push('_'); - braced(sub, output, depth); - } - if matches!(kind, ScriptKind::Sup | ScriptKind::SubSup) { - output.push('^'); - braced(sup, output, depth); - } -} - -fn radical_node(degree: &[MNode], degree_hidden: bool, expression: &[MNode], output: &mut String, depth: usize) { - output.push_str("\\sqrt"); - if !degree_hidden && !degree.is_empty() { - output.push('['); - arg(degree, output, depth); - output.push(']'); - } - braced(expression, output, depth); -} - -fn nary_node(node: &MNode, output: &mut String, depth: usize) { - let MNode::Nary { chr, lim_loc, sub_hide, sup_hide, sub, sup, e, .. } = node else { return }; - match name_of(*chr) { - Some(name) => { - output.push('\\'); - output.push_str(name); - } - None => output.push(*chr), - } - match lim_loc { - Some(crate::math::LimLoc::UndOvr) => output.push_str("\\limits"), - Some(crate::math::LimLoc::SubSup) if !crate::math::is_integral(*chr) => output.push_str("\\nolimits"), - _ => {} - } - if !sub_hide && !sub.is_empty() { - output.push('_'); - braced(sub, output, depth); - } - if !sup_hide && !sup.is_empty() { - output.push('^'); - braced(sup, output, depth); - } - output.push(' '); - braced(e, output, depth); -} - -fn delimiter_node(begin: Option, end: Option, separator: Option, elements: &[Arg], output: &mut String, depth: usize) { - output.push_str("\\left"); - output.push_str(&delim_tex(begin)); - let middle = separator.map(|c| format!("\\middle{}", delim_tex(Some(c)))).unwrap_or_else(|| "\\middle|".to_string()); - for (index, element) in elements.iter().enumerate() { - if index > 0 { - output.push_str(&middle); - } - arg(element, output, depth); - } - output.push_str("\\right"); - output.push_str(&delim_tex(end)); -} - -fn limit_node(upper: bool, expression: &[MNode], limit: &[MNode], output: &mut String, depth: usize) { - let simple = matches!(expression, [MNode::Run(run)] if run.sty == Some(MSty::Plain) && FUNCTION_NAMES.contains(&run.text.as_str())); - if simple && !upper { - arg(expression, output, depth); - output.push('_'); - braced(limit, output, depth); - } else if let [MNode::GroupChr { chr, top, e: inner }] = expression { - output.push_str(group_chr_tex(*chr, *top)); - braced(inner, output, depth); - output.push(if upper { '^' } else { '_' }); - braced(limit, output, depth); - } else { - output.push_str(if upper { "\\overset" } else { "\\underset" }); - braced(limit, output, depth); - braced(expression, output, depth); - } -} - -fn accent_node(character: char, expression: &[MNode], output: &mut String, depth: usize) { - let command = match character { - '\u{302}' => "hat", - '\u{303}' => "tilde", - '\u{307}' => "dot", - '\u{308}' => "ddot", - '\u{20DB}' => "dddot", - '\u{30C}' => "check", - '\u{306}' => "breve", - '\u{301}' => "acute", - '\u{300}' => "grave", - '\u{305}' | '\u{AF}' => "bar", - '\u{20D6}' => "overleftarrow", - '\u{20E1}' => "overleftrightarrow", - _ => "vec", - }; - output.push('\\'); - output.push_str(command); - braced(expression, output, depth); -} - -fn equation_array_node(rows: &[Arg], output: &mut String, depth: usize) { - if rows.len() == 1 - && let Some(row) = rows.first() - && crate::math::has_number_mark(row) - { - arg(row, output, depth); - return; - } - output.push_str("\\begin{aligned}"); - for (index, row) in rows.iter().enumerate() { - if index > 0 { - output.push_str("\\\\"); - } - for child_node in row { - match child_node { - MNode::Run(run) if !run.lit && run.text.contains('&') => { - for (index, part) in run.text.split('&').enumerate() { - if index > 0 { - output.push('&'); - } - let mut piece = run.clone(); - piece.text = part.to_string(); - run_node(&piece, output); - } - } - other => node(other, output, depth + 1), - } - } - } - output.push_str("\\end{aligned}"); -} - -fn matrix_node(rows: &[Vec], output: &mut String, depth: usize) { - output.push_str("\\begin{matrix}"); - for (row_index, row) in rows.iter().enumerate() { - if row_index > 0 { - output.push_str("\\\\"); - } - for (column, cell) in row.iter().enumerate() { - if column > 0 { - output.push('&'); - } - arg(cell, output, depth); - } - } - output.push_str("\\end{matrix}"); -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::math::{LimLoc, MathJc}; - - #[test] - fn latex_structures() { - let n = parse_latex(r"x=\frac{-b\pm\sqrt{b^2-4ac}}{2a}"); - assert_eq!(crate::math::to_linear(&n), "x=(-b±√(b^2-4ac))/2a"); - let n = parse_latex(r"\sum_{i=1}^{n} i^2 = \frac{n(n+1)(2n+1)}{6}"); - let MNode::Nary { chr, sub, sup, e, .. } = &n[0] else { panic!("{n:#?}") }; - assert_eq!((*chr, sub.len(), sup.len()), ('∑', 1, 1)); - assert!(matches!(e.first(), Some(MNode::Script { .. })), "{e:#?}"); - let n = parse_latex(r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}"); - assert!( - matches!(&n[0], MNode::Delim { beg: Some('('), elems, .. } if matches!(elems[0][0], MNode::Matrix { ref rows, .. } if rows.len() == 2)) - ); - let n = parse_latex(r"f(x)=\begin{cases} x & x\ge 0\\ -x & x<0\end{cases}"); - assert!(n.iter().any(|m| matches!(m, MNode::Delim { beg: Some('{'), end: None, .. })), "{n:#?}"); - let n = parse_latex(r"\lim_{n\to\infty}\left(1+\frac{1}{n}\right)^n"); - assert!(matches!(&n[0], MNode::Func { .. }), "{n:#?}"); - let n = parse_latex(r"\mathbb{R}\hat{x}\text{ if }\overbrace{a+b}^{k}"); - assert!(matches!(&n[0], MNode::Run(r) if r.scr == MScr::DoubleStruck)); - let _ = (LimLoc::UndOvr, MathJc::Center); - } - - #[test] - fn latex_binom_round_trip() { - let a = parse_latex(r"\binom{n}{k}"); - let l = to_latex(&a); - assert_eq!(parse_latex(&l), a, "exported as {l}"); - let nobar = |n: &str, k: &str| MNode::Frac { kind: FracKind::NoBar, num: parse_latex(n), den: parse_latex(k) }; - let b = parse_latex(r"\genfrac{}{}{0pt}{}{n}{k}"); - assert_eq!(b, vec![nobar("n", "k")]); - assert_eq!(parse_latex(&to_latex(&b)), b); - // Other spellings of a zero thickness. - for z in ["0 pt", "0.0pt", r"0\,pt", "0em"] { - let c = parse_latex(&format!(r"\genfrac{{}}{{}}{{{z}}}{{}}{{n}}{{k}}")); - assert_eq!(c, vec![nobar("n", "k")], "thickness {z}"); - } - // A non-zero or default thickness keeps the bar. - for t in ["1pt", "0.4pt", ""] { - let c = parse_latex(&format!(r"\genfrac{{}}{{}}{{{t}}}{{}}{{n}}{{k}}")); - let bar = MNode::Frac { kind: FracKind::Bar, num: parse_latex("n"), den: parse_latex("k") }; - assert_eq!(c, vec![bar], "thickness {t:?}"); - } - // Delimiters are kept. - let d = parse_latex(r"\genfrac(){0pt}{}{n}{k}"); - assert_eq!(d, a); - let e = parse_latex(r"\genfrac[]{0pt}{}{n}{k}"); - assert!(matches!(&e[..], [MNode::Delim { beg: Some('['), end: Some(']'), .. }]), "{e:#?}"); - assert_eq!(parse_latex(&to_latex(&e)), e); - } - - #[test] - fn latex_round_trip() { - for lin in ["x=(-b±√(b^2-4ac))/2a", "∑_(i=1)^n▒i^2", "√(3&x)", "a⃗+x̂", "■(a&b@c&d)", "sin⁡x+cos⁡y", "e^(-x^2)"] { - let n = crate::math::parse_linear(lin); - let tex = to_latex(&n); - let back = parse_latex(&tex); - assert_eq!(crate::math::to_linear(&back), crate::math::to_linear(&n), "{lin} → {tex}"); - } - } - - #[test] - fn group_character_latex_roundtrip() { - for cmd in ["\\overparen{a+b}", "\\underparen{a+b}", "\\overbracket{x}", "\\underbracket{x}"] { - let n = parse_latex(cmd); - let tex = to_latex(&n); - assert_eq!(tex, cmd); - } - } - - #[test] - fn hostile_latex_never_panics() { - for s in ["", "\\", "{", "}", "^", "_", "\\frac", "\\sqrt[", "\\left(", "\\right)", "\\begin{matrix", "&&\\\\", "{{{{", "\\end{x}", "a^^b__c"] - { - let _ = to_latex(&parse_latex(s)); - } - let deep = "{".repeat(5000); - let _ = parse_latex(&deep); - let deep = "\\frac{".repeat(3000); - let _ = parse_latex(&deep); - let deep = "\\sqrt".repeat(5000); - let _ = parse_latex(&deep); - let deep = "\\hat".repeat(5000) + "x"; - let _ = parse_latex(&deep); - } - - #[test] - fn array_column_specs_and_aligned_optional_args() { - let n = parse_latex(r"\begin{array}{lcr} a & b & c \\ d & e & f \end{array}"); - let MNode::Matrix { rows, col_jc } = &n[0] else { panic!("{n:#?}") }; - assert_eq!(col_jc, &[ColJc::Left, ColJc::Center, ColJc::Right]); - assert_eq!(rows.len(), 2); - assert_eq!(crate::math::to_linear(&rows[0][0]), "a"); - assert_eq!(crate::math::to_linear(&rows[0][1]), "b"); - assert_eq!(crate::math::to_linear(&rows[0][2]), "c"); - - let n = parse_latex(r"\begin{aligned}[t] x &= 1 \\ y &= 2 \end{aligned}"); - assert!(matches!(&n[0], MNode::EqArr { rows } if rows.len() == 2)); - } - - #[test] - fn operatorname_starred_and_tag() { - let n = parse_latex(r"\operatorname*{argmin}_{x} f(x)"); - assert!(matches!(&n[0], MNode::Lim { upper: false, .. })); - - let n = parse_latex(r"E=mc^2 \tag{1}"); - let MNode::EqArr { rows } = &n[0] else { panic!("{n:#?}") }; - assert!(crate::math::has_number_mark(&rows[0])); - - let n = parse_latex(r"E=mc^2 \tag*{(A)}"); - let MNode::EqArr { rows } = &n[0] else { panic!("{n:#?}") }; - assert!(crate::math::has_number_mark(&rows[0])); - } - - #[test] - fn delim_preserves_custom_separator() { - let delim = MNode::Delim { - beg: Some('{'), - end: Some('}'), - sep: Some('‖'), - grow: true, - shp_match: false, - elems: vec![vec![MNode::Run(MRun::new("x"))], vec![MNode::Run(MRun::new("y"))]], - }; - let tex = to_latex(&[delim]); - assert!(tex.contains(r"\middle\|"), "Expected \\middle\\|, got: {tex}"); - } -} diff --git a/crates/wordmodel/src/math_linear.rs b/crates/wordmodel/src/math_linear.rs deleted file mode 100644 index 39a25ab5..00000000 --- a/crates/wordmodel/src/math_linear.rs +++ /dev/null @@ -1,762 +0,0 @@ -//! The linear format (UnicodeMath, the plain text Word builds equations from) → math tree. -//! -//! Grammar, informally: an expression is a sequence of atoms and operators. Atoms are characters, -//! bracket groups (`(…)`, `[…]`, `{…}`, `|…|`, `〖…〗` invisible, `├…┤` one-sided, `│` -//! separators), prefix structures (`√x`, `∑_a^b▒x`, `█(…)`, `■(…)`, `▭x`, `□x`, `¯x`, `▁x`, -//! `⏞x`, `⏟x`, `⟡x`), function names (`sin x`, `log_2 x`, `lim┬(n→∞) x`) and placeholders (`⬚`). -//! Postfix operators apply to the atom before them: `^` `_` scripts, combining accents, `┬` `┴` -//! limits. `/` `⁄` `∕` `¦` take the operands on either side (an operand is a run of atoms up to -//! an operator or space; a parenthesised operand loses its parentheses). `^`/`_` with nothing -//! before them are pre-scripts. A top-level `#` numbers the equation (`E=mc^2#(1)`). - -use crate::math::{Arg, FracKind, MAX_DEPTH, MClass, MNode, MRun, ScriptKind, any_too_deep, is_nary_char, math_class, merge_runs, too_deep}; - -/// Function names set upright and applied to the following operand. -pub const FUNCTION_NAMES: &[&str] = &[ - "arccos", "arccot", "arccsc", "arcsec", "arcsin", "arctan", "arg", "cos", "cosh", "cot", "coth", "csc", "csch", "deg", "det", "dim", "exp", - "gcd", "hom", "inf", "ker", "lg", "lim", "liminf", "limsup", "ln", "log", "max", "min", "mod", "Pr", "sec", "sech", "sin", "sinh", "sup", "tan", - "tanh", -]; - -/// Length of the longest of [`FUNCTION_NAMES`]. -const MAX_NAME: usize = { - let (mut max, mut i) = (0, 0); - while i < FUNCTION_NAMES.len() { - if FUNCTION_NAMES[i].len() > max { - max = FUNCTION_NAMES[i].len(); - } - i += 1; - } - max -}; - -/// Function names whose subscripts go underneath (`lim_(n→∞)`). -const LIMIT_NAMES: &[&str] = &["lim", "liminf", "limsup", "max", "min", "sup", "inf", "det", "gcd", "Pr"]; - -/// The empty-argument placeholder. -pub const PLACEHOLDER: char = '⬚'; - -/// Parse linear-format text. Never fails: what isn't understood stays as text. -pub fn parse(s: &str) -> Arg { - let mut p = P { c: s.chars().take(20_000).collect(), i: 0, depth: 0, no_limits: 0, steps: 0 }; - let mut out = Vec::new(); - // Stray closers at the top level are text. - while p.i < p.c.len() { - out.extend(p.expr(&[])); - if let Some(c) = p.peek() { - p.i += 1; - out.push(text(c)); - } - } - let mut out = clean(out); - // `expr#(n)`: a numbered equation. - if out.iter().any(|n| matches!(n, MNode::Run(r) if r.text.contains('#'))) { - out = vec![MNode::EqArr { rows: vec![out] }]; - } - out -} - -fn text(c: char) -> MNode { - MNode::Run(MRun::new(c.to_string())) -} - -/// Closing bracket for an opening one. -pub fn closer(c: char) -> Option { - Some(match c { - '(' => ')', - '[' => ']', - '{' => '}', - '⟨' => '⟩', - '〈' => '〉', - '⌈' => '⌉', - '⌊' => '⌋', - '⟦' => '⟧', - '〖' => '〗', - _ => return None, - }) -} - -fn is_closer(c: char) -> bool { - matches!(c, ')' | ']' | '}' | '⟩' | '〉' | '⌉' | '⌋' | '⟧' | '〗' | '┤') -} - -/// Prefix characters that make a structure from the operand after them. -fn is_prefix(c: char) -> bool { - matches!(c, '√' | '∛' | '∜' | '█' | '■' | '▭' | '□' | '¯' | '▁' | '⟡' | '⏞' | '⏟' | '⏜' | '⏝' | '⎴' | '⎵') || is_nary_char(c) -} - -/// Combining marks used as accents. -pub fn is_combining(c: char) -> bool { - matches!(c as u32, 0x300..=0x36F | 0x20D0..=0x20FF) -} - -/// Characters that end an operand. -fn is_boundary(c: char) -> bool { - matches!(c, ' ' | '&' | '@' | '#' | '▒' | '│' | '/' | '⁄' | '∕' | '¦' | '\u{2061}') - || is_closer(c) - || matches!(math_class(c), MClass::Bin | MClass::Rel | MClass::Punct) -} - -struct P { - c: Vec, - i: usize, - depth: usize, - /// Inside a brace's operand: limits are left for the brace. - no_limits: usize, - /// Work done (bounds hostile input). - steps: usize, -} - -const MAX_STEPS: usize = 400_000; - -impl P { - fn peek(&self) -> Option { - self.c.get(self.i).copied() - } - fn peek_at(&self, k: usize) -> Option { - self.c.get(self.i + k).copied() - } - fn tired(&mut self) -> bool { - self.steps += 1; - self.steps > MAX_STEPS || self.depth > MAX_DEPTH - } - /// The rest of the input as text (when too deep or too long). - fn rest(&mut self) -> Arg { - let t: String = self.c.get(self.i..).unwrap_or(&[]).iter().collect(); - self.i = self.c.len(); - if t.is_empty() { Vec::new() } else { vec![MNode::Run(MRun::new(t))] } - } - - /// A sequence up to one of `stops` (not consumed) or the end. - fn expr(&mut self, stops: &[char]) -> Arg { - self.depth += 1; - let mut out: Arg = Vec::new(); - if self.tired() { - self.depth -= 1; - return self.rest(); - } - while let Some(c) = self.peek() { - if stops.contains(&c) || (is_closer(c) && !stops.is_empty()) { - break; - } - if self.tired() { - out.extend(self.rest()); - break; - } - match c { - '/' | '⁄' | '∕' | '¦' => { - self.i += 1; - let start = operand_start(&out); - // A numerator that is already deep stays as it is: `/` is text then. - if out.get(start..).is_some_and(any_too_deep) { - out.push(text(c)); - continue; - } - let num = strip_parens(out.drain(start..).collect()); - let den = self.operand(stops); - let kind = match c { - '⁄' => FracKind::Skewed, - '∕' => FracKind::Linear, - '¦' => FracKind::NoBar, - _ => FracKind::Bar, - }; - out.push(MNode::Frac { kind, num, den }); - } - '^' | '_' => { - let pre = match out.last() { - None => true, - Some(MNode::Run(r)) => r.text == " " || r.text.chars().all(|c| is_boundary(c) && c != '#'), - _ => false, - }; - if pre { - out.push(self.prescript(stops)); - } else { - self.i += 1; - let s = self.script_operand(stops); - if out.last().is_some_and(too_deep) { - out.push(text(c)); - out.extend(s); - } else { - let base = out.pop(); - out.push(attach_script(base, c == '^', s)); - } - } - } - '┬' | '┴' => { - self.i += 1; - let lim = self.script_operand(stops); - if out.last().is_some_and(too_deep) { - out.push(text(c)); - out.extend(lim); - } else { - let e = out.pop().map(|n| vec![n]).unwrap_or_default(); - out.push(MNode::Lim { upper: c == '┴', e: strip_parens(e), lim }); - } - } - c if is_combining(c) => { - self.i += 1; - if out.last().is_some_and(too_deep) { - out.push(text(c)); - } else { - let e = out.pop().map(|n| strip_parens(vec![n])).unwrap_or_default(); - out.push(MNode::Acc { chr: c, e }); - } - } - '▒' | '\u{2061}' => self.i += 1, - ' ' => { - self.i += 1; - out.push(text(' ')); - } - _ => { - let atom = self.atom(stops); - out.extend(atom); - } - } - } - self.depth -= 1; - out - } - - /// One atom (a character, a bracket group, a structure or a function application). - fn atom(&mut self, stops: &[char]) -> Arg { - let Some(c) = self.peek() else { return Vec::new() }; - if let Some(close) = closer(c) { - self.i += 1; - return vec![self.group(Some(c), &[close])]; - } - if c == '├' { - self.i += 1; - if let Some(d) = self.explicit_group() { - return vec![d]; - } - return vec![self.group(None, &[')', ']', '}', '⟩', '⌉', '⌋', '|', '‖'])]; - } - if (c == '|' || c == '‖') && self.c.get(self.i + 1..).is_some_and(|r| r.contains(&c)) { - self.i += 1; - return vec![self.group(Some(c), &[c])]; - } - if is_prefix(c) { - self.i += 1; - return vec![self.prefix(c, stops)]; - } - if c.is_ascii_alphabetic() { - return self.word(stops); - } - self.i += 1; - vec![text(c)] - } - - /// A bracket group after its opening character; `closers` end it. - fn group(&mut self, open: Option, closers: &[char]) -> MNode { - let mut stops: Vec = closers.to_vec(); - stops.extend(['┤', '│']); - let mut elems = Vec::new(); - let mut end = None; - loop { - let e = self.expr(&stops); - elems.push(e); - match self.peek() { - Some('│') => self.i += 1, - Some('┤') => { - self.i += 1; - break; - } - Some(c) if closers.contains(&c) => { - self.i += 1; - end = Some(c); - break; - } - // Another closer: the group ends unclosed there. - _ => break, - } - if self.tired() { - break; - } - } - let beg = open.filter(|c| *c != '〖'); - let end = end.filter(|c| *c != '〗'); - let sep = if elems.len() > 1 { Some('|') } else { None }; - MNode::Delim { beg, end, sep, grow: true, shp_match: false, elems } - } - - /// `├X … Y┤`: brackets given explicitly (any bracket characters, either side optional), the - /// cursor just after `├`. `None` (cursor unchanged) when there's no matching `┤`. - fn explicit_group(&mut self) -> Option { - let mut level = 1usize; - let mut k = self.i; - while let Some(c) = self.c.get(k) { - match c { - '├' => level += 1, - '┤' => { - level -= 1; - if level == 0 { - break; - } - } - _ => {} - } - k += 1; - } - if level != 0 { - return None; - } - let bracket = |c: &char| matches!(c, '(' | ')' | '[' | ']' | '{' | '}' | '⟨' | '⟩' | '⌈' | '⌉' | '⌊' | '⌋' | '|' | '‖' | '⟦' | '⟧'); - let mut inner: Vec = self.c.get(self.i..k).unwrap_or(&[]).to_vec(); - self.i = k + 1; - let beg = if inner.first().is_some_and(bracket) { Some(inner.remove(0)) } else { None }; - // A closing bracket at the end that pairs with an opening one inside is content. - let paired = |inner: &[char]| -> bool { - let Some((last, body)) = inner.split_last() else { return false }; - let opener = match last { - ')' => '(', - ']' => '[', - '}' => '{', - '⟩' => '⟨', - '⌉' => '⌈', - '⌋' => '⌊', - '⟧' => '⟦', - _ => return false, - }; - body.iter().filter(|c| **c == opener).count() > body.iter().filter(|c| *c == last).count() - }; - let end = if !inner.is_empty() && inner.last().is_some_and(bracket) && !paired(&inner) { inner.pop() } else { None }; - let mut sub = P { c: inner, i: 0, depth: self.depth + 1, no_limits: 0, steps: self.steps }; - let mut elems = vec![Vec::new()]; - while sub.i < sub.c.len() { - if let Some(e) = elems.last_mut() { - e.extend(sub.expr(&['│'])); - } - match sub.peek() { - Some('│') => { - sub.i += 1; - elems.push(Vec::new()); - } - Some(c) => { - sub.i += 1; - if let Some(e) = elems.last_mut() { - e.push(text(c)); - } - } - None => {} - } - if sub.tired() { - break; - } - } - self.steps = sub.steps; - let sep = if elems.len() > 1 { Some('|') } else { None }; - Some(MNode::Delim { beg, end, sep, grow: true, shp_match: false, elems }) - } - - /// A prefix structure after its character. - fn prefix(&mut self, c: char, stops: &[char]) -> MNode { - match c { - '√'..='∜' => { - let mut deg = match c { - '∛' => vec![text('3')], - '∜' => vec![text('4')], - _ => Vec::new(), - }; - let mut e = self.operand(stops); - if let Some(k) = e.iter().position(|n| matches!(n, MNode::Run(r) if r.text == "&")) { - let rest = e.split_off(k + 1); - e.pop(); - deg = e; - e = rest; - } - MNode::Rad { deg_hide: deg.is_empty(), deg, e } - } - '█' | '■' => { - if self.peek() == Some('(') { - self.i += 1; - let inner = self.expr(&[')']); - if self.peek() == Some(')') { - self.i += 1; - } - grid(c == '■', inner) - } else { - text(c) - } - } - '▭' => MNode::BorderBox { hide: [false; 4], strike: [false; 4], e: self.operand(stops) }, - '□' => MNode::Boxed { e: self.operand(stops) }, - '¯' => MNode::Bar { top: true, e: self.operand(stops) }, - '▁' => MNode::Bar { top: false, e: self.operand(stops) }, - '⟡' => MNode::Phant { show: false, zero_wid: false, zero_asc: false, zero_desc: false, e: self.operand(stops) }, - // A label (`┴`/`┬`) after the operand belongs to the whole brace. - '⏞' | '⏜' | '⎴' | '⏟' | '⏝' | '⎵' => { - self.no_limits += 1; - let e = self.operand(stops); - self.no_limits -= 1; - MNode::GroupChr { chr: c, top: matches!(c, '⏞' | '⏜' | '⎴'), e } - } - _ => { - // N-ary operator: limits, then the operand. - let (mut sub, mut sup) = (Vec::new(), Vec::new()); - let (mut has_sub, mut has_sup) = (false, false); - loop { - match self.peek() { - Some('_') => { - self.i += 1; - sub = self.script_operand(stops); - has_sub = true; - } - Some('^') => { - self.i += 1; - sup = self.script_operand(stops); - has_sup = true; - } - _ => break, - } - } - if self.peek() == Some('▒') { - self.i += 1; - } - let e = if self.peek().is_some_and(|c| c == ' ') { Vec::new() } else { self.operand(stops) }; - MNode::Nary { chr: c, lim_loc: None, grow: false, sub_hide: !has_sub, sup_hide: !has_sup, sub, sup, e } - } - } - } - - /// Letters: a function application when they spell a function name, else one letter. - fn word(&mut self, stops: &[char]) -> Arg { - let start = self.i; - let mut end = start; - // Look no further than the longest name (and one past it, to see the run is longer): a - // scan to the end of every run made a long run of letters quadratic (CWE-407). - while end - start <= MAX_NAME && self.c.get(end).is_some_and(char::is_ascii_alphabetic) { - end += 1; - } - let word: String = if end - start > MAX_NAME { String::new() } else { self.c.get(start..end).unwrap_or(&[]).iter().collect() }; - if !FUNCTION_NAMES.contains(&word.as_str()) { - self.i = start + 1; - return vec![text(self.c.get(start).copied().unwrap_or(' '))]; - } - self.i = end; - let plain = vec![MNode::Run(MRun::plain(word.clone()))]; - // Limits and scripts on the name. - let name = match self.peek() { - Some('┬') | Some('┴') => { - let upper = self.peek() == Some('┴'); - self.i += 1; - let lim = self.script_operand(stops); - vec![MNode::Lim { upper, e: plain, lim }] - } - Some('_') if LIMIT_NAMES.contains(&word.as_str()) => { - self.i += 1; - let lim = self.script_operand(stops); - vec![MNode::Lim { upper: false, e: plain, lim }] - } - Some(c @ ('^' | '_')) => { - self.i += 1; - let s = self.script_operand(stops); - let mut n = attach_script(Some(MNode::Run(MRun::plain(word.clone()))), c == '^', s); - if let Some(c2 @ ('^' | '_')) = self.peek() { - self.i += 1; - let s2 = self.script_operand(stops); - n = attach_script(Some(n), c2 == '^', s2); - } - vec![n] - } - _ => plain, - }; - // The operand, after an optional space or function-application mark. - if matches!(self.peek(), Some(' ') | Some('\u{2061}') | Some('▒')) { - self.i += 1; - } - let next = self.peek(); - let applies = next.is_some_and(|c| !stops.contains(&c) && !is_closer(c) && (!is_boundary(c) || c == '(' || matches!(c, '−' | '-' | '+'))); - if !applies { - return name; - } - let e = self.operand(stops); - if e.is_empty() { - return name; - } - vec![MNode::Func { name, e }] - } - - /// Pre-scripts (`_a^b x`): the scripts, then the base. - fn prescript(&mut self, stops: &[char]) -> MNode { - let (mut sub, mut sup) = (Vec::new(), Vec::new()); - while let Some(c @ ('^' | '_')) = self.peek() { - self.i += 1; - let s = self.script_operand(stops); - if c == '^' { - sup = s; - } else { - sub = s; - } - } - if self.peek() == Some('▒') { - self.i += 1; - } - let base = self.operand(stops); - MNode::Script { kind: ScriptKind::Pre, base, sub, sup } - } - - /// An operand: a parenthesised group (parentheses dropped) or a run of atoms with their - /// postfix scripts, accents and limits, up to an operator or space. - fn operand(&mut self, stops: &[char]) -> Arg { - if self.tired() { - return self.rest(); - } - self.depth += 1; - let mut out: Arg = Vec::new(); - if self.peek() == Some('(') { - self.i += 1; - let g = self.group(Some('('), &[')']); - let inner = match g { - MNode::Delim { elems, end: Some(')'), .. } if elems.len() == 1 => elems.into_iter().next().unwrap_or_default(), - other => vec![other], - }; - out = inner; - self.postfix(&mut out, stops, true); - } else if self.peek() == Some('〖') { - self.i += 1; - let g = self.group(Some('〖'), &['〗']); - out = match g { - MNode::Delim { elems, .. } if elems.len() == 1 => elems.into_iter().next().unwrap_or_default(), - other => vec![other], - }; - self.postfix(&mut out, stops, true); - } else { - // A signed operand (`e^-x`, `−b`). - if let Some(c @ ('−' | '-' | '+' | '±' | '∓')) = self.peek() - && self.peek_at(1).is_some_and(|n| !is_boundary(n)) - { - self.i += 1; - out.push(text(c)); - } - while let Some(c) = self.peek() { - if stops.contains(&c) || is_boundary(c) || self.tired() { - break; - } - let a = self.atom(stops); - if a.is_empty() { - break; - } - out.extend(a); - self.postfix(&mut out, stops, false); - } - if out.is_empty() - && let Some(c) = self.peek() - && c != ' ' - && !stops.contains(&c) - && !is_closer(c) - && !matches!(c, '/' | '⁄' | '∕' | '¦' | '&' | '@' | '#' | '│' | '▒') - { - // A lone operator as operand. - self.i += 1; - out.push(text(c)); - } - } - self.depth -= 1; - out - } - - /// Postfix operators on the last atom of `out` (`whole`: on all of `out` as one group). - fn postfix(&mut self, out: &mut Arg, stops: &[char], whole: bool) { - while let Some(c) = self.peek() { - if self.tired() { - break; - } - let base = |out: &mut Arg| -> Option { - if whole && out.len() != 1 { - if out.is_empty() { - return None; - } - return Some(MNode::Delim { beg: None, end: None, sep: None, grow: true, shp_match: false, elems: vec![std::mem::take(out)] }); - } - out.pop() - }; - if !(matches!(c, '^' | '_') || matches!(c, '┬' | '┴') && self.no_limits == 0 || is_combining(c)) { - break; - } - // The base is already deep: the operator is text (see `too_deep`). - let deep = if whole { any_too_deep(out) } else { out.last().is_some_and(too_deep) }; - self.i += 1; - if deep { - let s = if matches!(c, '^' | '_' | '┬' | '┴') { self.script_operand(stops) } else { Vec::new() }; - out.push(text(c)); - out.extend(s); - continue; - } - match c { - '^' | '_' => { - let s = self.script_operand(stops); - let b = base(out); - out.push(attach_script(b, c == '^', s)); - } - '┬' | '┴' => { - let lim = self.script_operand(stops); - let b = base(out); - out.push(MNode::Lim { upper: c == '┴', e: b.map(|n| vec![n]).unwrap_or_default(), lim }); - } - _ => { - let b = base(out); - out.push(MNode::Acc { chr: c, e: b.map(|n| strip_parens(vec![n])).unwrap_or_default() }); - } - } - } - } - - /// A script or limit: a parenthesised group, a structure, a number, a signed operand, a run of - /// letters (and digits) or one other character. - fn script_operand(&mut self, stops: &[char]) -> Arg { - if self.tired() { - return self.rest(); - } - self.depth += 1; - let out = match self.peek() { - Some('(') => { - self.i += 1; - match self.group(Some('('), &[')']) { - MNode::Delim { elems, end: Some(')'), .. } if elems.len() == 1 => elems.into_iter().next().unwrap_or_default(), - other => vec![other], - } - } - Some('〖') => { - self.i += 1; - match self.group(Some('〖'), &['〗']) { - MNode::Delim { elems, .. } if elems.len() == 1 => elems.into_iter().next().unwrap_or_default(), - other => vec![other], - } - } - Some(c) if is_prefix(c) || closer(c).is_some() => self.atom(stops), - Some(c) if c.is_ascii_digit() => { - let start = self.i; - while self.peek().is_some_and(|k| k.is_alphanumeric() || k == '.') { - self.i += 1; - } - let t: String = self.c.get(start..self.i).unwrap_or(&[]).iter().collect(); - vec![MNode::Run(MRun::new(t))] - } - Some(c @ ('−' | '-' | '+' | '±' | '∓')) if self.peek_at(1).is_some_and(|n| !is_boundary(n)) => { - self.i += 1; - let mut v = vec![text(c)]; - v.extend(self.script_operand(stops)); - v - } - Some(c) if c.is_alphabetic() => { - // Word (#1609): the whole run of letters and digits up to a space or operator, so - // `V_dd` is V with a subscript `dd`, exactly like `V_(dd)`. - let start = self.i; - while self.peek().is_some_and(char::is_alphanumeric) { - self.i += 1; - } - let t: String = self.c.get(start..self.i).unwrap_or(&[]).iter().collect(); - // A known function name stays upright, as Word builds it. - vec![MNode::Run(if FUNCTION_NAMES.contains(&t.as_str()) { MRun::plain(t) } else { MRun::new(t) })] - } - Some(c) if !stops.contains(&c) && c != ' ' && !is_closer(c) => { - self.i += 1; - vec![text(c)] - } - _ => Vec::new(), - }; - self.depth -= 1; - out - } -} - -/// Where the trailing operand of `out` starts (after the last operator or space). -fn operand_start(out: &[MNode]) -> usize { - let mut k = out.len(); - while k > 0 { - match out.get(k - 1) { - Some(MNode::Run(r)) if r.text.chars().all(is_boundary) => break, - _ => k -= 1, - } - } - k -} - -/// A single parenthesised group used as an operand loses its parentheses. -pub fn strip_parens(mut a: Arg) -> Arg { - if a.len() == 1 - && let Some(MNode::Delim { beg: Some('('), end: Some(')'), elems, .. } | MNode::Delim { beg: None, end: None, elems, .. }) = a.first_mut() - && elems.len() == 1 - { - return elems.pop().unwrap_or_default(); - } - a -} - -fn attach_script(base: Option, sup: bool, s: Arg) -> MNode { - match base { - Some(MNode::Script { kind: ScriptKind::Sub, base, sub, .. }) if sup => MNode::Script { kind: ScriptKind::SubSup, base, sub, sup: s }, - Some(MNode::Script { kind: ScriptKind::Sup, base, sup: sp, .. }) if !sup => MNode::Script { kind: ScriptKind::SubSup, base, sub: s, sup: sp }, - b => { - let base = b.map(|b| vec![b]).unwrap_or_default(); - if sup { - MNode::Script { kind: ScriptKind::Sup, base, sub: Vec::new(), sup: s } - } else { - MNode::Script { kind: ScriptKind::Sub, base, sub: s, sup: Vec::new() } - } - } - } -} - -/// `█(a@b)` equation array / `■(a&b@c&d)` matrix from the parsed inside. -fn grid(matrix: bool, inner: Arg) -> MNode { - let mut rows: Vec> = vec![vec![Vec::new()]]; - for n in inner { - match &n { - MNode::Run(r) if r.text == "@" => rows.push(vec![Vec::new()]), - MNode::Run(r) if r.text == "&" && matrix => { - if let Some(row) = rows.last_mut() { - row.push(Vec::new()); - } - } - _ => { - if let Some(cell) = rows.last_mut().and_then(|r| r.last_mut()) { - cell.push(n); - } - } - } - } - if matrix { - MNode::Matrix { rows, col_jc: Vec::new() } - } else { - MNode::EqArr { rows: rows.into_iter().map(|r| r.into_iter().flatten().collect()).collect() } - } -} - -/// Drop placeholders and build-up spaces, merge runs; recursively. -fn clean(a: Arg) -> Arg { - let mut out = Vec::with_capacity(a.len()); - for n in a { - match n { - MNode::Run(r) if r.text == " " || r.text.chars().all(|c| c == PLACEHOLDER) => {} - other => out.push(map_args(other, &clean)), - } - } - merge_runs(out) -} - -/// Apply `f` to every argument of a node. -pub fn map_args(n: MNode, f: &dyn Fn(Arg) -> Arg) -> MNode { - match n { - MNode::Run(r) => MNode::Run(r), - MNode::Frac { kind, num, den } => MNode::Frac { kind, num: f(num), den: f(den) }, - MNode::Script { kind, base, sub, sup } => MNode::Script { kind, base: f(base), sub: f(sub), sup: f(sup) }, - MNode::Rad { deg, deg_hide, e } => MNode::Rad { deg: f(deg), deg_hide, e: f(e) }, - MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub, sup, e } => { - MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub: f(sub), sup: f(sup), e: f(e) } - } - MNode::Delim { beg, end, sep, grow, shp_match, elems } => { - MNode::Delim { beg, end, sep, grow, shp_match, elems: elems.into_iter().map(f).collect() } - } - MNode::Func { name, e } => MNode::Func { name: f(name), e: f(e) }, - MNode::Lim { upper, e, lim } => MNode::Lim { upper, e: f(e), lim: f(lim) }, - MNode::Acc { chr, e } => MNode::Acc { chr, e: f(e) }, - MNode::Bar { top, e } => MNode::Bar { top, e: f(e) }, - MNode::BorderBox { hide, strike, e } => MNode::BorderBox { hide, strike, e: f(e) }, - MNode::Boxed { e } => MNode::Boxed { e: f(e) }, - MNode::GroupChr { chr, top, e } => MNode::GroupChr { chr, top, e: f(e) }, - MNode::EqArr { rows } => MNode::EqArr { rows: rows.into_iter().map(f).collect() }, - MNode::Matrix { rows, col_jc } => MNode::Matrix { rows: rows.into_iter().map(|r| r.into_iter().map(f).collect()).collect(), col_jc }, - MNode::Phant { show, zero_wid, zero_asc, zero_desc, e } => MNode::Phant { show, zero_wid, zero_asc, zero_desc, e: f(e) }, - } -} diff --git a/crates/wordmodel/src/math_symbols.rs b/crates/wordmodel/src/math_symbols.rs deleted file mode 100644 index 28b3a7f8..00000000 --- a/crates/wordmodel/src/math_symbols.rs +++ /dev/null @@ -1,390 +0,0 @@ -//! Named math characters: Math AutoCorrect while typing in an equation (`\alpha` + space → α) -//! and LaTeX conversion. Names are the usual TeX names plus the linear-format operators -//! (`\sqrt` → √, `\above` → ┴, `\matrix` → ■…). - -/// (name without the backslash, character). When a character has several names the first one -/// listed is the one LaTeX output uses. -pub const SYMBOLS: &[(&str, char)] = &[ - // Greek. - ("alpha", 'α'), - ("beta", 'β'), - ("gamma", 'γ'), - ("delta", 'δ'), - ("epsilon", 'ϵ'), - ("varepsilon", 'ε'), - ("zeta", 'ζ'), - ("eta", 'η'), - ("theta", 'θ'), - ("vartheta", 'ϑ'), - ("iota", 'ι'), - ("kappa", 'κ'), - ("varkappa", 'ϰ'), - ("lambda", 'λ'), - ("mu", 'μ'), - ("nu", 'ν'), - ("xi", 'ξ'), - ("omicron", 'ο'), - ("pi", 'π'), - ("varpi", 'ϖ'), - ("rho", 'ρ'), - ("varrho", 'ϱ'), - ("sigma", 'σ'), - ("varsigma", 'ς'), - ("tau", 'τ'), - ("upsilon", 'υ'), - ("phi", 'ϕ'), - ("varphi", 'φ'), - ("chi", 'χ'), - ("psi", 'ψ'), - ("omega", 'ω'), - ("Alpha", 'Α'), - ("Beta", 'Β'), - ("Gamma", 'Γ'), - ("Delta", 'Δ'), - ("Epsilon", 'Ε'), - ("Zeta", 'Ζ'), - ("Eta", 'Η'), - ("Theta", 'Θ'), - ("Iota", 'Ι'), - ("Kappa", 'Κ'), - ("Lambda", 'Λ'), - ("Mu", 'Μ'), - ("Nu", 'Ν'), - ("Xi", 'Ξ'), - ("Omicron", 'Ο'), - ("Pi", 'Π'), - ("Rho", 'Ρ'), - ("Sigma", 'Σ'), - ("Tau", 'Τ'), - ("Upsilon", 'Υ'), - ("Phi", 'Φ'), - ("Chi", 'Χ'), - ("Psi", 'Ψ'), - ("Omega", 'Ω'), - ("digamma", 'ϝ'), - // Binary operators. - ("pm", '±'), - ("mp", '∓'), - ("times", '×'), - ("div", '÷'), - ("cdot", '⋅'), - ("bullet", '∙'), - ("ast", '∗'), - ("star", '⋆'), - ("circ", '∘'), - ("oplus", '⊕'), - ("ominus", '⊖'), - ("otimes", '⊗'), - ("oslash", '⊘'), - ("odot", '⊙'), - ("cup", '∪'), - ("cap", '∩'), - ("uplus", '⊎'), - ("sqcup", '⊔'), - ("sqcap", '⊓'), - ("vee", '∨'), - ("wedge", '∧'), - ("setminus", '∖'), - ("backslash", '\\'), - ("wr", '≀'), - ("diamond", '⋄'), - ("dagger", '†'), - ("ddagger", '‡'), - ("amalg", '⨿'), - ("dotplus", '∔'), - ("ltimes", '⋉'), - ("rtimes", '⋊'), - // Relations. - ("le", '≤'), - ("leq", '≤'), - ("ge", '≥'), - ("geq", '≥'), - ("ne", '≠'), - ("neq", '≠'), - ("approx", '≈'), - ("equiv", '≡'), - ("sim", '∼'), - ("simeq", '≃'), - ("cong", '≅'), - ("propto", '∝'), - ("ll", '≪'), - ("gg", '≫'), - ("prec", '≺'), - ("succ", '≻'), - ("preceq", '≼'), - ("succeq", '≽'), - ("subset", '⊂'), - ("supset", '⊃'), - ("subseteq", '⊆'), - ("supseteq", '⊇'), - ("sqsubseteq", '⊑'), - ("sqsupseteq", '⊒'), - ("in", '∈'), - ("ni", '∋'), - ("notin", '∉'), - ("perp", '⊥'), - ("parallel", '∥'), - ("mid", '∣'), - ("nmid", '∤'), - ("asymp", '≍'), - ("doteq", '≐'), - ("models", '⊨'), - ("vdash", '⊢'), - ("dashv", '⊣'), - ("bowtie", '⋈'), - ("coloneq", '≔'), - ("defeq", '≝'), - ("triangleq", '≜'), - ("nless", '≮'), - ("ngtr", '≯'), - ("nleq", '≰'), - ("ngeq", '≱'), - ("nsim", '≁'), - ("ncong", '≇'), - ("nequiv", '≢'), - ("nsubset", '⊄'), - ("nsupset", '⊅'), - ("nsubseteq", '⊈'), - ("nsupseteq", '⊉'), - ("subsetneq", '⊊'), - ("supsetneq", '⊋'), - ("lesssim", '≲'), - ("gtrsim", '≳'), - ("therefore", '∴'), - ("because", '∵'), - // Arrows. - ("rightarrow", '→'), - ("to", '→'), - ("leftarrow", '←'), - ("gets", '←'), - ("leftrightarrow", '↔'), - ("uparrow", '↑'), - ("downarrow", '↓'), - ("updownarrow", '↕'), - ("Rightarrow", '⇒'), - ("Leftarrow", '⇐'), - ("Leftrightarrow", '⇔'), - ("Uparrow", '⇑'), - ("Downarrow", '⇓'), - ("Updownarrow", '⇕'), - ("longrightarrow", '⟶'), - ("longleftarrow", '⟵'), - ("longleftrightarrow", '⟷'), - ("Longrightarrow", '⟹'), - ("Longleftarrow", '⟸'), - ("Longleftrightarrow", '⟺'), - ("implies", '⟹'), - ("iff", '⟺'), - ("mapsto", '↦'), - ("longmapsto", '⟼'), - ("nearrow", '↗'), - ("searrow", '↘'), - ("swarrow", '↙'), - ("nwarrow", '↖'), - ("hookleftarrow", '↩'), - ("hookrightarrow", '↪'), - ("leftharpoonup", '↼'), - ("leftharpoondown", '↽'), - ("rightharpoonup", '⇀'), - ("rightharpoondown", '⇁'), - ("rightleftharpoons", '⇌'), - ("leftrightharpoons", '⇋'), - ("circlearrowleft", '↺'), - ("circlearrowright", '↻'), - // Big operators. - ("sum", '∑'), - ("prod", '∏'), - ("coprod", '∐'), - ("int", '∫'), - ("iint", '∬'), - ("iiint", '∭'), - ("oint", '∮'), - ("oiint", '∯'), - ("oiiint", '∰'), - ("bigcup", '⋃'), - ("bigcap", '⋂'), - ("bigvee", '⋁'), - ("bigwedge", '⋀'), - ("bigoplus", '⨁'), - ("bigotimes", '⨂'), - ("bigodot", '⨀'), - ("biguplus", '⨄'), - ("bigsqcup", '⨆'), - // Brackets. - ("langle", '⟨'), - ("rangle", '⟩'), - ("lceil", '⌈'), - ("rceil", '⌉'), - ("lfloor", '⌊'), - ("rfloor", '⌋'), - ("lbrack", '['), - ("rbrack", ']'), - ("lbrace", '{'), - ("rbrace", '}'), - ("Vert", '‖'), - ("norm", '‖'), - ("vert", '|'), - ("lbbrack", '⟦'), - ("rbbrack", '⟧'), - // Miscellaneous. - ("infty", '∞'), - ("partial", '∂'), - ("nabla", '∇'), - ("forall", '∀'), - ("exists", '∃'), - ("nexists", '∄'), - ("neg", '¬'), - ("lnot", '¬'), - ("emptyset", '∅'), - ("varnothing", '∅'), - ("hbar", 'ℏ'), - ("ell", 'ℓ'), - ("Re", 'ℜ'), - ("Im", 'ℑ'), - ("wp", '℘'), - ("aleph", 'ℵ'), - ("beth", 'ℶ'), - ("gimel", 'ℷ'), - ("daleth", 'ℸ'), - ("degree", '°'), - ("angle", '∠'), - ("measuredangle", '∡'), - ("sphericalangle", '∢'), - ("rightangle", '∟'), - ("triangle", '△'), - ("square", '□'), - ("blacksquare", '■'), - ("qed", '∎'), - ("top", '⊤'), - ("bot", '⊥'), - ("cdots", '⋯'), - ("ldots", '…'), - ("dots", '…'), - ("vdots", '⋮'), - ("ddots", '⋱'), - ("adots", '⋰'), - ("prime", '′'), - ("dprime", '″'), - ("trprime", '‴'), - ("dd", 'ⅆ'), - ("Dd", 'ⅅ'), - ("ee", 'ⅇ'), - ("ii", 'ⅈ'), - ("jj", 'ⅉ'), - ("imath", 'ı'), - ("jmath", 'ȷ'), - ("complement", '∁'), - ("Angstrom", 'Å'), - ("mho", '℧'), - ("eth", 'ð'), - ("flat", '♭'), - ("natural", '♮'), - ("sharp", '♯'), - ("clubsuit", '♣'), - ("diamondsuit", '♢'), - ("heartsuit", '♡'), - ("spadesuit", '♠'), - // Spaces. - ("quad", '\u{2003}'), - ("emsp", '\u{2003}'), - ("ensp", '\u{2002}'), - ("thicksp", '\u{2005}'), - ("medsp", '\u{205F}'), - ("thinsp", '\u{2009}'), - ("hairsp", '\u{200A}'), - ("nbsp", '\u{A0}'), - ("zwsp", '\u{200B}'), - // Linear-format operators (build-up). - ("sqrt", '√'), - ("cbrt", '∛'), - ("qdrt", '∜'), - ("matrix", '■'), - ("eqarray", '█'), - ("above", '┴'), - ("below", '┬'), - ("of", '▒'), - ("naryand", '▒'), - ("funcapply", '\u{2061}'), - ("box", '□'), - ("rect", '▭'), - ("overbar", '¯'), - ("underbar", '▁'), - ("phantom", '⟡'), - ("overbrace", '⏞'), - ("underbrace", '⏟'), - ("overparen", '⏜'), - ("underparen", '⏝'), - ("atop", '¦'), - ("over", '/'), - ("ldiv", '∕'), - ("sdiv", '⁄'), - ("begin", '〖'), - ("end", '〗'), - ("left", '├'), - ("right", '┤'), - ("eqno", '#'), - ("placeholder", '⬚'), - // Accents (combining, typed after the base). - ("hat", '\u{302}'), - ("check", '\u{30C}'), - ("tilde", '\u{303}'), - ("acute", '\u{301}'), - ("grave", '\u{300}'), - ("dot", '\u{307}'), - ("ddot", '\u{308}'), - ("dddot", '\u{20DB}'), - ("breve", '\u{306}'), - ("bar", '\u{305}'), - ("Bar", '\u{33F}'), - ("vec", '\u{20D7}'), - ("tvec", '\u{20E1}'), - ("lhvec", '\u{20D0}'), - ("hvec", '\u{20D1}'), -]; - -/// The character for a name (`alpha` → α), including `doubleR`, `scriptL`, `frakturg`. -pub fn symbol(name: &str) -> Option { - if let Some((_, c)) = SYMBOLS.iter().find(|(n, _)| *n == name) { - return Some(*c); - } - for (prefix, scr) in [("double", crate::math::MScr::DoubleStruck), ("script", crate::math::MScr::Script), ("fraktur", crate::math::MScr::Fraktur)] - { - if let Some(rest) = name.strip_prefix(prefix) { - let mut cs = rest.chars(); - if let (Some(c), None) = (cs.next(), cs.next()) - && c.is_ascii_alphanumeric() - { - let m = crate::math::math_alnum(c, false, false, scr); - if m != c { - return Some(m); - } - } - } - } - None -} - -/// The (first) name of a character. -pub fn name_of(c: char) -> Option<&'static str> { - SYMBOLS.iter().find(|(_, s)| *s == c).map(|(n, _)| *n) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn names_resolve() { - assert_eq!(symbol("alpha"), Some('α')); - assert_eq!(symbol("doubleR"), Some('ℝ')); - assert_eq!(symbol("scriptL"), Some('ℒ')); - assert_eq!(symbol("nonesuch"), None); - assert_eq!(name_of('≤'), Some("le")); - // No name is listed twice. - let mut names: Vec<&str> = SYMBOLS.iter().map(|(n, _)| *n).collect(); - names.sort_unstable(); - let before = names.len(); - names.dedup(); - assert_eq!(before, names.len()); - } -} diff --git a/crates/wordmodel/src/math_tree.rs b/crates/wordmodel/src/math_tree.rs deleted file mode 100644 index 45d2c603..00000000 --- a/crates/wordmodel/src/math_tree.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Structural access to an equation tree: the arguments of a node, and every run in a tree. - -use crate::math::{Arg, MAX_DEPTH, MNode, MRun}; - -/// Number of child arguments of a node. -pub fn child_count(n: &MNode) -> usize { - match n { - MNode::Run(_) => 0, - MNode::Frac { .. } | MNode::Func { .. } | MNode::Lim { .. } => 2, - MNode::Rad { .. } => 2, - MNode::Script { .. } | MNode::Nary { .. } => 3, - MNode::Delim { elems, .. } => elems.len(), - MNode::EqArr { rows } => rows.len(), - MNode::Matrix { rows, .. } => rows.iter().map(|r| r.len()).sum(), - MNode::Acc { .. } | MNode::Bar { .. } | MNode::BorderBox { .. } | MNode::Boxed { .. } | MNode::GroupChr { .. } | MNode::Phant { .. } => 1, - } -} - -/// Child argument `i` of a node (see [`child_count`] for the numbering: fraction num 0 / den 1; -/// script base 0 / sub 1 / sup 2; radical degree 0 / base 1; n-ary sub 0 / sup 1 / operand 2; -/// function name 0 / argument 1; limit base 0 / limit 1; matrices row by row). -pub fn child(n: &MNode, i: usize) -> Option<&Arg> { - match (n, i) { - (MNode::Frac { num, .. }, 0) => Some(num), - (MNode::Frac { den, .. }, 1) => Some(den), - (MNode::Script { base, .. }, 0) => Some(base), - (MNode::Script { sub, .. }, 1) => Some(sub), - (MNode::Script { sup, .. }, 2) => Some(sup), - (MNode::Rad { deg, .. }, 0) => Some(deg), - (MNode::Rad { e, .. }, 1) => Some(e), - (MNode::Nary { sub, .. }, 0) => Some(sub), - (MNode::Nary { sup, .. }, 1) => Some(sup), - (MNode::Nary { e, .. }, 2) => Some(e), - (MNode::Delim { elems, .. }, i) => elems.get(i), - (MNode::Func { name, .. }, 0) => Some(name), - (MNode::Func { e, .. }, 1) => Some(e), - (MNode::Lim { e, .. }, 0) => Some(e), - (MNode::Lim { lim, .. }, 1) => Some(lim), - ( - MNode::Acc { e, .. } - | MNode::Bar { e, .. } - | MNode::BorderBox { e, .. } - | MNode::Boxed { e } - | MNode::GroupChr { e, .. } - | MNode::Phant { e, .. }, - 0, - ) => Some(e), - (MNode::EqArr { rows }, i) => rows.get(i), - (MNode::Matrix { rows, .. }, i) => rows.iter().flatten().nth(i), - _ => None, - } -} - -pub fn child_mut(n: &mut MNode, i: usize) -> Option<&mut Arg> { - match (n, i) { - (MNode::Frac { num, .. }, 0) => Some(num), - (MNode::Frac { den, .. }, 1) => Some(den), - (MNode::Script { base, .. }, 0) => Some(base), - (MNode::Script { sub, .. }, 1) => Some(sub), - (MNode::Script { sup, .. }, 2) => Some(sup), - (MNode::Rad { deg, .. }, 0) => Some(deg), - (MNode::Rad { e, .. }, 1) => Some(e), - (MNode::Nary { sub, .. }, 0) => Some(sub), - (MNode::Nary { sup, .. }, 1) => Some(sup), - (MNode::Nary { e, .. }, 2) => Some(e), - (MNode::Delim { elems, .. }, i) => elems.get_mut(i), - (MNode::Func { name, .. }, 0) => Some(name), - (MNode::Func { e, .. }, 1) => Some(e), - (MNode::Lim { e, .. }, 0) => Some(e), - (MNode::Lim { lim, .. }, 1) => Some(lim), - ( - MNode::Acc { e, .. } - | MNode::Bar { e, .. } - | MNode::BorderBox { e, .. } - | MNode::Boxed { e } - | MNode::GroupChr { e, .. } - | MNode::Phant { e, .. }, - 0, - ) => Some(e), - (MNode::EqArr { rows }, i) => rows.get_mut(i), - (MNode::Matrix { rows, .. }, i) => rows.iter_mut().flatten().nth(i), - _ => None, - } -} - -/// Call `f` on every run in the tree. -pub fn each_run_mut(root: &mut Arg, f: &mut dyn FnMut(&mut MRun)) { - fn go(a: &mut Arg, f: &mut dyn FnMut(&mut MRun), depth: usize) { - if depth > MAX_DEPTH { - return; - } - for n in a.iter_mut() { - if let MNode::Run(r) = n { - f(r); - } - for c in 0..child_count(n) { - if let Some(ch) = child_mut(n, c) { - go(ch, f, depth + 1); - } - } - } - } - go(root, f, 0); -} diff --git a/crates/wordmodel/src/props/color.rs b/crates/wordmodel/src/props/color.rs index baf2d948..9fce9003 100644 --- a/crates/wordmodel/src/props/color.rs +++ b/crates/wordmodel/src/props/color.rs @@ -1,83 +1,7 @@ use super::{Deserialize, Serialize}; -/// An sRGB colour. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Default)] -pub struct Rgb(pub u8, pub u8, pub u8); - -impl<'de> Deserialize<'de> for Rgb { - /// Reads what it writes (`[r, g, b]`) and also a string [`Rgb::parse`] reads, so the colours in - /// `CharProps` / `ParaProps` take the `RRGGBB` that `para.borders` and `table.shading` take (#865). - fn deserialize>(d: D) -> Result { - use serde::de::{Error, SeqAccess, Unexpected, Visitor}; - struct V; - impl<'de> Visitor<'de> for V { - type Value = Rgb; - fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.write_str("`[r, g, b]`, `RRGGBB` or `#RRGGBB`") - } - fn visit_str(self, s: &str) -> Result { - Rgb::parse(s).ok_or_else(|| E::invalid_value(Unexpected::Str(s), &self)) - } - fn visit_seq>(self, mut a: A) -> Result { - let mut c = [0u8; 3]; - for (i, x) in c.iter_mut().enumerate() { - *x = a.next_element()?.ok_or_else(|| A::Error::invalid_length(i, &self))?; - } - if a.next_element::()?.is_some() { - return Err(A::Error::invalid_length(4, &self)); - } - Ok(Rgb(c[0], c[1], c[2])) - } - } - d.deserialize_any(V) - } -} - -impl Rgb { - pub const BLACK: Rgb = Rgb(0, 0, 0); - pub const WHITE: Rgb = Rgb(255, 255, 255); - /// `RRGGBB` (no `#`). - pub fn hex(self) -> String { - format!("{:02X}{:02X}{:02X}", self.0, self.1, self.2) - } - /// Parse `RRGGBB` or `#RRGGBB`. - pub fn parse(s: &str) -> Option { - let s = s.trim(); - let s = s.strip_prefix('#').unwrap_or(s); - if s.len() != 6 || !s.bytes().all(|b| b.is_ascii_hexdigit()) { - return None; - } - let p = |i: usize| s.get(i..i + 2).and_then(|h| u8::from_str_radix(h, 16).ok()); - Some(Rgb(p(0)?, p(2)?, p(4)?)) - } - /// This colour with the DrawingML colour modifier `name` (`lumMod`, `lumOff`, `tint`, - /// `shade`, `satMod`, ECMA-376 §20.1.2.3) at `v` (1.0 = 100%); other names leave it - /// unchanged. - pub fn modified(self, name: &str, v: f64) -> Rgb { - match name { - "lumMod" => adjust_lum(self, |l| l * v), - "lumOff" => adjust_lum(self, |l| l + v), - "tint" => mix(self, 255.0, v), - "shade" => mix(self, 0.0, v), - "satMod" => { - let (h, sat, l) = to_hsl(self); - from_hsl(h, (sat * v).clamp(0.0, 1.0), l) - } - _ => self, - } - } - /// The colour with hue `h` (0..1 of a turn), saturation `s` and lightness `l` (0..1). - pub fn from_hsl(h: f64, s: f64, l: f64) -> Rgb { - from_hsl(h, s, l) - } - /// This colour with its HSL luminance changed by `f`. - pub fn with_lum(self, f: impl Fn(f64) -> f64) -> Rgb { - adjust_lum(self, f) - } - pub fn luma(self) -> f32 { - 0.299 * self.0 as f32 + 0.587 * self.1 as f32 + 0.114 * self.2 as f32 - } -} +/// An sRGB colour: craft-math's, which equations and text share. +pub use craft_math::Rgb; /// A text colour: automatic (black on light backgrounds, white on dark) or a fixed colour. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Default)] @@ -115,57 +39,3 @@ impl<'de> Deserialize<'de> for TextColor { } } } - -/// `tint` (towards `to` = 255) and `shade` (towards 0): `v` is the share of the original colour kept. -fn mix(c: Rgb, to: f64, v: f64) -> Rgb { - let f = |x: u8| (x as f64 * v + to * (1.0 - v)).round().clamp(0.0, 255.0) as u8; - Rgb(f(c.0), f(c.1), f(c.2)) -} - -/// `c` with its HSL luminance changed by `f`. -fn adjust_lum(c: Rgb, f: impl Fn(f64) -> f64) -> Rgb { - let (h, s, l) = to_hsl(c); - from_hsl(h, s, f(l).clamp(0.0, 1.0)) -} - -fn to_hsl(c: Rgb) -> (f64, f64, f64) { - let (r, g, b) = (c.0 as f64 / 255.0, c.1 as f64 / 255.0, c.2 as f64 / 255.0); - let (max, min) = (r.max(g).max(b), r.min(g).min(b)); - let l = (max + min) / 2.0; - if max == min { - return (0.0, 0.0, l); - } - let d = max - min; - let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) }; - let h = if max == r { - (g - b) / d + if g < b { 6.0 } else { 0.0 } - } else if max == g { - (b - r) / d + 2.0 - } else { - (r - g) / d + 4.0 - }; - (h / 6.0, s, l) -} - -fn from_hsl(h: f64, s: f64, l: f64) -> Rgb { - let byte = |v: f64| (v * 255.0).round().clamp(0.0, 255.0) as u8; - if s == 0.0 { - return Rgb(byte(l), byte(l), byte(l)); - } - let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }; - let p = 2.0 * l - q; - let channel = |t: f64| { - let t = t.rem_euclid(1.0); - let v = if t < 1.0 / 6.0 { - p + (q - p) * 6.0 * t - } else if t < 0.5 { - q - } else if t < 2.0 / 3.0 { - p + (q - p) * (2.0 / 3.0 - t) * 6.0 - } else { - p - }; - byte(v) - }; - Rgb(channel(h + 1.0 / 3.0), channel(h), channel(h - 1.0 / 3.0)) -} diff --git a/crates/wordmodel/src/symbol_fonts.rs b/crates/wordmodel/src/symbol_fonts.rs deleted file mode 100644 index 1d517554..00000000 --- a/crates/wordmodel/src/symbol_fonts.rs +++ /dev/null @@ -1,316 +0,0 @@ -//! Word's symbol fonts — Symbol, Wingdings, Wingdings 2, Wingdings 3 and Webdings — mapped to -//! standard Unicode characters, so text set in them shows the intended symbols when the font -//! isn't installed (they are proprietary and never bundled) and exports as real characters. -//! -//! Word stores a symbol-font character either as the private-use code U+F000 + the font's own -//! 8-bit code (U+F020–U+F0FF, what Insert › Symbol and `w:sym` write) or as the plain 8-bit code -//! (U+0020–U+00FF) in a run whose font is the symbol font. Both map the same way. The stored text -//! never changes: the mapping is for display, layout and plain-text output only. -//! -//! Sources (Unicode's own published data; no font file was read): -//! - Symbol: the Adobe Symbol encoding table published by Unicode, -//! (codes it maps to the -//! private use area are left out; 0x6D maps to GREEK SMALL LETTER MU). -//! - Wingdings, Wingdings 2, Wingdings 3, Webdings: the correspondence tables in "Status of -//! encoding of Wingdings and Webdings Symbols", Unicode L2/12-368 (WG2 N4384, M. Suignard, -//! 2012), the frozen repertoire of the Unicode 7.0 encoding (earlier drafts L2/11-052, -//! L2/11-344), checked against the Unicode Character Database names. The one character not -//! encoded (Wingdings 0xFF) has no entry. -//! -//! Tables are indexed by code − 0x20; 0 = no Unicode equivalent. - -/// The symbol table for a single family name, by lowercase family name. -fn single_table(family: &str) -> Option<&'static [u32; 224]> { - let f = family.trim(); - if f.eq_ignore_ascii_case("symbol") || f.eq_ignore_ascii_case("symbol (pcl6)") { - Some(&SYMBOL) - } else if f.eq_ignore_ascii_case("wingdings") { - Some(&WINGDINGS) - } else if f.eq_ignore_ascii_case("wingdings 2") { - Some(&WINGDINGS_2) - } else if f.eq_ignore_ascii_case("wingdings 3") { - Some(&WINGDINGS_3) - } else if f.eq_ignore_ascii_case("webdings") { - Some(&WEBDINGS) - } else { - None - } -} - -/// The symbol fonts this module knows, by lowercase family name. -fn table(family: &str) -> Option<&'static [u32; 224]> { - if family.contains(';') { family.split(';').map(str::trim).filter(|s| !s.is_empty()).find_map(single_table) } else { single_table(family) } -} - -/// Whether `family` is one of Word's symbol fonts (Symbol, Wingdings, Wingdings 2, Wingdings 3, -/// Webdings), whose characters [`symbol_char`] maps. -pub fn is_symbol_font(family: &str) -> bool { - table(family).is_some() -} - -/// The standard Unicode character for `c` in a run set in the symbol font `family`: `c` is the -/// font's code stored as U+F020–U+F0FF or as U+0020–U+00FF (except the soft hyphen U+00AD). -/// `None` for other fonts, other characters and codes with no Unicode equivalent. -pub fn symbol_char(family: &str, c: char) -> Option { - let t = table(family)?; - let u = u32::from(c); - let code = match u { - 0xF020..=0xF0FF => u - 0xF000, - // A soft hyphen stays a soft hyphen (Word stores `w:softHyphen` as U+00AD). - 0xAD => return None, - 0x20..=0xFF => u, - _ => return None, - }; - let i = usize::try_from(code.checked_sub(0x20)?).ok()?; - t.get(i).copied().filter(|v| *v != 0).and_then(char::from_u32) -} - -/// `text` (in a run set in `family`) with every symbol-font character replaced by its Unicode -/// equivalent; unchanged for other fonts. -pub fn map_text(family: &str, text: &str) -> String { - if !is_symbol_font(family) { - return text.to_string(); - } - text.chars().map(|c| symbol_char(family, c).unwrap_or(c)).collect() -} - -const SYMBOL: [u32; 224] = [ - 0x0020, 0x0021, 0x2200, 0x0023, 0x2203, 0x0025, 0x0026, 0x220B, // 0x20 - 0x0028, 0x0029, 0x2217, 0x002B, 0x002C, 0x2212, 0x002E, 0x002F, // 0x28 - 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, // 0x30 - 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, // 0x38 - 0x2245, 0x0391, 0x0392, 0x03A7, 0x0394, 0x0395, 0x03A6, 0x0393, // 0x40 - 0x0397, 0x0399, 0x03D1, 0x039A, 0x039B, 0x039C, 0x039D, 0x039F, // 0x48 - 0x03A0, 0x0398, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03C2, 0x03A9, // 0x50 - 0x039E, 0x03A8, 0x0396, 0x005B, 0x2234, 0x005D, 0x22A5, 0x005F, // 0x58 - 0x0000, 0x03B1, 0x03B2, 0x03C7, 0x03B4, 0x03B5, 0x03C6, 0x03B3, // 0x60 - 0x03B7, 0x03B9, 0x03D5, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BF, // 0x68 - 0x03C0, 0x03B8, 0x03C1, 0x03C3, 0x03C4, 0x03C5, 0x03D6, 0x03C9, // 0x70 - 0x03BE, 0x03C8, 0x03B6, 0x007B, 0x007C, 0x007D, 0x223C, 0x0000, // 0x78 - 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0x80 - 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0x88 - 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0x90 - 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0x98 - 0x20AC, 0x03D2, 0x2032, 0x2264, 0x2044, 0x221E, 0x0192, 0x2663, // 0xA0 - 0x2666, 0x2665, 0x2660, 0x2194, 0x2190, 0x2191, 0x2192, 0x2193, // 0xA8 - 0x00B0, 0x00B1, 0x2033, 0x2265, 0x00D7, 0x221D, 0x2202, 0x2022, // 0xB0 - 0x00F7, 0x2260, 0x2261, 0x2248, 0x2026, 0x0000, 0x0000, 0x21B5, // 0xB8 - 0x2135, 0x2111, 0x211C, 0x2118, 0x2297, 0x2295, 0x2205, 0x2229, // 0xC0 - 0x222A, 0x2283, 0x2287, 0x2284, 0x2282, 0x2286, 0x2208, 0x2209, // 0xC8 - 0x2220, 0x2207, 0x0000, 0x0000, 0x0000, 0x220F, 0x221A, 0x22C5, // 0xD0 - 0x00AC, 0x2227, 0x2228, 0x21D4, 0x21D0, 0x21D1, 0x21D2, 0x21D3, // 0xD8 - 0x25CA, 0x2329, 0x0000, 0x0000, 0x0000, 0x2211, 0x0000, 0x0000, // 0xE0 - 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0xE8 - 0x0000, 0x232A, 0x222B, 0x2320, 0x0000, 0x2321, 0x0000, 0x0000, // 0xF0 - 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0xF8 -]; - -const WEBDINGS: [u32; 224] = [ - 0x0020, 0x1F577, 0x1F578, 0x1F572, 0x1F576, 0x1F3C6, 0x1F396, 0x1F587, // 0x20 - 0x1F5E8, 0x1F5E9, 0x1F5F0, 0x1F5F1, 0x1F336, 0x1F397, 0x1F67E, 0x1F67C, // 0x28 - 0x1F5D5, 0x1F5D6, 0x1F5D7, 0x23F4, 0x23F5, 0x23F6, 0x23F7, 0x23EA, // 0x30 - 0x23E9, 0x23EE, 0x23ED, 0x23F8, 0x23F9, 0x23FA, 0x1F5DA, 0x1F5F3, // 0x38 - 0x1F6E0, 0x1F3D7, 0x1F3D8, 0x1F3D9, 0x1F3DA, 0x1F3DC, 0x1F3ED, 0x1F3DB, // 0x40 - 0x1F3E0, 0x1F3D6, 0x1F3DD, 0x1F6E3, 0x1F50D, 0x1F3D4, 0x1F441, 0x1F442, // 0x48 - 0x1F3DE, 0x1F3D5, 0x1F6E4, 0x1F3DF, 0x1F6F3, 0x1F56C, 0x1F56B, 0x1F568, // 0x50 - 0x1F508, 0x1F394, 0x1F395, 0x1F5EC, 0x1F67D, 0x1F5ED, 0x1F5EA, 0x1F5EB, // 0x58 - 0x2B94, 0x2714, 0x1F6B2, 0x2B1C, 0x1F6E1, 0x1F4E6, 0x1F6F1, 0x2B1B, // 0x60 - 0x1F691, 0x1F6C8, 0x1F6E9, 0x1F6F0, 0x1F7C8, 0x1F574, 0x2B24, 0x1F6E5, // 0x68 - 0x1F694, 0x1F5D8, 0x1F5D9, 0x2753, 0x1F6F2, 0x1F687, 0x1F68D, 0x26F3, // 0x70 - 0x29B8, 0x2296, 0x1F6AD, 0x1F5EE, 0x23D0, 0x1F5EF, 0x1F5F2, 0x0000, // 0x78 - 0x1F6B9, 0x1F6BA, 0x1F6C9, 0x1F6CA, 0x1F6BC, 0x1F47D, 0x1F3CB, 0x26F7, // 0x80 - 0x1F3C2, 0x1F3CC, 0x1F3CA, 0x1F3C4, 0x1F3CD, 0x1F3CE, 0x1F698, 0x1F5E0, // 0x88 - 0x1F6E2, 0x1F4B0, 0x1F3F7, 0x1F4B3, 0x1F46A, 0x1F5E1, 0x1F5E2, 0x1F5E3, // 0x90 - 0x272F, 0x1F584, 0x1F585, 0x1F583, 0x1F586, 0x1F5B9, 0x1F5BA, 0x1F5BB, // 0x98 - 0x1F575, 0x1F570, 0x1F5BD, 0x1F5BE, 0x1F4CB, 0x1F5D2, 0x1F5D3, 0x1F56E, // 0xA0 - 0x1F4DA, 0x1F5DE, 0x1F5DF, 0x1F5C3, 0x1F5C2, 0x1F5BC, 0x1F3AD, 0x1F39C, // 0xA8 - 0x1F398, 0x1F399, 0x1F3A7, 0x1F4BF, 0x1F39E, 0x1F4F7, 0x1F39F, 0x1F3AC, // 0xB0 - 0x1F4FD, 0x1F4F9, 0x1F4FE, 0x1F4FB, 0x1F39A, 0x1F39B, 0x1F4FA, 0x1F4BB, // 0xB8 - 0x1F5A5, 0x1F5A6, 0x1F5A7, 0x1F579, 0x1F3AE, 0x1F57B, 0x1F57C, 0x1F4DF, // 0xC0 - 0x1F581, 0x1F580, 0x1F5A8, 0x1F5A9, 0x1F5BF, 0x1F5AA, 0x1F5DC, 0x1F512, // 0xC8 - 0x1F513, 0x1F5DD, 0x1F4E5, 0x1F4E4, 0x1F573, 0x1F323, 0x1F324, 0x1F325, // 0xD0 - 0x1F326, 0x2601, 0x1F328, 0x1F327, 0x1F329, 0x1F32A, 0x1F32C, 0x1F32B, // 0xD8 - 0x1F31C, 0x1F321, 0x1F6CB, 0x1F6CF, 0x1F37D, 0x1F378, 0x1F6CE, 0x1F6CD, // 0xE0 - 0x24C5, 0x267F, 0x1F6C6, 0x1F588, 0x1F393, 0x1F5E4, 0x1F5E5, 0x1F5E6, // 0xE8 - 0x1F5E7, 0x1F6EA, 0x1F43F, 0x1F426, 0x1F41F, 0x1F415, 0x1F408, 0x1F66C, // 0xF0 - 0x1F66E, 0x1F66D, 0x1F66F, 0x1F5FA, 0x1F30D, 0x1F30F, 0x1F30E, 0x1F54A, // 0xF8 -]; - -const WINGDINGS: [u32; 224] = [ - 0x0020, 0x1F589, 0x2702, 0x2701, 0x1F453, 0x1F56D, 0x1F56E, 0x1F56F, // 0x20 - 0x1F57F, 0x2706, 0x1F582, 0x1F583, 0x1F4EA, 0x1F4EB, 0x1F4EC, 0x1F4ED, // 0x28 - 0x1F5C0, 0x1F5C1, 0x1F5CE, 0x1F5CF, 0x1F5D0, 0x1F5C4, 0x23F3, 0x1F5AE, // 0x30 - 0x1F5B0, 0x1F5B2, 0x1F5B3, 0x1F5B4, 0x1F5AB, 0x1F5AC, 0x2707, 0x270D, // 0x38 - 0x1F58E, 0x270C, 0x1F58F, 0x1F44D, 0x1F44E, 0x261C, 0x261E, 0x261D, // 0x40 - 0x1F597, 0x1F590, 0x263A, 0x1F610, 0x2639, 0x1F4A3, 0x1F571, 0x1F3F3, // 0x48 - 0x1F3F1, 0x2708, 0x263C, 0x1F322, 0x2744, 0x1F546, 0x271E, 0x1F548, // 0x50 - 0x2720, 0x2721, 0x262A, 0x262F, 0x1F549, 0x2638, 0x2648, 0x2649, // 0x58 - 0x264A, 0x264B, 0x264C, 0x264D, 0x264E, 0x264F, 0x2650, 0x2651, // 0x60 - 0x2652, 0x2653, 0x1F670, 0x1F675, 0x26AB, 0x1F53E, 0x25FC, 0x1F78F, // 0x68 - 0x1F790, 0x2751, 0x2752, 0x1F79F, 0x29EB, 0x25C6, 0x2756, 0x1F799, // 0x70 - 0x2327, 0x2BB9, 0x2318, 0x1F3F5, 0x1F3F6, 0x1F676, 0x1F677, 0x0000, // 0x78 - 0x1F10B, 0x2780, 0x2781, 0x2782, 0x2783, 0x2784, 0x2785, 0x2786, // 0x80 - 0x2787, 0x2788, 0x2789, 0x1F10C, 0x278A, 0x278B, 0x278C, 0x278D, // 0x88 - 0x278E, 0x278F, 0x2790, 0x2791, 0x2792, 0x2793, 0x1F662, 0x1F660, // 0x90 - 0x1F661, 0x1F663, 0x1F65E, 0x1F65C, 0x1F65D, 0x1F65F, 0x2219, 0x2022, // 0x98 - 0x2B1D, 0x2B58, 0x1F786, 0x1F788, 0x1F78A, 0x1F78B, 0x1F53F, 0x25AA, // 0xA0 - 0x1F78E, 0x1F7C1, 0x1F7C5, 0x2605, 0x1F7CB, 0x1F7CF, 0x1F7D3, 0x1F7D1, // 0xA8 - 0x2BD0, 0x2316, 0x2BCE, 0x2BCF, 0x2BD1, 0x272A, 0x2730, 0x1F550, // 0xB0 - 0x1F551, 0x1F552, 0x1F553, 0x1F554, 0x1F555, 0x1F556, 0x1F557, 0x1F558, // 0xB8 - 0x1F559, 0x1F55A, 0x1F55B, 0x2BB0, 0x2BB1, 0x2BB2, 0x2BB3, 0x2BB4, // 0xC0 - 0x2BB5, 0x2BB6, 0x2BB7, 0x1F66A, 0x1F66B, 0x1F655, 0x1F654, 0x1F657, // 0xC8 - 0x1F656, 0x1F650, 0x1F651, 0x1F652, 0x1F653, 0x232B, 0x2326, 0x2B98, // 0xD0 - 0x2B9A, 0x2B99, 0x2B9B, 0x2B88, 0x2B8A, 0x2B89, 0x2B8B, 0x1F868, // 0xD8 - 0x1F86A, 0x1F869, 0x1F86B, 0x1F86C, 0x1F86D, 0x1F86F, 0x1F86E, 0x1F878, // 0xE0 - 0x1F87A, 0x1F879, 0x1F87B, 0x1F87C, 0x1F87D, 0x1F87F, 0x1F87E, 0x21E6, // 0xE8 - 0x21E8, 0x21E7, 0x21E9, 0x2B04, 0x21F3, 0x2B01, 0x2B00, 0x2B03, // 0xF0 - 0x2B02, 0x1F8AC, 0x1F8AD, 0x1F5F6, 0x2713, 0x1F5F7, 0x1F5F9, 0x0000, // 0xF8 -]; - -const WINGDINGS_2: [u32; 224] = [ - 0x0020, 0x1F58A, 0x1F58B, 0x1F58C, 0x1F58D, 0x2704, 0x2700, 0x1F57E, // 0x20 - 0x1F57D, 0x1F5C5, 0x1F5C6, 0x1F5C7, 0x1F5C8, 0x1F5C9, 0x1F5CA, 0x1F5CB, // 0x28 - 0x1F5CC, 0x1F5CD, 0x1F4CB, 0x1F5D1, 0x1F5D4, 0x1F5B5, 0x1F5B6, 0x1F5B7, // 0x30 - 0x1F5B8, 0x1F5AD, 0x1F5AF, 0x1F5B1, 0x1F592, 0x1F593, 0x1F598, 0x1F599, // 0x38 - 0x1F59A, 0x1F59B, 0x1F448, 0x1F449, 0x1F59C, 0x1F59D, 0x1F59E, 0x1F59F, // 0x40 - 0x1F5A0, 0x1F5A1, 0x1F446, 0x1F447, 0x1F5A2, 0x1F5A3, 0x1F591, 0x1F5F4, // 0x48 - 0x1F5F8, 0x1F5F5, 0x2611, 0x2BBD, 0x2612, 0x2BBE, 0x2BBF, 0x1F6C7, // 0x50 - 0x29B8, 0x1F671, 0x1F674, 0x1F672, 0x1F673, 0x203D, 0x1F679, 0x1F67A, // 0x58 - 0x1F67B, 0x1F666, 0x1F664, 0x1F665, 0x1F667, 0x1F65A, 0x1F658, 0x1F659, // 0x60 - 0x1F65B, 0x24EA, 0x2460, 0x2461, 0x2462, 0x2463, 0x2464, 0x2465, // 0x68 - 0x2466, 0x2467, 0x2468, 0x2469, 0x24FF, 0x2776, 0x2777, 0x2778, // 0x70 - 0x2779, 0x277A, 0x277B, 0x277C, 0x277D, 0x277E, 0x277F, 0x0000, // 0x78 - 0x2609, 0x1F315, 0x263D, 0x263E, 0x2E3F, 0x271D, 0x1F547, 0x1F55C, // 0x80 - 0x1F55D, 0x1F55E, 0x1F55F, 0x1F560, 0x1F561, 0x1F562, 0x1F563, 0x1F564, // 0x88 - 0x1F565, 0x1F566, 0x1F567, 0x1F668, 0x1F669, 0x22C5, 0x1F784, 0x2981, // 0x90 - 0x25CF, 0x25CB, 0x1F785, 0x1F787, 0x1F789, 0x2299, 0x29BF, 0x1F78C, // 0x98 - 0x1F78D, 0x25FE, 0x25A0, 0x25A1, 0x1F791, 0x1F792, 0x1F793, 0x1F794, // 0xA0 - 0x25A3, 0x1F795, 0x1F796, 0x1F797, 0x1F798, 0x2B29, 0x2B25, 0x25C7, // 0xA8 - 0x1F79A, 0x25C8, 0x1F79B, 0x1F79C, 0x1F79D, 0x1F79E, 0x2B2A, 0x2B27, // 0xB0 - 0x25CA, 0x1F7A0, 0x25D6, 0x25D7, 0x2BCA, 0x2BCB, 0x2BC0, 0x2BC1, // 0xB8 - 0x2B1F, 0x2BC2, 0x2B23, 0x2B22, 0x2BC3, 0x2BC4, 0x1F7A1, 0x1F7A2, // 0xC0 - 0x1F7A3, 0x1F7A4, 0x1F7A5, 0x1F7A6, 0x1F7A7, 0x1F7A8, 0x1F7A9, 0x1F7AA, // 0xC8 - 0x1F7AB, 0x1F7AC, 0x1F7AD, 0x1F7AE, 0x1F7AF, 0x1F7B0, 0x1F7B1, 0x1F7B2, // 0xD0 - 0x1F7B3, 0x1F7B4, 0x1F7B5, 0x1F7B6, 0x1F7B7, 0x1F7B8, 0x1F7B9, 0x1F7BA, // 0xD8 - 0x1F7BB, 0x1F7BC, 0x1F7BD, 0x1F7BE, 0x1F7BF, 0x1F7C0, 0x1F7C2, 0x1F7C4, // 0xE0 - 0x1F7C6, 0x1F7C9, 0x1F7CA, 0x2736, 0x1F7CC, 0x1F7CE, 0x1F7D0, 0x1F7D2, // 0xE8 - 0x2739, 0x1F7C3, 0x1F7C7, 0x272F, 0x1F7CD, 0x1F7D4, 0x2BCC, 0x2BCD, // 0xF0 - 0x203B, 0x2042, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0xF8 -]; - -const WINGDINGS_3: [u32; 224] = [ - 0x0020, 0x2B60, 0x2B62, 0x2B61, 0x2B63, 0x2B66, 0x2B67, 0x2B69, // 0x20 - 0x2B68, 0x2B70, 0x2B72, 0x2B71, 0x2B73, 0x2B76, 0x2B78, 0x2B7B, // 0x28 - 0x2B7D, 0x2B64, 0x2B65, 0x2B6A, 0x2B6C, 0x2B6B, 0x2B6D, 0x2B4D, // 0x30 - 0x2BA0, 0x2BA1, 0x2BA2, 0x2BA3, 0x2BA4, 0x2BA5, 0x2BA6, 0x2BA7, // 0x38 - 0x2B90, 0x2B91, 0x2B92, 0x2B93, 0x2B80, 0x2B83, 0x2B7E, 0x2B7F, // 0x40 - 0x2B84, 0x2B86, 0x2B85, 0x2B87, 0x2B8F, 0x2B8D, 0x2B8E, 0x2B8C, // 0x48 - 0x2B6E, 0x2B6F, 0x238B, 0x2324, 0x2303, 0x2325, 0x2423, 0x237D, // 0x50 - 0x21EA, 0x2BB8, 0x1F8A0, 0x1F8A1, 0x1F8A2, 0x1F8A3, 0x1F8A4, 0x1F8A5, // 0x58 - 0x1F8A6, 0x1F8A7, 0x1F8A8, 0x1F8A9, 0x1F8AA, 0x1F8AB, 0x1F850, 0x1F852, // 0x60 - 0x1F851, 0x1F853, 0x1F854, 0x1F855, 0x1F857, 0x1F856, 0x1F858, 0x1F859, // 0x68 - 0x25B2, 0x25BC, 0x25B3, 0x25BD, 0x25C0, 0x25B6, 0x25C1, 0x25B7, // 0x70 - 0x25E3, 0x25E2, 0x25E4, 0x25E5, 0x1F780, 0x1F782, 0x1F781, 0x0000, // 0x78 - 0x1F783, 0x2BC5, 0x2BC6, 0x2BC7, 0x2BC8, 0x2B9C, 0x2B9E, 0x2B9D, // 0x80 - 0x2B9F, 0x1F810, 0x1F812, 0x1F811, 0x1F813, 0x1F814, 0x1F816, 0x1F815, // 0x88 - 0x1F817, 0x1F818, 0x1F81A, 0x1F819, 0x1F81B, 0x1F81C, 0x1F81E, 0x1F81D, // 0x90 - 0x1F81F, 0x1F800, 0x1F802, 0x1F801, 0x1F803, 0x1F804, 0x1F806, 0x1F805, // 0x98 - 0x1F807, 0x1F808, 0x1F80A, 0x1F809, 0x1F80B, 0x1F820, 0x1F822, 0x1F824, // 0xA0 - 0x1F826, 0x1F828, 0x1F82A, 0x1F82C, 0x1F89C, 0x1F89D, 0x1F89E, 0x1F89F, // 0xA8 - 0x1F82E, 0x1F830, 0x1F832, 0x1F834, 0x1F836, 0x1F838, 0x1F83A, 0x1F839, // 0xB0 - 0x1F83B, 0x1F898, 0x1F89A, 0x1F899, 0x1F89B, 0x1F83C, 0x1F83E, 0x1F83D, // 0xB8 - 0x1F83F, 0x1F840, 0x1F842, 0x1F841, 0x1F843, 0x1F844, 0x1F846, 0x1F845, // 0xC0 - 0x1F847, 0x2BA8, 0x2BA9, 0x2BAA, 0x2BAB, 0x2BAC, 0x2BAD, 0x2BAE, // 0xC8 - 0x2BAF, 0x1F860, 0x1F862, 0x1F861, 0x1F863, 0x1F864, 0x1F865, 0x1F867, // 0xD0 - 0x1F866, 0x1F870, 0x1F872, 0x1F871, 0x1F873, 0x1F874, 0x1F875, 0x1F877, // 0xD8 - 0x1F876, 0x1F880, 0x1F882, 0x1F881, 0x1F883, 0x1F884, 0x1F885, 0x1F887, // 0xE0 - 0x1F886, 0x1F890, 0x1F892, 0x1F891, 0x1F893, 0x1F894, 0x1F896, 0x1F895, // 0xE8 - 0x1F897, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0xF0 - 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, // 0xF8 -]; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn both_storage_forms_map_alike() { - for f in ["Symbol", "Wingdings", "Wingdings 2", "Wingdings 3", "Webdings"] { - assert!(is_symbol_font(f)); - for code in 0x20u32..=0xFF { - let (Some(pua), Some(plain)) = (char::from_u32(0xF000 + code), char::from_u32(code)) else { continue }; - if code == 0xAD { - assert_eq!(symbol_char(f, plain), None); - continue; - } - assert_eq!(symbol_char(f, pua), symbol_char(f, plain), "{f} {code:#X}"); - } - } - } - - #[test] - fn pcl6_symbol_encoding() { - for family in ["Symbol (PCL6)", " symbol (pcl6) "] { - assert!(is_symbol_font(family)); - for code in 0x20..=0xFF { - let c = char::from_u32(code).unwrap(); - assert_eq!(symbol_char(family, c), symbol_char("Symbol", c)); - let pua = char::from_u32(0xF000 + code).unwrap(); - assert_eq!(symbol_char(family, pua), symbol_char("Symbol", pua)); - } - assert_eq!(symbol_char(family, '\u{F0B3}'), Some('≥')); - } - assert!(!is_symbol_font("Symbol (unknown)")); - } - - #[test] - fn symbol_uses_the_adobe_symbol_encoding() { - assert_eq!(symbol_char("Symbol", '\u{F061}'), Some('α')); - assert_eq!(symbol_char("symbol", 'a'), Some('α')); - assert_eq!(symbol_char("Symbol", 'm'), Some('μ')); - assert_eq!(symbol_char("Symbol", 'W'), Some('Ω')); - assert_eq!(symbol_char("Symbol", '\u{F0B7}'), Some('•')); - assert_eq!(symbol_char("Symbol", '\u{F0A5}'), Some('∞')); - assert_eq!(symbol_char("Symbol", '\u{F0D6}'), Some('√')); - // The radical extender is private use in the Adobe table: no equivalent. - assert_eq!(symbol_char("Symbol", '\u{F060}'), None); - } - - #[test] - fn wingdings_codes() { - assert_eq!(symbol_char("Wingdings", '\u{F04A}'), Some('☺')); - assert_eq!(symbol_char("Wingdings", 'J'), Some('☺')); - assert_eq!(symbol_char("Wingdings", 'L'), Some('☹')); - assert_eq!(symbol_char("Wingdings", '\u{F0FC}'), Some('✓')); - assert_eq!(symbol_char("Wingdings", '\u{F0FE}'), Some('\u{1F5F9}')); - assert_eq!(symbol_char("Wingdings", '\u{F06E}'), Some('◼')); - assert_eq!(symbol_char("Wingdings", '\u{F0A7}'), Some('▪')); - assert_eq!(symbol_char("Wingdings", '\u{F076}'), Some('❖')); - assert_eq!(symbol_char("Wingdings", '\u{F028}'), Some('\u{1F57F}')); - // The one character Unicode didn't encode. - assert_eq!(symbol_char("Wingdings", '\u{F0FF}'), None); - } - - #[test] - fn wingdings_2_3_and_webdings_codes() { - assert_eq!(symbol_char("Wingdings 2", '\u{F050}'), Some('\u{1F5F8}')); - assert_eq!(symbol_char("Wingdings 2", 'O'), Some('\u{1F5F4}')); - assert_eq!(symbol_char("Wingdings 2", '\u{F0FA}'), None); - assert_eq!(symbol_char("Wingdings 3", '\u{F021}'), Some('⭠')); - assert_eq!(symbol_char("Wingdings 3", '\u{F0F1}'), None); - assert_eq!(symbol_char("Webdings", '\u{F059}'), Some('\u{1F394}')); - assert_eq!(symbol_char("Webdings", 'a'), Some('\u{2714}')); - assert_eq!(symbol_char("Webdings", '\u{F0FF}'), Some('\u{1F54A}')); - } - - #[test] - fn other_fonts_and_characters_are_left_alone() { - assert!(!is_symbol_font("Arial")); - assert_eq!(symbol_char("Arial", '\u{F04A}'), None); - assert_eq!(symbol_char("Wingdings", '\u{1F600}'), None); - assert_eq!(symbol_char("Wingdings", '\u{10FFFF}'), None); - assert_eq!(symbol_char("Wingdings", '\u{1F}'), None); - assert_eq!(map_text("Wingdings", "J\u{F04C}é"), "☺☹\u{1F879}"); - assert_eq!(map_text("Arial", "J\u{F04C}"), "J\u{F04C}"); - } -}