mod encode;
mod rule;
mod samples;
use std::collections::BTreeMap;
use std::sync::OnceLock;
use mf2_catalog::format::locale_key;
pub use encode::{encode, encode_rule};
pub use rule::{Category, Condition, Operand, Relation, Rule, parse_rule};
pub use samples::{SampleItem, SampleList, is_sample_value, parse_samples};
use crate::error::Error;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PluralKind {
Cardinal,
Ordinal,
}
impl PluralKind {
pub const ALL: [PluralKind; 2] = [PluralKind::Cardinal, PluralKind::Ordinal];
pub const fn name(self) -> &'static str {
match self {
PluralKind::Cardinal => "cardinal",
PluralKind::Ordinal => "ordinal",
}
}
pub const fn locale_key(self) -> u32 {
match self {
PluralKind::Cardinal => locale_key::PLURAL_CARDINAL,
PluralKind::Ordinal => locale_key::PLURAL_ORDINAL,
}
}
fn from_name(s: &str) -> Option<PluralKind> {
PluralKind::ALL.into_iter().find(|k| k.name() == s)
}
}
const TABLE: &str = include_str!("../data/plurals.txt");
const ROOT: &str = "und";
struct Table {
rules: BTreeMap<(PluralKind, String), Vec<Rule>>,
}
fn parse_table(text: &str) -> Result<Table, Error> {
let mut rules: BTreeMap<(PluralKind, String), Vec<Rule>> = BTreeMap::new();
for (i, line) in text.lines().enumerate() {
let line_no = i + 1;
let bad = |message: &str| Error::Table {
line: line_no,
message: message.to_owned(),
};
if line.is_empty() || line.starts_with('#') || line.starts_with("cldr ") {
continue;
}
let mut fields = line.splitn(4, ' ');
let (Some(kind), Some(locale), Some(category), text) =
(fields.next(), fields.next(), fields.next(), fields.next())
else {
return Err(bad("expected `kind locale category rule`"));
};
let kind = PluralKind::from_name(kind).ok_or_else(|| bad("unknown kind"))?;
let category = Category::from_name(category)?;
let rule = parse_rule(category, text.unwrap_or("")).map_err(|source| Error::Rule {
kind: kind.name(),
locale: locale.to_owned(),
category: category.as_str().to_owned(),
source,
})?;
rules
.entry((kind, locale.to_owned()))
.or_default()
.push(rule);
}
Ok(Table { rules })
}
fn table() -> Result<&'static Table, Error> {
static PARSED: OnceLock<Result<Table, String>> = OnceLock::new();
match PARSED.get_or_init(|| parse_table(TABLE).map_err(|e| e.to_string())) {
Ok(t) => Ok(t),
Err(message) => Err(Error::Table {
line: 0,
message: message.clone(),
}),
}
}
#[derive(Clone, Copy, Debug)]
pub struct LocaleRules {
pub locale: &'static str,
pub rules: &'static [Rule],
}
pub fn plural_rules(kind: PluralKind, locale: &str) -> Result<LocaleRules, Error> {
let t = table()?;
let tag = locale.replace('_', "-");
let mut cut = tag.as_str();
loop {
let found = t
.rules
.range((kind, String::new())..)
.take_while(|((k, _), _)| *k == kind)
.find(|((_, l), _)| l.eq_ignore_ascii_case(cut));
if let Some(((_, l), rules)) = found {
return Ok(LocaleRules {
locale: l.as_str(),
rules: rules.as_slice(),
});
}
match cut.rfind('-') {
Some(at) => cut = &cut[..at],
None if cut == ROOT => {
return Ok(LocaleRules {
locale: ROOT,
rules: &[],
});
}
None => cut = ROOT,
}
}
}
pub fn plural_entry(kind: PluralKind, locale: &str) -> Result<Vec<u8>, Error> {
Ok(encode(plural_rules(kind, locale)?.rules))
}
pub fn plural_locale_entries(
locale: &str,
kinds: &[PluralKind],
) -> Result<Vec<(u32, Vec<u8>)>, Error> {
let mut out = Vec::new();
for &kind in kinds {
out.push((kind.locale_key(), plural_entry(kind, locale)?));
}
out.sort_by_key(|(k, _)| *k);
out.dedup_by_key(|(k, _)| *k);
Ok(out)
}
pub fn plural_locales(kind: PluralKind) -> Result<Vec<&'static str>, Error> {
let t = table()?;
Ok(t.rules
.keys()
.filter(|(k, _)| *k == kind)
.map(|(_, l)| l.as_str())
.collect())
}