1use alloc::string::String;
6use alloc::vec::Vec;
7
8use std::path::{Path, PathBuf};
9
10use crate::FcFontCache;
11use crate::OperatingSystem;
12
13pub const GENERIC_FAMILIES: &[&str] = &[
18 "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
19];
20
21pub fn is_generic_family(family: &str) -> bool {
23 let lower = family.to_lowercase();
24 GENERIC_FAMILIES.iter().any(|g| *g == lower.as_str())
25}
26
27pub const FONT_STYLE_TOKENS: &[&str] = &[
33 "Regular", "Bold", "Italic", "Light", "Medium", "Thin",
34 "Black", "ExtraLight", "ExtraBold", "SemiBold", "DemiBold",
35 "Heavy", "Oblique", "Condensed", "Expanded",
36 "Extra", "Semi", "Demi",
39];
40
41pub fn system_font_dirs(os: OperatingSystem) -> &'static [&'static str] {
46 match os {
47 OperatingSystem::MacOS => &[
48 "/System/Library/Fonts",
49 "/Library/Fonts",
50 "/System/Library/AssetsV2",
51 ],
52 OperatingSystem::Linux => &[
53 "/usr/share/fonts",
54 "/usr/local/share/fonts",
55 ],
56 OperatingSystem::Android => &[
61 "/system/fonts",
62 "/product/fonts",
63 "/system_ext/fonts",
64 "/data/fonts",
65 ],
66 OperatingSystem::IOS => &[],
73 OperatingSystem::Windows => &[],
75 OperatingSystem::Wasm => &[],
76 }
77}
78
79pub fn font_directories(os: OperatingSystem) -> Vec<PathBuf> {
84 let mut dirs: Vec<PathBuf> = system_font_dirs(os)
85 .iter()
86 .map(PathBuf::from)
87 .collect();
88
89 match os {
90 OperatingSystem::MacOS => {
91 if let Ok(home) = std::env::var("HOME") {
92 dirs.push(PathBuf::from(format!("{}/Library/Fonts", home)));
93 }
94 }
95 OperatingSystem::Linux => {
96 if let Ok(home) = std::env::var("HOME") {
97 dirs.push(PathBuf::from(format!("{}/.fonts", home)));
98 dirs.push(PathBuf::from(format!("{}/.local/share/fonts", home)));
99 }
100 }
101 OperatingSystem::Windows => {
102 let system_root = std::env::var("SystemRoot")
103 .or_else(|_| std::env::var("WINDIR"))
104 .unwrap_or_else(|_| "C:\\Windows".to_string());
105 let user_profile = std::env::var("USERPROFILE")
106 .unwrap_or_else(|_| "C:\\Users\\Default".to_string());
107 dirs.push(PathBuf::from(format!("{}\\Fonts", system_root)));
108 dirs.push(PathBuf::from(format!(
109 "{}\\AppData\\Local\\Microsoft\\Windows\\Fonts",
110 user_profile
111 )));
112 }
113 OperatingSystem::IOS | OperatingSystem::Android => {}
117 OperatingSystem::Wasm => {}
118 }
119
120 dirs
121}
122
123pub fn common_font_families(os: OperatingSystem) -> &'static [&'static str] {
133 match os {
134 OperatingSystem::MacOS => &[
135 "San Francisco", "SFNS", "System Font",
137 "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande",
139 "Times New Roman", "Georgia",
141 "Menlo", "SF Mono", "Courier",
143 ],
144 OperatingSystem::Linux => &[
145 "DejaVu Sans", "Ubuntu", "Roboto", "Noto Sans",
147 "Liberation Sans", "Droid Sans", "Arial",
148 "DejaVu Serif", "Noto Serif",
150 "DejaVu Sans Mono",
152 ],
153 OperatingSystem::Windows => &[
154 "Segoe UI", "Arial", "Tahoma", "Verdana",
156 "Times New Roman", "Calibri",
158 "Consolas", "Courier New",
160 ],
161 OperatingSystem::IOS => &[
162 "San Francisco", "SFNS", "SFNSDisplay", "SFNSText", "SFUI",
164 ".AppleSystemUIFont", "System Font",
165 "Helvetica Neue", "Helvetica", "Avenir", "Avenir Next",
167 "Times New Roman", "Georgia",
169 "Menlo", "SF Mono", "Courier",
171 ],
172 OperatingSystem::Android => &[
173 "Roboto", "Roboto Flex", "Roboto Condensed",
175 "Noto Sans", "Droid Sans",
177 "Noto Serif", "Roboto Serif", "Droid Serif",
179 "Roboto Mono", "Droid Sans Mono", "Noto Sans Mono",
181 ],
182 OperatingSystem::Wasm => &[],
183 }
184}
185
186pub fn tokenize_common_families(os: OperatingSystem) -> Vec<Vec<String>> {
191 common_font_families(os)
192 .iter()
193 .map(|family| tokenize_lowercase(family))
194 .collect()
195}
196
197pub fn matches_common_family_tokens(
204 file_tokens: &[String],
205 common_token_sets: &[Vec<String>],
206) -> bool {
207 let file_joined: String = file_tokens.concat();
208 common_token_sets.iter().any(|family_tokens| {
209 let family_joined: String = family_tokens.concat();
210 file_joined.contains(&family_joined)
211 })
212}
213
214pub fn tokenize_lowercase(name: &str) -> Vec<String> {
229 FcFontCache::extract_font_name_tokens(name)
230 .into_iter()
231 .map(|t| t.to_lowercase())
232 .collect()
233}
234
235pub fn tokenize_font_stem(stem: &str) -> Vec<String> {
237 tokenize_lowercase(stem)
238 .into_iter()
239 .filter(|t| !FONT_STYLE_TOKENS.iter().any(|s| s.eq_ignore_ascii_case(t)))
240 .collect()
241}
242
243pub fn guess_family_from_filename(path: &Path) -> String {
254 let stem = path
255 .file_stem()
256 .and_then(|s| s.to_str())
257 .unwrap_or("");
258
259 tokenize_font_stem(stem).join("")
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
269 fn generic_families_recognized() {
270 assert!(is_generic_family("sans-serif"));
271 assert!(is_generic_family("Sans-Serif")); assert!(is_generic_family("monospace"));
273 assert!(is_generic_family("SERIF"));
274 assert!(!is_generic_family("Arial"));
275 assert!(!is_generic_family("Noto Sans"));
276 }
277
278 #[test]
281 fn font_style_tokens_covers_common_styles() {
282 for token in &[
283 "Regular", "Bold", "Italic", "Light", "Medium",
284 "Thin", "Black", "Oblique", "SemiBold",
285 ] {
286 assert!(
287 FONT_STYLE_TOKENS.contains(token),
288 "missing style token: {}", token
289 );
290 }
291 }
292
293 #[test]
296 fn system_font_dirs_static_and_nonempty() {
297 assert!(!system_font_dirs(OperatingSystem::MacOS).is_empty());
298 assert!(!system_font_dirs(OperatingSystem::Linux).is_empty());
299 assert!(system_font_dirs(OperatingSystem::Wasm).is_empty());
300 }
301
302 #[test]
305 fn common_font_families_nonempty_for_desktop() {
306 assert!(!common_font_families(OperatingSystem::MacOS).is_empty());
307 assert!(!common_font_families(OperatingSystem::Linux).is_empty());
308 assert!(!common_font_families(OperatingSystem::Windows).is_empty());
309 assert!(common_font_families(OperatingSystem::Wasm).is_empty());
310 }
311
312 #[test]
315 fn guess_family_strips_style_suffixes() {
316 assert_eq!(
317 guess_family_from_filename(Path::new("ArialBold.ttf")),
318 "arial"
319 );
320 assert_eq!(
321 guess_family_from_filename(Path::new("NotoSansJP-Regular.otf")),
322 "notosansjp"
323 );
324 assert_eq!(
325 guess_family_from_filename(Path::new("Helvetica Neue Bold Italic.ttf")),
326 "helveticaneue"
327 );
328 }
329
330 #[test]
331 fn guess_family_handles_underscores() {
332 assert_eq!(
333 guess_family_from_filename(Path::new("Liberation_Sans_Bold.ttf")),
334 "liberationsans"
335 );
336 }
337
338 #[test]
339 fn guess_family_handles_compound_styles() {
340 assert_eq!(
341 guess_family_from_filename(Path::new("LiberationSans-BoldItalic.ttf")),
342 "liberationsans"
343 );
344 assert_eq!(
345 guess_family_from_filename(Path::new("DejaVuSansMono-ExtraBold.ttf")),
346 "dejavusansmono"
347 );
348 assert_eq!(
349 guess_family_from_filename(Path::new("SFMono-SemiBold.otf")),
350 "sfmono"
351 );
352 }
353
354 #[test]
357 fn matches_common_family_macos() {
358 let common = tokenize_common_families(OperatingSystem::MacOS);
359
360 let tokens = tokenize_all("SFNSDisplay");
362 assert!(matches_common_family_tokens(&tokens, &common));
363
364 let tokens = tokenize_all("HelveticaNeue");
366 assert!(matches_common_family_tokens(&tokens, &common));
367
368 let tokens = tokenize_all("Arial");
370 assert!(matches_common_family_tokens(&tokens, &common));
371
372 let tokens = tokenize_all("SomeRandomFont");
374 assert!(!matches_common_family_tokens(&tokens, &common));
375 }
376
377 #[test]
378 fn matches_common_family_linux() {
379 let common = tokenize_common_families(OperatingSystem::Linux);
380
381 let tokens = tokenize_all("DejaVuSans");
382 assert!(matches_common_family_tokens(&tokens, &common));
383
384 let tokens = tokenize_all("NotoSansCJK");
385 assert!(matches_common_family_tokens(&tokens, &common));
386
387 let tokens = tokenize_all("UbuntuMono-Regular");
388 assert!(matches_common_family_tokens(&tokens, &common));
389 }
390
391 #[test]
392 fn matches_common_family_windows() {
393 let common = tokenize_common_families(OperatingSystem::Windows);
394
395 let tokens = tokenize_all("SegoeUI-Regular");
396 assert!(matches_common_family_tokens(&tokens, &common));
397
398 let tokens = tokenize_all("Consolas");
399 assert!(matches_common_family_tokens(&tokens, &common));
400 }
401
402 #[test]
405 fn tokenize_font_stem_filters_styles() {
406 assert_eq!(tokenize_font_stem("ArialBold"), vec!["arial"]);
407 assert_eq!(
408 tokenize_font_stem("NotoSansJP-Regular"),
409 vec!["noto", "sans", "jp"]
410 );
411 assert_eq!(
413 tokenize_font_stem("SFMono-SemiBold"),
414 vec!["sfmono"]
415 );
416 }
417
418 fn tokenize_all(stem: &str) -> Vec<String> {
420 tokenize_lowercase(stem)
421 }
422}