mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
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:
1 parent
dc14024a19
commit
85b4dabf0f
29 files changed
+6934
No files matched your search
@@ -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
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
@@ -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).
|
||||
@@ -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
@@ -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) },
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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("&"),
|
||||
'<' => o.push_str("<"),
|
||||
'>' => o.push_str(">"),
|
||||
'"' => o.push_str("""),
|
||||
'\'' => o.push_str("'"),
|
||||
'\t' => o.push_str("	"),
|
||||
'\n' => o.push_str(" "),
|
||||
'\r' => o.push_str(" "),
|
||||
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;
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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></m:t><w:noBreakHyphen/></m:r><w:r><w:rPr><w:rFonts w:ascii="Symbol"/></w:rPr><w:t></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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user