From 865671e1fd938baccccfbbb93a7df61fd5786fb5 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 11 Nov 2026 21:32:31 -0500 Subject: [PATCH] craft-docx: equations through craft_math::omml (#1035) * Add craft-wordmodel: WordCraft's document model without its editing code WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX, RTF, ODT and other text-format crates can read into and write from one model. Module paths, type names and serde attributes are unchanged: an app that re-exports the crate keeps its native file format. Left in the app: range editing (edit.rs), IME text and equation editing. Not model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs. * craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs The DOCX reader and writer walk an equation's runs. The walk and the child accessors it uses were in WordCraft's equation editor (math_edit.rs), which stays in the app; they are structure, not editing. * craft-wordmodel: keep the document operations (edit.rs) with the model The DOCX and RTF readers use them: alternative-format chunks are merged into a document with adopt_blocks, and a reader formats ranges with format_range and inserts objects. They operate on the document, not on a view, so they belong beside it. The tests that drove them come back with them; the paragraph walks stay in walk.rs and edit re-exports them. base64 0.23, as WordCraft and craft-offcrypto use. * craft-docx: WordCraft's crates/docx as it is at wordcraft 07f97737 The source, unchanged, so the next commit shows only what the move changes. This commit does not build on its own (it still names wordcraft-doc and wordcraft-geom, and is not a workspace member yet). * Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath The math tree, LaTeX, linear-format, symbol and tree-walk modules come from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at 07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f / 07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in WordCraft; the rest moved with the code. mathml::parse and omml::parse/read_str/write_str are new: string entry points for callers that have no tree of their own. * craft-math: read MathLive's and other spellings of LaTeX math commands \abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel, \backprime, \infinity, \cross, \differentialD, \exponentialE and \imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox, \mathnormal and \underleftarrow / \underrightarrow / \underleftrightarrow were unknown and read as literal text. They are read as equaSHion's typed-input parser reads them (storytold/craft-libs#29). * craft-math: equaSHion's equation corpus as test data corpus/equations.tsv, corpus/latex.tsv and the OMML samples of storytold/craft-libs#29 (7a6ef6b9), unmodified. * craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README The corpus equations go through OMML, MathML and LaTeX and back; the OMML samples are written back as kept and from the tree. GridCraft's OMML to LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the same through the tree. omml::parse and read_str are tested against other prefixes, the strict namespace, kept OMML, junk, the element cap and deep nesting. * craft-docx: on craft-wordmodel, craft-opc, craft-xml and craft-offcrypto - The package (zip reading, part names, relationships, content types, zip writing) is craft-opc's; the module keeps WordCraft's bookkeeping (passthrough keys, the embedded-part manifest) and its own Rel/Rels shape. - Parts are parsed with craft-xml's canonical names (the Strict drawing names and mc:Choice/@Requires through its hooks), then given the tree shape the reader was written against. - Encryption and Restrict Editing hashes are craft-offcrypto's; this crate maps its errors and the w:documentProtection attributes. - wordcraft-doc is craft-wordmodel; wordcraft-geom's helpers are its geom. Tests that covered what now lives in craft-opc and craft-offcrypto are left to those crates; the DIFAT regression is craft-cfb's. Test-only zip use follows zip 9's fallible names. The attribute-cap test times the read only. * 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. * craft-docx: equations (OMML) are read and written by craft_math::omml read/math.rs and write/math.rs keep their functions as adapters: the reader's tree is handed to craft-math as a craft-xml tree, and the writer's output is written into this crate's writer. Their tests are in craft-math. --------- Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com> --- crates/docx/Cargo.toml | 1 + crates/docx/README.md | 3 +- crates/docx/src/read/math.rs | 507 +---------- crates/docx/src/write/math.rs | 481 +--------- 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 ------- 13 files changed, 45 insertions(+), 5043 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/docx/Cargo.toml b/crates/docx/Cargo.toml index 046d81589..cdc6144eb 100644 --- a/crates/docx/Cargo.toml +++ b/crates/docx/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true workspace = true [dependencies] +craft-math = { workspace = true } craft-offcrypto = { workspace = true } craft-opc = { workspace = true } craft-wordmodel = { workspace = true } diff --git a/crates/docx/README.md b/crates/docx/README.md index 0e5cbf11e..8dab3fc55 100644 --- a/crates/docx/README.md +++ b/crates/docx/README.md @@ -42,8 +42,9 @@ written back as it was read. `write(doc)`, `write_as(doc, Flavor)` for `.docx`, `.docm`, `.dotx` and `.dotm`; `write_flat` and `write_wordml2003` for the XML forms; `encrypt` for a password. +Equations (OMML) are read and written by `craft_math::omml`. + ## Not here yet - DrawingML (`a:`) shape fills, lines, custom geometry and colours are still read with this crate's own code; they move to `craft-drawingml`. -- Equations (OMML) are read and written by this crate; they move to `craft-math`. diff --git a/crates/docx/src/read/math.rs b/crates/docx/src/read/math.rs index 27c78122f..e42edb646 100644 --- a/crates/docx/src/read/math.rs +++ b/crates/docx/src/read/math.rs @@ -1,496 +1,41 @@ -//! Office Math (OMML, ECMA-376 Part 1 §22.1) → [`craft_wordmodel::math`] tree. +//! Office Math (OMML, ECMA-376 Part 1 §22.1) read with `craft_math::omml`. -use craft_wordmodel::math::{ - Arg, BrkBin, BrkBinSub, ColJc, FracKind, LimLoc, MAX_DEPTH, MNode, MRun, MScr, MSty, Math, MathJc, MathProps, ScriptKind, merge_runs, -}; +use craft_wordmodel::math::{Math, MathJc, MathProps}; use craft_wordmodel::props::Rgb; -use craft_wordmodel::symbol_fonts; -use crate::xml::El; +use crate::xml::{El, Node}; -/// An `m:oMath` element as an equation (structure plus its source XML). -pub fn read_omath(e: &El, jc: MathJc) -> Math { - Math { nodes: merge_runs(arg(e, 0)), omml: e.to_xml(), jc, linear: false } -} - -/// A colour every math run shares, taken off the runs: it is the equation's own colour, as -/// [`write_equation`](crate::write::math::write_equation) spreads it (#1467). The source OMML -/// is kept: the writer replaces it only when the colour changes. -pub fn lift_color(math: &mut Math) -> Option { - let (mut shared, mut uniform) = (None, true); - craft_wordmodel::math_tree::each_run_mut(&mut math.nodes, &mut |r| match shared { - None => shared = Some(r.color), - Some(c) => uniform &= c == r.color, - }); - let c = shared.flatten().filter(|_| uniform)?; - craft_wordmodel::math_tree::each_run_mut(&mut math.nodes, &mut |r| r.color = None); - Some(c) -} - -/// `m:oMathParaPr/m:jc`. -pub fn para_jc(para: &El) -> MathJc { - match para.child("m:oMathParaPr").and_then(|p| p.child("m:jc")).and_then(|j| j.attr("m:val")) { - Some("left") => MathJc::Left, - Some("right") => MathJc::Right, - Some("center") => MathJc::Center, - _ => MathJc::CenterGroup, - } -} - -/// The settings part's `m:mathPr`: how display equations wrap. -pub fn read_math_pr(e: &El) -> MathProps { - let mut m = MathProps::default(); - let pr = Some(e); - m.brk_bin = match val(pr, "m:brkBin") { - Some("after") => BrkBin::After, - Some("repeat") => BrkBin::Repeat, - _ => BrkBin::Before, - }; - m.brk_bin_sub = match val(pr, "m:brkBinSub") { - Some("-+") => BrkBinSub::MinusPlus, - Some("+-") => BrkBinSub::PlusMinus, - _ => BrkBinSub::MinusMinus, - }; - if let Some(v) = e.child("m:wrapIndent").and_then(|c| crate::units::tw(c, "m:val")).filter(|v| v.is_finite() && *v >= 0.0) { - m.wrap_indent = v.min(1584.0); - } - m.wrap_right = prop(pr, "m:wrapRight", false); - m -} - -/// OMML on/off property: absent `m:val` = on. -fn on(e: &El) -> bool { - match e.attr("m:val") { - None => true, - Some(v) => !matches!(v.trim(), "0" | "false" | "off" | "False" | "FALSE"), - } -} - -/// An on/off property `name` in the properties element `pr` (absent = `default`). -fn prop(pr: Option<&El>, name: &str, default: bool) -> bool { - pr.and_then(|p| p.child(name)).map(on).unwrap_or(default) -} - -/// A character property (`m:chr`, `m:begChr`…): `None` when absent, `Some(None)` when empty. -fn chr(pr: Option<&El>, name: &str) -> Option> { - let c = pr?.child(name)?; - match c.attr("m:val") { - Some(v) => Some(v.chars().next()), - None => Some(None), - } -} - -fn val<'a>(pr: Option<&'a El>, name: &str) -> Option<&'a str> { - pr.and_then(|p| p.child(name)).and_then(|c| c.attr("m:val")) -} - -/// The argument held by child `name` of `e`. -fn sub_arg(e: &El, name: &str, depth: usize) -> Arg { - e.child(name).map(|c| arg(c, depth + 1)).unwrap_or_default() -} - -/// Read the math content of an element (`m:oMath`, `m:e`, `m:num`…). -fn arg(e: &El, depth: usize) -> Arg { - let mut out = Vec::new(); - if depth > MAX_DEPTH { - return out; - } - for k in e.els() { - node(k, depth, &mut out); +/// This reader's tree as `craft-xml`'s, which `craft_math::omml` reads. Both use the canonical +/// prefixes (`m:`, `w:`, `mc:`). +// shortcut: copies the equation's subtree; goes when the reader reads craft_xml::El directly. +fn to_craft(e: &El) -> craft_xml::El { + let mut out = craft_xml::El { name: e.name.clone(), attrs: e.attrs.clone(), children: Vec::with_capacity(e.kids.len()) }; + for k in &e.kids { + out.children.push(match k { + Node::El(c) => craft_xml::Node::El(to_craft(c)), + Node::Text(t) => craft_xml::Node::Text(t.clone()), + }); } out } -fn node(k: &El, depth: usize, out: &mut Arg) { - let d = depth + 1; - match k.name.as_str() { - "m:r" => { - if let Some(r) = run(k) { - out.push(MNode::Run(r)); - } - } - // Word puts ordinary runs in math zones for normal text in some files. - "w:r" => { - let text = run_text(k); - if !text.is_empty() { - let mut r = MRun { text, nor: true, sty: Some(MSty::Plain), ..Default::default() }; - wrpr(k.child("w:rPr"), &mut r); - out.push(MNode::Run(r)); - } - } - "m:f" => { - let pr = k.child("m:fPr"); - let kind = match val(pr, "m:type") { - Some("skw") => FracKind::Skewed, - Some("lin") => FracKind::Linear, - Some("noBar") => FracKind::NoBar, - _ => FracKind::Bar, - }; - out.push(MNode::Frac { kind, num: sub_arg(k, "m:num", d), den: sub_arg(k, "m:den", d) }); - } - "m:sSup" => out.push(MNode::Script { kind: ScriptKind::Sup, base: sub_arg(k, "m:e", d), sub: Vec::new(), sup: sub_arg(k, "m:sup", d) }), - "m:sSub" => out.push(MNode::Script { kind: ScriptKind::Sub, base: sub_arg(k, "m:e", d), sub: sub_arg(k, "m:sub", d), sup: Vec::new() }), - "m:sSubSup" => { - out.push(MNode::Script { kind: ScriptKind::SubSup, base: sub_arg(k, "m:e", d), sub: sub_arg(k, "m:sub", d), sup: sub_arg(k, "m:sup", d) }) - } - "m:sPre" => { - out.push(MNode::Script { kind: ScriptKind::Pre, base: sub_arg(k, "m:e", d), sub: sub_arg(k, "m:sub", d), sup: sub_arg(k, "m:sup", d) }) - } - "m:rad" => { - let pr = k.child("m:radPr"); - out.push(MNode::Rad { deg: sub_arg(k, "m:deg", d), deg_hide: prop(pr, "m:degHide", false), e: sub_arg(k, "m:e", d) }); - } - "m:nary" => { - let pr = k.child("m:naryPr"); - let chr = chr(pr, "m:chr").map(|c| c.unwrap_or('∫')).unwrap_or('∫'); - let lim_loc = match val(pr, "m:limLoc") { - Some("undOvr") => Some(LimLoc::UndOvr), - Some("subSup") => Some(LimLoc::SubSup), - _ => None, - }; - out.push(MNode::Nary { - chr, - lim_loc, - grow: prop(pr, "m:grow", false), - sub_hide: prop(pr, "m:subHide", false), - sup_hide: prop(pr, "m:supHide", false), - sub: sub_arg(k, "m:sub", d), - sup: sub_arg(k, "m:sup", d), - e: sub_arg(k, "m:e", d), - }); - } - "m:d" => { - let pr = k.child("m:dPr"); - let beg = chr(pr, "m:begChr").unwrap_or(Some('(')); - let end = chr(pr, "m:endChr").unwrap_or(Some(')')); - let sep = chr(pr, "m:sepChr").unwrap_or(Some('|')); - let elems: Vec = k.children("m:e").map(|e| arg(e, d)).collect(); - out.push(MNode::Delim { - beg, - end, - sep, - grow: prop(pr, "m:grow", true), - shp_match: val(pr, "m:shp") == Some("match"), - elems: if elems.is_empty() { vec![Vec::new()] } else { elems }, - }); - } - "m:func" => out.push(MNode::Func { name: sub_arg(k, "m:fName", d), e: sub_arg(k, "m:e", d) }), - "m:limLow" => out.push(MNode::Lim { upper: false, e: sub_arg(k, "m:e", d), lim: sub_arg(k, "m:lim", d) }), - "m:limUpp" => out.push(MNode::Lim { upper: true, e: sub_arg(k, "m:e", d), lim: sub_arg(k, "m:lim", d) }), - "m:acc" => { - let c = chr(k.child("m:accPr"), "m:chr").map(|c| c.unwrap_or('\u{302}')).unwrap_or('\u{302}'); - out.push(MNode::Acc { chr: c, e: sub_arg(k, "m:e", d) }); - } - "m:bar" => out.push(MNode::Bar { top: val(k.child("m:barPr"), "m:pos") == Some("top"), e: sub_arg(k, "m:e", d) }), - "m:borderBox" => { - let pr = k.child("m:borderBoxPr"); - out.push(MNode::BorderBox { - hide: [prop(pr, "m:hideTop", false), prop(pr, "m:hideBot", false), prop(pr, "m:hideLeft", false), prop(pr, "m:hideRight", false)], - strike: [ - prop(pr, "m:strikeH", false), - prop(pr, "m:strikeV", false), - prop(pr, "m:strikeBLTR", false), - prop(pr, "m:strikeTLBR", false), - ], - e: sub_arg(k, "m:e", d), - }); - } - "m:box" => out.push(MNode::Boxed { e: sub_arg(k, "m:e", d) }), - "m:groupChr" => { - let pr = k.child("m:groupChrPr"); - let c = chr(pr, "m:chr").map(|c| c.unwrap_or('\u{23DF}')).unwrap_or('\u{23DF}'); - out.push(MNode::GroupChr { chr: c, top: val(pr, "m:pos") == Some("top"), e: sub_arg(k, "m:e", d) }); - } - "m:eqArr" => out.push(MNode::EqArr { rows: k.children("m:e").map(|e| arg(e, d)).collect() }), - "m:m" => { - let mut col_jc = Vec::new(); - if let Some(mcs) = k.child("m:mPr").and_then(|p| p.child("m:mcs")) { - for mc in mcs.children("m:mc") { - let pr = mc.child("m:mcPr"); - let jc = match val(pr, "m:mcJc") { - Some("left") => ColJc::Left, - Some("right") => ColJc::Right, - _ => ColJc::Center, - }; - let count = val(pr, "m:count").and_then(|c| c.trim().parse::().ok()).unwrap_or(1).clamp(1, 64); - for _ in 0..count { - if col_jc.len() < 64 { - col_jc.push(jc); - } - } - } - } - let rows = k.children("m:mr").take(1000).map(|r| r.children("m:e").take(64).map(|e| arg(e, d)).collect()).collect(); - out.push(MNode::Matrix { rows, col_jc }); - } - "m:phant" => { - let pr = k.child("m:phantPr"); - out.push(MNode::Phant { - show: prop(pr, "m:show", true), - zero_wid: prop(pr, "m:zeroWid", false), - zero_asc: prop(pr, "m:zeroAsc", false), - zero_desc: prop(pr, "m:zeroDesc", false), - e: sub_arg(k, "m:e", d), - }); - } - // Tracked insertions show; deletions don't. Hyperlinks, simple fields and direction - // wrappers (EG_PContentMath, §22.1.2.77) keep their runs (#1273). - "w:ins" | "w:moveTo" | "w:smartTag" | "w:customXml" | "w:hyperlink" | "w:fldSimple" | "w:dir" | "w:bdo" | "m:oMath" if depth <= MAX_DEPTH => { - for c in k.els() { - node(c, d, out); - } - } - // Markup compatibility: the math choice (we read math), else the fallback. - "mc:AlternateContent" if depth <= MAX_DEPTH => { - if let Some(c) = k.child("mc:Choice").or_else(|| k.child("mc:Fallback")) { - for c in c.els() { - node(c, d, out); - } - } - } - "w:sdt" => { - if let Some(c) = k.child("w:sdtContent") { - for c in c.els() { - node(c, d, out); - } - } - } - _ => {} - } +/// An `m:oMath` element as an equation (structure plus its source XML). +pub fn read_omath(e: &El, jc: MathJc) -> Math { + craft_math::omml::read_omath(&to_craft(e), jc) } -/// `m:t` (and `w:t`) text of a run, with symbol characters (`w:sym`) and special hyphens. -/// Text in a symbol font (Symbol, Wingdings…) becomes its Unicode equivalent: the math font has -/// no glyphs at those private-use codes (#1273). -fn run_text(r: &El) -> String { - let font = r.child("w:rPr").and_then(|p| p.child("w:rFonts")).and_then(|f| f.attr("w:ascii").or_else(|| f.attr("w:hAnsi"))).unwrap_or(""); - let mut s = String::new(); - for c in r.els() { - match c.name.as_str() { - "m:t" | "w:t" => s.push_str(&symbol_fonts::map_text(font, &c.text())), - "w:tab" => s.push('\t'), - "w:noBreakHyphen" => s.push('\u{2011}'), - "w:sym" => { - let ch = c.attr("w:char").and_then(|h| u32::from_str_radix(h.trim(), 16).ok()).and_then(char::from_u32).filter(|c| *c as u32 >= 0x20); - if let Some(ch) = ch { - let f = c.attr("w:font").unwrap_or(font); - s.push(symbol_fonts::symbol_char(f, ch).unwrap_or(ch)); - } - } - _ => {} - } - } - s +/// The colour every run of `math` shares, taken off the runs (the run around the equation +/// carries it). +pub fn lift_color(math: &mut Math) -> Option { + craft_math::omml::lift_color(math) } -fn run(r: &El) -> Option { - let text = run_text(r); - if text.is_empty() { - return None; - } - let mut out = MRun { text, ..Default::default() }; - if let Some(pr) = r.child("m:rPr") { - out.sty = match pr.child("m:sty").and_then(|s| s.attr("m:val")) { - Some("p") => Some(MSty::Plain), - Some("b") => Some(MSty::Bold), - Some("i") => Some(MSty::Italic), - Some("bi") => Some(MSty::BoldItalic), - _ => None, - }; - out.scr = match pr.child("m:scr").and_then(|s| s.attr("m:val")) { - Some("script") => MScr::Script, - Some("fraktur") => MScr::Fraktur, - Some("double-struck") => MScr::DoubleStruck, - Some("sans-serif") => MScr::SansSerif, - Some("monospace") => MScr::Monospace, - _ => MScr::Roman, - }; - out.nor = pr.child("m:nor").is_some_and(on); - out.lit = pr.child("m:lit").is_some_and(on); - if let Some(b) = pr.child("m:brk") { - let aln = b.attr("m:alnAt").and_then(|v| v.trim().parse::().ok()).unwrap_or(0); - out.brk = Some(aln.min(255) as u8); - } - } - wrpr(r.child("w:rPr"), &mut out); - Some(out) +/// The justification an `m:oMathPara` gives its equations. +pub fn para_jc(para: &El) -> MathJc { + craft_math::omml::para_jc(&to_craft(para)) } -/// The run formatting a math run takes from `w:rPr`: size, colour, normal-text font and weight. -fn wrpr(pr: Option<&El>, out: &mut MRun) { - let Some(pr) = pr else { return }; - if let Some(sz) = pr.child("w:sz").and_then(|s| s.attr("w:val")).and_then(|v| v.trim().parse::().ok()) { - let pt = sz / 2.0; - if pt.is_finite() && (1.0..=1638.0).contains(&pt) { - out.size = Some(pt); - } - } - if let Some(c) = pr.child("w:color").and_then(|c| c.attr("w:val")).and_then(Rgb::parse) { - out.color = Some(c); - } - if out.nor { - // Symbol-font text was mapped to Unicode: it takes the text font. - if let Some(f) = - pr.child("w:rFonts").and_then(|f| f.attr("w:ascii").or_else(|| f.attr("w:hAnsi"))).filter(|f| !symbol_fonts::is_symbol_font(f)) - { - out.font = Some(f.to_string()); - } - let bold = pr.child("w:b").is_some_and(crate::units::on_off); - let italic = pr.child("w:i").is_some_and(crate::units::on_off); - if bold || italic { - out.sty = Some(match (bold, italic) { - (true, true) => MSty::BoldItalic, - (true, false) => MSty::Bold, - _ => MSty::Italic, - }); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn omath(inner: &str) -> El { - let x = format!( - r#"{inner}"# - ); - crate::xml::parse(x.as_bytes()).unwrap() - } - - #[test] - fn reads_fraction_radical_and_scripts() { - let e = omath( - r#"x=-b±b2-4ac2a"#, - ); - let m = read_omath(&e, MathJc::CenterGroup); - assert_eq!(m.nodes.len(), 2); - let MNode::Frac { num, den, kind } = &m.nodes[1] else { panic!("{:#?}", m.nodes) }; - assert_eq!(*kind, FracKind::Bar); - assert_eq!(den, &vec![MNode::Run(MRun::new("2a"))]); - let MNode::Rad { deg_hide, e, .. } = &num[1] else { panic!() }; - assert!(*deg_hide); - assert!(matches!(e[0], MNode::Script { kind: ScriptKind::Sup, .. })); - assert_eq!(craft_wordmodel::math::to_linear(&m.nodes), "x=(-b±√(b^2-4ac))/2a"); - assert!(m.omml.starts_with("")); - } - - #[test] - fn reads_nary_delims_matrix_and_defaults() { - let e = omath( - r#"iiab"#, - ); - let m = read_omath(&e, MathJc::CenterGroup); - let MNode::Nary { chr, lim_loc, .. } = &m.nodes[0] else { panic!() }; - assert_eq!((*chr, *lim_loc), ('∑', Some(LimLoc::UndOvr))); - let MNode::Delim { beg, end, sep, elems, .. } = &m.nodes[1] else { panic!() }; - assert_eq!((*beg, *end, *sep, elems.len()), (Some('['), None, Some('|'), 2)); - assert!(matches!(m.nodes[2], MNode::Nary { chr: '∫', .. })); - assert!(matches!(&m.nodes[3], MNode::Matrix { rows, .. } if rows.len() == 1 && rows[0].len() == 2)); - } - - /// Every run's text in the tree, in reading order. - fn texts(m: &mut Math) -> Vec { - let mut out = Vec::new(); - craft_wordmodel::math_tree::each_run_mut(&mut m.nodes, &mut |r| out.push(r.text.clone())); - out - } - - fn t(s: &str) -> String { - format!("{s}") - } - - /// #1273: one equation using every Office Math element of ECMA-376 §22.1 (each argument - /// holding a distinct marker) keeps every argument's text: nothing is silently dropped. - #[test] - fn every_omml_element_keeps_its_content() { - let parts = [ - format!("{}", t("A1")), - format!("{}", t("B1")), - format!("{}", t("C1")), - format!("{}", t("D1")), - format!("{}{}", t("E1"), t("E2")), - format!("{}{}", t("F1"), t("F2")), - format!("{}{}", t("G1"), t("G2")), - format!("{}{}", t("H1"), t("H2")), - format!("{}", t("I1")), - format!("{}{}", t("J1"), t("J2")), - format!("{}{}", t("K1"), t("K2")), - format!("{}{}{}", t("L1"), t("L2"), t("L3")), - format!( - "{}{}{}", - t("M1"), - t("M2"), - t("M3") - ), - format!("{}", t("N1")), - format!("{}{}", t("O1"), t("O2")), - format!("{}{}{}", t("P1"), t("P2"), t("P3")), - format!("{}{}", t("Q1"), t("Q2")), - format!("{}{}{}", t("R1"), t("R2"), t("R3")), - format!("{}{}", t("S1"), t("S2")), - // Run-level wrappers allowed in math (EG_PContentMath): hyperlinks, simple fields, - // tracked insertions, content controls, markup compatibility. - format!("{}", t("T1")), - format!("{}", t("T2")), - format!("{}", t("T3")), - format!("{}", t("T4")), - format!( - "{}picture", - t("T5") - ), - "T6".to_string(), - ]; - let x = format!( - r#"{}"#, - parts.concat() - ); - let e = crate::xml::parse(x.as_bytes()).unwrap(); - let mut m = read_omath(&e, MathJc::CenterGroup); - let got = texts(&mut m).concat(); - let markers = [ - "A1", "B1", "C1", "D1", "E1", "E2", "F1", "F2", "G1", "G2", "H1", "H2", "I1", "J1", "J2", "K1", "K2", "L1", "L2", "L3", "M1", "M2", "M3", - "N1", "O1", "O2", "P1", "P2", "P3", "Q1", "Q2", "R1", "R2", "R3", "S1", "S2", "T1", "T2", "T3", "T4", "T5", "T6", - ]; - for k in markers { - assert!(got.contains(k), "{k} dropped from {got}"); - } - assert!(!got.contains("picture"), "the math choice wins over the fallback: {got}"); - } - - /// #1273: deleted text stays out; symbol characters and symbol-font text become Unicode. - #[test] - fn symbols_and_deletions_in_math() { - let e = omath( - r#"xgone"#, - ); - let mut m = read_omath(&e, MathJc::CenterGroup); - let got = texts(&mut m).concat(); - assert_eq!(got, "xαβ\u{2011}γ"); - let mut font = None; - craft_wordmodel::math_tree::each_run_mut(&mut m.nodes, &mut |r| { - if r.nor { - font = Some(r.font.clone()); - } - }); - assert_eq!(font, Some(None), "mapped symbol text takes the text font"); - } - - /// Hostile nesting through the wrappers is bounded (no stack overflow, no panic). - #[test] - fn deeply_nested_wrappers_are_bounded() { - // Deeper than the math limit, within the XML parser's own depth limit. - let n = 120; - let inner = format!("{}{}{}", "".repeat(n), t("deep"), "".repeat(n)); - let e = omath(&inner); - let _ = read_omath(&e, MathJc::CenterGroup); - let inner = format!( - "{}{}{}", - "".repeat(n), - t("deep"), - "".repeat(n) - ); - let e = omath(&inner); - let _ = read_omath(&e, MathJc::CenterGroup); - } +/// `m:mathPr` in the settings part. +pub fn read_math_pr(e: &El) -> MathProps { + craft_math::omml::read_math_pr(&to_craft(e)) } diff --git a/crates/docx/src/write/math.rs b/crates/docx/src/write/math.rs index 571c5978f..d22d548ef 100644 --- a/crates/docx/src/write/math.rs +++ b/crates/docx/src/write/math.rs @@ -1,486 +1,25 @@ -//! [`craft_wordmodel::math`] tree → Office Math (OMML). +//! Office Math (OMML) written with `craft_math::omml`. -use craft_wordmodel::math::{ - Arg, BrkBin, BrkBinSub, ColJc, FracKind, LimLoc, MAX_DEPTH, MAX_MATRIX_COLS, MAX_MATRIX_ROWS, MNode, MRun, MScr, MSty, Math, MathJc, MathProps, - ScriptKind, parse_linear, -}; +use craft_wordmodel::math::{Math, MathProps}; use craft_wordmodel::props::Rgb; use crate::xml::W; /// Write an equation: `m:oMath`, inside `m:oMathPara` for a display equation. The source OMML /// is written back unchanged when the equation came from a file. `color` (the equation's run -/// colour) goes on every math run without its own: OMML has no equation-level colour (#1467). +/// colour) goes on every math run without its own: OMML has no equation-level colour. pub fn write_equation(w: &mut W, linear: &str, display: bool, math: &Math, color: Option) { - let tinted; - let math = if color == omml_color(&math.omml) { - math - } else { - // The kept source XML has the old colour baked in: write from the nodes instead. - let mut m = math.clone(); - if m.nodes.is_empty() { - m.nodes = parse_linear(linear); - } - if let Some(c) = color { - craft_wordmodel::math_tree::each_run_mut(&mut m.nodes, &mut |r| { - r.color.get_or_insert(c); - }); - } - m.omml.clear(); - tinted = m; - &tinted - }; - if display { - w.open("m:oMathPara", &[]); - let jc = match math.jc { - MathJc::CenterGroup => None, - MathJc::Center => Some("center"), - MathJc::Left => Some("left"), - MathJc::Right => Some("right"), - }; - if let Some(jc) = jc { - w.open("m:oMathParaPr", &[]); - w.empty("m:jc", &[("m:val", jc)]); - w.close("m:oMathParaPr"); - } - } - if !math.omml.is_empty() && math.omml.starts_with(" Option { - if !display { - return None; - } - let [MNode::EqArr { rows }] = math.nodes.as_slice() else { return None }; - let mut out = rows.clone(); - let mut changed = false; - for row in out.iter_mut() { - if !craft_wordmodel::math::has_number_mark(row) { - continue; - } - *counter = counter.saturating_add(1); - // Nothing after the `#`: an automatic number. - let mut after = false; - let mut empty = true; - for n in row.iter() { - match n { - MNode::Run(r) if !after => { - if let Some(k) = r.text.find('#') { - after = true; - if !r.text.get(k + 1..).unwrap_or("").is_empty() { - empty = false; - } - } - } - _ if after => empty = false, - _ => {} - } - } - if empty { - let n = vec![MNode::Run(MRun::new(counter.to_string()))]; - row.push(MNode::Delim { beg: Some('('), end: Some(')'), sep: None, grow: true, shp_match: false, elems: vec![n] }); - changed = true; - } - } - changed.then(|| Math { nodes: vec![MNode::EqArr { rows: out }], omml: String::new(), ..math.clone() }) + craft_math::omml::resolve_numbers(math, display, counter) } -/// The equation colour the kept source OMML carries, as the reader lifts it. -fn omml_color(omml: &str) -> Option { - if omml.is_empty() { - return None; - } - let root = crate::xml::parse(wrap_ns(omml).as_bytes()).ok()?; - let e = root.child("m:oMath")?; - crate::read::math::lift_color(&mut crate::read::math::read_omath(e, MathJc::default())) -} - -/// The kept XML with namespace declarations, for validating it on its own. -fn wrap_ns(xml: &str) -> String { - let decl: String = crate::xml::body_ns().iter().filter(|(k, _)| k.starts_with("xmlns:")).map(|(k, v)| format!(" {k}=\"{v}\"")).collect(); - format!("{xml}") -} - -/// The settings part's `m:mathPr` (CT_MathPr order; `m:wrapIndent` and `m:wrapRight` are a choice). +/// `m:mathPr`. pub fn math_pr(w: &mut W, m: &MathProps) { - w.open("m:mathPr", &[]); - let bin = match m.brk_bin { - BrkBin::Before => "before", - BrkBin::After => "after", - BrkBin::Repeat => "repeat", - }; - val(w, "m:brkBin", bin); - let sub = match m.brk_bin_sub { - BrkBinSub::MinusMinus => "--", - BrkBinSub::MinusPlus => "-+", - BrkBinSub::PlusMinus => "+-", - }; - val(w, "m:brkBinSub", sub); - if m.wrap_right { - w.empty("m:wrapRight", &[]); - } else { - let indent = if m.wrap_indent.is_finite() { m.wrap_indent.clamp(0.0, 1584.0) } else { craft_wordmodel::math::DEFAULT_WRAP_INDENT }; - val(w, "m:wrapIndent", &crate::units::twips(indent)); - } - w.close("m:mathPr"); -} - -fn val(w: &mut W, name: &str, v: &str) { - w.empty(name, &[("m:val", v)]); -} - -fn arg(w: &mut W, name: &str, a: &[MNode], depth: usize) { - if a.is_empty() { - w.empty(name, &[]); - return; - } - w.open(name, &[]); - arg_inner(w, a, depth + 1); - w.close(name); -} - -fn arg_inner(w: &mut W, a: &[MNode], depth: usize) { - if depth > MAX_DEPTH { - return; - } - for n in a { - node(w, n, depth); - } -} - -fn ctrl(w: &mut W) { - w.open("m:ctrlPr", &[]); - w.open("w:rPr", &[]); - w.empty("w:rFonts", &[("w:ascii", "Cambria Math"), ("w:hAnsi", "Cambria Math")]); - w.empty("w:i", &[]); - w.close("w:rPr"); - w.close("m:ctrlPr"); -} - -fn char_prop(w: &mut W, name: &str, c: Option) { - let s = c.map(|c| c.to_string()).unwrap_or_default(); - val(w, name, &s); -} - -fn write_matrix(w: &mut W, n: &MNode, depth: usize) { - let MNode::Matrix { rows, col_jc } = n else { return }; - // Every row is padded to the widest one, so a ragged matrix is clamped to the reader's - // caps rather than written as rows × widest cells (#3643). - let rows = rows.get(..rows.len().min(MAX_MATRIX_ROWS)).unwrap_or_default(); - let cols = rows.iter().map(|r| r.len()).max().unwrap_or(0).clamp(1, MAX_MATRIX_COLS); - w.open("m:m", &[]); - w.open("m:mPr", &[]); - w.open("m:mcs", &[]); - for c in 0..cols { - let jc = match col_jc.get(c).copied().unwrap_or_default() { - ColJc::Center => "center", - ColJc::Left => "left", - ColJc::Right => "right", - }; - w.open("m:mc", &[]); - w.open("m:mcPr", &[]); - val(w, "m:count", "1"); - val(w, "m:mcJc", jc); - w.close("m:mcPr"); - w.close("m:mc"); - } - w.close("m:mcs"); - ctrl(w); - w.close("m:mPr"); - for row in rows { - w.open("m:mr", &[]); - for c in 0..cols { - arg(w, "m:e", row.get(c).map(|v| v.as_slice()).unwrap_or(&[]), depth); - } - w.close("m:mr"); - } - w.close("m:m"); -} - -fn write_nary(w: &mut W, n: &MNode, depth: usize) { - let MNode::Nary { chr, lim_loc, grow, sub_hide, sup_hide, sub, sup, e } = n else { return }; - w.open("m:nary", &[]); - w.open("m:naryPr", &[]); - if *chr != '∫' { - char_prop(w, "m:chr", Some(*chr)); - } - match lim_loc { - Some(LimLoc::UndOvr) => val(w, "m:limLoc", "undOvr"), - Some(LimLoc::SubSup) => val(w, "m:limLoc", "subSup"), - None => {} - } - if *grow { - val(w, "m:grow", "1"); - } - if *sub_hide { - val(w, "m:subHide", "1"); - } - if *sup_hide { - val(w, "m:supHide", "1"); - } - ctrl(w); - w.close("m:naryPr"); - arg(w, "m:sub", sub, depth); - arg(w, "m:sup", sup, depth); - arg(w, "m:e", e, depth); - w.close("m:nary"); -} - -fn node(w: &mut W, n: &MNode, depth: usize) { - match n { - MNode::Run(r) => run(w, r), - MNode::Frac { kind, num, den } => { - w.open("m:f", &[]); - w.open("m:fPr", &[]); - match kind { - FracKind::Bar => {} - FracKind::Skewed => val(w, "m:type", "skw"), - FracKind::Linear => val(w, "m:type", "lin"), - FracKind::NoBar => val(w, "m:type", "noBar"), - } - ctrl(w); - w.close("m:fPr"); - arg(w, "m:num", num, depth); - arg(w, "m:den", den, depth); - w.close("m:f"); - } - MNode::Script { kind, base, sub, sup } => { - let (el, pr) = match kind { - ScriptKind::Sup => ("m:sSup", "m:sSupPr"), - ScriptKind::Sub => ("m:sSub", "m:sSubPr"), - ScriptKind::SubSup => ("m:sSubSup", "m:sSubSupPr"), - ScriptKind::Pre => ("m:sPre", "m:sPrePr"), - }; - w.open(el, &[]); - w.open(pr, &[]); - ctrl(w); - w.close(pr); - if *kind == ScriptKind::Pre { - arg(w, "m:sub", sub, depth); - arg(w, "m:sup", sup, depth); - arg(w, "m:e", base, depth); - } else { - arg(w, "m:e", base, depth); - if matches!(kind, ScriptKind::Sub | ScriptKind::SubSup) { - arg(w, "m:sub", sub, depth); - } - if matches!(kind, ScriptKind::Sup | ScriptKind::SubSup) { - arg(w, "m:sup", sup, depth); - } - } - w.close(el); - } - MNode::Rad { deg, deg_hide, e } => { - w.open("m:rad", &[]); - w.open("m:radPr", &[]); - if *deg_hide { - val(w, "m:degHide", "1"); - } - ctrl(w); - w.close("m:radPr"); - arg(w, "m:deg", deg, depth); - arg(w, "m:e", e, depth); - w.close("m:rad"); - } - MNode::Nary { .. } => write_nary(w, n, depth), - MNode::Delim { beg, end, sep, grow, shp_match, elems } => { - w.open("m:d", &[]); - w.open("m:dPr", &[]); - if *beg != Some('(') { - char_prop(w, "m:begChr", *beg); - } - if *sep != Some('|') { - char_prop(w, "m:sepChr", *sep); - } - if *end != Some(')') { - char_prop(w, "m:endChr", *end); - } - if !grow { - val(w, "m:grow", "0"); - } - if *shp_match { - val(w, "m:shp", "match"); - } - ctrl(w); - w.close("m:dPr"); - for e in elems { - arg(w, "m:e", e, depth); - } - w.close("m:d"); - } - MNode::Func { name, e } => { - w.open("m:func", &[]); - w.open("m:funcPr", &[]); - ctrl(w); - w.close("m:funcPr"); - arg(w, "m:fName", name, depth); - arg(w, "m:e", e, depth); - w.close("m:func"); - } - MNode::Lim { upper, e, lim } => { - let (el, pr) = if *upper { ("m:limUpp", "m:limUppPr") } else { ("m:limLow", "m:limLowPr") }; - w.open(el, &[]); - w.open(pr, &[]); - ctrl(w); - w.close(pr); - arg(w, "m:e", e, depth); - arg(w, "m:lim", lim, depth); - w.close(el); - } - MNode::Acc { chr, e } => { - w.open("m:acc", &[]); - w.open("m:accPr", &[]); - char_prop(w, "m:chr", Some(*chr)); - ctrl(w); - w.close("m:accPr"); - arg(w, "m:e", e, depth); - w.close("m:acc"); - } - MNode::Bar { top, e } => { - w.open("m:bar", &[]); - w.open("m:barPr", &[]); - val(w, "m:pos", if *top { "top" } else { "bot" }); - ctrl(w); - w.close("m:barPr"); - arg(w, "m:e", e, depth); - w.close("m:bar"); - } - MNode::BorderBox { hide, strike, e } => { - w.open("m:borderBox", &[]); - w.open("m:borderBoxPr", &[]); - for (on, name) in hide.iter().zip(["m:hideTop", "m:hideBot", "m:hideLeft", "m:hideRight"]) { - if *on { - val(w, name, "1"); - } - } - for (on, name) in strike.iter().zip(["m:strikeH", "m:strikeV", "m:strikeBLTR", "m:strikeTLBR"]) { - if *on { - val(w, name, "1"); - } - } - ctrl(w); - w.close("m:borderBoxPr"); - arg(w, "m:e", e, depth); - w.close("m:borderBox"); - } - MNode::Boxed { e } => { - w.open("m:box", &[]); - w.open("m:boxPr", &[]); - ctrl(w); - w.close("m:boxPr"); - arg(w, "m:e", e, depth); - w.close("m:box"); - } - MNode::GroupChr { chr, top, e } => { - w.open("m:groupChr", &[]); - w.open("m:groupChrPr", &[]); - char_prop(w, "m:chr", Some(*chr)); - val(w, "m:pos", if *top { "top" } else { "bot" }); - val(w, "m:vertJc", if *top { "bot" } else { "top" }); - ctrl(w); - w.close("m:groupChrPr"); - arg(w, "m:e", e, depth); - w.close("m:groupChr"); - } - MNode::EqArr { rows } => { - w.open("m:eqArr", &[]); - w.open("m:eqArrPr", &[]); - // Numbered rows (`#`) stretch across the line so the number reaches the margin. - if rows.iter().any(|r| craft_wordmodel::math::has_number_mark(r)) { - val(w, "m:maxDist", "1"); - } - ctrl(w); - w.close("m:eqArrPr"); - for r in rows { - arg(w, "m:e", r, depth); - } - w.close("m:eqArr"); - } - MNode::Matrix { .. } => write_matrix(w, n, depth), - MNode::Phant { show, zero_wid, zero_asc, zero_desc, e } => { - w.open("m:phant", &[]); - w.open("m:phantPr", &[]); - val(w, "m:show", if *show { "1" } else { "0" }); - for (on, name) in [(*zero_wid, "m:zeroWid"), (*zero_asc, "m:zeroAsc"), (*zero_desc, "m:zeroDesc")] { - if on { - val(w, name, "1"); - } - } - ctrl(w); - w.close("m:phantPr"); - arg(w, "m:e", e, depth); - w.close("m:phant"); - } - } -} - -fn run(w: &mut W, r: &MRun) { - w.open("m:r", &[]); - let sty = r.sty.map(|s| match s { - MSty::Plain => "p", - MSty::Bold => "b", - MSty::Italic => "i", - MSty::BoldItalic => "bi", - }); - let scr = match r.scr { - MScr::Roman => None, - MScr::Script => Some("script"), - MScr::Fraktur => Some("fraktur"), - MScr::DoubleStruck => Some("double-struck"), - MScr::SansSerif => Some("sans-serif"), - MScr::Monospace => Some("monospace"), - }; - if sty.is_some() || scr.is_some() || r.nor || r.lit || r.brk.is_some() { - w.open("m:rPr", &[]); - if r.lit { - w.empty("m:lit", &[]); - } - if r.nor { - w.empty("m:nor", &[]); - } - if let Some(s) = scr { - val(w, "m:scr", s); - } - if let Some(s) = sty { - val(w, "m:sty", s); - } - match r.brk { - Some(0) => w.empty("m:brk", &[]), - Some(n) => w.empty("m:brk", &[("m:alnAt", &n.to_string())]), - None => {} - } - w.close("m:rPr"); - } - w.open("w:rPr", &[]); - let font = r.font.as_deref().filter(|_| r.nor).unwrap_or("Cambria Math"); - w.empty("w:rFonts", &[("w:ascii", font), ("w:hAnsi", font)]); - if let Some(c) = r.color { - w.val("w:color", &c.hex()); - } - if let Some(sz) = r.size { - let hp = (sz * 2.0).round().clamp(2.0, 3276.0) as i64; - w.val("w:sz", &hp.to_string()); - w.val("w:szCs", &hp.to_string()); - } - w.close("w:rPr"); - w.leaf("m:t", &[("xml:space", "preserve")], &r.text); - w.close("m:r"); + let mut o = craft_math::omml::W::default(); + craft_math::omml::math_pr(&mut o, m); + w.raw(&o.s); } diff --git a/crates/wordmodel/Cargo.toml b/crates/wordmodel/Cargo.toml index 7426a738d..200cd1653 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 9034bd8af..5fb2d14d8 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 db9875fdb..000000000 --- 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 92979534c..000000000 --- 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 39a25ab5d..000000000 --- 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 28b3a7f83..000000000 --- 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 45d2c603a..000000000 --- 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 baf2d948d..9fce90039 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 1d5175540..000000000 --- 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}"); - } -}