craft-docx: equations through craft_math::omml (#1035)

* Add craft-wordmodel: WordCraft's document model without its editing code

WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX,
RTF, ODT and other text-format crates can read into and write from one model.
Module paths, type names and serde attributes are unchanged: an app that
re-exports the crate keeps its native file format.

Left in the app: range editing (edit.rs), IME text and equation editing. Not
model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count
check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks
moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs.

* craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs

The DOCX reader and writer walk an equation's runs. The walk and the child
accessors it uses were in WordCraft's equation editor (math_edit.rs), which
stays in the app; they are structure, not editing.

* craft-wordmodel: keep the document operations (edit.rs) with the model

The DOCX and RTF readers use them: alternative-format chunks are merged into
a document with adopt_blocks, and a reader formats ranges with format_range
and inserts objects. They operate on the document, not on a view, so they
belong beside it. The tests that drove them come back with them; the
paragraph walks stay in walk.rs and edit re-exports them.

base64 0.23, as WordCraft and craft-offcrypto use.

* craft-docx: WordCraft's crates/docx as it is at wordcraft 07f97737

The source, unchanged, so the next commit shows only what the move changes.
This commit does not build on its own (it still names wordcraft-doc and
wordcraft-geom, and is not a workspace member yet).

* Add craft-math: the equation tree with OMML, MathML, LaTeX and UnicodeMath

The math tree, LaTeX, linear-format, symbol and tree-walk modules come
from craft-wordmodel (storytold/craft-libs#963, WordCraft's crates/doc at
07f97737) unchanged, with the run colour (Rgb) and the symbol-font tables
they use. MathML is WordCraft's crates/formats/src/mathml.rs and OMML is
WordCraft's crates/docx/src/read/math.rs and write/math.rs (06c2ae6f /
07f97737), moved onto craft-xml. The HTML-specific MathML tests stay in
WordCraft; the rest moved with the code.

mathml::parse and omml::parse/read_str/write_str are new: string entry
points for callers that have no tree of their own.

* craft-math: read MathLive's and other spellings of LaTeX math commands

\abs{...} (an absolute value), \lvert \rvert \lVert \rVert, \nparallel,
\backprime, \infinity, \cross, \differentialD, \exponentialE and
\imaginaryI (MathLive's d, upright e and i), \emph, \textsf, \hbox,
\mathnormal and \underleftarrow / \underrightarrow /
\underleftrightarrow were unknown and read as literal text. They are read
as equaSHion's typed-input parser reads them (storytold/craft-libs#29).

* craft-math: equaSHion's equation corpus as test data

corpus/equations.tsv, corpus/latex.tsv and the OMML samples of
storytold/craft-libs#29 (7a6ef6b9), unmodified.

* craft-math: corpus round trips, GridCraft's OMML cases, entry points and limits tested; README

The corpus equations go through OMML, MathML and LaTeX and back; the OMML
samples are written back as kept and from the tree. GridCraft's OMML to
LaTeX test cases (crates/xlsx/src/omml.rs) give LaTeX that reads back the
same through the tree. omml::parse and read_str are tested against other
prefixes, the strict namespace, kept OMML, junk, the element cap and deep
nesting.

* craft-docx: on craft-wordmodel, craft-opc, craft-xml and craft-offcrypto

- The package (zip reading, part names, relationships, content types,
  zip writing) is craft-opc's; the module keeps WordCraft's bookkeeping
  (passthrough keys, the embedded-part manifest) and its own Rel/Rels shape.
- Parts are parsed with craft-xml's canonical names (the Strict drawing
  names and mc:Choice/@Requires through its hooks), then given the tree
  shape the reader was written against.
- Encryption and Restrict Editing hashes are craft-offcrypto's; this crate
  maps its errors and the w:documentProtection attributes.
- wordcraft-doc is craft-wordmodel; wordcraft-geom's helpers are its geom.

Tests that covered what now lives in craft-opc and craft-offcrypto are left to
those crates; the DIFAT regression is craft-cfb's. Test-only zip use follows
zip 9's fallible names. The attribute-cap test times the read only.

* craft-wordmodel: the math modules, symbol_fonts and Rgb are craft-math's

math, math_latex, math_linear, math_symbols, math_tree and symbol_fonts are
re-exported from craft-math at the paths they had, and props::Rgb is
craft-math's Rgb (TextColor stays), so the serde forms and the app's imports
do not change. Their tests are in craft-math.

* craft-docx: equations (OMML) are read and written by craft_math::omml

read/math.rs and write/math.rs keep their functions as adapters: the reader's
tree is handed to craft-math as a craft-xml tree, and the writer's output is
written into this crate's writer. Their tests are in craft-math.

---------

