Skip to main content

term_maths/
mathfont.rs

1//! Unicode Mathematical Alphanumeric Symbols mapping (U+1D400–U+1D7FF).
2//!
3//! Maps ASCII Latin letters (and digits) to their styled variants in the
4//! Unicode Mathematical Alphanumeric Symbols block, keyed by `MathFontKind`.
5
6use rust_latex_parser::MathFontKind;
7
8/// Convert a character to its mathematical font variant.
9/// Returns the original character if no mapping exists.
10pub fn map_char(kind: &MathFontKind, ch: char) -> char {
11    match kind {
12        MathFontKind::Bold => to_bold(ch),
13        MathFontKind::Blackboard => to_double_struck(ch),
14        MathFontKind::Calligraphic => to_script(ch),
15        MathFontKind::Fraktur => to_fraktur(ch),
16        MathFontKind::Roman => ch, // upright, no transformation
17        MathFontKind::SansSerif => to_sans_serif(ch),
18        MathFontKind::Monospace => to_monospace(ch),
19    }
20}
21
22/// Convert a string by mapping each character through the font transform.
23pub fn map_str(kind: &MathFontKind, s: &str) -> String {
24    s.chars().map(|c| map_char(kind, c)).collect()
25}
26
27// U+1D400 MATHEMATICAL BOLD CAPITAL A .. U+1D419 MATHEMATICAL BOLD CAPITAL Z
28// U+1D41A MATHEMATICAL BOLD SMALL A .. U+1D433 MATHEMATICAL BOLD SMALL Z
29// U+1D7CE MATHEMATICAL BOLD DIGIT ZERO .. U+1D7D7 MATHEMATICAL BOLD DIGIT NINE
30fn to_bold(ch: char) -> char {
31    match ch {
32        'A'..='Z' => char::from_u32(0x1D400 + (ch as u32 - 'A' as u32)).unwrap_or(ch),
33        'a'..='z' => char::from_u32(0x1D41A + (ch as u32 - 'a' as u32)).unwrap_or(ch),
34        '0'..='9' => char::from_u32(0x1D7CE + (ch as u32 - '0' as u32)).unwrap_or(ch),
35        // Bold Greek uppercase: U+1D6A8–U+1D6C0
36        'Α'..='Ω' => char::from_u32(0x1D6A8 + (ch as u32 - 'Α' as u32)).unwrap_or(ch),
37        // Bold Greek lowercase: U+1D6C2–U+1D6DA
38        'α'..='ω' => char::from_u32(0x1D6C2 + (ch as u32 - 'α' as u32)).unwrap_or(ch),
39        _ => ch,
40    }
41}
42
43// U+1D538 MATHEMATICAL DOUBLE-STRUCK CAPITAL A .. U+1D551
44// Exceptions: C=ℂ, H=ℍ, N=ℕ, P=ℙ, Q=ℚ, R=ℝ, Z=ℤ (in Letterlike Symbols block)
45// U+1D552 MATHEMATICAL DOUBLE-STRUCK SMALL A .. U+1D56B
46// U+1D7D8 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO .. U+1D7E1
47fn to_double_struck(ch: char) -> char {
48    match ch {
49        'C' => 'ℂ',
50        'H' => 'ℍ',
51        'N' => 'ℕ',
52        'P' => 'ℙ',
53        'Q' => 'ℚ',
54        'R' => 'ℝ',
55        'Z' => 'ℤ',
56        'A' | 'B' | 'D'..='G' | 'I'..='M' | 'O' | 'S'..='Y' => {
57            char::from_u32(0x1D538 + (ch as u32 - 'A' as u32)).unwrap_or(ch)
58        }
59        'a'..='z' => char::from_u32(0x1D552 + (ch as u32 - 'a' as u32)).unwrap_or(ch),
60        '0'..='9' => char::from_u32(0x1D7D8 + (ch as u32 - '0' as u32)).unwrap_or(ch),
61        _ => ch,
62    }
63}
64
65// U+1D49C MATHEMATICAL SCRIPT CAPITAL A .. U+1D4B5
66// Exceptions: B=ℬ, E=ℰ, F=ℱ, H=ℋ, I=ℐ, L=ℒ, M=ℳ, R=ℛ (Letterlike Symbols)
67// U+1D4B6 MATHEMATICAL SCRIPT SMALL A .. U+1D4CF
68// Exceptions: e=ℯ, g=ℊ, o=ℴ
69fn to_script(ch: char) -> char {
70    match ch {
71        'B' => 'ℬ',
72        'E' => 'ℰ',
73        'F' => 'ℱ',
74        'H' => 'ℋ',
75        'I' => 'ℐ',
76        'L' => 'ℒ',
77        'M' => 'ℳ',
78        'R' => 'ℛ',
79        'e' => 'ℯ',
80        'g' => 'ℊ',
81        'o' => 'ℴ',
82        'A' | 'C' | 'D' | 'G' | 'J' | 'K' | 'N'..='Q' | 'S'..='Z' => {
83            char::from_u32(0x1D49C + (ch as u32 - 'A' as u32)).unwrap_or(ch)
84        }
85        'a'..='d' | 'f' | 'h'..='n' | 'p'..='z' => {
86            char::from_u32(0x1D4B6 + (ch as u32 - 'a' as u32)).unwrap_or(ch)
87        }
88        _ => ch,
89    }
90}
91
92// U+1D504 MATHEMATICAL FRAKTUR CAPITAL A .. U+1D51C
93// Exceptions: C=ℭ, H=ℌ, I=ℑ, R=ℜ, Z=ℨ
94// U+1D51E MATHEMATICAL FRAKTUR SMALL A .. U+1D537
95fn to_fraktur(ch: char) -> char {
96    match ch {
97        'C' => 'ℭ',
98        'H' => 'ℌ',
99        'I' => 'ℑ',
100        'R' => 'ℜ',
101        'Z' => 'ℨ',
102        'A' | 'B' | 'D'..='G' | 'J'..='Q' | 'S'..='Y' => {
103            char::from_u32(0x1D504 + (ch as u32 - 'A' as u32)).unwrap_or(ch)
104        }
105        'a'..='z' => char::from_u32(0x1D51E + (ch as u32 - 'a' as u32)).unwrap_or(ch),
106        _ => ch,
107    }
108}
109
110// U+1D5A0 MATHEMATICAL SANS-SERIF CAPITAL A .. U+1D5B9
111// U+1D5BA MATHEMATICAL SANS-SERIF SMALL A .. U+1D5D3
112// U+1D7E2 MATHEMATICAL SANS-SERIF DIGIT ZERO .. U+1D7EB
113fn to_sans_serif(ch: char) -> char {
114    match ch {
115        'A'..='Z' => char::from_u32(0x1D5A0 + (ch as u32 - 'A' as u32)).unwrap_or(ch),
116        'a'..='z' => char::from_u32(0x1D5BA + (ch as u32 - 'a' as u32)).unwrap_or(ch),
117        '0'..='9' => char::from_u32(0x1D7E2 + (ch as u32 - '0' as u32)).unwrap_or(ch),
118        _ => ch,
119    }
120}
121
122// U+1D670 MATHEMATICAL MONOSPACE CAPITAL A .. U+1D689
123// U+1D68A MATHEMATICAL MONOSPACE SMALL A .. U+1D6A3
124// U+1D7F6 MATHEMATICAL MONOSPACE DIGIT ZERO .. U+1D7FF
125fn to_monospace(ch: char) -> char {
126    match ch {
127        'A'..='Z' => char::from_u32(0x1D670 + (ch as u32 - 'A' as u32)).unwrap_or(ch),
128        'a'..='z' => char::from_u32(0x1D68A + (ch as u32 - 'a' as u32)).unwrap_or(ch),
129        '0'..='9' => char::from_u32(0x1D7F6 + (ch as u32 - '0' as u32)).unwrap_or(ch),
130        _ => ch,
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_bold() {
140        assert_eq!(to_bold('A'), '𝐀');
141        assert_eq!(to_bold('Z'), '𝐙');
142        assert_eq!(to_bold('a'), '𝐚');
143        assert_eq!(to_bold('z'), '𝐳');
144        assert_eq!(to_bold('0'), '𝟎');
145    }
146
147    #[test]
148    fn test_double_struck() {
149        assert_eq!(to_double_struck('R'), 'ℝ');
150        assert_eq!(to_double_struck('Z'), 'ℤ');
151        assert_eq!(to_double_struck('N'), 'ℕ');
152        assert_eq!(to_double_struck('C'), 'ℂ');
153        assert_eq!(to_double_struck('Q'), 'ℚ');
154        // Non-exception uppercase
155        assert_eq!(to_double_struck('A'), '𝔸');
156    }
157
158    #[test]
159    fn test_script() {
160        assert_eq!(to_script('L'), 'ℒ');
161        assert_eq!(to_script('H'), 'ℋ');
162        assert_eq!(to_script('B'), 'ℬ');
163        // Non-exception
164        assert_eq!(to_script('A'), '𝒜');
165    }
166
167    #[test]
168    fn test_fraktur() {
169        assert_eq!(to_fraktur('H'), 'ℌ');
170        assert_eq!(to_fraktur('R'), 'ℜ');
171        assert_eq!(to_fraktur('a'), '𝔞');
172        assert_eq!(to_fraktur('g'), '𝔤');
173    }
174
175    #[test]
176    fn test_sans_serif() {
177        assert_eq!(to_sans_serif('A'), '𝖠');
178        assert_eq!(to_sans_serif('a'), '𝖺');
179    }
180
181    #[test]
182    fn test_monospace() {
183        assert_eq!(to_monospace('A'), '𝙰');
184        assert_eq!(to_monospace('a'), '𝚊');
185        assert_eq!(to_monospace('0'), '𝟶');
186    }
187
188    #[test]
189    fn test_non_letter_passthrough() {
190        // Non-letter characters should pass through unchanged
191        assert_eq!(map_char(&MathFontKind::Bold, '+'), '+');
192        assert_eq!(map_char(&MathFontKind::Blackboard, ' '), ' ');
193    }
194}