1pub fn is_cjk(ch: char) -> bool {
14 matches!(ch,
15 '\u{3040}'..='\u{309F}' |
17 '\u{30A0}'..='\u{30FF}' |
19 '\u{02EA}'..='\u{02EB}' |
21 '\u{3105}'..='\u{312F}' |
22 '\u{31A0}'..='\u{31BF}' |
23 '\u{1100}'..='\u{11FF}' |
25 '\u{302E}'..='\u{302F}' |
26 '\u{3131}'..='\u{318E}' |
27 '\u{3200}'..='\u{321E}' |
28 '\u{3260}'..='\u{327E}' |
29 '\u{A960}'..='\u{A97C}' |
30 '\u{AC00}'..='\u{D7A3}' |
31 '\u{D7B0}'..='\u{D7C6}' |
32 '\u{D7CB}'..='\u{D7FB}' |
33 '\u{FFA0}'..='\u{FFBE}' |
34 '\u{FFC2}'..='\u{FFC7}' |
35 '\u{FFCA}'..='\u{FFCF}' |
36 '\u{FFD2}'..='\u{FFD7}' |
37 '\u{FFDA}'..='\u{FFDC}' |
38 '\u{2E80}'..='\u{2EFF}' |
40 '\u{2F00}'..='\u{2FDF}' |
42 '\u{2FF0}'..='\u{2FFF}' |
44 '\u{3000}'..='\u{303F}' |
46 '\u{3400}'..='\u{4DBF}' |
48 '\u{4E00}'..='\u{9FFF}' |
50 '\u{A000}'..='\u{A48F}' |
52 '\u{A490}'..='\u{A4CF}' |
54 '\u{F900}'..='\u{FAFF}' |
56 '\u{FE30}'..='\u{FE4F}' |
58 '\u{20000}'..='\u{3134F}'
60 )
61}
62
63pub fn is_emoji(ch: char) -> bool {
65 matches!(ch,
66 '\u{1F600}'..='\u{1F64F}' |
68 '\u{1F300}'..='\u{1F5FF}' |
70 '\u{1F680}'..='\u{1F6FF}' |
72 '\u{1F900}'..='\u{1F9FF}' |
74 '\u{1FA00}'..='\u{1FA6F}' |
76 '\u{1FA70}'..='\u{1FAFF}' |
77 '\u{1F1E0}'..='\u{1F1FF}' |
79 '\u{2702}'..='\u{27B0}' |
81 '\u{2600}'..='\u{26FF}'
83 )
84}
85
86pub type FamilyEntry = (String, fn(char) -> bool);
88
89pub struct FallbackChain {
97 families: Vec<FamilyEntry>,
99}
100
101fn accept_all(_ch: char) -> bool {
103 true
104}
105
106impl FallbackChain {
107 pub fn default_chain() -> Self {
113 Self {
114 families: vec![
115 ("Noto Sans CJK".to_owned(), is_cjk as fn(char) -> bool),
116 ("Noto Emoji".to_owned(), is_emoji as fn(char) -> bool),
117 ("DejaVu Sans".to_owned(), accept_all as fn(char) -> bool),
118 ],
119 }
120 }
121
122 pub fn add_family(&mut self, family: String) {
126 let len = self.families.len();
128 let insert_pos = if len > 0 { len - 1 } else { 0 };
129 self.families
130 .insert(insert_pos, (family, accept_all as fn(char) -> bool));
131 }
132
133 pub fn resolve_glyph(&self, ch: char) -> Option<&str> {
138 for (family, predicate) in &self.families {
139 if predicate(ch) {
140 return Some(family.as_str());
141 }
142 }
143 None
144 }
145
146 pub fn families(&self) -> &[FamilyEntry] {
148 &self.families
149 }
150}
151
152#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn fallback_cjk_detected() {
160 assert!(is_cjk('中'), "'中' must be CJK");
161 assert!(is_cjk('あ'), "'あ' (Hiragana) must be CJK");
162 assert!(!is_cjk('a'), "'a' must not be CJK");
163 assert!(!is_cjk('!'), "'!' must not be CJK");
164 }
165
166 #[test]
167 fn fallback_emoji_detected() {
168 assert!(is_emoji('😀'), "'😀' must be emoji");
169 assert!(is_emoji('🎉'), "'🎉' must be emoji");
170 assert!(!is_emoji('a'), "'a' must not be emoji");
171 }
172
173 #[test]
174 fn fallback_chain_has_entries() {
175 let chain = FallbackChain::default_chain();
176 assert!(
177 !chain.families().is_empty(),
178 "default chain must have entries"
179 );
180 }
181
182 #[test]
183 fn fallback_resolves_cjk_to_cjk_family() {
184 let chain = FallbackChain::default_chain();
185 let family = chain.resolve_glyph('中').unwrap();
186 assert!(
187 family.contains("CJK"),
188 "CJK char should resolve to a CJK family"
189 );
190 }
191
192 #[test]
193 fn fallback_resolves_emoji() {
194 let chain = FallbackChain::default_chain();
195 let family = chain.resolve_glyph('😀').unwrap();
196 assert!(
197 family.to_lowercase().contains("emoji"),
198 "emoji should resolve to emoji family"
199 );
200 }
201
202 #[test]
203 fn fallback_resolves_latin_to_last_resort() {
204 let chain = FallbackChain::default_chain();
205 let family = chain.resolve_glyph('a').unwrap();
207 assert!(!family.is_empty());
208 }
209
210 #[test]
211 fn fallback_add_family_inserts_before_tofu() {
212 let mut chain = FallbackChain::default_chain();
213 let original_len = chain.families().len();
214 chain.add_family("My Custom Font".to_owned());
215 assert_eq!(chain.families().len(), original_len + 1);
216 }
217}