use mf2_catalog::{Catalog, Entry, MsgId, Names, StrRef};
use crate::datetime::TimeZone;
use crate::error::FormatError;
use crate::eval::{self, Args, Env, Out};
use crate::function::Registry;
use crate::host::Host;
use crate::parts::{ExpressionPart, FallbackSource, Isolation, MarkupPart, Part, PartSink};
use crate::scratch::Scratch;
use crate::sink::{ErrorSink, Sink};
use crate::text::nfc_quick;
use crate::value::Arg;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
#[non_exhaustive]
pub enum BidiStrategy {
#[default]
Default,
None,
}
#[non_exhaustive]
pub struct FormatContext {
pub bidi: BidiStrategy,
pub host: &'static dyn Host,
pub time_zone: TimeZone,
}
impl FormatContext {
pub const fn new(host: &'static dyn Host) -> FormatContext {
FormatContext {
bidi: BidiStrategy::Default,
host,
time_zone: TimeZone::UTC,
}
}
}
impl core::fmt::Debug for FormatContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("FormatContext")
.field("bidi", &self.bidi)
.field("time_zone", &self.time_zone)
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy)]
pub struct Formatter<'c> {
catalog: &'c Catalog,
registry: &'c Registry,
cx: &'c FormatContext,
}
impl core::fmt::Debug for Formatter<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Formatter")
.field("catalog", self.catalog)
.finish_non_exhaustive()
}
}
impl<'c> Formatter<'c> {
pub const fn new(catalog: &'c Catalog, registry: &'c Registry, cx: &'c FormatContext) -> Self {
Formatter {
catalog,
registry,
cx,
}
}
pub fn catalog(&self) -> &'c Catalog {
self.catalog
}
#[inline]
pub fn simple(&self, id: MsgId) -> Option<&'c str> {
match self.catalog.get(id) {
Entry::Simple(r) => self.catalog.text(r),
_ => None,
}
}
#[inline]
#[doc(hidden)]
pub fn simple_ref(&self, id: MsgId) -> Option<StrRef> {
match self.catalog.get(id) {
Entry::Simple(r) => Some(r),
_ => None,
}
}
pub fn write(&self, id: MsgId, args: &[Arg<'_>], out: &mut dyn Sink, errs: &mut dyn ErrorSink) {
self.run(id, Args::Positional(args), &mut StrOut { out }, errs);
}
pub fn parts(
&self,
id: MsgId,
args: &[Arg<'_>],
out: &mut dyn PartSink,
errs: &mut dyn ErrorSink,
) {
self.run(id, Args::Positional(args), &mut PartsOut { out }, errs);
}
pub fn write_named(
&self,
id: MsgId,
args: &[(&str, Arg<'_>)],
out: &mut dyn Sink,
errs: &mut dyn ErrorSink,
) {
let map = self.slot_map(id, args);
self.run(id, Args::Named(args, &map), &mut StrOut { out }, errs);
}
pub fn parts_named(
&self,
id: MsgId,
args: &[(&str, Arg<'_>)],
out: &mut dyn PartSink,
errs: &mut dyn ErrorSink,
) {
let map = self.slot_map(id, args);
self.run(id, Args::Named(args, &map), &mut PartsOut { out }, errs);
}
fn slot_map(&self, id: MsgId, args: &[(&str, Arg<'_>)]) -> Scratch<u32> {
let names = match self.catalog.get(id) {
Entry::Pattern(m) | Entry::Select(m) => m.names(),
_ => Names::EMPTY,
};
let mut map = Scratch::new();
let mut buf = alloc::string::String::new();
for slot in 0..names.external_count() {
let name = names.external(slot).and_then(|r| self.catalog.text(r));
let found = name.and_then(|name| {
args.iter().position(|(n, _)| {
if nfc_quick(n) {
*n == name
} else {
buf.clear();
self.cx.host.nfc(n, &mut buf) == name
}
})
});
let i = found
.and_then(|i| u32::try_from(i).ok())
.unwrap_or(u32::MAX);
if !map.push(i) {
break;
}
}
map
}
fn run(&self, id: MsgId, args: Args<'_, '_>, out: &mut dyn Out, errs: &mut dyn ErrorSink) {
match self.catalog.get(id) {
Entry::Simple(r) => {
if !out.text(self.catalog, r) {
errs.error(FormatError::Malformed);
let iso = (self.cx.bidi == BidiStrategy::Default).then_some(Isolation::Fsi);
out.fallback(FallbackSource::Unknown, iso);
}
}
Entry::Pattern(m) | Entry::Select(m) => {
let names = m.names();
let args = match args {
Args::Positional(a) => {
Args::Positional(a.get(..names.external_count() as usize).unwrap_or(a))
}
named @ Args::Named(..) => named,
};
let env = Env {
catalog: self.catalog,
registry: self.registry,
host: self.cx.host,
bidi: self.cx.bidi,
time_zone: &self.cx.time_zone,
names,
args,
};
eval::run(&env, m, out, errs);
}
Entry::Absent => errs.error(FormatError::MissingMessage),
}
}
}
struct StrOut<'s> {
out: &'s mut dyn Sink,
}
impl Out for StrOut<'_> {
fn text(&mut self, catalog: &Catalog, r: StrRef) -> bool {
self.out.push_catalog_text(catalog, r)
}
fn expression(&mut self, part: ExpressionPart<'_>, iso: Option<Isolation>) {
if let Some(i) = iso {
self.out.push_str(i.as_str());
}
part.write(self.out);
if iso.is_some() {
self.out.push_str(Isolation::Pdi.as_str());
}
}
fn fallback(&mut self, source: FallbackSource<'_>, iso: Option<Isolation>) {
if let Some(i) = iso {
self.out.push_str(i.as_str());
}
self.out.push_str("{");
source.write(self.out);
self.out.push_str("}");
if iso.is_some() {
self.out.push_str(Isolation::Pdi.as_str());
}
}
fn markup(&mut self, _part: MarkupPart<'_>) {}
fn wants_markup(&self) -> bool {
false
}
}
struct PartsOut<'s> {
out: &'s mut dyn PartSink,
}
impl Out for PartsOut<'_> {
fn text(&mut self, catalog: &Catalog, r: StrRef) -> bool {
self.out.part_catalog_text(catalog, r)
}
fn expression(&mut self, part: ExpressionPart<'_>, iso: Option<Isolation>) {
if let Some(i) = iso {
self.out.part(Part::BidiIsolation(i));
}
self.out.part(Part::Expression(part));
if iso.is_some() {
self.out.part(Part::BidiIsolation(Isolation::Pdi));
}
}
fn fallback(&mut self, source: FallbackSource<'_>, iso: Option<Isolation>) {
if let Some(i) = iso {
self.out.part(Part::BidiIsolation(i));
}
self.out.part(Part::Fallback(source));
if iso.is_some() {
self.out.part(Part::BidiIsolation(Isolation::Pdi));
}
}
fn markup(&mut self, part: MarkupPart<'_>) {
self.out.part(Part::Markup(part));
}
fn wants_markup(&self) -> bool {
true
}
}