Skip to main content

mf2_runtime/
format.rs

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