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.
This commit is contained in:
Dylan M. Taylor
2026-11-12 00:51:55 +00:00
parent 1612eb6224
commit c87877cd43
9 changed files with 6 additions and 4090 deletions
+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}");
}
}