Skip to main content

mf2_runtime/
function.rs

1//! Function handlers and the closed-world registry
2//! (budget B13).
3
4use mf2_catalog::Catalog;
5use mf2_model::Dir;
6
7use crate::datetime::TimeZone;
8use crate::error::FormatError;
9use crate::host::Host;
10use crate::scratch::Scratch;
11use crate::sink::{ErrorSink, Sink, SubPartSink};
12use crate::unannotated;
13use crate::value::Value;
14
15/// A function handler. `:string`, `:number`, `:integer`, `:offset` are in
16/// [`crate::functions`]; custom functions implement the same trait (the
17/// suite's `:test:*` functions are written against it).
18///
19/// A handler first *resolves* an expression to a [`Value`]; the runtime then
20/// asks the same handler, for that value, whether and how it formats, its
21/// direction, and whether and how it selects.
22pub trait Function: Sync {
23    /// Function resolution (formatting.md): `operand` resolved — a
24    /// [`Value::Fallback`] when it failed to resolve, so the handler decides
25    /// — and `options` resolved with
26    /// `u:id`/`u:dir` removed. `None` makes the expression a fallback value;
27    /// the handler has reported why.
28    fn resolve<'a>(
29        &self,
30        cx: &FnContext<'_>,
31        operand: Option<&Value<'a>>,
32        options: &Options<'_, 'a>,
33        errs: &mut dyn ErrorSink,
34    ) -> Option<Value<'a>>;
35
36    /// Whether `value` can be formatted. `Err(e)`: the placeholder is a
37    /// fallback value and `e` is reported.
38    fn formattable(&self, cx: &FnContext<'_>, value: &Value<'_>) -> Result<(), FormatError> {
39        let _ = (cx, value);
40        Ok(())
41    }
42
43    /// Writes `value`'s formatted text.
44    fn format(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn Sink);
45
46    /// Writes `value`'s formatted sub-parts (none by default).
47    fn format_parts(&self, cx: &FnContext<'_>, value: &Value<'_>, out: &mut dyn SubPartSink) {
48        let _ = (cx, value, out);
49    }
50
51    /// The `type` of this handler's expression parts. A handler whose parts
52    /// are `"datetime"` is a *date function* (`:datetime`, `:date`, `:time`,
53    /// and so may an application's own): a message that calls one is a
54    /// *date message*, which a Leptos client rewrites after hydration when
55    /// the server's date formatter or zone was not its own (`plan/08` §4.3).
56    /// Only a date function formats a date: an unannotated date/time is a
57    /// Bad Operand. The kind is the mark, so no build pays a method of its
58    /// own for it.
59    fn part_kind(&self) -> &'static str {
60        "string"
61    }
62
63    /// The direction of `value`'s formatted text (Default Bidi Strategy);
64    /// `Auto` = unknown.
65    fn dir(&self, cx: &FnContext<'_>, value: &Value<'_>) -> Dir {
66        let _ = (cx, value);
67        Dir::Auto
68    }
69
70    /// Whether `value` supports selection (formatting.md, "Resolve
71    /// Selectors"). A value that does not matches only `*`, with *Bad
72    /// Selector*.
73    fn selectable(&self, value: &Value<'_>) -> bool {
74        let _ = value;
75        false
76    }
77
78    /// Match(`value`, `key`); `key` is NFC. Report e.g. *Bad Variant Key*
79    /// through `errs`.
80    fn matches(
81        &self,
82        cx: &FnContext<'_>,
83        value: &Value<'_>,
84        key: &str,
85        errs: &mut dyn ErrorSink,
86    ) -> bool {
87        let _ = (cx, value, key, errs);
88        false
89    }
90
91    /// `BetterThan(value, key1, key2)`, for two keys that both match.
92    fn better_than(&self, cx: &FnContext<'_>, value: &Value<'_>, key1: &str, key2: &str) -> bool {
93        let _ = (cx, value, key1, key2);
94        false
95    }
96}
97
98/// What a handler may see of the formatting context: read-only and minimal
99/// (formatting.md, "Function Handler").
100#[derive(Clone, Copy)]
101pub struct FnContext<'x> {
102    pub(crate) catalog: &'x Catalog,
103    pub(crate) host: &'static dyn Host,
104    pub(crate) dir: Option<Dir>,
105    pub(crate) time_zone: &'x TimeZone,
106}
107
108impl<'x> FnContext<'x> {
109    /// The locale (the catalog's).
110    pub fn locale(&self) -> &'x str {
111        self.catalog.locale()
112    }
113
114    /// The expression's `u:dir` (`Ltr`, `Rtl` or `Auto`), if set.
115    pub fn dir(&self) -> Option<Dir> {
116        self.dir
117    }
118
119    /// The platform services.
120    pub fn host(&self) -> &'static dyn Host {
121        self.host
122    }
123
124    /// The catalog, for its locale data (`Catalog::locale_entry`).
125    #[doc(hidden)]
126    pub fn catalog(&self) -> &'x Catalog {
127        self.catalog
128    }
129
130    /// Whether `value`, a string the program passed in, is canonically
131    /// equivalent to `key`, a string this catalog holds in NFC — the
132    /// comparison MF2 asks for between a selector value and a variant key,
133    /// and the one `:string` makes. Decided from the small map the catalog
134    /// carries (`plan/01` §4.3), so a custom selector can make it without
135    /// the normalization tables and without allocating.
136    ///
137    /// The map answers exactly for any `key` whose characters, decomposed,
138    /// it holds, and every key and name of the catalog is such a key.
139    /// `None` means that `key` holds a character the map does not reach, so
140    /// it cannot decide. A custom selector comparing against a string of
141    /// its own may then fall back to byte equality (`value == key`), which
142    /// never matches wrongly but misses equivalent spellings, or treat the
143    /// key as unsupported. An identical `value`, and two strings below
144    /// U+0300, are always answered; a catalog whose keys and names are all
145    /// below U+0300 carries an empty map, so there any other pair is `None`.
146    pub fn equivalent(&self, value: &str, key: &str) -> Option<bool> {
147        crate::nfc::check(self.catalog.nfc_map(), value, key)
148    }
149
150    /// The formatting context's time zone (the default of `timeZone`).
151    pub fn time_zone(&self) -> &'x TimeZone {
152        self.time_zone
153    }
154}
155
156/// The catalog, `u:dir` and the time zone; the host is left out.
157impl core::fmt::Debug for FnContext<'_> {
158    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
159        f.debug_struct("FnContext")
160            .field("catalog", self.catalog)
161            .field("dir", &self.dir)
162            .field("time_zone", self.time_zone)
163            .finish_non_exhaustive()
164    }
165}
166
167/// A resolved option value.
168#[derive(Clone, Copy, Debug)]
169#[non_exhaustive]
170pub struct OptionValue<'o, 'a> {
171    /// The value.
172    pub value: &'o Value<'a>,
173    /// Whether it was set directly by a literal (formatting.md, "Resolved
174    /// Values": handlers may require literals, like `select`).
175    pub literal: bool,
176}
177
178/// The resolved options of an expression, in source order.
179pub(crate) type OptionList<'o, 'a> = Scratch<(&'a str, OptionValue<'o, 'a>)>;
180
181/// A view of an [`OptionList`] (possibly empty).
182#[derive(Clone, Copy)]
183pub(crate) struct OptionEntries<'o, 'a> {
184    list: Option<&'o OptionList<'o, 'a>>,
185}
186
187impl<'o, 'a> OptionEntries<'o, 'a> {
188    pub(crate) fn new(list: &'o OptionList<'o, 'a>) -> Self {
189        OptionEntries { list: Some(list) }
190    }
191
192    pub(crate) fn len(self) -> usize {
193        self.list.map_or(0, Scratch::len)
194    }
195
196    pub(crate) fn get(self, i: usize) -> Option<&'o (&'a str, OptionValue<'o, 'a>)> {
197        self.list?.get(i)
198    }
199}
200
201/// The resolved options passed to [`Function::resolve`]. Their order is not
202/// significant; a repeated name (a Duplicate Option Name, which the build
203/// rejects) resolves to the last.
204#[derive(Clone, Copy)]
205pub struct Options<'o, 'a> {
206    pub(crate) entries: OptionEntries<'o, 'a>,
207}
208
209impl<'o, 'a> Options<'o, 'a> {
210    /// The option named `name` (NFC).
211    pub fn get(&self, name: &str) -> Option<OptionValue<'o, 'a>> {
212        let mut found = None;
213        for i in 0..self.entries.len() {
214            if let Some((n, v)) = self.entries.get(i)
215                && *n == name
216            {
217                found = Some(*v);
218            }
219        }
220        found
221    }
222
223    /// Every option, in source order.
224    pub fn iter(&self) -> impl Iterator<Item = (&'a str, OptionValue<'o, 'a>)> + 'o {
225        let entries = self.entries;
226        (0..entries.len()).filter_map(move |i| entries.get(i).copied())
227    }
228
229    /// The number of options.
230    pub fn len(&self) -> usize {
231        self.entries.len()
232    }
233
234    /// Whether there are none.
235    pub fn is_empty(&self) -> bool {
236        self.entries.len() == 0
237    }
238}
239
240/// How many options there are. Written apart from the iterator a handler
241/// uses (`iter`), so that a client's build inlines that as before.
242impl core::fmt::Debug for Options<'_, '_> {
243    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244        f.debug_struct("Options")
245            .field("len", &self.entries.len())
246            .finish_non_exhaustive()
247    }
248}
249
250/// The function handlers an application links: closed world (B13). Build
251/// code generates it from exactly the functions the corpus uses, e.g.
252/// `static REGISTRY: Registry = Registry::new(&[("integer",
253/// &mf2_runtime::functions::INTEGER)]);` — an unused handler is never
254/// referenced, so never linked.
255#[derive(Clone, Copy)]
256pub struct Registry {
257    functions: &'static [(&'static str, &'static dyn Function)],
258    numbers: Option<&'static dyn Function>,
259}
260
261impl Registry {
262    /// No functions: every annotation is an Unknown Function.
263    pub const EMPTY: Registry = Registry {
264        functions: &[],
265        numbers: None,
266    };
267
268    /// The registry of `functions`: `(identifier, handler)`, the identifier
269    /// as the catalog's FUNCS has it (`ns:name`, NFC). The first entry of a
270    /// repeated identifier wins.
271    pub const fn new(functions: &'static [(&'static str, &'static dyn Function)]) -> Registry {
272        Registry {
273            functions,
274            numbers: None,
275        }
276    }
277
278    /// This registry, with `f` formatting unannotated numeric values
279    /// (integer, float and decimal arguments): `mf2-fn-number`'s localized
280    /// exact value. The evaluator checks such a
281    /// value as any unannotated value (a non-finite float is a Bad Operand)
282    /// and then asks `f` for its direction, text, sub-parts and part kind;
283    /// it still does not select. Without it they format in neutral digits
284    /// (§2.6).
285    #[must_use]
286    pub const fn with_numbers(self, f: &'static dyn Function) -> Registry {
287        Registry {
288            numbers: Some(f),
289            ..self
290        }
291    }
292
293    /// The handler for an unannotated value `v`, if the registry has one:
294    /// only a number has one. An unannotated date/time is a Bad Operand —
295    /// only a date function formats a date (`plan/08` §4.3).
296    pub(crate) fn unannotated(&self, v: &Value<'_>) -> Option<&'static dyn Function> {
297        if unannotated::is_numeric(v) {
298            self.numbers
299        } else {
300            None
301        }
302    }
303
304    /// The handler for `name`.
305    pub fn get(&self, name: &str) -> Option<&'static dyn Function> {
306        self.functions
307            .iter()
308            .find(|(n, _)| *n == name)
309            .map(|&(_, f)| f)
310    }
311
312    /// Whether `name` (a FUNCS identifier) is a handler here that formats
313    /// dates: its parts are `"datetime"` ([`Function::part_kind`]).
314    pub fn is_date_function(&self, name: &str) -> bool {
315        self.get(name).is_some_and(|f| f.part_kind() == "datetime")
316    }
317}
318
319impl core::fmt::Debug for Registry {
320    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
321        f.debug_list()
322            .entries(self.functions.iter().map(|(n, _)| n))
323            .finish()
324    }
325}