use std::collections::HashSet;
use read_fonts::types::Tag;
use unicode_script::{Script, UnicodeScript};
use crate::tags::tag4;
pub const MIN_UNICODE_COVERAGE: usize = 4;
#[derive(Debug, Clone)]
pub struct ScriptRequirement {
input: String,
ot_tags: Vec<Tag>,
unicode_script: Option<Script>,
}
impl ScriptRequirement {
pub fn input(&self) -> &str {
&self.input
}
pub fn ot_tags(&self) -> &[Tag] {
&self.ot_tags
}
pub fn unicode_script(&self) -> Option<Script> {
self.unicode_script
}
pub fn ot_satisfied(&self, font_scripts: &HashSet<Tag>) -> bool {
self.ot_tags.iter().any(|tag| font_scripts.contains(tag))
}
pub fn unicode_satisfied<I>(&self, codepoints: I) -> bool
where
I: IntoIterator<Item = char>,
{
let Some(script) = self.unicode_script else {
return false;
};
let mut count = 0usize;
for ch in codepoints {
if ch.script() == script {
count += 1;
if count >= MIN_UNICODE_COVERAGE {
return true;
}
}
}
false
}
}
pub fn resolve_scripts(raw: &[String]) -> Vec<ScriptRequirement> {
raw.iter()
.filter_map(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
Some(resolve_one(trimmed))
}
})
.collect()
}
fn resolve_one(input: &str) -> ScriptRequirement {
let key = input.to_ascii_lowercase();
let (ot_strings, unicode_short): (Vec<&str>, Option<&str>) = match lookup(&key) {
Some((tags, uni)) => (tags.to_vec(), Some(uni)),
None => (vec![key.as_str()], None),
};
let ot_tags: Vec<Tag> = ot_strings.iter().filter_map(|t| tag4(t).ok()).collect();
let unicode_script = unicode_short
.and_then(Script::from_short_name)
.or_else(|| Script::from_short_name(&title_case(&key)))
.filter(|s| *s != Script::Unknown);
ScriptRequirement {
input: input.to_string(),
ot_tags,
unicode_script,
}
}
fn title_case(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
first.to_ascii_uppercase().to_string() + &chars.as_str().to_ascii_lowercase()
}
None => String::new(),
}
}
fn lookup(key: &str) -> Option<(&'static [&'static str], &'static str)> {
const DEVA: &[&str] = &["deva", "dev2", "dev3"];
const BENG: &[&str] = &["beng", "bng2"];
const GUJR: &[&str] = &["gujr", "gjr2"];
const GURU: &[&str] = &["guru", "gur2"];
const KNDA: &[&str] = &["knda", "knd2"];
const MLYM: &[&str] = &["mlym", "mlm2"];
const ORYA: &[&str] = &["orya", "ory2"];
const TAML: &[&str] = &["taml", "tml2"];
const TELU: &[&str] = &["telu", "tel2"];
let entry: (&[&str], &str) = match key {
"deva" | "dev2" | "dev3" => (DEVA, "Deva"),
"beng" | "bng2" => (BENG, "Beng"),
"gujr" | "gjr2" => (GUJR, "Gujr"),
"guru" | "gur2" => (GURU, "Guru"),
"knda" | "knd2" => (KNDA, "Knda"),
"mlym" | "mlm2" => (MLYM, "Mlym"),
"orya" | "ory2" => (ORYA, "Orya"),
"taml" | "tml2" => (TAML, "Taml"),
"telu" | "tel2" => (TELU, "Telu"),
"latf" | "latg" => (&["latn"], "Latn"),
"aran" => (&["arab"], "Arab"),
"syre" | "syrj" | "syrn" => (&["syrc"], "Syrc"),
"hans" | "hant" => (&["hani"], "Hani"),
"lao" | "laoo" => (&["lao "], "Laoo"),
"yiii" => (&["yi "], "Yiii"),
"nkoo" => (&["nko "], "Nkoo"),
_ => return None,
};
Some(entry)
}
#[cfg(test)]
mod tests {
use super::*;
fn tags(req: &ScriptRequirement) -> Vec<String> {
req.ot_tags()
.iter()
.map(|t| String::from_utf8_lossy(&t.to_be_bytes()).trim().to_string())
.collect()
}
#[test]
fn resolve_iso_devanagari_expands_to_ot_group() {
let reqs = resolve_scripts(&["deva".to_string()]);
assert_eq!(reqs.len(), 1);
assert_eq!(tags(&reqs[0]), vec!["deva", "dev2", "dev3"]);
assert_eq!(reqs[0].unicode_script(), Some(Script::Devanagari));
}
#[test]
fn resolve_v2_tag_maps_to_same_group_and_script() {
let reqs = resolve_scripts(&["dev2".to_string()]);
assert_eq!(tags(&reqs[0]), vec!["deva", "dev2", "dev3"]);
assert_eq!(reqs[0].unicode_script(), Some(Script::Devanagari));
}
#[test]
fn resolve_latin_fraktur_alias_maps_to_latn() {
let reqs = resolve_scripts(&["latf".to_string()]);
assert_eq!(tags(&reqs[0]), vec!["latn"]);
assert_eq!(reqs[0].unicode_script(), Some(Script::Latin));
}
#[test]
fn resolve_plain_opentype_tag_via_fallback() {
let reqs = resolve_scripts(&["latn".to_string()]);
assert_eq!(tags(&reqs[0]), vec!["latn"]);
assert_eq!(reqs[0].unicode_script(), Some(Script::Latin));
}
#[test]
fn resolve_is_case_insensitive() {
let reqs = resolve_scripts(&["LATN".to_string(), "Deva".to_string()]);
assert_eq!(tags(&reqs[0]), vec!["latn"]);
assert_eq!(reqs[0].unicode_script(), Some(Script::Latin));
assert_eq!(reqs[1].unicode_script(), Some(Script::Devanagari));
}
#[test]
fn resolve_unknown_tag_falls_back_to_literal_ot() {
let reqs = resolve_scripts(&["zzzz".to_string()]);
assert_eq!(tags(&reqs[0]), vec!["zzzz"]);
assert_eq!(reqs[0].unicode_script(), None);
}
#[test]
fn empty_and_blank_entries_are_skipped() {
let reqs = resolve_scripts(&["".to_string(), " ".to_string(), "latn".to_string()]);
assert_eq!(reqs.len(), 1);
}
#[test]
fn ot_satisfied_matches_any_group_member() {
let reqs = resolve_scripts(&["deva".to_string()]);
let mut font: HashSet<Tag> = HashSet::new();
font.insert(tag4("dev2").unwrap());
assert!(reqs[0].ot_satisfied(&font));
let empty: HashSet<Tag> = HashSet::new();
assert!(!reqs[0].ot_satisfied(&empty));
}
#[test]
fn unicode_satisfied_needs_min_coverage() {
let reqs = resolve_scripts(&["latn".to_string()]);
assert!(!reqs[0].unicode_satisfied(['a', 'b', 'c']));
assert!(reqs[0].unicode_satisfied(['a', 'b', 'c', 'd']));
assert!(!reqs[0].unicode_satisfied(['α', 'β', 'γ', 'δ']));
}
}