use alloc::string::String;
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
use crate::FcFontCache;
use crate::OperatingSystem;
pub const GENERIC_FAMILIES: &[&str] = &[
"serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
];
pub fn is_generic_family(family: &str) -> bool {
let lower = family.to_lowercase();
GENERIC_FAMILIES.iter().any(|g| *g == lower.as_str())
}
pub const FONT_STYLE_TOKENS: &[&str] = &[
"Regular", "Bold", "Italic", "Light", "Medium", "Thin",
"Black", "ExtraLight", "ExtraBold", "SemiBold", "DemiBold",
"Heavy", "Oblique", "Condensed", "Expanded",
"Extra", "Semi", "Demi",
];
pub fn system_font_dirs(os: OperatingSystem) -> &'static [&'static str] {
match os {
OperatingSystem::MacOS => &[
"/System/Library/Fonts",
"/Library/Fonts",
"/System/Library/AssetsV2",
],
OperatingSystem::Linux => &[
"/usr/share/fonts",
"/usr/local/share/fonts",
],
OperatingSystem::Windows => &[],
OperatingSystem::Wasm => &[],
}
}
pub fn font_directories(os: OperatingSystem) -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = system_font_dirs(os)
.iter()
.map(PathBuf::from)
.collect();
match os {
OperatingSystem::MacOS => {
if let Ok(home) = std::env::var("HOME") {
dirs.push(PathBuf::from(format!("{}/Library/Fonts", home)));
}
}
OperatingSystem::Linux => {
if let Ok(home) = std::env::var("HOME") {
dirs.push(PathBuf::from(format!("{}/.fonts", home)));
dirs.push(PathBuf::from(format!("{}/.local/share/fonts", home)));
}
}
OperatingSystem::Windows => {
let system_root = std::env::var("SystemRoot")
.or_else(|_| std::env::var("WINDIR"))
.unwrap_or_else(|_| "C:\\Windows".to_string());
let user_profile = std::env::var("USERPROFILE")
.unwrap_or_else(|_| "C:\\Users\\Default".to_string());
dirs.push(PathBuf::from(format!("{}\\Fonts", system_root)));
dirs.push(PathBuf::from(format!(
"{}\\AppData\\Local\\Microsoft\\Windows\\Fonts",
user_profile
)));
}
OperatingSystem::Wasm => {}
}
dirs
}
pub fn common_font_families(os: OperatingSystem) -> &'static [&'static str] {
match os {
OperatingSystem::MacOS => &[
"San Francisco", "SFNS", "System Font",
"Helvetica Neue", "Helvetica", "Arial", "Lucida Grande",
"Times New Roman", "Georgia",
"Menlo", "SF Mono", "Courier",
],
OperatingSystem::Linux => &[
"DejaVu Sans", "Ubuntu", "Roboto", "Noto Sans",
"Liberation Sans", "Droid Sans", "Arial",
"DejaVu Serif", "Noto Serif",
"DejaVu Sans Mono",
],
OperatingSystem::Windows => &[
"Segoe UI", "Arial", "Tahoma", "Verdana",
"Times New Roman", "Calibri",
"Consolas", "Courier New",
],
OperatingSystem::Wasm => &[],
}
}
pub fn tokenize_common_families(os: OperatingSystem) -> Vec<Vec<String>> {
common_font_families(os)
.iter()
.map(|family| tokenize_lowercase(family))
.collect()
}
pub fn matches_common_family_tokens(
file_tokens: &[String],
common_token_sets: &[Vec<String>],
) -> bool {
let file_joined: String = file_tokens.concat();
common_token_sets.iter().any(|family_tokens| {
let family_joined: String = family_tokens.concat();
file_joined.contains(&family_joined)
})
}
pub fn tokenize_lowercase(name: &str) -> Vec<String> {
FcFontCache::extract_font_name_tokens(name)
.into_iter()
.map(|t| t.to_lowercase())
.collect()
}
pub fn tokenize_font_stem(stem: &str) -> Vec<String> {
tokenize_lowercase(stem)
.into_iter()
.filter(|t| !FONT_STYLE_TOKENS.iter().any(|s| s.eq_ignore_ascii_case(t)))
.collect()
}
pub fn guess_family_from_filename(path: &Path) -> String {
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("");
tokenize_font_stem(stem).join("")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generic_families_recognized() {
assert!(is_generic_family("sans-serif"));
assert!(is_generic_family("Sans-Serif")); assert!(is_generic_family("monospace"));
assert!(is_generic_family("SERIF"));
assert!(!is_generic_family("Arial"));
assert!(!is_generic_family("Noto Sans"));
}
#[test]
fn font_style_tokens_covers_common_styles() {
for token in &[
"Regular", "Bold", "Italic", "Light", "Medium",
"Thin", "Black", "Oblique", "SemiBold",
] {
assert!(
FONT_STYLE_TOKENS.contains(token),
"missing style token: {}", token
);
}
}
#[test]
fn system_font_dirs_static_and_nonempty() {
assert!(!system_font_dirs(OperatingSystem::MacOS).is_empty());
assert!(!system_font_dirs(OperatingSystem::Linux).is_empty());
assert!(system_font_dirs(OperatingSystem::Wasm).is_empty());
}
#[test]
fn common_font_families_nonempty_for_desktop() {
assert!(!common_font_families(OperatingSystem::MacOS).is_empty());
assert!(!common_font_families(OperatingSystem::Linux).is_empty());
assert!(!common_font_families(OperatingSystem::Windows).is_empty());
assert!(common_font_families(OperatingSystem::Wasm).is_empty());
}
#[test]
fn guess_family_strips_style_suffixes() {
assert_eq!(
guess_family_from_filename(Path::new("ArialBold.ttf")),
"arial"
);
assert_eq!(
guess_family_from_filename(Path::new("NotoSansJP-Regular.otf")),
"notosansjp"
);
assert_eq!(
guess_family_from_filename(Path::new("Helvetica Neue Bold Italic.ttf")),
"helveticaneue"
);
}
#[test]
fn guess_family_handles_underscores() {
assert_eq!(
guess_family_from_filename(Path::new("Liberation_Sans_Bold.ttf")),
"liberationsans"
);
}
#[test]
fn guess_family_handles_compound_styles() {
assert_eq!(
guess_family_from_filename(Path::new("LiberationSans-BoldItalic.ttf")),
"liberationsans"
);
assert_eq!(
guess_family_from_filename(Path::new("DejaVuSansMono-ExtraBold.ttf")),
"dejavusansmono"
);
assert_eq!(
guess_family_from_filename(Path::new("SFMono-SemiBold.otf")),
"sfmono"
);
}
#[test]
fn matches_common_family_macos() {
let common = tokenize_common_families(OperatingSystem::MacOS);
let tokens = tokenize_all("SFNSDisplay");
assert!(matches_common_family_tokens(&tokens, &common));
let tokens = tokenize_all("HelveticaNeue");
assert!(matches_common_family_tokens(&tokens, &common));
let tokens = tokenize_all("Arial");
assert!(matches_common_family_tokens(&tokens, &common));
let tokens = tokenize_all("SomeRandomFont");
assert!(!matches_common_family_tokens(&tokens, &common));
}
#[test]
fn matches_common_family_linux() {
let common = tokenize_common_families(OperatingSystem::Linux);
let tokens = tokenize_all("DejaVuSans");
assert!(matches_common_family_tokens(&tokens, &common));
let tokens = tokenize_all("NotoSansCJK");
assert!(matches_common_family_tokens(&tokens, &common));
let tokens = tokenize_all("UbuntuMono-Regular");
assert!(matches_common_family_tokens(&tokens, &common));
}
#[test]
fn matches_common_family_windows() {
let common = tokenize_common_families(OperatingSystem::Windows);
let tokens = tokenize_all("SegoeUI-Regular");
assert!(matches_common_family_tokens(&tokens, &common));
let tokens = tokenize_all("Consolas");
assert!(matches_common_family_tokens(&tokens, &common));
}
#[test]
fn tokenize_font_stem_filters_styles() {
assert_eq!(tokenize_font_stem("ArialBold"), vec!["arial"]);
assert_eq!(
tokenize_font_stem("NotoSansJP-Regular"),
vec!["noto", "sans", "jp"]
);
assert_eq!(
tokenize_font_stem("SFMono-SemiBold"),
vec!["sfmono"]
);
}
fn tokenize_all(stem: &str) -> Vec<String> {
tokenize_lowercase(stem)
}
}