use std::borrow::Borrow;
use std::borrow::Cow;
use std::collections::hash_map::{Entry as HashEntry, HashMap};
use std::default::Default;
use std::sync::Mutex;
use fluent_syntax::ast;
use intl_memoizer::IntlLangMemoizer;
use unic_langid::LanguageIdentifier;
use crate::entry::Entry;
use crate::entry::GetEntry;
use crate::errors::FluentError;
use crate::resolve::{ResolveValue, Scope};
use crate::resource::FluentResource;
use crate::types::FluentValue;
#[derive(Debug, PartialEq)]
pub struct FluentMessage<'m> {
pub value: Option<&'m ast::Pattern<'m>>,
pub attributes: HashMap<&'m str, &'m ast::Pattern<'m>>,
}
pub type FluentArgs<'args> = HashMap<&'args str, FluentValue<'args>>;
pub struct FluentBundle<R> {
pub locales: Vec<LanguageIdentifier>,
pub(crate) resources: Vec<R>,
pub(crate) entries: HashMap<String, Entry>,
pub(crate) intls: Mutex<IntlLangMemoizer>,
pub(crate) use_isolating: bool,
pub(crate) transform: Option<Box<dyn Fn(&str) -> Cow<str> + Send + Sync>>,
pub(crate) formatter:
Option<Box<dyn Fn(&FluentValue, &Mutex<IntlLangMemoizer>) -> Option<String> + Send + Sync>>,
}
impl<R> FluentBundle<R> {
pub fn new<'a, L: 'a + Into<LanguageIdentifier> + PartialEq + Clone>(
locales: impl IntoIterator<Item = &'a L>,
) -> Self {
let locales = locales
.into_iter()
.map(|s| s.clone().into())
.collect::<Vec<_>>();
let lang = locales.get(0).cloned().unwrap_or_default();
FluentBundle {
locales,
resources: vec![],
entries: HashMap::new(),
intls: Mutex::new(IntlLangMemoizer::new(lang)),
use_isolating: true,
transform: None,
formatter: None,
}
}
pub fn add_resource(&mut self, r: R) -> Result<(), Vec<FluentError>>
where
R: Borrow<FluentResource>,
{
let mut errors = vec![];
let res = r.borrow();
let res_pos = self.resources.len();
for (entry_pos, entry) in res.ast().body.iter().enumerate() {
let id = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(ast::Message { ref id, .. }))
| ast::ResourceEntry::Entry(ast::Entry::Term(ast::Term { ref id, .. })) => id.name,
_ => continue,
};
let (entry, kind) = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(..)) => {
(Entry::Message([res_pos, entry_pos]), "message")
}
ast::ResourceEntry::Entry(ast::Entry::Term(..)) => {
(Entry::Term([res_pos, entry_pos]), "term")
}
_ => continue,
};
match self.entries.entry(id.to_string()) {
HashEntry::Vacant(empty) => {
empty.insert(entry);
}
HashEntry::Occupied(_) => {
errors.push(FluentError::Overriding {
kind,
id: id.to_string(),
});
}
}
}
self.resources.push(r);
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
pub fn add_resource_overriding(&mut self, r: R)
where
R: Borrow<FluentResource>,
{
let res = r.borrow();
let res_pos = self.resources.len();
for (entry_pos, entry) in res.ast().body.iter().enumerate() {
let id = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(ast::Message { ref id, .. }))
| ast::ResourceEntry::Entry(ast::Entry::Term(ast::Term { ref id, .. })) => id.name,
_ => continue,
};
let entry = match entry {
ast::ResourceEntry::Entry(ast::Entry::Message(..)) => {
Entry::Message([res_pos, entry_pos])
}
ast::ResourceEntry::Entry(ast::Entry::Term(..)) => {
Entry::Term([res_pos, entry_pos])
}
_ => continue,
};
self.entries.insert(id.to_string(), entry);
}
self.resources.push(r);
}
pub fn set_use_isolating(&mut self, value: bool) {
self.use_isolating = value;
}
pub fn set_transform<F>(&mut self, func: Option<F>)
where
F: Fn(&str) -> Cow<str> + Send + Sync + 'static,
{
if let Some(f) = func {
self.transform = Some(Box::new(f));
} else {
self.transform = None;
}
}
pub fn set_formatter<F>(&mut self, func: Option<F>)
where
F: Fn(&FluentValue, &Mutex<IntlLangMemoizer>) -> Option<String> + Send + Sync + 'static,
{
if let Some(f) = func {
self.formatter = Some(Box::new(f));
} else {
self.formatter = None;
}
}
pub fn has_message(&self, id: &str) -> bool
where
R: Borrow<FluentResource>,
{
self.get_entry_message(id).is_some()
}
pub fn get_message(&self, id: &str) -> Option<FluentMessage>
where
R: Borrow<FluentResource>,
{
let message = self.get_entry_message(id)?;
let value = message.value.as_ref();
let mut attributes = if message.attributes.is_empty() {
HashMap::new()
} else {
HashMap::with_capacity(message.attributes.len())
};
for attr in message.attributes.iter() {
attributes.insert(attr.id.name, &attr.value);
}
Some(FluentMessage { value, attributes })
}
pub fn format_pattern<'bundle>(
&'bundle self,
pattern: &'bundle ast::Pattern,
args: Option<&'bundle FluentArgs>,
errors: &mut Vec<FluentError>,
) -> Cow<'bundle, str>
where
R: Borrow<FluentResource>,
{
let mut scope = Scope::new(self, args);
let result = pattern.resolve(&mut scope).as_string(&scope);
for err in scope.errors {
errors.push(err.into());
}
result
}
pub fn add_function<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
where
F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static,
{
match self.entries.entry(id.to_owned()) {
HashEntry::Vacant(entry) => {
entry.insert(Entry::Function(Box::new(func)));
Ok(())
}
HashEntry::Occupied(_) => Err(FluentError::Overriding {
kind: "function",
id: id.to_owned(),
}),
}
}
}
impl<R> Default for FluentBundle<R> {
fn default() -> Self {
let langid = LanguageIdentifier::default();
FluentBundle {
locales: vec![langid.clone()],
resources: vec![],
entries: Default::default(),
use_isolating: true,
intls: Mutex::new(IntlLangMemoizer::new(langid)),
transform: None,
formatter: None,
}
}
}