use crate::hbox::HorzStringInfo;
use crate::length::Length;
use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MathKind {
Ord,
Bin,
Rel,
Op,
Punct,
Open,
Close,
Prefix,
Inner,
End,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MathGlyph {
pub info: HorzStringInfo,
pub text: String,
pub gid: Option<u16>,
pub dx: Length,
pub dy: Length,
pub width: Length,
pub height: Length,
pub depth: Length,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MathCharClass {
Italic,
BoldItalic,
Roman,
BoldRoman,
Script,
BoldScript,
Fraktur,
BoldFraktur,
DoubleStruck,
SansSerif,
BoldSansSerif,
ItalicSansSerif,
BoldItalicSansSerif,
Typewriter,
}
pub(crate) fn default_math_class_map() -> BTreeMap<String, (String, MathKind)> {
[
("=", "=", MathKind::Rel),
("<", "<", MathKind::Rel),
(">", ">", MathKind::Rel),
(":", ":", MathKind::Rel),
("+", "+", MathKind::Bin),
("-", "\u{2212}", MathKind::Bin),
("|", "|", MathKind::Bin),
("/", "/", MathKind::Ord),
(",", ",", MathKind::Punct),
]
.into_iter()
.map(|(k, v, mk)| (k.to_string(), (v.to_string(), mk)))
.collect()
}
pub fn default_math_variant_char(class: MathCharClass, c: char) -> Option<char> {
let cap = c.is_ascii_uppercase().then(|| c as u32 - 'A' as u32);
let small = c.is_ascii_lowercase().then(|| c as u32 - 'a' as u32);
fn cp(base: u32, i: u32) -> Option<char> {
char::from_u32(base + i)
}
match class {
MathCharClass::Italic => match (cap, small) {
(Some(i), _) => cp(0x1D434, i),
(_, Some(7)) => Some('\u{210E}'),
(_, Some(i)) => cp(0x1D44E, i),
_ => None,
},
MathCharClass::BoldItalic => match (cap, small) {
(Some(i), _) => cp(0x1D468, i),
(_, Some(i)) => cp(0x1D482, i),
_ => None,
},
MathCharClass::Roman => (cap.is_some() || small.is_some()).then_some(c),
MathCharClass::BoldRoman => match (cap, small) {
(Some(i), _) => cp(0x1D400, i),
(_, Some(i)) => cp(0x1D41A, i),
_ => None,
},
MathCharClass::Script => match c {
'B' => Some('\u{212C}'),
'E' => Some('\u{2130}'),
'F' => Some('\u{2131}'),
'H' => Some('\u{210B}'),
'I' => Some('\u{2110}'),
'L' => Some('\u{2112}'),
'M' => Some('\u{2133}'),
'R' => Some('\u{211B}'),
'e' => Some('\u{212F}'),
'g' => Some('\u{210A}'),
'o' => Some('\u{2134}'),
_ => match (cap, small) {
(Some(i), _) => cp(0x1D49C, i),
(_, Some(i)) => cp(0x1D4B6, i),
_ => None,
},
},
MathCharClass::BoldScript => match (cap, small) {
(Some(i), _) => cp(0x1D4D0, i),
(_, Some(i)) => cp(0x1D4EA, i),
_ => None,
},
MathCharClass::Fraktur => match c {
'C' => Some('\u{212D}'),
'H' => Some('\u{210C}'),
'I' => Some('\u{2111}'),
'R' => Some('\u{211C}'),
'Z' => Some('\u{2128}'),
_ => match (cap, small) {
(Some(i), _) => cp(0x1D504, i),
(_, Some(i)) => cp(0x1D51E, i),
_ => None,
},
},
MathCharClass::BoldFraktur => match (cap, small) {
(Some(i), _) => cp(0x1D56C, i),
(_, Some(i)) => cp(0x1D586, i),
_ => None,
},
MathCharClass::DoubleStruck => match c {
'C' => Some('\u{2102}'),
'H' => Some('\u{210D}'),
'N' => Some('\u{2115}'),
'P' => Some('\u{2119}'),
'Q' => Some('\u{211A}'),
'R' => Some('\u{211D}'),
'Z' => Some('\u{2124}'),
_ => match (cap, small) {
(Some(i), _) => cp(0x1D538, i),
(_, Some(i)) => cp(0x1D552, i),
_ => None,
},
},
MathCharClass::SansSerif => match (cap, small) {
(Some(i), _) => cp(0x1D5A0, i),
(_, Some(i)) => cp(0x1D5BA, i),
_ => None,
},
MathCharClass::BoldSansSerif => match (cap, small) {
(Some(i), _) => cp(0x1D5D4, i),
(_, Some(i)) => cp(0x1D5EE, i),
_ => None,
},
MathCharClass::ItalicSansSerif => match (cap, small) {
(Some(i), _) => cp(0x1D608, i),
(_, Some(i)) => cp(0x1D622, i),
_ => None,
},
MathCharClass::BoldItalicSansSerif => match (cap, small) {
(Some(i), _) => cp(0x1D63C, i),
(_, Some(i)) => cp(0x1D656, i),
_ => None,
},
MathCharClass::Typewriter => match (cap, small) {
(Some(i), _) => cp(0x1D670, i),
(_, Some(i)) => cp(0x1D68A, i),
_ => None,
},
}
}
pub fn math_alphanumeric_base(c: char) -> Option<char> {
let u = c as u32;
let letterlike = |u: u32| -> Option<char> {
Some(match u {
0x210E => 'h', 0x212C => 'B', 0x2130 => 'E',
0x2131 => 'F',
0x210B => 'H',
0x2110 => 'I',
0x2112 => 'L',
0x2133 => 'M',
0x211B => 'R',
0x212F => 'e', 0x210A => 'g',
0x2134 => 'o',
0x2113 => 'l', 0x212D => 'C', 0x210C => 'H',
0x2111 => 'I',
0x211C => 'R',
0x2128 => 'Z',
0x2102 => 'C', 0x210D => 'H',
0x2115 => 'N',
0x2119 => 'P',
0x211A => 'Q',
0x211D => 'R',
0x2124 => 'Z',
0x210F => '\u{127}',
0x2139 => 'i',
0x213C => '\u{3C0}', 0x213D => '\u{3B3}',
0x213E => '\u{393}',
0x213F => '\u{3A0}',
0x2140 => '\u{2211}',
0x2145 => 'D', 0x2146 => 'd',
0x2147 => 'e',
0x2148 => 'i',
0x2149 => 'j',
_ => return None,
})
};
if let Some(base) = letterlike(u) {
return Some(base);
}
if !(0x1D400..=0x1D7FF).contains(&u) {
return None;
}
if u < 0x1D6A4 {
let off = (u - 0x1D400) % 52;
return Some(if off < 26 {
(b'A' + off as u8) as char
} else {
(b'a' + (off - 26) as u8) as char
});
}
if u == 0x1D6A4 {
return Some('\u{131}');
}
if u == 0x1D6A5 {
return Some('\u{237}');
}
if u < 0x1D6A8 {
return None; }
if u < 0x1D7CA {
let off = (u - 0x1D6A8) % 58;
return Some(match off {
0..=16 => char::from_u32(0x391 + off)?,
17 => '\u{3F4}',
18..=24 => char::from_u32(0x3A3 + (off - 18))?,
25 => '\u{2207}', 26..=50 => char::from_u32(0x3B1 + (off - 26))?,
51 => '\u{2202}', 52 => '\u{3F5}', 53 => '\u{3D1}', 54 => '\u{3F0}', 55 => '\u{3D5}', 56 => '\u{3F1}', 57 => '\u{3D6}', _ => unreachable!("offset is `% 58`"),
});
}
if u == 0x1D7CA {
return Some('\u{3DC}');
}
if u == 0x1D7CB {
return Some('\u{3DD}');
}
if u < 0x1D7CE {
return None; }
let off = (u - 0x1D7CE) % 10;
Some((b'0' + off as u8) as char)
}
#[cfg(test)]
mod alphanumeric_base_tests {
use super::*;
#[test]
fn alphanumeric_block_strides_tile_the_block() {
assert_eq!(0x1D400 + 13 * 52, 0x1D6A4, "13 alphabetic runs of 52");
assert_eq!(0x1D6A8 + 5 * 58, 0x1D7CA, "5 Greek runs of 58");
assert_eq!(0x1D7CE + 5 * 10, 0x1D800, "5 digit runs of 10");
}
#[test]
fn every_alphabetic_run_decomposes_to_a_z_a_z() {
let expected = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
assert_eq!(expected.chars().count(), 52);
for run in 0..13u32 {
let base = 0x1D400 + run * 52;
let got: String = (0..52)
.map(|i| math_alphanumeric_base(char::from_u32(base + i).unwrap()).unwrap())
.collect();
assert_eq!(got, expected, "run {run} at U+{base:04X}");
}
}
#[test]
fn every_greek_run_decomposes_to_the_same_58_characters() {
let expected = "\u{391}\u{392}\u{393}\u{394}\u{395}\u{396}\u{397}\u{398}\u{399}\u{39A}\
\u{39B}\u{39C}\u{39D}\u{39E}\u{39F}\u{3A0}\u{3A1}\u{3F4}\u{3A3}\u{3A4}\
\u{3A5}\u{3A6}\u{3A7}\u{3A8}\u{3A9}\u{2207}\u{3B1}\u{3B2}\u{3B3}\u{3B4}\
\u{3B5}\u{3B6}\u{3B7}\u{3B8}\u{3B9}\u{3BA}\u{3BB}\u{3BC}\u{3BD}\u{3BE}\
\u{3BF}\u{3C0}\u{3C1}\u{3C2}\u{3C3}\u{3C4}\u{3C5}\u{3C6}\u{3C7}\u{3C8}\
\u{3C9}\u{2202}\u{3F5}\u{3D1}\u{3F0}\u{3D5}\u{3F1}\u{3D6}";
assert_eq!(expected.chars().count(), 58);
for run in 0..5u32 {
let base = 0x1D6A8 + run * 58;
let got: String = (0..58)
.map(|i| math_alphanumeric_base(char::from_u32(base + i).unwrap()).unwrap())
.collect();
assert_eq!(got, expected, "Greek run {run} at U+{base:04X}");
}
}
#[test]
fn every_digit_run_decomposes_to_ascii_digits() {
for run in 0..5u32 {
let base = 0x1D7CE + run * 10;
let got: String = (0..10)
.map(|i| math_alphanumeric_base(char::from_u32(base + i).unwrap()).unwrap())
.collect();
assert_eq!(got, "0123456789", "digit run {run} at U+{base:04X}");
assert!(
got.chars().all(|c| c.is_ascii_digit()),
"a styled digit must never decompose to a letter"
);
}
}
#[test]
fn letterlike_table_is_exactly_unicodes_font_set() {
let expected: &[(u32, char)] = &[
(0x2102, 'C'), (0x210A, 'g'), (0x210B, 'H'), (0x210C, 'H'), (0x210D, 'H'), (0x210E, 'h'), (0x210F, '\u{127}'), (0x2110, 'I'), (0x2111, 'I'), (0x2112, 'L'), (0x2113, 'l'), (0x2115, 'N'), (0x2119, 'P'), (0x211A, 'Q'), (0x211B, 'R'), (0x211C, 'R'), (0x211D, 'R'), (0x2124, 'Z'), (0x2128, 'Z'), (0x212C, 'B'), (0x212D, 'C'), (0x212F, 'e'), (0x2130, 'E'), (0x2131, 'F'), (0x2133, 'M'), (0x2134, 'o'), (0x2139, 'i'), (0x213C, '\u{3C0}'), (0x213D, '\u{3B3}'), (0x213E, '\u{393}'), (0x213F, '\u{3A0}'), (0x2140, '\u{2211}'), (0x2145, 'D'), (0x2146, 'd'), (0x2147, 'e'), (0x2148, 'i'), (0x2149, 'j'), ];
assert_eq!(expected.len(), 37, "Unicode 14.0 has 37 of them");
for &(u, base) in expected {
let c = char::from_u32(u).unwrap();
assert_eq!(
math_alphanumeric_base(c),
Some(base),
"U+{u:04X} should decompose to {base:?}"
);
}
for u in 0x2100..0x2150u32 {
if expected.iter().any(|&(e, _)| e == u) {
continue;
}
let c = char::from_u32(u).unwrap();
assert_eq!(
math_alphanumeric_base(c),
None,
"U+{u:04X} has no <font> decomposition and must not be remapped"
);
}
}
#[test]
fn it_inverts_default_math_variant_char_for_every_class_and_letter() {
let classes = [
MathCharClass::Italic,
MathCharClass::BoldItalic,
MathCharClass::Roman,
MathCharClass::BoldRoman,
MathCharClass::Script,
MathCharClass::BoldScript,
MathCharClass::Fraktur,
MathCharClass::BoldFraktur,
MathCharClass::DoubleStruck,
MathCharClass::SansSerif,
MathCharClass::BoldSansSerif,
MathCharClass::ItalicSansSerif,
MathCharClass::BoldItalicSansSerif,
MathCharClass::Typewriter,
];
for class in classes {
for c in ('A'..='Z').chain('a'..='z') {
let Some(styled) = default_math_variant_char(class, c) else {
continue;
};
if styled == c {
continue; }
assert_eq!(
math_alphanumeric_base(styled),
Some(c),
"{class:?} {c:?} -> U+{:04X} did not invert",
styled as u32
);
}
}
}
#[test]
fn greek_runs_decompose_including_their_irregular_slots() {
assert_eq!(math_alphanumeric_base('\u{1D70B}'), Some('\u{3C0}'));
assert_eq!(math_alphanumeric_base('\u{1D745}'), Some('\u{3C0}'));
assert_eq!(math_alphanumeric_base('\u{1D6A8}'), Some('\u{391}'));
assert_eq!(math_alphanumeric_base('\u{1D6C0}'), Some('\u{3A9}'));
assert_eq!(math_alphanumeric_base('\u{1D6B9}'), Some('\u{3F4}'));
assert_eq!(math_alphanumeric_base('\u{1D6C1}'), Some('\u{2207}'));
assert_eq!(math_alphanumeric_base('\u{1D6DB}'), Some('\u{2202}'));
assert_eq!(math_alphanumeric_base('\u{1D7C9}'), Some('\u{3D6}'));
}
#[test]
fn non_letter_runs_and_the_none_cases() {
assert_eq!(math_alphanumeric_base('\u{1D7CE}'), Some('0'));
assert_eq!(math_alphanumeric_base('\u{1D7FF}'), Some('9'));
assert_eq!(math_alphanumeric_base('\u{1D6A4}'), Some('\u{131}'));
assert_eq!(math_alphanumeric_base('\u{1D6A5}'), Some('\u{237}'));
assert_eq!(math_alphanumeric_base('\u{1D7CA}'), Some('\u{3DC}'));
assert_eq!(math_alphanumeric_base('\u{1D6A6}'), None);
assert_eq!(math_alphanumeric_base('\u{1D7CC}'), None);
for c in ['a', 'Z', '0', 'π', '∑', '∫', '√', '±', '∞', '→', '(', ' '] {
assert_eq!(math_alphanumeric_base(c), None, "{c:?} is not a remap");
}
}
#[test]
fn letterlike_holes_decompose_to_their_plain_letter() {
assert_eq!(math_alphanumeric_base('\u{210E}'), Some('h'));
assert_eq!(math_alphanumeric_base('\u{212C}'), Some('B'));
assert_eq!(math_alphanumeric_base('\u{2112}'), Some('L'));
assert_eq!(math_alphanumeric_base('\u{211C}'), Some('R'));
assert_eq!(math_alphanumeric_base('\u{2115}'), Some('N'));
assert_eq!(math_alphanumeric_base('\u{2147}'), Some('e'));
}
}