Co-authored-by: shuff57 <62350898+shuff57@users.noreply.github.com>
This commit is contained in:
Dylan M. Taylorandshuff57 authored and GitHub committed 2026-11-11 21:32:31 -05:00
1 parent d93991706b
commit 865671e1fd
13 files changed
+45 -5043

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ repository.workspace = true
workspace = true
[dependencies]
craft-math = { workspace = true }
craft-offcrypto = { workspace = true }
craft-opc = { workspace = true }
craft-wordmodel = { workspace = true }
+2 -1
View File
@@ -42,8 +42,9 @@ written back as it was read.
`write(doc)`, `write_as(doc, Flavor)` for `.docx`, `.docm`, `.dotx` and `.dotm`; `write_flat`
and `write_wordml2003` for the XML forms; `encrypt` for a password.
Equations (OMML) are read and written by `craft_math::omml`.
## Not here yet
- DrawingML (`a:`) shape fills, lines, custom geometry and colours are still read with this
crate's own code; they move to `craft-drawingml`.
- Equations (OMML) are read and written by this crate; they move to `craft-math`.
+26 -481
View File
@@ -1,496 +1,41 @@
//! Office Math (OMML, ECMA-376 Part 1 §22.1) → [`craft_wordmodel::math`] tree.
//! Office Math (OMML, ECMA-376 Part 1 §22.1) read with `craft_math::omml`.
use craft_wordmodel::math::{
Arg, BrkBin, BrkBinSub, ColJc, FracKind, LimLoc, MAX_DEPTH, MNode, MRun, MScr, MSty, Math, MathJc, MathProps, ScriptKind, merge_runs,
};
use craft_wordmodel::math::{Math, MathJc, MathProps};
use craft_wordmodel::props::Rgb;
use craft_wordmodel::symbol_fonts;
use crate::xml::El;
use crate::xml::{El, Node};
/// An `m:oMath` element as an equation (structure plus its source XML).
pub fn read_omath(e: &El, jc: MathJc) -> Math {
Math { nodes: merge_runs(arg(e, 0)), omml: e.to_xml(), jc, linear: false }
}
/// A colour every math run shares, taken off the runs: it is the equation's own colour, as
/// [`write_equation`](crate::write::math::write_equation) spreads it (#1467). The source OMML
/// is kept: the writer replaces it only when the colour changes.
pub fn lift_color(math: &mut Math) -> Option<Rgb> {
let (mut shared, mut uniform) = (None, true);
craft_wordmodel::math_tree::each_run_mut(&mut math.nodes, &mut |r| match shared {
None => shared = Some(r.color),
Some(c) => uniform &= c == r.color,
});
let c = shared.flatten().filter(|_| uniform)?;
craft_wordmodel::math_tree::each_run_mut(&mut math.nodes, &mut |r| r.color = None);
Some(c)
}
/// `m:oMathParaPr/m:jc`.
pub fn para_jc(para: &El) -> MathJc {
match para.child("m:oMathParaPr").and_then(|p| p.child("m:jc")).and_then(|j| j.attr("m:val")) {
Some("left") => MathJc::Left,
Some("right") => MathJc::Right,
Some("center") => MathJc::Center,
_ => MathJc::CenterGroup,
}
}
/// The settings part's `m:mathPr`: how display equations wrap.
pub fn read_math_pr(e: &El) -> MathProps {
let mut m = MathProps::default();
let pr = Some(e);
m.brk_bin = match val(pr, "m:brkBin") {
Some("after") => BrkBin::After,
Some("repeat") => BrkBin::Repeat,
_ => BrkBin::Before,
};
m.brk_bin_sub = match val(pr, "m:brkBinSub") {
Some("-+") => BrkBinSub::MinusPlus,
Some("+-") => BrkBinSub::PlusMinus,
_ => BrkBinSub::MinusMinus,
};
if let Some(v) = e.child("m:wrapIndent").and_then(|c| crate::units::tw(c, "m:val")).filter(|v| v.is_finite() && *v >= 0.0) {
m.wrap_indent = v.min(1584.0);
}
m.wrap_right = prop(pr, "m:wrapRight", false);
m
}
/// OMML on/off property: absent `m:val` = on.
fn on(e: &El) -> bool {
match e.attr("m:val") {
None => true,
Some(v) => !matches!(v.trim(), "0" | "false" | "off" | "False" | "FALSE"),
}
}
/// An on/off property `name` in the properties element `pr` (absent = `default`).
fn prop(pr: Option<&El>, name: &str, default: bool) -> bool {
pr.and_then(|p| p.child(name)).map(on).unwrap_or(default)
}
/// A character property (`m:chr`, `m:begChr`…): `None` when absent, `Some(None)` when empty.
fn chr(pr: Option<&El>, name: &str) -> Option<Option<char>> {
let c = pr?.child(name)?;
match c.attr("m:val") {
Some(v) => Some(v.chars().next()),
None => Some(None),
}
}
fn val<'a>(pr: Option<&'a El>, name: &str) -> Option<&'a str> {
pr.and_then(|p| p.child(name)).and_then(|c| c.attr("m:val"))
}
/// The argument held by child `name` of `e`.
fn sub_arg(e: &El, name: &str, depth: usize) -> Arg {
e.child(name).map(|c| arg(c, depth + 1)).unwrap_or_default()
}
/// Read the math content of an element (`m:oMath`, `m:e`, `m:num`…).
fn arg(e: &El, depth: usize) -> Arg {
let mut out = Vec::new();
if depth > MAX_DEPTH {
return out;
}
for k in e.els() {
node(k, depth, &mut out);
/// This reader's tree as `craft-xml`'s, which `craft_math::omml` reads. Both use the canonical
/// prefixes (`m:`, `w:`, `mc:`).
// shortcut: copies the equation's subtree; goes when the reader reads craft_xml::El directly.
fn to_craft(e: &El) -> craft_xml::El {
let mut out = craft_xml::El { name: e.name.clone(), attrs: e.attrs.clone(), children: Vec::with_capacity(e.kids.len()) };
for k in &e.kids {
out.children.push(match k {
Node::El(c) => craft_xml::Node::El(to_craft(c)),
Node::Text(t) => craft_xml::Node::Text(t.clone()),
});
}
out
}
fn node(k: &El, depth: usize, out: &mut Arg) {
let d = depth + 1;
match k.name.as_str() {
"m:r" => {
if let Some(r) = run(k) {
out.push(MNode::Run(r));
}
}
// Word puts ordinary runs in math zones for normal text in some files.
"w:r" => {
let text = run_text(k);
if !text.is_empty() {
let mut r = MRun { text, nor: true, sty: Some(MSty::Plain), ..Default::default() };
wrpr(k.child("w:rPr"), &mut r);
out.push(MNode::Run(r));
}
}
"m:f" => {
let pr = k.child("m:fPr");
let kind = match val(pr, "m:type") {
Some("skw") => FracKind::Skewed,
Some("lin") => FracKind::Linear,
Some("noBar") => FracKind::NoBar,
_ => FracKind::Bar,
};
out.push(MNode::Frac { kind, num: sub_arg(k, "m:num", d), den: sub_arg(k, "m:den", d) });
}
"m:sSup" => out.push(MNode::Script { kind: ScriptKind::Sup, base: sub_arg(k, "m:e", d), sub: Vec::new(), sup: sub_arg(k, "m:sup", d) }),
"m:sSub" => out.push(MNode::Script { kind: ScriptKind::Sub, base: sub_arg(k, "m:e", d), sub: sub_arg(k, "m:sub", d), sup: Vec::new() }),
"m:sSubSup" => {
out.push(MNode::Script { kind: ScriptKind::SubSup, base: sub_arg(k, "m:e", d), sub: sub_arg(k, "m:sub", d), sup: sub_arg(k, "m:sup", d) })
}
"m:sPre" => {
out.push(MNode::Script { kind: ScriptKind::Pre, base: sub_arg(k, "m:e", d), sub: sub_arg(k, "m:sub", d), sup: sub_arg(k, "m:sup", d) })
}
"m:rad" => {
let pr = k.child("m:radPr");
out.push(MNode::Rad { deg: sub_arg(k, "m:deg", d), deg_hide: prop(pr, "m:degHide", false), e: sub_arg(k, "m:e", d) });
}
"m:nary" => {
let pr = k.child("m:naryPr");
let chr = chr(pr, "m:chr").map(|c| c.unwrap_or('∫')).unwrap_or('∫');
let lim_loc = match val(pr, "m:limLoc") {
Some("undOvr") => Some(LimLoc::UndOvr),
Some("subSup") => Some(LimLoc::SubSup),
_ => None,
};
out.push(MNode::Nary {
chr,
lim_loc,
grow: prop(pr, "m:grow", false),
sub_hide: prop(pr, "m:subHide", false),
sup_hide: prop(pr, "m:supHide", false),
sub: sub_arg(k, "m:sub", d),
sup: sub_arg(k, "m:sup", d),
e: sub_arg(k, "m:e", d),
});
}
"m:d" => {
let pr = k.child("m:dPr");
let beg = chr(pr, "m:begChr").unwrap_or(Some('('));
let end = chr(pr, "m:endChr").unwrap_or(Some(')'));
let sep = chr(pr, "m:sepChr").unwrap_or(Some('|'));
let elems: Vec<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.els() {
node(c, d, out);
}
}
// Markup compatibility: the math choice (we read math), else the fallback.
"mc:AlternateContent" if depth <= MAX_DEPTH => {
if let Some(c) = k.child("mc:Choice").or_else(|| k.child("mc:Fallback")) {
for c in c.els() {
node(c, d, out);
}
}
}
"w:sdt" => {
if let Some(c) = k.child("w:sdtContent") {
for c in c.els() {
node(c, d, out);
}
}
}
_ => {}
}
/// An `m:oMath` element as an equation (structure plus its source XML).
pub fn read_omath(e: &El, jc: MathJc) -> Math {
craft_math::omml::read_omath(&to_craft(e), jc)
}
/// `m:t` (and `w:t`) text of a run, with symbol characters (`w:sym`) and special hyphens.
/// Text in a symbol font (Symbol, Wingdings…) becomes its Unicode equivalent: the math font has
/// no glyphs at those private-use codes (#1273).
fn run_text(r: &El) -> String {
let font = r.child("w:rPr").and_then(|p| p.child("w:rFonts")).and_then(|f| f.attr("w:ascii").or_else(|| f.attr("w:hAnsi"))).unwrap_or("");
let mut s = String::new();
for c in r.els() {
match c.name.as_str() {
"m:t" | "w:t" => s.push_str(&symbol_fonts::map_text(font, &c.text())),
"w:tab" => s.push('\t'),
"w:noBreakHyphen" => s.push('\u{2011}'),
"w:sym" => {
let ch = c.attr("w:char").and_then(|h| u32::from_str_radix(h.trim(), 16).ok()).and_then(char::from_u32).filter(|c| *c as u32 >= 0x20);
if let Some(ch) = ch {
let f = c.attr("w:font").unwrap_or(font);
s.push(symbol_fonts::symbol_char(f, ch).unwrap_or(ch));
}
}
_ => {}
}
}
s
/// The colour every run of `math` shares, taken off the runs (the run around the equation
/// carries it).
pub fn lift_color(math: &mut Math) -> Option<Rgb> {
craft_math::omml::lift_color(math)
}
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("m:val")) {
Some("p") => Some(MSty::Plain),
Some("b") => Some(MSty::Bold),
Some("i") => Some(MSty::Italic),
Some("bi") => Some(MSty::BoldItalic),
_ => None,
};
out.scr = match pr.child("m:scr").and_then(|s| s.attr("m:val")) {
Some("script") => MScr::Script,
Some("fraktur") => MScr::Fraktur,
Some("double-struck") => MScr::DoubleStruck,
Some("sans-serif") => MScr::SansSerif,
Some("monospace") => MScr::Monospace,
_ => MScr::Roman,
};
out.nor = pr.child("m:nor").is_some_and(on);
out.lit = pr.child("m:lit").is_some_and(on);
if let Some(b) = pr.child("m:brk") {
let aln = b.attr("m:alnAt").and_then(|v| v.trim().parse::<u32>().ok()).unwrap_or(0);
out.brk = Some(aln.min(255) as u8);
}
}
wrpr(r.child("w:rPr"), &mut out);
Some(out)
/// The justification an `m:oMathPara` gives its equations.
pub fn para_jc(para: &El) -> MathJc {
craft_math::omml::para_jc(&to_craft(para))
}
/// The run formatting a math run takes from `w:rPr`: size, colour, normal-text font and weight.
fn wrpr(pr: Option<&El>, out: &mut MRun) {
let Some(pr) = pr else { return };
if let Some(sz) = pr.child("w:sz").and_then(|s| s.attr("w:val")).and_then(|v| v.trim().parse::<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("w:val")).and_then(Rgb::parse) {
out.color = Some(c);
}
if out.nor {
// Symbol-font text was mapped to Unicode: it takes the text font.
if let Some(f) =
pr.child("w:rFonts").and_then(|f| f.attr("w:ascii").or_else(|| f.attr("w:hAnsi"))).filter(|f| !symbol_fonts::is_symbol_font(f))
{
out.font = Some(f.to_string());
}
let bold = pr.child("w:b").is_some_and(crate::units::on_off);
let italic = pr.child("w:i").is_some_and(crate::units::on_off);
if bold || italic {
out.sty = Some(match (bold, italic) {
(true, true) => MSty::BoldItalic,
(true, false) => MSty::Bold,
_ => MSty::Italic,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn omath(inner: &str) -> El {
let x = format!(
r#"<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">{inner}</m:oMath>"#
);
crate::xml::parse(x.as_bytes()).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!(craft_wordmodel::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();
craft_wordmodel::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 = crate::xml::parse(x.as_bytes()).unwrap();
let mut m = read_omath(&e, MathJc::CenterGroup);
let got = texts(&mut m).concat();
let markers = [
"A1", "B1", "C1", "D1", "E1", "E2", "F1", "F2", "G1", "G2", "H1", "H2", "I1", "J1", "J2", "K1", "K2", "L1", "L2", "L3", "M1", "M2", "M3",
"N1", "O1", "O2", "P1", "P2", "P3", "Q1", "Q2", "R1", "R2", "R3", "S1", "S2", "T1", "T2", "T3", "T4", "T5", "T6",
];
for k in markers {
assert!(got.contains(k), "{k} dropped from {got}");
}
assert!(!got.contains("picture"), "the math choice wins over the fallback: {got}");
}
/// #1273: deleted text stays out; symbol characters and symbol-font text become Unicode.
#[test]
fn symbols_and_deletions_in_math() {
let e = omath(
r#"<m:r><m:t>x</m:t></m:r><w:del w:id="1" w:author="a"><m:r><m:t>gone</m:t></m:r></w:del><m:r><w:sym w:font="Symbol" w:char="F061"/></m:r><m:r><w:rPr><w:rFonts w:ascii="Symbol" w:hAnsi="Symbol"/></w:rPr><m:t>&#xF062;</m:t><w:noBreakHyphen/></m:r><w:r><w:rPr><w:rFonts w:ascii="Symbol"/></w:rPr><w:t>&#xF067;</w:t></w:r>"#,
);
let mut m = read_omath(&e, MathJc::CenterGroup);
let got = texts(&mut m).concat();
assert_eq!(got, "xαβ\u{2011}γ");
let mut font = None;
craft_wordmodel::math_tree::each_run_mut(&mut m.nodes, &mut |r| {
if r.nor {
font = Some(r.font.clone());
}
});
assert_eq!(font, Some(None), "mapped symbol text takes the text font");
}
/// Hostile nesting through the wrappers is bounded (no stack overflow, no panic).
#[test]
fn deeply_nested_wrappers_are_bounded() {
// Deeper than the math limit, within the XML parser's own depth limit.
let n = 120;
let inner = format!("{}{}{}", "<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);
}
/// `m:mathPr` in the settings part.
pub fn read_math_pr(e: &El) -> MathProps {
craft_math::omml::read_math_pr(&to_craft(e))
}
+10 -471
View File
@@ -1,486 +1,25 @@
//! [`craft_wordmodel::math`] tree → Office Math (OMML).
//! Office Math (OMML) written with `craft_math::omml`.
use craft_wordmodel::math::{
Arg, BrkBin, BrkBinSub, ColJc, FracKind, LimLoc, MAX_DEPTH, MAX_MATRIX_COLS, MAX_MATRIX_ROWS, MNode, MRun, MScr, MSty, Math, MathJc, MathProps,
ScriptKind, parse_linear,
};
use craft_wordmodel::math::{Math, MathProps};
use craft_wordmodel::props::Rgb;
use crate::xml::W;
/// Write an equation: `m:oMath`, inside `m:oMathPara` for a display equation. The source OMML
/// is written back unchanged when the equation came from a file. `color` (the equation's run
/// colour) goes on every math run without its own: OMML has no equation-level colour (#1467).
/// colour) goes on every math run without its own: OMML has no equation-level colour.
pub fn write_equation(w: &mut W, linear: &str, display: bool, math: &Math, color: Option<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 {
craft_wordmodel::math_tree::each_run_mut(&mut m.nodes, &mut |r| {
r.color.get_or_insert(c);
});
}
m.omml.clear();
tinted = m;
&tinted
};
if display {
w.open("m:oMathPara", &[]);
let jc = match math.jc {
MathJc::CenterGroup => None,
MathJc::Center => Some("center"),
MathJc::Left => Some("left"),
MathJc::Right => Some("right"),
};
if let Some(jc) = jc {
w.open("m:oMathParaPr", &[]);
w.empty("m:jc", &[("m:val", jc)]);
w.close("m:oMathParaPr");
}
}
if !math.omml.is_empty() && math.omml.starts_with("<m:oMath") && crate::xml::parse(wrap_ns(&math.omml).as_bytes()).is_ok() {
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");
}
w.raw(&craft_math::omml::write_str(linear, display, math, color));
}
/// Count the equation's numbered rows; when some use an automatic number (`#` alone), a copy
/// with the numbers written out as `(n)`.
/// `math` with its `#(n)` equation numbers resolved from `counter`, when it has any.
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 !craft_wordmodel::math::has_number_mark(row) {
continue;
}
*counter = counter.saturating_add(1);
// Nothing after the `#`: an automatic number.
let mut after = false;
let mut empty = true;
for n in row.iter() {
match n {
MNode::Run(r) if !after => {
if let Some(k) = r.text.find('#') {
after = true;
if !r.text.get(k + 1..).unwrap_or("").is_empty() {
empty = false;
}
}
}
_ if after => empty = false,
_ => {}
}
}
if empty {
let n = vec![MNode::Run(MRun::new(counter.to_string()))];
row.push(MNode::Delim { beg: Some('('), end: Some(')'), sep: None, grow: true, shp_match: false, elems: vec![n] });
changed = true;
}
}
changed.then(|| Math { nodes: vec![MNode::EqArr { rows: out }], omml: String::new(), ..math.clone() })
craft_math::omml::resolve_numbers(math, display, counter)
}
/// The equation colour the kept source OMML carries, as the reader lifts it.
fn omml_color(omml: &str) -> Option<Rgb> {
if omml.is_empty() {
return None;
}
let root = crate::xml::parse(wrap_ns(omml).as_bytes()).ok()?;
let e = root.child("m:oMath")?;
crate::read::math::lift_color(&mut crate::read::math::read_omath(e, MathJc::default()))
}
/// The kept XML with namespace declarations, for validating it on its own.
fn wrap_ns(xml: &str) -> String {
let decl: String = crate::xml::body_ns().iter().filter(|(k, _)| k.starts_with("xmlns:")).map(|(k, v)| format!(" {k}=\"{v}\"")).collect();
format!("<w:x{decl}>{xml}</w:x>")
}
/// The settings part's `m:mathPr` (CT_MathPr order; `m:wrapIndent` and `m:wrapRight` are a choice).
/// `m:mathPr`.
pub fn math_pr(w: &mut W, m: &MathProps) {
w.open("m:mathPr", &[]);
let bin = match m.brk_bin {
BrkBin::Before => "before",
BrkBin::After => "after",
BrkBin::Repeat => "repeat",
};
val(w, "m:brkBin", bin);
let sub = match m.brk_bin_sub {
BrkBinSub::MinusMinus => "--",
BrkBinSub::MinusPlus => "-+",
BrkBinSub::PlusMinus => "+-",
};
val(w, "m:brkBinSub", sub);
if m.wrap_right {
w.empty("m:wrapRight", &[]);
} else {
let indent = if m.wrap_indent.is_finite() { m.wrap_indent.clamp(0.0, 1584.0) } else { craft_wordmodel::math::DEFAULT_WRAP_INDENT };
val(w, "m:wrapIndent", &crate::units::twips(indent));
}
w.close("m:mathPr");
}
fn val(w: &mut W, name: &str, v: &str) {
w.empty(name, &[("m:val", v)]);
}
fn arg(w: &mut W, name: &str, a: &[MNode], depth: usize) {
if a.is_empty() {
w.empty(name, &[]);
return;
}
w.open(name, &[]);
arg_inner(w, a, depth + 1);
w.close(name);
}
fn arg_inner(w: &mut W, a: &[MNode], depth: usize) {
if depth > MAX_DEPTH {
return;
}
for n in a {
node(w, n, depth);
}
}
fn ctrl(w: &mut W) {
w.open("m:ctrlPr", &[]);
w.open("w:rPr", &[]);
w.empty("w:rFonts", &[("w:ascii", "Cambria Math"), ("w:hAnsi", "Cambria Math")]);
w.empty("w:i", &[]);
w.close("w:rPr");
w.close("m:ctrlPr");
}
fn char_prop(w: &mut W, name: &str, c: Option<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| craft_wordmodel::math::has_number_mark(r)) {
val(w, "m:maxDist", "1");
}
ctrl(w);
w.close("m:eqArrPr");
for r in rows {
arg(w, "m:e", r, depth);
}
w.close("m:eqArr");
}
MNode::Matrix { .. } => write_matrix(w, n, depth),
MNode::Phant { show, zero_wid, zero_asc, zero_desc, e } => {
w.open("m:phant", &[]);
w.open("m:phantPr", &[]);
val(w, "m:show", if *show { "1" } else { "0" });
for (on, name) in [(*zero_wid, "m:zeroWid"), (*zero_asc, "m:zeroAsc"), (*zero_desc, "m:zeroDesc")] {
if on {
val(w, name, "1");
}
}
ctrl(w);
w.close("m:phantPr");
arg(w, "m:e", e, depth);
w.close("m:phant");
}
}
}
fn run(w: &mut W, r: &MRun) {
w.open("m:r", &[]);
let sty = r.sty.map(|s| match s {
MSty::Plain => "p",
MSty::Bold => "b",
MSty::Italic => "i",
MSty::BoldItalic => "bi",
});
let scr = match r.scr {
MScr::Roman => None,
MScr::Script => Some("script"),
MScr::Fraktur => Some("fraktur"),
MScr::DoubleStruck => Some("double-struck"),
MScr::SansSerif => Some("sans-serif"),
MScr::Monospace => Some("monospace"),
};
if sty.is_some() || scr.is_some() || r.nor || r.lit || r.brk.is_some() {
w.open("m:rPr", &[]);
if r.lit {
w.empty("m:lit", &[]);
}
if r.nor {
w.empty("m:nor", &[]);
}
if let Some(s) = scr {
val(w, "m:scr", s);
}
if let Some(s) = sty {
val(w, "m:sty", s);
}
match r.brk {
Some(0) => w.empty("m:brk", &[]),
Some(n) => w.empty("m:brk", &[("m:alnAt", &n.to_string())]),
None => {}
}
w.close("m:rPr");
}
w.open("w:rPr", &[]);
let font = r.font.as_deref().filter(|_| r.nor).unwrap_or("Cambria Math");
w.empty("w:rFonts", &[("w:ascii", font), ("w:hAnsi", font)]);
if let Some(c) = r.color {
w.val("w:color", &c.hex());
}
if let Some(sz) = r.size {
let hp = (sz * 2.0).round().clamp(2.0, 3276.0) as i64;
w.val("w:sz", &hp.to_string());
w.val("w:szCs", &hp.to_string());
}
w.close("w:rPr");
w.leaf("m:t", &[("xml:space", "preserve")], &r.text);
w.close("m:r");
let mut o = craft_math::omml::W::default();
craft_math::omml::math_pr(&mut o, m);
w.raw(&o.s);
}
+3
View File
@@ -15,6 +15,9 @@ workspace = true
test-support = []
[dependencies]
# The equation tree and its forms (math, math_latex, math_linear, math_symbols, math_tree), with
# symbol_fonts and Rgb, re-exported here under the paths they had.
craft-math = { workspace = true }
base64 = "0.23"
log = "0.4"
serde = { version = "1", features = ["derive", "rc"] }
+1 -6
View File
@@ -37,11 +37,7 @@ pub mod legacy_form;
pub mod line_dash;
pub mod link;
pub mod locale_number;
pub mod math;
pub mod math_latex;
pub mod math_linear;
pub mod math_symbols;
pub mod math_tree;
pub use craft_math::{math, math_latex, math_linear, math_symbols, math_tree, symbol_fonts};
pub mod numbering;
pub mod para;
pub mod picfx;
@@ -50,7 +46,6 @@ pub mod resolve;
pub mod section;
pub mod smart_art;
pub mod styles;
pub mod symbol_fonts;
pub mod table;
pub mod walk;
pub mod wordart;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-762
View File
@@ -1,762 +0,0 @@
//! The linear format (UnicodeMath, the plain text Word builds equations from) → math tree.
//!
//! Grammar, informally: an expression is a sequence of atoms and operators. Atoms are characters,
//! bracket groups (`(…)`, `[…]`, `{…}`, `|…|`, `〖…〗` invisible, `├…┤` one-sided, `│`
//! separators), prefix structures (`√x`, `∑_a^b▒x`, `█(…)`, `■(…)`, `▭x`, `□x`, `¯x`, `▁x`,
//! `⏞x`, `⏟x`, `⟡x`), function names (`sin x`, `log_2 x`, `lim┬(n→∞) x`) and placeholders (`⬚`).
//! Postfix operators apply to the atom before them: `^` `_` scripts, combining accents, `┬` `┴`
//! limits. `/` `⁄` `∕` `¦` take the operands on either side (an operand is a run of atoms up to
//! an operator or space; a parenthesised operand loses its parentheses). `^`/`_` with nothing
//! before them are pre-scripts. A top-level `#` numbers the equation (`E=mc^2#(1)`).
use crate::math::{Arg, FracKind, MAX_DEPTH, MClass, MNode, MRun, ScriptKind, any_too_deep, is_nary_char, math_class, merge_runs, too_deep};
/// Function names set upright and applied to the following operand.
pub const FUNCTION_NAMES: &[&str] = &[
"arccos", "arccot", "arccsc", "arcsec", "arcsin", "arctan", "arg", "cos", "cosh", "cot", "coth", "csc", "csch", "deg", "det", "dim", "exp",
"gcd", "hom", "inf", "ker", "lg", "lim", "liminf", "limsup", "ln", "log", "max", "min", "mod", "Pr", "sec", "sech", "sin", "sinh", "sup", "tan",
"tanh",
];
/// Length of the longest of [`FUNCTION_NAMES`].
const MAX_NAME: usize = {
let (mut max, mut i) = (0, 0);
while i < FUNCTION_NAMES.len() {
if FUNCTION_NAMES[i].len() > max {
max = FUNCTION_NAMES[i].len();
}
i += 1;
}
max
};
/// Function names whose subscripts go underneath (`lim_(n→∞)`).
const LIMIT_NAMES: &[&str] = &["lim", "liminf", "limsup", "max", "min", "sup", "inf", "det", "gcd", "Pr"];
/// The empty-argument placeholder.
pub const PLACEHOLDER: char = '⬚';
/// Parse linear-format text. Never fails: what isn't understood stays as text.
pub fn parse(s: &str) -> Arg {
let mut p = P { c: s.chars().take(20_000).collect(), i: 0, depth: 0, no_limits: 0, steps: 0 };
let mut out = Vec::new();
// Stray closers at the top level are text.
while p.i < p.c.len() {
out.extend(p.expr(&[]));
if let Some(c) = p.peek() {
p.i += 1;
out.push(text(c));
}
}
let mut out = clean(out);
// `expr#(n)`: a numbered equation.
if out.iter().any(|n| matches!(n, MNode::Run(r) if r.text.contains('#'))) {
out = vec![MNode::EqArr { rows: vec![out] }];
}
out
}
fn text(c: char) -> MNode {
MNode::Run(MRun::new(c.to_string()))
}
/// Closing bracket for an opening one.
pub fn closer(c: char) -> Option<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) },
}
}
-390
View File
@@ -1,390 +0,0 @@
//! Named math characters: Math AutoCorrect while typing in an equation (`\alpha` + space → α)
//! and LaTeX conversion. Names are the usual TeX names plus the linear-format operators
//! (`\sqrt` → √, `\above` → ┴, `\matrix` → ■…).
/// (name without the backslash, character). When a character has several names the first one
/// listed is the one LaTeX output uses.
pub const SYMBOLS: &[(&str, char)] = &[
// Greek.
("alpha", 'α'),
("beta", 'β'),
("gamma", 'γ'),
("delta", 'δ'),
("epsilon", 'ϵ'),
("varepsilon", 'ε'),
("zeta", 'ζ'),
("eta", 'η'),
("theta", 'θ'),
("vartheta", 'ϑ'),
("iota", 'ι'),
("kappa", 'κ'),
("varkappa", 'ϰ'),
("lambda", 'λ'),
("mu", 'μ'),
("nu", 'ν'),
("xi", 'ξ'),
("omicron", 'ο'),
("pi", 'π'),
("varpi", 'ϖ'),
("rho", 'ρ'),
("varrho", 'ϱ'),
("sigma", 'σ'),
("varsigma", 'ς'),
("tau", 'τ'),
("upsilon", 'υ'),
("phi", 'ϕ'),
("varphi", 'φ'),
("chi", 'χ'),
("psi", 'ψ'),
("omega", 'ω'),
("Alpha", 'Α'),
("Beta", 'Β'),
("Gamma", 'Γ'),
("Delta", 'Δ'),
("Epsilon", 'Ε'),
("Zeta", 'Ζ'),
("Eta", 'Η'),
("Theta", 'Θ'),
("Iota", 'Ι'),
("Kappa", 'Κ'),
("Lambda", 'Λ'),
("Mu", 'Μ'),
("Nu", 'Ν'),
("Xi", 'Ξ'),
("Omicron", 'Ο'),
("Pi", 'Π'),
("Rho", 'Ρ'),
("Sigma", 'Σ'),
("Tau", 'Τ'),
("Upsilon", 'Υ'),
("Phi", 'Φ'),
("Chi", 'Χ'),
("Psi", 'Ψ'),
("Omega", 'Ω'),
("digamma", 'ϝ'),
// Binary operators.
("pm", '±'),
("mp", '∓'),
("times", '×'),
("div", '÷'),
("cdot", '⋅'),
("bullet", '∙'),
("ast", '∗'),
("star", '⋆'),
("circ", '∘'),
("oplus", '⊕'),
("ominus", '⊖'),
("otimes", '⊗'),
("oslash", '⊘'),
("odot", '⊙'),
("cup", '∪'),
("cap", '∩'),
("uplus", '⊎'),
("sqcup", '⊔'),
("sqcap", '⊓'),
("vee", '∨'),
("wedge", '∧'),
("setminus", '∖'),
("backslash", '\\'),
("wr", '≀'),
("diamond", '⋄'),
("dagger", '†'),
("ddagger", '‡'),
("amalg", '⨿'),
("dotplus", '∔'),
("ltimes", '⋉'),
("rtimes", '⋊'),
// Relations.
("le", '≤'),
("leq", '≤'),
("ge", '≥'),
("geq", '≥'),
("ne", '≠'),
("neq", '≠'),
("approx", '≈'),
("equiv", '≡'),
("sim", '∼'),
("simeq", '≃'),
("cong", '≅'),
("propto", '∝'),
("ll", '≪'),
("gg", '≫'),
("prec", '≺'),
("succ", '≻'),
("preceq", '≼'),
("succeq", '≽'),
("subset", '⊂'),
("supset", '⊃'),
("subseteq", '⊆'),
("supseteq", '⊇'),
("sqsubseteq", '⊑'),
("sqsupseteq", '⊒'),
("in", '∈'),
("ni", '∋'),
("notin", '∉'),
("perp", '⊥'),
("parallel", '∥'),
("mid", '∣'),
("nmid", '∤'),
("asymp", '≍'),
("doteq", '≐'),
("models", '⊨'),
("vdash", '⊢'),
("dashv", '⊣'),
("bowtie", '⋈'),
("coloneq", '≔'),
("defeq", '≝'),
("triangleq", '≜'),
("nless", '≮'),
("ngtr", '≯'),
("nleq", '≰'),
("ngeq", '≱'),
("nsim", '≁'),
("ncong", '≇'),
("nequiv", '≢'),
("nsubset", '⊄'),
("nsupset", '⊅'),
("nsubseteq", '⊈'),
("nsupseteq", '⊉'),
("subsetneq", '⊊'),
("supsetneq", '⊋'),
("lesssim", '≲'),
("gtrsim", '≳'),
("therefore", '∴'),
("because", '∵'),
// Arrows.
("rightarrow", '→'),
("to", '→'),
("leftarrow", '←'),
("gets", '←'),
("leftrightarrow", '↔'),
("uparrow", '↑'),
("downarrow", '↓'),
("updownarrow", '↕'),
("Rightarrow", '⇒'),
("Leftarrow", '⇐'),
("Leftrightarrow", '⇔'),
("Uparrow", '⇑'),
("Downarrow", '⇓'),
("Updownarrow", '⇕'),
("longrightarrow", '⟶'),
("longleftarrow", '⟵'),
("longleftrightarrow", '⟷'),
("Longrightarrow", '⟹'),
("Longleftarrow", '⟸'),
("Longleftrightarrow", '⟺'),
("implies", '⟹'),
("iff", '⟺'),
("mapsto", '↦'),
("longmapsto", '⟼'),
("nearrow", '↗'),
("searrow", '↘'),
("swarrow", '↙'),
("nwarrow", '↖'),
("hookleftarrow", '↩'),
("hookrightarrow", '↪'),
("leftharpoonup", '↼'),
("leftharpoondown", '↽'),
("rightharpoonup", '⇀'),
("rightharpoondown", '⇁'),
("rightleftharpoons", '⇌'),
("leftrightharpoons", '⇋'),
("circlearrowleft", '↺'),
("circlearrowright", '↻'),
// Big operators.
("sum", '∑'),
("prod", '∏'),
("coprod", '∐'),
("int", '∫'),
("iint", '∬'),
("iiint", '∭'),
("oint", '∮'),
("oiint", '∯'),
("oiiint", '∰'),
("bigcup", '⋃'),
("bigcap", '⋂'),
("bigvee", '⋁'),
("bigwedge", '⋀'),
("bigoplus", '⨁'),
("bigotimes", '⨂'),
("bigodot", '⨀'),
("biguplus", '⨄'),
("bigsqcup", '⨆'),
// Brackets.
("langle", '⟨'),
("rangle", '⟩'),
("lceil", '⌈'),
("rceil", '⌉'),
("lfloor", '⌊'),
("rfloor", '⌋'),
("lbrack", '['),
("rbrack", ']'),
("lbrace", '{'),
("rbrace", '}'),
("Vert", '‖'),
("norm", '‖'),
("vert", '|'),
("lbbrack", '⟦'),
("rbbrack", '⟧'),
// Miscellaneous.
("infty", '∞'),
("partial", '∂'),
("nabla", '∇'),
("forall", '∀'),
("exists", '∃'),
("nexists", '∄'),
("neg", '¬'),
("lnot", '¬'),
("emptyset", '∅'),
("varnothing", '∅'),
("hbar", 'ℏ'),
("ell", 'ℓ'),
("Re", 'ℜ'),
("Im", 'ℑ'),
("wp", '℘'),
("aleph", 'ℵ'),
("beth", 'ℶ'),
("gimel", 'ℷ'),
("daleth", 'ℸ'),
("degree", '°'),
("angle", '∠'),
("measuredangle", '∡'),
("sphericalangle", '∢'),
("rightangle", '∟'),
("triangle", '△'),
("square", '□'),
("blacksquare", '■'),
("qed", '∎'),
("top", '⊤'),
("bot", '⊥'),
("cdots", '⋯'),
("ldots", '…'),
("dots", '…'),
("vdots", '⋮'),
("ddots", '⋱'),
("adots", '⋰'),
("prime", '′'),
("dprime", '″'),
("trprime", '‴'),
("dd", 'ⅆ'),
("Dd", 'ⅅ'),
("ee", 'ⅇ'),
("ii", 'ⅈ'),
("jj", 'ⅉ'),
("imath", 'ı'),
("jmath", 'ȷ'),
("complement", '∁'),
("Angstrom", 'Å'),
("mho", '℧'),
("eth", 'ð'),
("flat", '♭'),
("natural", '♮'),
("sharp", '♯'),
("clubsuit", '♣'),
("diamondsuit", '♢'),
("heartsuit", '♡'),
("spadesuit", '♠'),
// Spaces.
("quad", '\u{2003}'),
("emsp", '\u{2003}'),
("ensp", '\u{2002}'),
("thicksp", '\u{2005}'),
("medsp", '\u{205F}'),
("thinsp", '\u{2009}'),
("hairsp", '\u{200A}'),
("nbsp", '\u{A0}'),
("zwsp", '\u{200B}'),
// Linear-format operators (build-up).
("sqrt", '√'),
("cbrt", '∛'),
("qdrt", '∜'),
("matrix", '■'),
("eqarray", '█'),
("above", '┴'),
("below", '┬'),
("of", '▒'),
("naryand", '▒'),
("funcapply", '\u{2061}'),
("box", '□'),
("rect", '▭'),
("overbar", '¯'),
("underbar", '▁'),
("phantom", '⟡'),
("overbrace", '⏞'),
("underbrace", '⏟'),
("overparen", '⏜'),
("underparen", '⏝'),
("atop", '¦'),
("over", '/'),
("ldiv", '∕'),
("sdiv", '⁄'),
("begin", '〖'),
("end", '〗'),
("left", '├'),
("right", '┤'),
("eqno", '#'),
("placeholder", '⬚'),
// Accents (combining, typed after the base).
("hat", '\u{302}'),
("check", '\u{30C}'),
("tilde", '\u{303}'),
("acute", '\u{301}'),
("grave", '\u{300}'),
("dot", '\u{307}'),
("ddot", '\u{308}'),
("dddot", '\u{20DB}'),
("breve", '\u{306}'),
("bar", '\u{305}'),
("Bar", '\u{33F}'),
("vec", '\u{20D7}'),
("tvec", '\u{20E1}'),
("lhvec", '\u{20D0}'),
("hvec", '\u{20D1}'),
];
/// The character for a name (`alpha` → α), including `doubleR`, `scriptL`, `frakturg`.
pub fn symbol(name: &str) -> Option<char> {
if let Some((_, c)) = SYMBOLS.iter().find(|(n, _)| *n == name) {
return Some(*c);
}
for (prefix, scr) in [("double", crate::math::MScr::DoubleStruck), ("script", crate::math::MScr::Script), ("fraktur", crate::math::MScr::Fraktur)]
{
if let Some(rest) = name.strip_prefix(prefix) {
let mut cs = rest.chars();
if let (Some(c), None) = (cs.next(), cs.next())
&& c.is_ascii_alphanumeric()
{
let m = crate::math::math_alnum(c, false, false, scr);
if m != c {
return Some(m);
}
}
}
}
None
}
/// The (first) name of a character.
pub fn name_of(c: char) -> Option<&'static str> {
SYMBOLS.iter().find(|(_, s)| *s == c).map(|(n, _)| *n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_resolve() {
assert_eq!(symbol("alpha"), Some('α'));
assert_eq!(symbol("doubleR"), Some('ℝ'));
assert_eq!(symbol("scriptL"), Some('ℒ'));
assert_eq!(symbol("nonesuch"), None);
assert_eq!(name_of('≤'), Some("le"));
// No name is listed twice.
let mut names: Vec<&str> = SYMBOLS.iter().map(|(n, _)| *n).collect();
names.sort_unstable();
let before = names.len();
names.dedup();
assert_eq!(before, names.len());
}
}
-104
View File
@@ -1,104 +0,0 @@
//! Structural access to an equation tree: the arguments of a node, and every run in a tree.
use crate::math::{Arg, MAX_DEPTH, MNode, MRun};
/// Number of child arguments of a node.
pub fn child_count(n: &MNode) -> usize {
match n {
MNode::Run(_) => 0,
MNode::Frac { .. } | MNode::Func { .. } | MNode::Lim { .. } => 2,
MNode::Rad { .. } => 2,
MNode::Script { .. } | MNode::Nary { .. } => 3,
MNode::Delim { elems, .. } => elems.len(),
MNode::EqArr { rows } => rows.len(),
MNode::Matrix { rows, .. } => rows.iter().map(|r| r.len()).sum(),
MNode::Acc { .. } | MNode::Bar { .. } | MNode::BorderBox { .. } | MNode::Boxed { .. } | MNode::GroupChr { .. } | MNode::Phant { .. } => 1,
}
}
/// Child argument `i` of a node (see [`child_count`] for the numbering: fraction num 0 / den 1;
/// script base 0 / sub 1 / sup 2; radical degree 0 / base 1; n-ary sub 0 / sup 1 / operand 2;
/// function name 0 / argument 1; limit base 0 / limit 1; matrices row by row).
pub fn child(n: &MNode, i: usize) -> Option<&Arg> {
match (n, i) {
(MNode::Frac { num, .. }, 0) => Some(num),
(MNode::Frac { den, .. }, 1) => Some(den),
(MNode::Script { base, .. }, 0) => Some(base),
(MNode::Script { sub, .. }, 1) => Some(sub),
(MNode::Script { sup, .. }, 2) => Some(sup),
(MNode::Rad { deg, .. }, 0) => Some(deg),
(MNode::Rad { e, .. }, 1) => Some(e),
(MNode::Nary { sub, .. }, 0) => Some(sub),
(MNode::Nary { sup, .. }, 1) => Some(sup),
(MNode::Nary { e, .. }, 2) => Some(e),
(MNode::Delim { elems, .. }, i) => elems.get(i),
(MNode::Func { name, .. }, 0) => Some(name),
(MNode::Func { e, .. }, 1) => Some(e),
(MNode::Lim { e, .. }, 0) => Some(e),
(MNode::Lim { lim, .. }, 1) => Some(lim),
(
MNode::Acc { e, .. }
| MNode::Bar { e, .. }
| MNode::BorderBox { e, .. }
| MNode::Boxed { e }
| MNode::GroupChr { e, .. }
| MNode::Phant { e, .. },
0,
) => Some(e),
(MNode::EqArr { rows }, i) => rows.get(i),
(MNode::Matrix { rows, .. }, i) => rows.iter().flatten().nth(i),
_ => None,
}
}
pub fn child_mut(n: &mut MNode, i: usize) -> Option<&mut Arg> {
match (n, i) {
(MNode::Frac { num, .. }, 0) => Some(num),
(MNode::Frac { den, .. }, 1) => Some(den),
(MNode::Script { base, .. }, 0) => Some(base),
(MNode::Script { sub, .. }, 1) => Some(sub),
(MNode::Script { sup, .. }, 2) => Some(sup),
(MNode::Rad { deg, .. }, 0) => Some(deg),
(MNode::Rad { e, .. }, 1) => Some(e),
(MNode::Nary { sub, .. }, 0) => Some(sub),
(MNode::Nary { sup, .. }, 1) => Some(sup),
(MNode::Nary { e, .. }, 2) => Some(e),
(MNode::Delim { elems, .. }, i) => elems.get_mut(i),
(MNode::Func { name, .. }, 0) => Some(name),
(MNode::Func { e, .. }, 1) => Some(e),
(MNode::Lim { e, .. }, 0) => Some(e),
(MNode::Lim { lim, .. }, 1) => Some(lim),
(
MNode::Acc { e, .. }
| MNode::Bar { e, .. }
| MNode::BorderBox { e, .. }
| MNode::Boxed { e }
| MNode::GroupChr { e, .. }
| MNode::Phant { e, .. },
0,
) => Some(e),
(MNode::EqArr { rows }, i) => rows.get_mut(i),
(MNode::Matrix { rows, .. }, i) => rows.iter_mut().flatten().nth(i),
_ => None,
}
}
/// Call `f` on every run in the tree.
pub fn each_run_mut(root: &mut Arg, f: &mut dyn FnMut(&mut MRun)) {
fn go(a: &mut Arg, f: &mut dyn FnMut(&mut MRun), depth: usize) {
if depth > MAX_DEPTH {
return;
}
for n in a.iter_mut() {
if let MNode::Run(r) = n {
f(r);
}
for c in 0..child_count(n) {
if let Some(ch) = child_mut(n, c) {
go(ch, f, depth + 1);
}
}
}
}
go(root, f, 0);
}
+2 -132
View File
@@ -1,83 +1,7 @@
use super::{Deserialize, Serialize};
/// An sRGB colour.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Default)]
pub struct Rgb(pub u8, pub u8, pub u8);
impl<'de> Deserialize<'de> for Rgb {
/// Reads what it writes (`[r, g, b]`) and also a string [`Rgb::parse`] reads, so the colours in
/// `CharProps` / `ParaProps` take the `RRGGBB` that `para.borders` and `table.shading` take (#865).
fn deserialize<D: 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
}
}
/// An sRGB colour: craft-math's, which equations and text share.
pub use craft_math::Rgb;
/// A text colour: automatic (black on light backgrounds, white on dark) or a fixed colour.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Default)]
@@ -115,57 +39,3 @@ impl<'de> Deserialize<'de> for TextColor {
}
}
}
/// `tint` (towards `to` = 255) and `shade` (towards 0): `v` is the share of the original colour kept.
fn mix(c: Rgb, to: f64, v: f64) -> Rgb {
let f = |x: u8| (x as f64 * v + to * (1.0 - v)).round().clamp(0.0, 255.0) as u8;
Rgb(f(c.0), f(c.1), f(c.2))
}
/// `c` with its HSL luminance changed by `f`.
fn adjust_lum(c: Rgb, f: impl Fn(f64) -> f64) -> Rgb {
let (h, s, l) = to_hsl(c);
from_hsl(h, s, f(l).clamp(0.0, 1.0))
}
fn to_hsl(c: Rgb) -> (f64, f64, f64) {
let (r, g, b) = (c.0 as f64 / 255.0, c.1 as f64 / 255.0, c.2 as f64 / 255.0);
let (max, min) = (r.max(g).max(b), r.min(g).min(b));
let l = (max + min) / 2.0;
if max == min {
return (0.0, 0.0, l);
}
let d = max - min;
let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
let h = if max == r {
(g - b) / d + if g < b { 6.0 } else { 0.0 }
} else if max == g {
(b - r) / d + 2.0
} else {
(r - g) / d + 4.0
};
(h / 6.0, s, l)
}
fn from_hsl(h: f64, s: f64, l: f64) -> Rgb {
let byte = |v: f64| (v * 255.0).round().clamp(0.0, 255.0) as u8;
if s == 0.0 {
return Rgb(byte(l), byte(l), byte(l));
}
let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s };
let p = 2.0 * l - q;
let channel = |t: f64| {
let t = t.rem_euclid(1.0);
let v = if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 0.5 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
};
byte(v)
};
Rgb(channel(h + 1.0 / 3.0), channel(h), channel(h - 1.0 / 3.0))
}
-316
View File
@@ -1,316 +0,0 @@
//! Word's symbol fonts — Symbol, Wingdings, Wingdings 2, Wingdings 3 and Webdings — mapped to
//! standard Unicode characters, so text set in them shows the intended symbols when the font
//! isn't installed (they are proprietary and never bundled) and exports as real characters.
//!
//! Word stores a symbol-font character either as the private-use code U+F000 + the font's own
//! 8-bit code (U+F020–U+F0FF, what Insert › Symbol and `w:sym` write) or as the plain 8-bit code
//! (U+0020–U+00FF) in a run whose font is the symbol font. Both map the same way. The stored text
//! never changes: the mapping is for display, layout and plain-text output only.
//!
//! Sources (Unicode's own published data; no font file was read):
//! - Symbol: the Adobe Symbol encoding table published by Unicode,
//! <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}");
}
}