mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
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:
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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) },
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user