Skip to main content

mf2_runtime/
format.rs

1//! The entry point.
2
3use mf2_catalog::{Catalog, Entry, MsgId, Names, StrRef};
4
5use crate::datetime::TimeZone;
6use crate::error::FormatError;
7use crate::eval::{self, Args, Env, Out};
8use crate::function::Registry;
9use crate::host::Host;
10use crate::parts::{ExpressionPart, FallbackSource, Isolation, MarkupPart, Part, PartSink};
11use crate::scratch::Scratch;
12use crate::sink::{ErrorSink, Sink};
13use crate::value::Arg;
14
15/// A bidirectional isolation strategy (formatting.md, "Handling
16/// Bidirectional Text").
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
18#[non_exhaustive]
19pub enum BidiStrategy {
20    /// The Default Bidi Strategy: isolate placeholders with U+2066–U+2069.
21    #[default]
22    Default,
23    /// No isolation.
24    None,
25}
26
27/// What formatting needs beyond the catalog and the registry; build it
28/// with [`FormatContext::new`].
29#[non_exhaustive]
30pub struct FormatContext {
31    /// The bidi strategy.
32    pub bidi: BidiStrategy,
33    /// The platform services.
34    pub host: &'static dyn Host,
35    /// The default time zone: what `timeZone` defaults to, and the zone of a
36    /// floating date/time.
37    pub time_zone: TimeZone,
38}
39
40impl FormatContext {
41    /// The Default Bidi Strategy over `host`, in UTC.
42    pub const fn new(host: &'static dyn Host) -> FormatContext {
43        FormatContext {
44            bidi: BidiStrategy::Default,
45            host,
46            time_zone: TimeZone::UTC,
47        }
48    }
49}
50
51impl core::fmt::Debug for FormatContext {
52    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
53        f.debug_struct("FormatContext")
54            .field("bidi", &self.bidi)
55            .field("time_zone", &self.time_zone)
56            .finish_non_exhaustive()
57    }
58}
59
60/// Formats messages of one catalog.
61#[derive(Clone, Copy)]
62pub struct Formatter<'c> {
63    catalog: &'c Catalog,
64    registry: &'c Registry,
65    cx: &'c FormatContext,
66}
67
68impl core::fmt::Debug for Formatter<'_> {
69    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70        f.debug_struct("Formatter")
71            .field("catalog", self.catalog)
72            .finish_non_exhaustive()
73    }
74}
75
76impl<'c> Formatter<'c> {
77    /// A formatter over `catalog`, with the handlers of `registry`.
78    pub const fn new(catalog: &'c Catalog, registry: &'c Registry, cx: &'c FormatContext) -> Self {
79        Formatter {
80            catalog,
81            registry,
82            cx,
83        }
84    }
85
86    /// The catalog.
87    pub fn catalog(&self) -> &'c Catalog {
88        self.catalog
89    }
90
91    /// The text of a `simple` message whose text is valid: no evaluator, no
92    /// allocation. `None` for any other message (use [`Formatter::write`]).
93    #[inline]
94    pub fn simple(&self, id: MsgId) -> Option<&'c str> {
95        match self.catalog.get(id) {
96            Entry::Simple(r) => self.catalog.text(r),
97            _ => None,
98        }
99    }
100
101    /// The text reference of a `simple` message — the seam for catalog text
102    /// as JS strings.
103    #[inline]
104    #[doc(hidden)]
105    pub fn simple_ref(&self, id: MsgId) -> Option<StrRef> {
106        match self.catalog.get(id) {
107            Entry::Simple(r) => Some(r),
108            _ => None,
109        }
110    }
111
112    /// Formats message `id` with positional `args` into `out`.
113    pub fn write(&self, id: MsgId, args: &[Arg<'_>], out: &mut dyn Sink, errs: &mut dyn ErrorSink) {
114        self.run(id, Args::Positional(args), &mut StrOut { out }, errs);
115    }
116
117    /// Formats message `id` with positional `args` to parts.
118    pub fn parts(
119        &self,
120        id: MsgId,
121        args: &[Arg<'_>],
122        out: &mut dyn PartSink,
123        errs: &mut dyn ErrorSink,
124    ) {
125        self.run(id, Args::Positional(args), &mut PartsOut { out }, errs);
126    }
127
128    /// Formats message `id` with named `args`: each slot takes the argument
129    /// whose name is canonically equivalent to the slot's name in NAMES.
130    pub fn write_named(
131        &self,
132        id: MsgId,
133        args: &[(&str, Arg<'_>)],
134        out: &mut dyn Sink,
135        errs: &mut dyn ErrorSink,
136    ) {
137        let map = self.slot_map(id, args);
138        self.run(id, Args::Named(args, &map), &mut StrOut { out }, errs);
139    }
140
141    /// Formats message `id` with named `args` to parts.
142    pub fn parts_named(
143        &self,
144        id: MsgId,
145        args: &[(&str, Arg<'_>)],
146        out: &mut dyn PartSink,
147        errs: &mut dyn ErrorSink,
148    ) {
149        let map = self.slot_map(id, args);
150        self.run(id, Args::Named(args, &map), &mut PartsOut { out }, errs);
151    }
152
153    /// Per slot of message `id`, the index of its argument in `args`.
154    fn slot_map(&self, id: MsgId, args: &[(&str, Arg<'_>)]) -> Scratch<u32> {
155        let names = match self.catalog.get(id) {
156            Entry::Pattern(m) | Entry::Select(m) => m.names(),
157            _ => Names::EMPTY,
158        };
159        let mut map = Scratch::new();
160        // The names are NFC in the catalog; a name the program passed in is
161        // whatever it wrote, so the comparison is canonical equivalence,
162        // decided from the catalog's own map (`plan/01` §4.3).
163        let nfc = self.catalog.nfc_map();
164        for slot in 0..names.external_count() {
165            let name = names.external(slot).and_then(|r| self.catalog.text(r));
166            let found = name.and_then(|name| {
167                args.iter()
168                    .position(|(n, _)| crate::nfc::equivalent(nfc, n, name))
169            });
170            let i = found
171                .and_then(|i| u32::try_from(i).ok())
172                .unwrap_or(u32::MAX);
173            if !map.push(i) {
174                break;
175            }
176        }
177        map
178    }
179
180    fn run(&self, id: MsgId, args: Args<'_, '_>, out: &mut dyn Out, errs: &mut dyn ErrorSink) {
181        match self.catalog.get(id) {
182            Entry::Simple(r) => {
183                if !out.text(self.catalog, r) {
184                    errs.error(FormatError::Malformed);
185                    let iso = (self.cx.bidi == BidiStrategy::Default).then_some(Isolation::Fsi);
186                    out.fallback(FallbackSource::Unknown, iso);
187                }
188            }
189            Entry::Pattern(m) | Entry::Select(m) => {
190                let names = m.names();
191                // Positional arguments are the message's slots: past the
192                // last slot there is none, as with named arguments (only a
193                // damaged catalog refers past it).
194                let args = match args {
195                    Args::Positional(a) => {
196                        Args::Positional(a.get(..names.external_count() as usize).unwrap_or(a))
197                    }
198                    named @ Args::Named(..) => named,
199                };
200                let env = Env {
201                    catalog: self.catalog,
202                    registry: self.registry,
203                    host: self.cx.host,
204                    bidi: self.cx.bidi,
205                    time_zone: &self.cx.time_zone,
206                    names,
207                    args,
208                };
209                eval::run(&env, m, out, errs);
210            }
211            Entry::Absent => errs.error(FormatError::MissingMessage),
212        }
213    }
214}
215
216/// String output.
217struct StrOut<'s> {
218    out: &'s mut dyn Sink,
219}
220
221impl Out for StrOut<'_> {
222    fn text(&mut self, catalog: &Catalog, r: StrRef) -> bool {
223        self.out.push_catalog_text(catalog, r)
224    }
225
226    fn expression(&mut self, part: ExpressionPart<'_>, iso: Option<Isolation>) {
227        if let Some(i) = iso {
228            self.out.push_str(i.as_str());
229        }
230        part.write(self.out);
231        if iso.is_some() {
232            self.out.push_str(Isolation::Pdi.as_str());
233        }
234    }
235
236    fn fallback(&mut self, source: FallbackSource<'_>, iso: Option<Isolation>) {
237        if let Some(i) = iso {
238            self.out.push_str(i.as_str());
239        }
240        self.out.push_str("{");
241        source.write(self.out);
242        self.out.push_str("}");
243        if iso.is_some() {
244            self.out.push_str(Isolation::Pdi.as_str());
245        }
246    }
247
248    fn markup(&mut self, _part: MarkupPart<'_>) {}
249
250    fn wants_markup(&self) -> bool {
251        false
252    }
253}
254
255/// Parts output.
256struct PartsOut<'s> {
257    out: &'s mut dyn PartSink,
258}
259
260impl Out for PartsOut<'_> {
261    fn text(&mut self, catalog: &Catalog, r: StrRef) -> bool {
262        self.out.part_catalog_text(catalog, r)
263    }
264
265    fn expression(&mut self, part: ExpressionPart<'_>, iso: Option<Isolation>) {
266        if let Some(i) = iso {
267            self.out.part(Part::BidiIsolation(i));
268        }
269        self.out.part(Part::Expression(part));
270        if iso.is_some() {
271            self.out.part(Part::BidiIsolation(Isolation::Pdi));
272        }
273    }
274
275    fn fallback(&mut self, source: FallbackSource<'_>, iso: Option<Isolation>) {
276        if let Some(i) = iso {
277            self.out.part(Part::BidiIsolation(i));
278        }
279        self.out.part(Part::Fallback(source));
280        if iso.is_some() {
281            self.out.part(Part::BidiIsolation(Isolation::Pdi));
282        }
283    }
284
285    fn markup(&mut self, part: MarkupPart<'_>) {
286        self.out.part(Part::Markup(part));
287    }
288
289    fn wants_markup(&self) -> bool {
290        true
291    }
292}