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}