use std::collections::BTreeMap;
use super::Locale;
pub type MessageValues = BTreeMap<String, MessageValue>;
#[derive(Clone, Debug, PartialEq)]
pub enum MessageValue {
Str(String),
Num(f64),
}
impl MessageValue {
fn render(&self) -> String {
match self {
Self::Str(s) => s.clone(),
Self::Num(n) if n.fract() == 0.0 && n.is_finite() => format!("{}", *n as i64),
Self::Num(n) => n.to_string(),
}
}
fn as_number(&self) -> Option<f64> {
match self {
Self::Num(n) => Some(*n),
Self::Str(s) => s.trim().parse().ok(),
}
}
}
impl From<&str> for MessageValue {
fn from(value: &str) -> Self {
Self::Str(value.to_owned())
}
}
impl From<String> for MessageValue {
fn from(value: String) -> Self {
Self::Str(value)
}
}
macro_rules! from_number {
($($t:ty),*) => {
$(impl From<$t> for MessageValue {
fn from(value: $t) -> Self { Self::Num(value as f64) }
})*
};
}
from_number!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64);
pub fn format_message(pattern: &str, locale: Locale, values: &MessageValues) -> String {
let chars: Vec<char> = pattern.chars().collect();
format(&chars, locale, values, None)
}
fn format(pattern: &[char], locale: Locale, values: &MessageValues, pound: Option<&str>) -> String {
let mut out = String::new();
let mut i = 0;
while i < pattern.len() {
let ch = pattern[i];
if ch == '\'' {
let next = pattern.get(i + 1).copied();
if next == Some('\'') {
out.push('\'');
i += 2;
continue;
}
if matches!(next, Some('{') | Some('}') | Some('#')) {
i += 1;
while i < pattern.len() {
if pattern[i] == '\'' {
if pattern.get(i + 1) == Some(&'\'') {
out.push('\'');
i += 2;
continue;
}
i += 1;
break;
}
out.push(pattern[i]);
i += 1;
}
continue;
}
out.push(ch);
i += 1;
continue;
}
if ch == '#'
&& let Some(pound) = pound
{
out.push_str(pound);
i += 1;
continue;
}
if ch == '{' {
match match_brace(pattern, i) {
Some(end) => {
out.push_str(&render_argument(&pattern[i + 1..end], locale, values, pound));
i = end + 1;
}
None => {
out.extend(&pattern[i..]);
break;
}
}
continue;
}
out.push(ch);
i += 1;
}
out
}
fn match_brace(source: &[char], start: usize) -> Option<usize> {
let mut depth = 0usize;
for (offset, ch) in source[start..].iter().enumerate() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some(start + offset);
}
}
_ => {}
}
}
None
}
fn top_level_index_of(source: &[char], sep: char) -> Option<usize> {
let mut depth = 0i32;
for (i, &ch) in source.iter().enumerate() {
match ch {
'{' => depth += 1,
'}' => depth -= 1,
c if c == sep && depth == 0 => return Some(i),
_ => {}
}
}
None
}
fn render_argument(inner: &[char], locale: Locale, values: &MessageValues, pound: Option<&str>) -> String {
let verbatim = || -> String { format!("{{{}}}", inner.iter().collect::<String>()) };
let Some(first_comma) = top_level_index_of(inner, ',') else {
let name: String = inner.iter().collect::<String>().trim().to_owned();
return match values.get(&name) {
Some(value) => value.render(),
None => verbatim(),
};
};
let name: String = inner[..first_comma].iter().collect::<String>().trim().to_owned();
let rest = &inner[first_comma + 1..];
let second_comma = top_level_index_of(rest, ',');
let arg_type: String = match second_comma {
Some(idx) => rest[..idx].iter().collect::<String>(),
None => rest.iter().collect::<String>(),
}
.trim()
.to_owned();
let body: &[char] = match second_comma {
Some(idx) => &rest[idx + 1..],
None => &[],
};
let raw = values.get(&name);
match arg_type.as_str() {
"plural" => {
let Some(count) = raw.and_then(MessageValue::as_number).filter(|n| n.is_finite()) else {
return String::new();
};
let branches = parse_branches(body);
let exact = if count.fract() == 0.0 { branches.get(&format!("={}", count as i64)) } else { None };
let category = locale.select_plural(count);
let chosen = exact.or_else(|| branches.get(category.as_str())).or_else(|| branches.get("other"));
match chosen {
Some(branch) => format(branch, locale, values, Some(&locale.format_number(count))),
None => String::new(),
}
}
"select" => {
let branches = parse_branches(body);
let key = raw.map(MessageValue::render).unwrap_or_default();
let chosen = branches.get(key.as_str()).or_else(|| branches.get("other"));
match chosen {
Some(branch) => format(branch, locale, values, pound),
None => String::new(),
}
}
_ => match raw {
Some(value) => value.render(),
None => verbatim(),
},
}
}
fn parse_branches(body: &[char]) -> BTreeMap<String, Vec<char>> {
let mut branches = BTreeMap::new();
let mut i = 0;
while i < body.len() {
while i < body.len() && body[i].is_whitespace() {
i += 1;
}
let key_start = i;
while i < body.len() && !body[i].is_whitespace() && body[i] != '{' {
i += 1;
}
let key: String = body[key_start..i].iter().collect();
while i < body.len() && body[i].is_whitespace() {
i += 1;
}
if body.get(i) != Some(&'{') {
break;
}
let Some(end) = match_brace(body, i) else { break };
if !key.is_empty() {
branches.insert(key, body[i + 1..end].to_vec());
}
i = end + 1;
}
branches
}