Add craft-math: equations as one tree with OMML, MathML, LaTeX and UnicodeMath (#1013)

* 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.

---------

Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
This commit is contained in:
Dylan M. Taylorandshuff57 authored and GitHub committed 2026-11-11 20:31:02 -05:00
1 parent dc14024a19
commit 85b4dabf0f
29 files changed
+6934

No files matched your search

+2
View File
@@ -29,6 +29,8 @@ author, source and license. Library code and original assets are under the repos
| `crates/crw/src/tables.rs`, `crates/crw/clean-room/logs/table_sel*.json` | craft-crw contributors (derived in LightCraft, 2026-10, by known-plaintext alignment of the sample files' entropy streams; `crates/crw/clean-room/02-crw-format.md`) | this repository | MIT OR Apache-2.0 |
| `crates/crw/clean-room/logs/private-corpus.tsv` (file names, sizes and SHA-256 of the maintainer's own EOS D60 files; no image content) | craft-crw contributors (LightCraft derivation record) | this repository | MIT OR Apache-2.0 |
| `crates/prc/src/acof/table.rs` (the rows of the PRC `Double` prefix-code table, reformatted as Rust tuples) | Kristof Ralovich (prc-rs); the table is defined by ISO 14739-1 | [ralovich/prc-rs@287b671](https://github.com/ralovich/prc-rs/blob/287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d/src/double.rs), through [PdfCraft `crates/prc`](https://github.com/storytold/pdfcraft/tree/80e26e674d1aa33078e83c8139875047f3289eb6/crates/prc) | MIT (`licenses/prc-rs/LICENSE.txt`; the notice is also in the file header) |
| `crates/math/tests/corpus/equations.tsv`, `crates/math/tests/corpus/latex.tsv` (unmodified) | shuff57 (equaSHion) | [storytold/craft-libs#29](https://github.com/storytold/craft-libs/pull/29) at `7a6ef6b9`, `corpus/` | MIT OR Apache-2.0 |
| `crates/math/tests/corpus/omml/*.xml` (unmodified) | WordCraft and DeckCraft contributors, collected by shuff57 (equaSHion) | [storytold/craft-libs#29](https://github.com/storytold/craft-libs/pull/29) at `7a6ef6b9`, `corpus/omml/` | MIT OR Apache-2.0 |
Original assets are covered by the repository licence.
Generated
+10
View File
@@ -1348,6 +1348,16 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "craft-math"
version = "0.1.0"
dependencies = [
"craft-xml",
"proptest",
"serde",
"serde_json",
]
[[package]]
name = "craft-media"
version = "0.1.0"
+1
View File
@@ -68,6 +68,7 @@ craft-assuan = { path = "crates/assuan" }
craft-kra = { path = "crates/kra" }
craft-audio = { path = "crates/audio" }
# No default features, so craft-audio's `encode` builds craft-media without `isobmff`.
craft-math = { path = "crates/math" }
craft-media = { path = "crates/media", default-features = false }
craft-media-testkit = { path = "crates/media-testkit" }
craft-midi = { path = "crates/midi" }
+1
View File
@@ -205,6 +205,7 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
- [`craft-audio`](crates/audio/README.md): audio file I/O and codecs (WAV, AIFF, FLAC, AAC, Opus,
AC-3/E-AC-3, MP1/MP2, MP3 encoding), streaming and seeking, and packet decoding for any demuxer. See its
README for formats and features.
- [`craft-math`](crates/math/README.md): equations as one tree (the Office Math object model) with its syntaxes, read and written: OMML, presentation MathML, LaTeX math and the UnicodeMath linear format.
- [`craft-media`](crates/media/README.md): bit-level I/O (`bitstream`), clean-room MP4/MOV
(`isobmff`), Matroska/WebM (`matroska`) and MXF (`mxf`) demuxers and muxers, Ogg (`ogg`), AVI (`avi`) and
MPEG-TS/PS (`mpegts`) demuxers, from FilmCraft, and an ISO BMFF box reader. Containers only; codecs are in `craft-audio`, and
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "craft-math"
description = "Equations as one tree (the Office Math object model) with its syntaxes: OMML, MathML, LaTeX math and the UnicodeMath linear format, read and written."
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
[lints]
workspace = true
[dependencies]
craft-xml = { workspace = true }
serde = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true }
proptest = { workspace = true }
+55
View File
@@ -0,0 +1,55 @@
# craft-math
Equations as one tree, and the syntaxes it is read from and written to:
| Module | Syntax | Read | Write |
| --- | --- | --- | --- |
| `math` | the tree: `MNode`, `MRun`, `Math` (an equation with its kept source OMML), modelled on the Office Math object model (ECMA-376 Part 1 §22.1) | | |
| `math_linear` | UnicodeMath, the linear format Word shows (`x=(-b±√(b^2-4ac))/2a`) | `math::parse_linear` | `math::to_linear` |
| `math_latex` | LaTeX math, including MathLive's spellings | `parse_latex` | `to_latex` |
| `omml` | Office Math Markup (`m:oMath`, DOCX/PPTX/XLSX) | `read_omath`, `read_str` | `write_equation`, `write_str` |
| `mathml` | presentation MathML (HTML, ODF formulas, the clipboard) | `parse`, `read`, `to_tree` | `to_mathml`, `linear_to_mathml` |
Beside them: `math_symbols` (LaTeX names of symbols), `math_tree` (the arguments of a node, a walk
over every run), `symbol_fonts` (Symbol, Wingdings and the other symbol fonts as Unicode) and
`Rgb` (a run's colour).
```rust
use craft_math::{Math, math, math_latex, mathml, omml};
let tree = math_latex::parse_latex(r"\frac{a}{b}+\sqrt{x}");
assert_eq!(math::to_linear(&tree), "a/b+√x");
// OMML out and back.
let eq = Math { nodes: tree.clone(), ..Default::default() };
let xml = omml::write_str("", false, &eq, None);
let read = omml::read_str(&xml).unwrap();
assert_eq!(read.nodes, tree);
// MathML carries the linear format in an annotation, so it reads back exactly.
let ml = mathml::linear_to_mathml("a/b+√x", true);
assert_eq!(mathml::parse(&ml).and_then(|m| mathml::read(&m)), Some(("a/b+√x".to_string(), true)));
```
## Reading OMML
`omml::read_omath` takes an `m:oMath` element of a `craft_xml` tree read with the canonical
prefixes of `omml::NAMESPACES` (`m`, `w` and `mc`, transitional and strict URIs), so a document
reader passes the element it already has. `omml::read_str` and `omml::parse` read a string. The
source XML is kept in `Math::omml`; `write_equation` writes it back unchanged while the
equation's colour is the same and the XML is well formed, so a saved document carries the
equation as Word wrote it.
## Limits
Nesting is bounded by `math::MAX_DEPTH` in every reader and writer; `omml::parse` reads at most
`omml::MAX_ELEMENTS` elements and `mathml::parse` keeps at most `mathml::MAX_NODES`; the LaTeX
reader looks at 20,000 characters. Nothing panics on any input.
## Provenance
The tree, the linear format, LaTeX, the symbol names, the tree walk, the symbol fonts and `Rgb`
are WordCraft's (`crates/doc`), as moved into craft-wordmodel; OMML is WordCraft's DOCX reader
and writer (`crates/docx/src/read/math.rs`, `write/math.rs`) and MathML its formats crate's
(`crates/formats/src/mathml.rs`), moved onto craft-xml. The MathLive command spellings and the
round-trip corpus in `tests/corpus/` come from equaSHion (storytold/craft-libs#29).
+55
View File
@@ -0,0 +1,55 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
// The code came from an app whose lints allow this.
#![allow(clippy::type_complexity)]
//! Equations: one tree and the syntaxes it is read from and written to.
//!
//! - [`math`]: the tree ([`MNode`], [`MRun`], an argument [`Arg`] is a `Vec<MNode>`), modelled on
//! the Office Math object model (ECMA-376 Part 1 §22.1), and [`Math`], an equation with its kept
//! source OMML.
//! - [`math_linear`]: the linear format (UnicodeMath, the plain text Word shows for an equation),
//! read and written ([`math::parse_linear`], [`math::to_linear`]).
//! - [`math_latex`]: LaTeX math, read and written ([`math_latex::parse_latex`],
//! [`math_latex::to_latex`]), with the named symbols of [`math_symbols`].
//! - [`omml`]: Office Math Markup, read ([`omml::read_omath`], [`omml::read_str`]) and written
//! ([`omml::write_equation`], [`omml::write_str`]).
//! - [`mathml`]: presentation MathML, written ([`mathml::to_mathml`]) and read
//! ([`mathml::parse`], [`mathml::read`], [`mathml::to_tree`]).
//! - [`math_tree`]: the arguments of a node and a walk over every run.
//! - [`symbol_fonts`]: text in symbol fonts (Symbol, Wingdings…) as Unicode, as OMML runs and
//! Word text need it.
//! - [`Rgb`]: the colour of a run.
//!
//! Every reader is bounded (nesting [`math::MAX_DEPTH`], node budgets) and never panics on any
//! input.
//!
//! ```
//! use craft_math::{math, math_latex, mathml, omml};
//!
//! let tree = math::parse_linear("x=(-b±√(b^2-4ac))/2a");
//! assert_eq!(math::to_linear(&tree), "x=(-b±√(b^2-4ac))/2a");
//! assert!(math_latex::to_latex(&tree).contains("\\frac"));
//!
//! let eq = math::Math { nodes: tree.clone(), ..Default::default() };
//! let xml = omml::write_str("", false, &eq, None);
//! assert_eq!(omml::read_str(&xml).map(|m| m.nodes), Some(tree.clone()));
//!
//! let ml = mathml::to_mathml(&tree, false, "");
//! assert_eq!(mathml::parse(&ml).as_ref().map(mathml::to_tree), Some(tree));
//! ```
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeExamples;
pub mod math;
pub mod math_latex;
pub mod math_linear;
pub mod math_symbols;
pub mod math_tree;
pub mod mathml;
pub mod omml;
mod rgb;
pub mod symbol_fonts;
pub use math::{Arg, MNode, MRun, Math, MathProps};
pub use rgb::Rgb;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+762
View File
@@ -0,0 +1,762 @@
//! 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<char> {
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<char>,
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<char> {
self.c.get(self.i).copied()
}
fn peek_at(&self, k: usize) -> Option<char> {
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<char>, closers: &[char]) -> MNode {
let mut stops: Vec<char> = 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<MNode> {
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<char> = 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<MNode> {
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<MNode>, 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<Arg>> = 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) },
}
}
+399
View File
@@ -0,0 +1,399 @@
//! 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}'),
// Other spellings (after the names the writer uses, which come first).
("nparallel", '∦'),
("backprime", '‵'),
("lvert", '|'),
("rvert", '|'),
("lVert", '‖'),
("rVert", '‖'),
("infinity", '∞'),
("cross", '×'),
];
/// The character for a name (`alpha` → α), including `doubleR`, `scriptL`, `frakturg`.
pub fn symbol(name: &str) -> Option<char> {
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());
}
}
+104
View File
@@ -0,0 +1,104 @@
//! 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);
}
File diff suppressed because it is too large. Load diff
+232
View File
@@ -0,0 +1,232 @@
//! Office Math Markup (OMML, ECMA-376 Part 1 §22.1) read into and written from the equation tree
//! ([`crate::math`]).
//!
//! The reader takes an `m:oMath` element of a [`craft_xml`] tree read with the canonical
//! prefixes of [`NAMESPACES`] (`m:` for math, `w:` for WordprocessingML, `mc:` for markup
//! compatibility), whatever prefixes the file used; [`parse`] does that for a string. It keeps
//! the source XML in [`Math::omml`], and the writer writes that back unchanged while the
//! equation's colour is the same, so a saved file carries the equation exactly as Word wrote it.
//! The writer otherwise writes the tree: `m:oMath`, inside `m:oMathPara` for a display equation.
use craft_xml::{Canonical, El, Options};
use crate::math::{Math, MathJc};
use crate::rgb::Rgb;
mod read;
mod write;
pub use read::{lift_color, para_jc, read_math_pr, read_omath};
pub use write::{math_pr, resolve_numbers, write_equation};
/// The OMML namespace (transitional), and the strict one.
pub const MATH_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/math";
pub const MATH_NS_STRICT: &str = "http://purl.oclc.org/ooxml/officeDocument/math";
/// The WordprocessingML namespace OMML runs borrow (`w:rPr`, `w:t`, `w:sym`), transitional and
/// strict.
pub const W_NS: &str = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
pub const W_NS_STRICT: &str = "http://purl.oclc.org/ooxml/wordprocessingml/main";
/// Markup compatibility (`mc:AlternateContent`).
pub const MC_NS: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
/// The canonical prefixes the reader expects, for [`craft_xml::Canonical`].
pub const NAMESPACES: &[(&str, &str)] = &[("m", MATH_NS), ("m", MATH_NS_STRICT), ("w", W_NS), ("w", W_NS_STRICT), ("mc", MC_NS)];
/// Most elements [`parse`] reads from one string.
pub const MAX_ELEMENTS: usize = 250_000;
/// OMML text read into a tree with the canonical prefixes of [`NAMESPACES`]: the first
/// `m:oMathPara` or `m:oMath` element in it (the root or any descendant). `None` when there is
/// none, or the text is not XML. Text kept by the reader ([`Math::omml`]) declares no
/// namespaces; its prefixes are already the canonical ones and are read as written.
pub fn parse(xml: &str) -> Option<El> {
let canonical = Canonical::new(NAMESPACES);
let limits = craft_xml::Limits { max_nodes: MAX_ELEMENTS.saturating_mul(2), ..craft_xml::Limits::DEFAULT };
let first = |root: El| -> Option<El> {
if matches!(root.name.as_str(), "m:oMath" | "m:oMathPara") {
return Some(root);
}
root.find("m:oMathPara").or_else(|| root.find("m:oMath")).cloned()
};
let read = Options::canonical(&canonical).with_limits(limits).parse(xml.as_bytes()).ok().and_then(first);
read.or_else(|| parse_kept(xml).and_then(first))
}
/// Kept OMML (canonical prefixes, no declarations) read as written.
fn parse_kept(xml: &str) -> Option<El> {
let limits = craft_xml::Limits { max_nodes: MAX_ELEMENTS.saturating_mul(2), ..craft_xml::Limits::DEFAULT };
Options { ns_decls: false, ..Options::default() }.with_limits(limits).parse(xml.as_bytes()).ok()
}
/// Is `xml` (kept OMML) well-formed enough to write back as it is?
fn is_well_formed(xml: &str) -> bool {
xml.starts_with("<m:oMath") && Options::default().strict().parse(xml.as_bytes()).is_ok()
}
/// An equation from OMML text ([`parse`]): an `m:oMathPara`'s first `m:oMath` with the
/// paragraph's justification, or an `m:oMath`. `None` when there is none.
pub fn read_str(xml: &str) -> Option<Math> {
let e = parse(xml)?;
if e.name == "m:oMathPara" {
let jc = para_jc(&e);
return e.find("m:oMath").map(|m| read_omath(m, jc));
}
Some(read_omath(&e, MathJc::default()))
}
/// An equation as OMML text ([`write_equation`] into a string, no XML declaration).
pub fn write_str(linear: &str, display: bool, math: &Math, color: Option<Rgb>) -> String {
let mut w = W::default();
write_equation(&mut w, linear, display, math, color);
w.s
}
/// OMML on/off property of WordprocessingML (`w:b`, `w:i`): absent `w:val` = on.
fn on_off(e: &El) -> bool {
match e.attr_or_plain("w:val") {
None => true,
Some(v) => !matches!(v.trim(), "0" | "false" | "off" | "False" | "FALSE"),
}
}
/// Largest absolute length accepted, points (200 inches).
const MAX_LEN_PT: f32 = 14_400.0;
/// A twips value (`m:wrapIndent`), with or without an ISO 29500 universal measure (`1in`,
/// `2.5cm`, `12pt`), in points.
fn measure_twips(s: Option<&str>) -> Option<f32> {
let t = s?.trim();
if t.is_empty() || t.len() > 40 {
return None;
}
let bytes = t.as_bytes();
let mut split = t.len();
let mut in_exp = false;
for (i, &b) in bytes.iter().enumerate() {
if b.is_ascii_digit() || b == b'.' {
in_exp = false;
continue;
}
if (b == b'-' || b == b'+') && (i == 0 || in_exp) {
in_exp = false;
continue;
}
if b == b'e' || b == b'E' {
let rest = bytes.get(i + 1..).unwrap_or_default();
let after_sign = match rest.first() {
Some(&s) if s == b'+' || s == b'-' => rest.get(1..).unwrap_or_default(),
_ => rest,
};
if after_sign.first().is_some_and(u8::is_ascii_digit) {
in_exp = true;
continue;
}
}
split = i;
break;
}
let (num, unit) = t.split_at(split);
let v: f64 = num.parse().ok()?;
if !v.is_finite() {
return None;
}
let pt = match unit.trim().to_ascii_lowercase().as_str() {
"" => v / 20.0,
"pt" => v,
"in" => v * 72.0,
"cm" => v * 72.0 / 2.54,
"mm" => v * 72.0 / 25.4,
"pc" | "pi" => v * 12.0,
"px" => v * 0.75,
"emu" => v / 12_700.0,
_ => return None,
};
let pt = pt as f32;
pt.is_finite().then_some(pt.clamp(-MAX_LEN_PT, MAX_LEN_PT))
}
/// Points as a twips attribute value.
fn twips(pt: f32) -> String {
let pt = pt.clamp(-MAX_LEN_PT, MAX_LEN_PT);
let pt = if pt.is_finite() { pt } else { 0.0 };
((pt * 20.0).round() as i64).to_string()
}
fn xml_char_ok(c: char) -> bool {
matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}')
}
/// Escape text for element content or attribute values (drops characters XML can't carry).
fn esc(s: &str) -> String {
let mut o = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => o.push_str("&amp;"),
'<' => o.push_str("&lt;"),
'>' => o.push_str("&gt;"),
'"' => o.push_str("&quot;"),
'\'' => o.push_str("&apos;"),
'\t' => o.push_str("&#9;"),
'\n' => o.push_str("&#10;"),
'\r' => o.push_str("&#13;"),
c if xml_char_ok(c) => o.push(c),
_ => {}
}
}
o
}
/// A string-building XML writer, as WordprocessingML writers write: names as given, values
/// escaped.
#[derive(Default)]
pub struct W {
pub s: String,
}
impl W {
fn attrs(&mut self, attrs: &[(&str, &str)]) {
for (k, v) in attrs {
self.s.push(' ');
self.s.push_str(k);
self.s.push_str("=\"");
self.s.push_str(&esc(v));
self.s.push('"');
}
}
pub fn open(&mut self, name: &str, attrs: &[(&str, &str)]) {
self.s.push('<');
self.s.push_str(name);
self.attrs(attrs);
self.s.push('>');
}
pub fn empty(&mut self, name: &str, attrs: &[(&str, &str)]) {
self.s.push('<');
self.s.push_str(name);
self.attrs(attrs);
self.s.push_str("/>");
}
pub fn close(&mut self, name: &str) {
self.s.push_str("</");
self.s.push_str(name);
self.s.push('>');
}
pub fn text(&mut self, t: &str) {
self.s.push_str(&esc(t));
}
/// `<name>text</name>`.
pub fn leaf(&mut self, name: &str, attrs: &[(&str, &str)], t: &str) {
self.open(name, attrs);
self.text(t);
self.close(name);
}
/// `<name w:val="v"/>`.
pub fn val(&mut self, name: &str, v: &str) {
self.empty(name, &[("w:val", v)]);
}
pub fn raw(&mut self, s: &str) {
self.s.push_str(s);
}
}
#[cfg(test)]
mod tests;
+359
View File
@@ -0,0 +1,359 @@
//! OMML into the tree.
use craft_xml::El;
use super::{measure_twips, on_off};
use crate::math::{
Arg, BrkBin, BrkBinSub, ColJc, FracKind, LimLoc, MAX_DEPTH, MNode, MRun, MScr, MSty, Math, MathJc, MathProps, ScriptKind, merge_runs,
};
use crate::rgb::Rgb;
use crate::symbol_fonts;
/// 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`] 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<Rgb> {
let (mut shared, mut uniform) = (None, true);
crate::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)?;
crate::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_or_plain("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| measure_twips(c.attr_or_plain("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_or_plain("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<Option<char>> {
let c = pr?.child(name)?;
match c.attr_or_plain("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_or_plain("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.elements() {
node(k, depth, &mut out);
}
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<Arg> = 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::<usize>().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.elements() {
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.elements() {
node(c, d, out);
}
}
}
"w:sdt" => {
if let Some(c) = k.child("w:sdtContent") {
for c in c.elements() {
node(c, d, out);
}
}
}
_ => {}
}
}
/// `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_or_plain("w:ascii").or_else(|| f.attr_or_plain("w:hAnsi")))
.unwrap_or("");
let mut s = String::new();
for c in r.elements() {
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_or_plain("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_or_plain("w:font").unwrap_or(font);
s.push(symbol_fonts::symbol_char(f, ch).unwrap_or(ch));
}
}
_ => {}
}
}
s
}
fn run(r: &El) -> Option<MRun> {
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_or_plain("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_or_plain("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_or_plain("m:alnAt").and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(0);
out.brk = Some(aln.min(255) as u8);
}
}
wrpr(r.child("w:rPr"), &mut out);
Some(out)
}
/// 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_or_plain("w:val")).and_then(|v| v.trim().parse::<f32>().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_or_plain("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_or_plain("w:ascii").or_else(|| f.attr_or_plain("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(on_off);
let italic = pr.child("w:i").is_some_and(on_off);
if bold || italic {
out.sty = Some(match (bold, italic) {
(true, true) => MSty::BoldItalic,
(true, false) => MSty::Bold,
_ => MSty::Italic,
});
}
}
}
+191
View File
@@ -0,0 +1,191 @@
use super::*;
use crate::math::*;
fn omath(inner: &str) -> El {
let x = format!(
r#"<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">{inner}</m:oMath>"#
);
parse(&x).unwrap()
}
#[test]
fn reads_fraction_radical_and_scripts() {
let e = omath(
r#"<m:r><m:t>x=</m:t></m:r><m:f><m:num><m:r><m:t>-b±</m:t></m:r><m:rad><m:radPr><m:degHide m:val="1"/></m:radPr><m:deg/><m:e><m:sSup><m:e><m:r><m:t>b</m:t></m:r></m:e><m:sup><m:r><m:t>2</m:t></m:r></m:sup></m:sSup><m:r><m:t>-4ac</m:t></m:r></m:e></m:rad></m:num><m:den><m:r><m:t>2a</m:t></m:r></m:den></m:f>"#,
);
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!(crate::math::to_linear(&m.nodes), "x=(-b±√(b^2-4ac))/2a");
assert!(m.omml.starts_with("<m:oMath>"));
}
#[test]
fn reads_nary_delims_matrix_and_defaults() {
let e = omath(
r#"<m:nary><m:naryPr><m:chr m:val="∑"/><m:limLoc m:val="undOvr"/></m:naryPr><m:sub><m:r><m:t>i</m:t></m:r></m:sub><m:sup/><m:e><m:r><m:t>i</m:t></m:r></m:e></m:nary><m:d><m:dPr><m:begChr m:val="["/><m:endChr m:val=""/></m:dPr><m:e><m:r><m:t>a</m:t></m:r></m:e><m:e><m:r><m:t>b</m:t></m:r></m:e></m:d><m:nary><m:e/></m:nary><m:m><m:mr><m:e/><m:e/></m:mr></m:m>"#,
);
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<String> {
let mut out = Vec::new();
crate::math_tree::each_run_mut(&mut m.nodes, &mut |r| out.push(r.text.clone()));
out
}
fn t(s: &str) -> String {
format!("<m:r><m:t>{s}</m:t></m:r>")
}
/// #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!("<m:acc><m:accPr><m:chr m:val=\"̃\"/></m:accPr><m:e>{}</m:e></m:acc>", t("A1")),
format!("<m:bar><m:barPr><m:pos m:val=\"top\"/></m:barPr><m:e>{}</m:e></m:bar>", t("B1")),
format!("<m:box><m:boxPr><m:opEmu m:val=\"1\"/></m:boxPr><m:e>{}</m:e></m:box>", t("C1")),
format!("<m:borderBox><m:borderBoxPr><m:strikeH/></m:borderBoxPr><m:e>{}</m:e></m:borderBox>", t("D1")),
format!("<m:d><m:dPr><m:begChr m:val=\"[\"/></m:dPr><m:e>{}</m:e><m:e>{}</m:e></m:d>", t("E1"), t("E2")),
format!("<m:eqArr><m:e>{}</m:e><m:e>{}</m:e></m:eqArr>", t("F1"), t("F2")),
format!("<m:f><m:fPr><m:type m:val=\"skw\"/></m:fPr><m:num>{}</m:num><m:den>{}</m:den></m:f>", t("G1"), t("G2")),
format!("<m:func><m:fName>{}</m:fName><m:e>{}</m:e></m:func>", t("H1"), t("H2")),
format!("<m:groupChr><m:groupChrPr><m:chr m:val=\"⏞\"/><m:pos m:val=\"top\"/></m:groupChrPr><m:e>{}</m:e></m:groupChr>", t("I1")),
format!("<m:limLow><m:e>{}</m:e><m:lim>{}</m:lim></m:limLow>", t("J1"), t("J2")),
format!("<m:limUpp><m:e>{}</m:e><m:lim>{}</m:lim></m:limUpp>", t("K1"), t("K2")),
format!("<m:m><m:mr><m:e>{}</m:e><m:e>{}</m:e></m:mr><m:mr><m:e>{}</m:e><m:e/></m:mr></m:m>", t("L1"), t("L2"), t("L3")),
format!(
"<m:nary><m:naryPr><m:chr m:val=\"∑\"/></m:naryPr><m:sub>{}</m:sub><m:sup>{}</m:sup><m:e>{}</m:e></m:nary>",
t("M1"),
t("M2"),
t("M3")
),
format!("<m:phant><m:phantPr><m:show m:val=\"0\"/></m:phantPr><m:e>{}</m:e></m:phant>", t("N1")),
format!("<m:rad><m:deg>{}</m:deg><m:e>{}</m:e></m:rad>", t("O1"), t("O2")),
format!("<m:sPre><m:sub>{}</m:sub><m:sup>{}</m:sup><m:e>{}</m:e></m:sPre>", t("P1"), t("P2"), t("P3")),
format!("<m:sSub><m:e>{}</m:e><m:sub>{}</m:sub></m:sSub>", t("Q1"), t("Q2")),
format!("<m:sSubSup><m:e>{}</m:e><m:sub>{}</m:sub><m:sup>{}</m:sup></m:sSubSup>", t("R1"), t("R2"), t("R3")),
format!("<m:sSup><m:e>{}</m:e><m:sup>{}</m:sup></m:sSup>", t("S1"), t("S2")),
// Run-level wrappers allowed in math (EG_PContentMath): hyperlinks, simple fields,
// tracked insertions, content controls, markup compatibility.
format!("<w:hyperlink w:anchor=\"x\">{}</w:hyperlink>", t("T1")),
format!("<w:fldSimple w:instr=\"QUOTE\">{}</w:fldSimple>", t("T2")),
format!("<w:ins w:id=\"1\" w:author=\"a\">{}</w:ins>", t("T3")),
format!("<w:sdt><w:sdtContent>{}</w:sdtContent></w:sdt>", t("T4")),
format!(
"<mc:AlternateContent><mc:Choice Requires=\"m\">{}</mc:Choice><mc:Fallback><w:r><w:t>picture</w:t></w:r></mc:Fallback></mc:AlternateContent>",
t("T5")
),
"<w:r><w:t>T6</w:t></w:r>".to_string(),
];
let x = format!(
r#"<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">{}</m:oMath>"#,
parts.concat()
);
let e = parse(&x).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#"<m:r><m:t>x</m:t></m:r><w:del w:id="1" w:author="a"><m:r><m:t>gone</m:t></m:r></w:del><m:r><w:sym w:font="Symbol" w:char="F061"/></m:r><m:r><w:rPr><w:rFonts w:ascii="Symbol" w:hAnsi="Symbol"/></w:rPr><m:t>&#xF062;</m:t><w:noBreakHyphen/></m:r><w:r><w:rPr><w:rFonts w:ascii="Symbol"/></w:rPr><w:t>&#xF067;</w:t></w:r>"#,
);
let mut m = read_omath(&e, MathJc::CenterGroup);
let got = texts(&mut m).concat();
assert_eq!(got, "xαβ\u{2011}γ");
let mut font = None;
crate::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!("{}{}{}", "<w:hyperlink>".repeat(n), t("deep"), "</w:hyperlink>".repeat(n));
let e = omath(&inner);
let _ = read_omath(&e, MathJc::CenterGroup);
let inner = format!(
"{}{}{}",
"<mc:AlternateContent xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"><mc:Choice Requires=\"m\">".repeat(n),
t("deep"),
"</mc:Choice></mc:AlternateContent>".repeat(n)
);
let e = omath(&inner);
let _ = read_omath(&e, MathJc::CenterGroup);
}
#[test]
fn text_entry_points_find_the_equation_and_refuse_junk() {
let ns = r#"xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math""#;
// Any prefix, inside other markup, as a display paragraph with its justification.
let x = r#"<p><o:oMathPara xmlns:o="http://schemas.openxmlformats.org/officeDocument/2006/math"><o:oMathParaPr><o:jc o:val="left"/></o:oMathParaPr><o:oMath><o:r><o:t>x</o:t></o:r></o:oMath></o:oMathPara></p>"#;
let m = read_str(x).unwrap();
assert_eq!((crate::math::to_linear(&m.nodes).as_str(), m.jc), ("x", MathJc::Left));
// The strict OOXML namespace.
let strict = r#"<m:oMath xmlns:m="http://purl.oclc.org/ooxml/officeDocument/math"><m:r><m:t>y</m:t></m:r></m:oMath>"#;
assert_eq!(read_str(strict).map(|m| crate::math::to_linear(&m.nodes)).as_deref(), Some("y"));
// Kept OMML, prefixes without declarations.
assert_eq!(read_str("<m:oMath><m:r><m:t>z</m:t></m:r></m:oMath>").map(|m| crate::math::to_linear(&m.nodes)).as_deref(), Some("z"));
for junk in ["", "<p/>", "not xml", "<m:oMath", "\u{0}"] {
let _ = read_str(junk);
}
assert!(read_str("<p/>").is_none() && read_str("").is_none());
// More elements than the reader keeps: refused, not read in part.
let wide = format!("<m:oMath {ns}>{}</m:oMath>", "<m:r/>".repeat(MAX_ELEMENTS * 2 + 1));
assert!(parse(&wide).is_none());
let fits = format!("<m:oMath {ns}>{}</m:oMath>", "<m:r><m:t>a</m:t></m:r>".repeat(1000));
assert!(parse(&fits).is_some());
// Deep nesting is bounded.
let deep = format!("<m:oMath {ns}>{}<m:r><m:t>x</m:t></m:r>{}</m:oMath>", "<m:box><m:e>".repeat(5000), "</m:e></m:box>".repeat(5000));
let _ = read_str(&deep);
}
#[test]
fn kept_omml_is_written_back_only_when_well_formed() {
let m = read_str(r#"<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><m:r><m:t>x</m:t></m:r></m:oMath>"#).unwrap();
assert_eq!(write_str("", false, &m, None), m.omml);
let broken = Math { omml: "<m:oMath><m:r>".into(), ..m.clone() };
let out = write_str("", false, &broken, None);
assert!(out.starts_with("<m:oMath>") && out.ends_with("</m:oMath>") && out.contains(">x</m:t>"), "{out}");
// A colour that differs from the kept one rewrites the equation from the tree, with the colour.
let red = write_str("", false, &m, Some(Rgb(255, 0, 0)));
assert!(red.contains(r#"<w:color w:val="FF0000"/>"#), "{red}");
}
proptest::proptest! {
#[test]
fn any_text_reads_without_panicking(s in "[<>/=\"a-z:m ]{0,120}") {
let _ = read_str(&s);
let _ = crate::mathml::parse(&s).map(|e| crate::mathml::to_tree(&e));
}
}
+480
View File
@@ -0,0 +1,480 @@
//! The tree as OMML.
use super::read::{lift_color, read_omath};
use super::{W, is_well_formed, parse_kept, twips};
use crate::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 crate::rgb::Rgb;
/// 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).
pub fn write_equation(w: &mut W, linear: &str, display: bool, math: &Math, color: Option<Rgb>) {
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 {
crate::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("<m:oMath") && is_well_formed(&math.omml) {
w.raw(&math.omml);
} else {
let parsed;
let nodes: &Arg = if math.nodes.is_empty() {
parsed = parse_linear(linear);
&parsed
} else {
&math.nodes
};
w.open("m:oMath", &[]);
arg_inner(w, nodes, 0);
w.close("m:oMath");
}
if display {
w.close("m:oMathPara");
}
}
/// Count the equation's numbered rows; when some use an automatic number (`#` alone), a copy
/// with the numbers written out as `(n)`.
pub fn resolve_numbers(math: &Math, display: bool, counter: &mut u32) -> Option<Math> {
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 !crate::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() })
}
/// The equation colour the kept source OMML carries, as the reader lifts it.
fn omml_color(omml: &str) -> Option<Rgb> {
if omml.is_empty() {
return None;
}
let root = parse_kept(omml)?;
let e = if root.name == "m:oMath" { &root } else { root.find("m:oMath")? };
lift_color(&mut read_omath(e, MathJc::default()))
}
/// The settings part's `m:mathPr` (CT_MathPr order; `m:wrapIndent` and `m:wrapRight` are a choice).
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 { crate::math::DEFAULT_WRAP_INDENT };
val(w, "m:wrapIndent", &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<char>) {
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| crate::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");
}
+173
View File
@@ -0,0 +1,173 @@
//! An sRGB colour, the one type of colour every Craft document model and this crate share.
use serde::{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: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
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<E: Error>(self, s: &str) -> Result<Rgb, E> {
Rgb::parse(s).ok_or_else(|| E::invalid_value(Unexpected::Str(s), &self))
}
fn visit_seq<A: SeqAccess<'de>>(self, mut a: A) -> Result<Rgb, A::Error> {
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::<serde::de::IgnoredAny>()?.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<Rgb> {
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
}
}
/// `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))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_hex() {
assert_eq!(Rgb(255, 0, 16).hex(), "FF0010");
assert_eq!(Rgb::parse("#FF0010"), Some(Rgb(255, 0, 16)));
assert_eq!(Rgb::parse("zzzzzz"), None);
assert_eq!(Rgb::parse("é1234"), None);
}
#[test]
fn rgb_parse_rejects_non_hex() {
assert_eq!(Rgb::parse("+1+2+3"), None);
assert_eq!(Rgb::parse("-1-2-3"), None);
assert_eq!(Rgb::parse("##ff0000"), None);
assert_eq!(Rgb::parse("ff00 0"), None);
assert_eq!(Rgb::parse("#ff0000"), Some(Rgb(255, 0, 0)));
assert_eq!(Rgb::parse("ff0000"), Some(Rgb(255, 0, 0)));
}
/// #865: an [`Rgb`] reads `RRGGBB` / `#RRGGBB` as well as the `[r, g, b]` it still writes.
#[test]
fn rgb_reads_hex_and_keeps_its_serialized_form() {
let read = |j: &str| serde_json::from_str::<Rgb>(j);
let ink = Rgb(0x1B, 0x16, 0x11);
assert_eq!(serde_json::to_string(&ink).unwrap(), "[27,22,17]", "written as before");
for j in ["[27,22,17]", r#""1B1611""#, r##""#1b1611""##] {
assert_eq!(read(j).unwrap(), ink, "{j}");
}
for bad in ["[27,22]", "[27,22,17,0]", "[27,22,300]", "[-1,0,0]", r#""auto""#, r#""1B161""#, r#""""#, "7", "null", r#"{"r":1}"#] {
assert!(read(bad).is_err(), "{bad}");
}
}
}
+316
View File
@@ -0,0 +1,316 @@
//! 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,
//! <https://www.unicode.org/Public/MAPPINGS/VENDORS/ADOBE/symbol.txt> (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<char> {
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}");
}
}
+81
View File
@@ -0,0 +1,81 @@
//! Round trips over equaSHion's corpus (storytold/craft-libs#29): its typed LaTeX equations
//! (`corpus/equations.tsv`), the MathLive-style LaTeX of `corpus/latex.tsv` and its OMML samples
//! from WordCraft and DeckCraft files (`corpus/omml/`).
use craft_math::{Math, math, math_latex, mathml, omml};
const DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus");
fn rows(file: &str) -> Vec<Vec<String>> {
let text = std::fs::read_to_string(format!("{DIR}/{file}")).unwrap();
text.lines().filter(|l| !l.trim().is_empty() && !l.starts_with('#')).map(|l| l.split('\t').map(str::to_string).collect()).collect()
}
/// The equations of `equations.tsv`: (id, tree, display).
fn equations() -> Vec<(String, math::Arg, bool)> {
let v: Vec<_> = rows("equations.tsv").into_iter().map(|f| (f[0].clone(), math_latex::parse_latex(&f[1]), f[2].trim() == "1")).collect();
assert!(v.len() >= 40, "{} rows", v.len());
v
}
/// Equations whose LaTeX this crate writes in a form it reads back differently (a function
/// raised to a power: `\sin^2\theta` comes back as `(\sin)^2 θ`). Known; kept visible here.
const LATEX_ROUND_TRIP_GAPS: &[&str] = &["func"];
#[test]
fn every_corpus_equation_reads_without_unknown_commands() {
for (id, tree, _) in equations() {
let lin = math::to_linear(&tree);
assert!(!tree.is_empty() && !lin.contains('\\'), "{id}: {lin}");
}
for f in rows("latex.tsv") {
let lin = math::to_linear(&math_latex::parse_latex(&f[0]));
assert!(!lin.contains('\\'), "{}: {lin}", f[0]);
}
}
#[test]
fn every_corpus_equation_survives_omml() {
for (id, tree, display) in equations() {
let eq = Math { nodes: tree.clone(), ..Default::default() };
let xml = omml::write_str("", display, &eq, None);
let back = omml::read_str(&xml).unwrap_or_else(|| panic!("{id}: {xml}"));
// The writer states matrix column alignment the parser left implicit; the text is the same.
assert_eq!(math::to_linear(&back.nodes), math::to_linear(&tree), "{id}: {xml}");
// And the OMML written from what was read is the same OMML.
let again = omml::write_str("", display, &Math { omml: String::new(), ..back }, None);
assert_eq!(again, xml, "{id}");
}
}
#[test]
fn every_corpus_equation_survives_mathml_and_latex() {
for (id, tree, display) in equations() {
let lin = math::to_linear(&tree);
let ml = mathml::to_mathml(&tree, display, "");
assert_eq!(mathml::parse(&ml).and_then(|m| mathml::read(&m)), Some((lin.clone(), display)), "{id}: {ml}");
if !LATEX_ROUND_TRIP_GAPS.contains(&id.as_str()) {
let tex = math_latex::to_latex(&tree);
assert_eq!(math::to_linear(&math_latex::parse_latex(&tex)), lin, "{id}: {tex}");
}
}
}
#[test]
fn omml_samples_read_and_write_back() {
let mut n = 0;
for e in std::fs::read_dir(format!("{DIR}/omml")).unwrap().flatten() {
let xml = std::fs::read_to_string(e.path()).unwrap();
let name = e.file_name();
let m = omml::read_str(&xml).unwrap_or_else(|| panic!("{name:?}"));
assert!(!m.nodes.is_empty() && m.omml.starts_with("<m:oMath"), "{name:?}");
// Kept: the source goes back as it was read.
let kept = omml::write_str("", false, &m, None);
assert_eq!(kept, m.omml, "{name:?}");
// Rewritten from the tree, it reads back to the same tree.
let fresh = omml::write_str("", false, &Math { omml: String::new(), ..m.clone() }, None);
assert_eq!(omml::read_str(&fresh).map(|b| b.nodes), Some(m.nodes.clone()), "{name:?}: {fresh}");
n += 1;
}
assert_eq!(n, 6);
}
+45
View File
@@ -0,0 +1,45 @@
# id linear display (1 = display style, 0 = inline)
frac_simple \frac{a}{b} 0
frac_nested \frac{1}{1+\frac{1}{x}} 1
frac_nested_inline \frac{a}{\frac{b}{c}} 0
cfrac a_0+\cfrac{1}{a_1+\cfrac{1}{a_2+\cfrac{1}{a_3}}} 1
sqrt \sqrt{x^2+y^2} 0
sqrt_degree \sqrt[3]{x+1} 0
sqrt_nested \sqrt{1+\sqrt{1+\sqrt{x}}} 1
script_sup x^2 0
script_sub a_n 0
script_both x_i^2 0
script_nested e^{-x^{2}} 0
sum_display \sum_{i=1}^{n} i^2 1
sum_inline \sum_{i=1}^{n} i^2 0
prod_display \prod_{k=1}^{n} k 1
int_display \int_0^1 x^2\,dx 1
int_inline \int_0^1 x^2\,dx 0
oint \oint_C f\,dz 1
lim_display \lim_{x\to0}\frac{\sin x}{x}=1 1
lim_inline \lim_{n\to\infty}\left(1+\frac{1}{n}\right)^n=e 0
pmatrix \begin{pmatrix}a&b\\c&d\end{pmatrix} 1
bmatrix \begin{bmatrix}1&0&0\\0&1&0\\0&0&1\end{bmatrix} 1
cases |x|=\begin{cases}x&x\geq0\\-x&x<0\end{cases} 1
accent_hat \hat{x}+\hat{y} 0
accent_vec \vec{v}\cdot\vec{w} 0
accent_bar \bar{z}z=|z|^2 0
overline \overline{AB} 0
overbrace \overbrace{a+b+c}^{3} 1
underbrace \underbrace{x+x+x+x}_{n} 1
overrightarrow \overrightarrow{AB} 0
quadratic x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} 1
euler e^{i\pi}+1=0 1
maxwell_gauss \nabla\cdot E=\frac{\rho}{\varepsilon_0} 1
maxwell_faraday \nabla\times E=-\frac{\partial B}{\partial t} 1
maxwell_ampere \nabla\times B=\mu_0J+\mu_0\varepsilon_0\frac{\partial E}{\partial t} 1
binom \binom{n}{k}=\frac{n!}{k!(n-k)!} 1
delims_tall \left[\frac{a}{b}\right]+\left\{\frac{c}{d}\right\} 1
floor_ceil ⌊x⌋+⌈y⌉ 0
abs_tall \left|\frac{a}{b}\right| 0
angle ⟨u,v⟩ 0
greek \alpha+\beta=\gamma 0
func \sin^2\theta+\cos^2\theta=1 0
boxed \boxed{E=mc^2} 0
wide_matrix \begin{matrix}a_0&a_1&a_2&a_3&a_4&a_5&a_6&a_7&a_8&a_9&a_{10}&a_{11}&a_{12}&a_{13}&a_{14}&a_{15}&a_{16}&a_{17}&a_{18}&a_{19}\end{matrix} 1
long_wrap (a+b)^{6}=a^{6}+6a^5b+15a^4b^2+20a^3b^3+15a^2b^4+6ab^5+b^{6}+c^{6}+6c^5d+15c^4d^2+20c^3d^3+15c^2d^4+6cd^5+d^{6} 0
1 # id linear display (1 = display style, 0 = inline)
2 frac_simple \frac{a}{b} 0
3 frac_nested \frac{1}{1+\frac{1}{x}} 1
4 frac_nested_inline \frac{a}{\frac{b}{c}} 0
5 cfrac a_0+\cfrac{1}{a_1+\cfrac{1}{a_2+\cfrac{1}{a_3}}} 1
6 sqrt \sqrt{x^2+y^2} 0
7 sqrt_degree \sqrt[3]{x+1} 0
8 sqrt_nested \sqrt{1+\sqrt{1+\sqrt{x}}} 1
9 script_sup x^2 0
10 script_sub a_n 0
11 script_both x_i^2 0
12 script_nested e^{-x^{2}} 0
13 sum_display \sum_{i=1}^{n} i^2 1
14 sum_inline \sum_{i=1}^{n} i^2 0
15 prod_display \prod_{k=1}^{n} k 1
16 int_display \int_0^1 x^2\,dx 1
17 int_inline \int_0^1 x^2\,dx 0
18 oint \oint_C f\,dz 1
19 lim_display \lim_{x\to0}\frac{\sin x}{x}=1 1
20 lim_inline \lim_{n\to\infty}\left(1+\frac{1}{n}\right)^n=e 0
21 pmatrix \begin{pmatrix}a&b\\c&d\end{pmatrix} 1
22 bmatrix \begin{bmatrix}1&0&0\\0&1&0\\0&0&1\end{bmatrix} 1
23 cases |x|=\begin{cases}x&x\geq0\\-x&x<0\end{cases} 1
24 accent_hat \hat{x}+\hat{y} 0
25 accent_vec \vec{v}\cdot\vec{w} 0
26 accent_bar \bar{z}z=|z|^2 0
27 overline \overline{AB} 0
28 overbrace \overbrace{a+b+c}^{3} 1
29 underbrace \underbrace{x+x+x+x}_{n} 1
30 overrightarrow \overrightarrow{AB} 0
31 quadratic x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} 1
32 euler e^{i\pi}+1=0 1
33 maxwell_gauss \nabla\cdot E=\frac{\rho}{\varepsilon_0} 1
34 maxwell_faraday \nabla\times E=-\frac{\partial B}{\partial t} 1
35 maxwell_ampere \nabla\times B=\mu_0J+\mu_0\varepsilon_0\frac{\partial E}{\partial t} 1
36 binom \binom{n}{k}=\frac{n!}{k!(n-k)!} 1
37 delims_tall \left[\frac{a}{b}\right]+\left\{\frac{c}{d}\right\} 1
38 floor_ceil ⌊x⌋+⌈y⌉ 0
39 abs_tall \left|\frac{a}{b}\right| 0
40 angle ⟨u,v⟩ 0
41 greek \alpha+\beta=\gamma 0
42 func \sin^2\theta+\cos^2\theta=1 0
43 boxed \boxed{E=mc^2} 0
44 wide_matrix \begin{matrix}a_0&a_1&a_2&a_3&a_4&a_5&a_6&a_7&a_8&a_9&a_{10}&a_{11}&a_{12}&a_{13}&a_{14}&a_{15}&a_{16}&a_{17}&a_{18}&a_{19}\end{matrix} 1
45 long_wrap (a+b)^{6}=a^{6}+6a^5b+15a^4b^2+20a^3b^3+15a^2b^4+6ab^5+b^{6}+c^{6}+6c^5d+15c^4d^2+20c^3d^3+15c^2d^4+6cd^5+d^{6} 0
+92
View File
@@ -0,0 +1,92 @@
# input (MathLive-style LaTeX) to_latex(from_linear(input))
x^2 x^2
x^{2} x^2
x^{2+1} x^{2+1}
x^{n^{2}} x^{n^2}
x^2+3x-2 x^2+3x-2
2x 2x
3x^2+2x-1 3x^2+2x-1
\frac{1}{x} \frac{1}{x}
\frac{x+1}{x-1} \frac{x+1}{x-1}
\frac{\frac{1}{2}}{3} \frac{\frac{1}{2}}{3}
\frac{x^{2}}{3} \frac{x^2}{3}
\sqrt{x} \sqrt{x}
\sqrt{x+1} \sqrt{x+1}
\sqrt[3]{x} \sqrt[3]{x}
\sqrt{\sqrt{x}} \sqrt{\sqrt{x}}
\sin(x) \sin\left(x\right)
\sin x \sin\left(x\right)
\sin{x} \sin\left(x\right)
\cos\left(x\right) \cos\left(x\right)
\sin\left(2x\right)+\cos(x) \sin\left(2x\right)+\cos\left(x\right)
\tan(x) \tan\left(x\right)
\arcsin(x) \arcsin\left(x\right)
\sinh(x) \sinh\left(x\right)
\ln(x) \ln\left(x\right)
\ln x \ln\left(x\right)
\log(x) \log\left(x\right)
\exp(x) \exp\left(x\right)
\abs{x} \left|x\right|
|x| \left|x\right|
|x-1|+|x+1| \left|x-1\right|+\left|x+1\right|
2\pi x 2\pi x
\sin(\pi x) \sin\left(\pi x\right)
\theta \theta
1+\cos\theta 1+\cos\left(\theta\right)
\infty \infty
\mathrm{e}^{x} \mathrm{e}^x
x_1+x_{12} x_1+x_{12}
2\cdot3 2\cdot3
2\times3 2\times3
4\div2 4\div2
x\pm1 x\pm1
(x+1)(x-1) \left(x+1\right)\left(x-1\right)
(x+1)2 \left(x+1\right)2
2(x+1) 2\left(x+1\right)
x^2+y^2-1 x^2+y^2-1
x--1 x--1
x++1 x++1
3\sqrt{x} 3\sqrt{x}
e^{-x^2} e^{-x^2}
\frac{\sin(x)}{x} \frac{\sin\left(x\right)}{x}
\left|x\right| \left|x\right|
\cot(x)\sec(x)\csc(x) \cot\left(x\right)\sec\left(x\right)\csc\left(x\right)
2\sin(3x) 2\sin\left(3x\right)
\sin^{2}(x) \sin^2\left(x\right)
\sqrt[n]{x+1} \sqrt[n]{x+1}
a b a b
\frac{1}{2}x \frac{1}{2}x
x^{\frac{1}{2}} x^{\frac{1}{2}}
\sin\theta \sin\left(\theta\right)
10x 10x
2.5x 2.5x
(2)(3)(4) \left(2\right)\left(3\right)\left(4\right)
\operatorname{abs}(x) \operatorname{abs}\left(x\right)
\frac{1}{} \frac{1}{}
\arctan(x)^{2} {\arctan\left(x\right)}^2
\cosh x \cosh\left(x\right)
\tanh(x) \tanh\left(x\right)
x^{2}_{3} x_3^2
x^2+y^2=25 x^2+y^2=25
y=\frac{x^2-1}{x+1} y=\frac{x^2-1}{x+1}
y=\sqrt{4-x^2} y=\sqrt{4-x^2}
r=1+\cos\theta r=1+\cos\left(\theta\right)
\left(x-2\right)^2+\left(y+1\right)^2=9 \left(x-2\right)^2+\left(y+1\right)^2=9
y=\left|x-3\right|+2 y=\left|x-3\right|+2
y=\sin\left(2x\right)\cos\left(x\right) y=\sin\left(2x\right)\cos\left(x\right)
y=\frac{1}{2}x+\frac{3}{4} y=\frac{1}{2}x+\frac{3}{4}
y=e^{x} y=e^x
y=\ln\left(x+1\right) y=\ln\left(x+1\right)
y=\sqrt[3]{x^2} y=\sqrt[3]{x^2}
f\left(x\right)=ax^2+bx+c f\left(x\right)=ax^2+bx+c
\frac{d}{dx}x^2 \frac{d}{dx}x^2
y=2^{x} y=2^x
y=x^{\frac{2}{3}} y=x^{\frac{2}{3}}
x^{2}+y^{2}\le 4 x^2+y^2\le 4
y=\sin^{2}\left(x\right) y=\sin^2\left(x\right)
\left(1,2\right) \left(1,2\right)
\left[1,2,3\right] \left[1,2,3\right]
a=\frac{-b\pm\sqrt{b^{2}-4ac}}{2a} a=\frac{-b\pm\sqrt{b^2-4ac}}{2a}
y=\left\{x<0:-x,x\right\} y=\left\{x<0:-x,x\right\}
\sum_{n=1}^{5}n^2 \sum_{n=1}^5n^2
\pi r^{2} \pi r^2
1 # input (MathLive-style LaTeX) to_latex(from_linear(input))
2 x^2 x^2
3 x^{2} x^2
4 x^{2+1} x^{2+1}
5 x^{n^{2}} x^{n^2}
6 x^2+3x-2 x^2+3x-2
7 2x 2x
8 3x^2+2x-1 3x^2+2x-1
9 \frac{1}{x} \frac{1}{x}
10 \frac{x+1}{x-1} \frac{x+1}{x-1}
11 \frac{\frac{1}{2}}{3} \frac{\frac{1}{2}}{3}
12 \frac{x^{2}}{3} \frac{x^2}{3}
13 \sqrt{x} \sqrt{x}
14 \sqrt{x+1} \sqrt{x+1}
15 \sqrt[3]{x} \sqrt[3]{x}
16 \sqrt{\sqrt{x}} \sqrt{\sqrt{x}}
17 \sin(x) \sin\left(x\right)
18 \sin x \sin\left(x\right)
19 \sin{x} \sin\left(x\right)
20 \cos\left(x\right) \cos\left(x\right)
21 \sin\left(2x\right)+\cos(x) \sin\left(2x\right)+\cos\left(x\right)
22 \tan(x) \tan\left(x\right)
23 \arcsin(x) \arcsin\left(x\right)
24 \sinh(x) \sinh\left(x\right)
25 \ln(x) \ln\left(x\right)
26 \ln x \ln\left(x\right)
27 \log(x) \log\left(x\right)
28 \exp(x) \exp\left(x\right)
29 \abs{x} \left|x\right|
30 |x| \left|x\right|
31 |x-1|+|x+1| \left|x-1\right|+\left|x+1\right|
32 2\pi x 2\pi x
33 \sin(\pi x) \sin\left(\pi x\right)
34 \theta \theta
35 1+\cos\theta 1+\cos\left(\theta\right)
36 \infty \infty
37 \mathrm{e}^{x} \mathrm{e}^x
38 x_1+x_{12} x_1+x_{12}
39 2\cdot3 2\cdot3
40 2\times3 2\times3
41 4\div2 4\div2
42 x\pm1 x\pm1
43 (x+1)(x-1) \left(x+1\right)\left(x-1\right)
44 (x+1)2 \left(x+1\right)2
45 2(x+1) 2\left(x+1\right)
46 x^2+y^2-1 x^2+y^2-1
47 x--1 x--1
48 x++1 x++1
49 3\sqrt{x} 3\sqrt{x}
50 e^{-x^2} e^{-x^2}
51 \frac{\sin(x)}{x} \frac{\sin\left(x\right)}{x}
52 \left|x\right| \left|x\right|
53 \cot(x)\sec(x)\csc(x) \cot\left(x\right)\sec\left(x\right)\csc\left(x\right)
54 2\sin(3x) 2\sin\left(3x\right)
55 \sin^{2}(x) \sin^2\left(x\right)
56 \sqrt[n]{x+1} \sqrt[n]{x+1}
57 a b a b
58 \frac{1}{2}x \frac{1}{2}x
59 x^{\frac{1}{2}} x^{\frac{1}{2}}
60 \sin\theta \sin\left(\theta\right)
61 10x 10x
62 2.5x 2.5x
63 (2)(3)(4) \left(2\right)\left(3\right)\left(4\right)
64 \operatorname{abs}(x) \operatorname{abs}\left(x\right)
65 \frac{1}{} \frac{1}{}
66 \arctan(x)^{2} {\arctan\left(x\right)}^2
67 \cosh x \cosh\left(x\right)
68 \tanh(x) \tanh\left(x\right)
69 x^{2}_{3} x_3^2
70 x^2+y^2=25 x^2+y^2=25
71 y=\frac{x^2-1}{x+1} y=\frac{x^2-1}{x+1}
72 y=\sqrt{4-x^2} y=\sqrt{4-x^2}
73 r=1+\cos\theta r=1+\cos\left(\theta\right)
74 \left(x-2\right)^2+\left(y+1\right)^2=9 \left(x-2\right)^2+\left(y+1\right)^2=9
75 y=\left|x-3\right|+2 y=\left|x-3\right|+2
76 y=\sin\left(2x\right)\cos\left(x\right) y=\sin\left(2x\right)\cos\left(x\right)
77 y=\frac{1}{2}x+\frac{3}{4} y=\frac{1}{2}x+\frac{3}{4}
78 y=e^{x} y=e^x
79 y=\ln\left(x+1\right) y=\ln\left(x+1\right)
80 y=\sqrt[3]{x^2} y=\sqrt[3]{x^2}
81 f\left(x\right)=ax^2+bx+c f\left(x\right)=ax^2+bx+c
82 \frac{d}{dx}x^2 \frac{d}{dx}x^2
83 y=2^{x} y=2^x
84 y=x^{\frac{2}{3}} y=x^{\frac{2}{3}}
85 x^{2}+y^{2}\le 4 x^2+y^2\le 4
86 y=\sin^{2}\left(x\right) y=\sin^2\left(x\right)
87 \left(1,2\right) \left(1,2\right)
88 \left[1,2,3\right] \left[1,2,3\right]
89 a=\frac{-b\pm\sqrt{b^{2}-4ac}}{2a} a=\frac{-b\pm\sqrt{b^2-4ac}}{2a}
90 y=\left\{x<0:-x,x\right\} y=\left\{x<0:-x,x\right\}
91 \sum_{n=1}^{5}n^2 \sum_{n=1}^5n^2
92 \pi r^{2} \pi r^2
@@ -0,0 +1 @@
<m:oMathPara xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><m:oMath><m:f><m:num><m:r><m:t>a</m:t></m:r></m:num><m:den><m:r><m:t>b</m:t></m:r></m:den></m:f></m:oMath></m:oMathPara>
@@ -0,0 +1 @@
<m:oMathPara xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><m:oMath><m:f><m:num><m:r><a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" lang="en-US" sz="2800" b="1"/><m:t>x</m:t></m:r></m:num><m:den><m:r><m:t>2</m:t></m:r></m:den></m:f></m:oMath></m:oMathPara>
@@ -0,0 +1 @@
<mm:oMath xmlns:mm="http://schemas.openxmlformats.org/officeDocument/2006/math"><mm:sSup><mm:e><mm:r><mm:t>x</mm:t></mm:r></mm:e><mm:sup><mm:r><mm:t>2</mm:t></mm:r></mm:sup></mm:sSup></mm:oMath>
@@ -0,0 +1 @@
<m:oMathPara xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><m:oMathParaPr><m:jc m:val="left"/></m:oMathParaPr><m:oMath><m:rad><m:radPr><m:degHide m:val="1"/></m:radPr><m:deg/><m:e><m:r><m:t>x</m:t></m:r></m:e></m:rad></m:oMath></m:oMathPara>
@@ -0,0 +1 @@
<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:p="urn:p" xmlns:q="urn:q" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><m:r xsi:type="foo" p:val="1" q:val="2"><p:ext/><m:t>x</m:t></m:r></m:oMath>
@@ -0,0 +1 @@
<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"><m:f><m:num><m:r><m:t>a</m:t></m:r></m:num><m:den><m:r><m:t>b</m:t></m:r></m:den></m:f><m:r><m:t>+1</m:t></m:r></m:oMath>
+41
View File
@@ -0,0 +1,41 @@
//! OMML read into the tree and written as LaTeX means what GridCraft's direct OMML-to-LaTeX
//! reader (`crates/xlsx/src/omml.rs`) gave for its test cases, so GridCraft can read equations in
//! drawings through this crate. The LaTeX is compared by what it reads back as, not byte for byte.
use craft_math::{math, math_latex, omml};
fn tex(inner: &str) -> String {
let x = format!(r#"<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">{inner}</m:oMath>"#);
math_latex::to_latex(&omml::read_str(&x).unwrap().nodes)
}
#[test]
fn gridcraft_omml_cases_mean_the_same_latex() {
let brace = |chr: &str, pos: &str| {
format!(
"<m:groupChr><m:groupChrPr><m:chr m:val=\"{chr}\"/><m:pos m:val=\"{pos}\"/></m:groupChrPr><m:e><m:r><m:t>x</m:t></m:r></m:e></m:groupChr>"
)
};
let lim = "<m:lim><m:r><m:t>n</m:t></m:r></m:lim>";
let under = format!("<m:limLow><m:e>{}</m:e>{lim}</m:limLow>", brace("\u{23DF}", "bot"));
let cases: Vec<(String, &str)> = vec![
("<m:r><m:t>x+1</m:t></m:r>".into(), "x+1"),
("<m:f><m:num><m:r><m:t>a</m:t></m:r></m:num><m:den><m:r><m:t>b</m:t></m:r></m:den></m:f>".into(), "\\frac{a}{b}"),
("<m:sSup><m:e><m:r><m:t>x</m:t></m:r></m:e><m:sup><m:r><m:t>2</m:t></m:r></m:sup></m:sSup>".into(), "{x}^{2}"),
("<m:rad><m:radPr><m:degHide m:val=\"1\"/></m:radPr><m:deg/><m:e><m:r><m:t>π</m:t></m:r></m:e></m:rad>".into(), "\\sqrt{\\pi }"),
("<m:nary><m:naryPr><m:chr m:val=\"∑\"/><m:limLoc m:val=\"undOvr\"/></m:naryPr><m:sub><m:r><m:t>i=1</m:t></m:r></m:sub><m:sup><m:r><m:t>n</m:t></m:r></m:sup><m:e><m:r><m:t>i</m:t></m:r></m:e></m:nary>".into(), "\\sum_{i=1}^{n} {i}"),
("<m:nary><m:naryPr><m:subHide m:val=\"1\"/><m:supHide m:val=\"1\"/></m:naryPr><m:sub/><m:sup/><m:e><m:r><m:t>f</m:t></m:r></m:e></m:nary>".into(), "\\int {f}"),
("<m:d><m:dPr><m:begChr m:val=\"[\"/><m:endChr m:val=\"]\"/></m:dPr><m:e><m:m><m:mr><m:e><m:r><m:t>1</m:t></m:r></m:e><m:e><m:r><m:t>0</m:t></m:r></m:e></m:mr><m:mr><m:e><m:r><m:t>0</m:t></m:r></m:e><m:e><m:r><m:t>1</m:t></m:r></m:e></m:mr></m:m></m:e></m:d>".into(), "\\left[ \\begin{matrix}1 & 0 \\\\ 0 & 1\\end{matrix} \\right]"),
("<m:r><m:rPr><m:sty m:val=\"p\"/></m:rPr><m:t>sin</m:t></m:r><m:r><m:t>x</m:t></m:r>".into(), "\\sin x"),
("<m:r><m:rPr><m:scr m:val=\"double-struck\"/></m:rPr><m:t>R</m:t></m:r>".into(), "\\mathbb{R}"),
("<m:r><m:rPr><m:nor/></m:rPr><m:t>if {x}</m:t></m:r>".into(), "\\text{if \\{x\\}}"),
("<m:acc><m:accPr><m:chr m:val=\"\u{0302}\"/></m:accPr><m:e><m:r><m:t>x</m:t></m:r></m:e></m:acc>".into(), "\\hat{x}"),
(under.clone(), "\\underbrace{x}_{n}"),
(format!("<m:sSup><m:e>{under}</m:e><m:sup><m:r><m:t>2</m:t></m:r></m:sup></m:sSup>"), "{\\underbrace{x}_{n}}^{2}"),
(format!("<m:limUpp><m:e>{}</m:e>{lim}</m:limUpp>", brace("\u{23DE}", "top")), "\\overbrace{x}^{n}"),
(format!("<m:limLow><m:e><m:r><m:t>a</m:t></m:r></m:e>{lim}</m:limLow>"), "\\underset{n}{a}"),
(format!("<m:limLow><m:e>{}<m:r><m:t>+1</m:t></m:r></m:e>{lim}</m:limLow>", brace("\u{23DF}", "bot")), "\\underset{n}{\\underbrace{x}+1}"),
];
for (x, want) in cases {
let got = tex(&x);
assert_eq!(math::to_linear(&math_latex::parse_latex(&got)), math::to_linear(&math_latex::parse_latex(want)), "{x}: {got} for {want}");
}
}