use alloc::collections::btree_map::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use std::collections::HashSet;
use std::path::PathBuf;
use crate::config;
use crate::utils::normalize_family_name;
use crate::FcPattern;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Priority {
Low = 0,
Medium = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone)]
pub struct FcBuildJob {
pub priority: Priority,
pub path: PathBuf,
pub font_index: Option<usize>,
pub guessed_family: String,
}
impl PartialEq for FcBuildJob {
fn eq(&self, other: &Self) -> bool {
self.priority == other.priority && self.path == other.path
}
}
impl Eq for FcBuildJob {}
impl PartialOrd for FcBuildJob {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FcBuildJob {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.priority.cmp(&other.priority)
}
}
pub fn assign_scout_priority(
file_tokens: &[String],
common_token_sets: &[Vec<String>],
) -> Priority {
if config::matches_common_family_tokens(file_tokens, common_token_sets) {
Priority::High
} else {
Priority::Low
}
}
pub fn find_family_paths(
family: &str,
known_paths: &BTreeMap<String, Vec<PathBuf>>,
) -> Vec<PathBuf> {
let mut result = HashSet::new();
if let Some(paths) = known_paths.get(family) {
result.extend(paths.iter().cloned());
}
for (known_fam, paths) in known_paths.iter() {
if known_fam != family
&& (known_fam.contains(family) || family.contains(known_fam.as_str()))
{
result.extend(paths.iter().cloned());
}
}
result.into_iter().collect()
}
pub fn find_incomplete_paths(
families: &[String],
known_paths: &BTreeMap<String, Vec<PathBuf>>,
completed_paths: &HashSet<PathBuf>,
) -> Vec<(PathBuf, String)> {
families
.iter()
.flat_map(|family| {
find_family_paths(family, known_paths)
.into_iter()
.filter(|p| !completed_paths.contains(p))
.map(move |p| (p, family.clone()))
})
.collect()
}
pub fn family_exists_in_patterns<'a>(
family: &str,
patterns: impl Iterator<Item = &'a FcPattern>,
) -> bool {
patterns.into_iter().any(|p| {
p.name
.as_ref()
.map(|n| normalize_family_name(n) == family)
.unwrap_or(false)
|| p.family
.as_ref()
.map(|f| normalize_family_name(f) == family)
.unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn priority_ordering() {
assert!(Priority::Critical > Priority::High);
assert!(Priority::High > Priority::Medium);
assert!(Priority::Medium > Priority::Low);
}
#[test]
fn build_job_sorts_by_priority() {
let low = FcBuildJob {
priority: Priority::Low,
path: PathBuf::from("a.ttf"),
font_index: None,
guessed_family: "a".into(),
};
let critical = FcBuildJob {
priority: Priority::Critical,
path: PathBuf::from("b.ttf"),
font_index: None,
guessed_family: "b".into(),
};
let mut jobs = vec![low.clone(), critical.clone()];
jobs.sort();
assert_eq!(jobs[0].priority, Priority::Low);
assert_eq!(jobs[1].priority, Priority::Critical);
}
#[test]
fn assign_scout_priority_common_family_gets_high() {
use crate::OperatingSystem;
let common = config::tokenize_common_families(OperatingSystem::MacOS);
let tokens = tokenize_all("Arial");
assert_eq!(assign_scout_priority(&tokens, &common), Priority::High);
let tokens = tokenize_all("HelveticaNeue-Bold");
assert_eq!(assign_scout_priority(&tokens, &common), Priority::High);
}
#[test]
fn assign_scout_priority_unknown_font_gets_low() {
use crate::OperatingSystem;
let common = config::tokenize_common_families(OperatingSystem::MacOS);
let tokens = tokenize_all("SomeObscureFont");
assert_eq!(assign_scout_priority(&tokens, &common), Priority::Low);
}
#[test]
fn find_family_paths_exact_match() {
let mut known = BTreeMap::new();
known.insert("arial".to_string(), vec![PathBuf::from("/fonts/Arial.ttf")]);
known.insert("helvetica".to_string(), vec![PathBuf::from("/fonts/Helvetica.ttf")]);
let paths = find_family_paths("arial", &known);
assert_eq!(paths.len(), 1);
assert!(paths.contains(&PathBuf::from("/fonts/Arial.ttf")));
}
#[test]
fn find_family_paths_fuzzy_substring() {
let mut known = BTreeMap::new();
known.insert("arial".to_string(), vec![PathBuf::from("/fonts/Arial.ttf")]);
known.insert(
"arialnarrow".to_string(),
vec![PathBuf::from("/fonts/ArialNarrow.ttf")],
);
known.insert("helvetica".to_string(), vec![PathBuf::from("/fonts/Helvetica.ttf")]);
let paths = find_family_paths("arial", &known);
assert_eq!(paths.len(), 2);
assert!(paths.contains(&PathBuf::from("/fonts/Arial.ttf")));
assert!(paths.contains(&PathBuf::from("/fonts/ArialNarrow.ttf")));
}
#[test]
fn find_family_paths_no_match() {
let mut known = BTreeMap::new();
known.insert("arial".to_string(), vec![PathBuf::from("/fonts/Arial.ttf")]);
let paths = find_family_paths("courier", &known);
assert!(paths.is_empty());
}
#[test]
fn find_incomplete_paths_filters_completed() {
let mut known = BTreeMap::new();
known.insert(
"arial".to_string(),
vec![
PathBuf::from("/fonts/Arial.ttf"),
PathBuf::from("/fonts/ArialBold.ttf"),
],
);
let mut completed = HashSet::new();
completed.insert(PathBuf::from("/fonts/Arial.ttf"));
let incomplete = find_incomplete_paths(
&["arial".to_string()],
&known,
&completed,
);
assert_eq!(incomplete.len(), 1);
assert_eq!(incomplete[0].0, PathBuf::from("/fonts/ArialBold.ttf"));
}
#[test]
fn family_exists_by_name() {
let pattern = FcPattern {
name: Some("Arial".to_string()),
..Default::default()
};
assert!(family_exists_in_patterns("arial", [&pattern].into_iter()));
}
#[test]
fn family_exists_by_family_field() {
let pattern = FcPattern {
family: Some("Helvetica Neue".to_string()),
..Default::default()
};
assert!(family_exists_in_patterns("helveticaneue", [&pattern].into_iter()));
}
#[test]
fn family_not_found() {
let pattern = FcPattern {
name: Some("Arial".to_string()),
..Default::default()
};
assert!(!family_exists_in_patterns("courier", [&pattern].into_iter()));
}
fn tokenize_all(stem: &str) -> Vec<String> {
crate::config::tokenize_lowercase(stem)
}
}