use super::{Language, Script, family::FamilyId};
use alloc::vec::Vec;
use hashbrown::HashMap;
use smallvec::SmallVec;
type FamilyList = SmallVec<[FamilyId; 1]>;
#[derive(Clone, Default, Debug)]
pub struct FallbackMap {
fallbacks: HashMap<Script, PerScript>,
}
impl FallbackMap {
pub fn get(&self, key: impl Into<FallbackKey>) -> Option<&[FamilyId]> {
let key = key.into();
let entry = self.fallbacks.get(&key.script)?;
if key.is_default() {
Some(entry.default.as_ref()?.as_slice())
} else {
for other in &entry.others {
if key.locale() == Some(other.0) {
return Some(other.1.as_slice());
}
}
None
}
}
pub fn set(
&mut self,
key: impl Into<FallbackKey>,
families: impl Iterator<Item = FamilyId>,
) -> bool {
self.set_or_append(key, families, true)
}
pub fn append(
&mut self,
key: impl Into<FallbackKey>,
families: impl Iterator<Item = FamilyId>,
) -> bool {
self.set_or_append(key, families, false)
}
fn set_or_append(
&mut self,
key: impl Into<FallbackKey>,
families: impl Iterator<Item = FamilyId>,
do_set: bool,
) -> bool {
let key = key.into();
let script = key.script;
if key.is_tracked() {
if key.is_default() {
let existing_families = self
.fallbacks
.entry(script)
.or_default()
.default
.get_or_insert(SmallVec::default());
if do_set {
existing_families.clear();
}
existing_families.extend(families);
true
} else {
let locale = key
.locale
.expect("non-default fallback keys must have a locale");
let script_fallbacks = self.fallbacks.entry(script).or_default();
if let Some(existing_families) = script_fallbacks
.others
.iter_mut()
.find(|x| x.0 == locale)
.map(|x| &mut x.1)
{
if do_set {
existing_families.clear();
}
existing_families.extend(families);
} else {
script_fallbacks.others.push((locale, families.collect()));
}
true
}
} else {
false
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub struct FallbackKey {
script: Script,
locale: Option<Language>,
is_default: bool,
is_tracked: bool,
}
impl FallbackKey {
pub fn new(script: impl Into<Script>, locale: Option<&Language>) -> Self {
let script = script.into();
let (locale, is_default, is_tracked) = match canonical_locale(script, locale) {
Some((is_default, locale)) => (locale, is_default, true),
None => (None, true, false),
};
Self {
script,
locale,
is_default,
is_tracked,
}
}
pub fn script(&self) -> Script {
self.script
}
pub fn locale(&self) -> Option<Language> {
self.locale
}
pub fn locale_str(&self) -> Option<&str> {
self.locale.as_ref().map(Language::as_str)
}
pub fn is_default(&self) -> bool {
self.is_default
}
pub fn is_tracked(&self) -> bool {
self.is_tracked
}
}
impl<S> From<(S, &str)> for FallbackKey
where
S: Into<Script>,
{
fn from(value: (S, &str)) -> Self {
let locale = Language::parse(value.1).ok();
Self::new(value.0, locale.as_ref())
}
}
impl<S> From<(S, &Language)> for FallbackKey
where
S: Into<Script>,
{
fn from(value: (S, &Language)) -> Self {
Self::new(value.0, Some(value.1))
}
}
#[derive(Clone, Default, Debug)]
struct PerScript {
default: Option<FamilyList>,
others: Vec<(Language, FamilyList)>,
}
fn canonical_locale(script: Script, locale: Option<&Language>) -> Option<(bool, Option<Language>)> {
let Some(locale) = locale else {
return Some((true, None));
};
let lang = locale.language();
let region = locale.region().unwrap_or_default();
let (is_default, token) = match &script.to_bytes() {
b"Arab" => match (lang, region) {
("ar", "") => (true, "ar"),
("ar", "IR") => (false, "ar-IR"),
("fa", "") => (false, "fa"),
("ks", "") => (false, "ks"),
("ku", "IQ") => (false, "ku-IQ"),
("ku", "IR") => (false, "ku-IR"),
("la", "") => (false, "la"),
("ota", "") => (false, "ota"),
("pa", "PK") => (false, "pa-PK"),
("ps", "AF") => (false, "ps-AF"),
("ps", "PK") => (false, "ps-PK"),
("sd", "") => (false, "sd"),
("ug", "") => (false, "ug"),
("ur", "") => (false, "ur"),
_ => return None,
},
b"Beng" => match (lang, region) {
("bn", "") => (true, "bn"),
("as", "") => (false, "as"),
("mni", "") => (false, "mni"),
_ => return None,
},
b"Deva" => match (lang, region) {
("hi", "") => (true, "hi"),
("bh", "") => (false, "bh"),
("bho", "") => (false, "bho"),
("brx", "") => (false, "brx"),
("doi", "") => (false, "doi"),
("hne", "") => (false, "hne"),
("kok", "") => (false, "kok"),
("mai", "") => (false, "mai"),
("mr", "") => (false, "mr"),
("bne", "") => (false, "bne"),
("sa", "") => (false, "sa"),
("sat", "") => (false, "sat"),
_ => return None,
},
b"Ethi" => match (lang, region) {
("gez", "") => (true, "gez"),
("am", "") => (false, "am"),
("byn", "") => (false, "byn"),
("sid", "") => (false, "sid"),
("ti", "ER") => (false, "ti-ER"),
("ti", "ET") => (false, "ti-ET"),
("tig", "") => (false, "tig"),
("wal", "") => (false, "wal"),
_ => return None,
},
b"Hani" => match lang {
"ja" => (false, "ja"),
"ko" => (false, "ko"),
"zh" => {
match region {
"HK" => (false, "zh-HK"),
"TW" => (false, "zh-TW"),
"MO" => (false, "zh-MO"),
"SG" => (false, "zh-SG"),
_ => {
if locale.script() == Some("Hant") {
(false, "zh-TW")
} else {
(true, "zh-CN")
}
}
}
}
_ => return None,
},
b"Hebr" => match (lang, region) {
("he", "") => (true, "he"),
("yi", "") => (false, "yi"),
_ => return None,
},
b"Tibt" => match (lang, region) {
("bo", "") => (true, "bo"),
("dz", "") => (false, "dz"),
_ => return None,
},
_ => return None,
};
Some((
is_default,
Some(Language::parse(token).expect("valid canonical locale")),
))
}