Skip to main content

mf2_runtime/
parts.rs

1//! Format to parts (`plans/03-runtime.md` §2.2): the shape the suite's
2//! `expParts` asserts, and what the Leptos layer builds elements from.
3
4use mf2_catalog::{Catalog, StrRef};
5use mf2_model::{Dir, MarkupKind};
6
7use crate::function::{FnContext, Function, OptionEntries};
8use crate::sink::{Sink, SubPartSink};
9use crate::text::write_escaped_literal;
10use crate::unannotated;
11use crate::value::Value;
12
13/// Receives the parts of a formatted message, in order.
14pub trait PartSink {
15    /// One part.
16    fn part(&mut self, part: Part<'_>);
17
18    /// The catalog string `r`, as a [`Part::Text`] — the seam for a sink
19    /// that keeps catalog text beyond the format (`mf2::ratatui`, over a
20    /// native application's `&'static` catalogs, borrows it instead of
21    /// copying it): the evaluator hands catalog text to a part sink only
22    /// through this method. Returns `false` (and hands over nothing) when
23    /// the string is not valid (F4).
24    #[doc(hidden)]
25    fn part_catalog_text(&mut self, catalog: &Catalog, r: StrRef) -> bool {
26        match catalog.text(r) {
27            Some(s) => {
28                if !s.is_empty() {
29                    self.part(Part::Text(s));
30                }
31                true
32            }
33            None => false,
34        }
35    }
36}
37
38/// A part of a formatted message. Concatenated, the parts are the string
39/// output.
40#[derive(Debug)]
41#[non_exhaustive]
42pub enum Part<'p> {
43    /// Literal text.
44    Text(&'p str),
45    /// A bidi isolation control (the Default Bidi Strategy).
46    BidiIsolation(Isolation),
47    /// A formatted placeholder.
48    Expression(ExpressionPart<'p>),
49    /// Markup (it writes no text).
50    Markup(MarkupPart<'p>),
51    /// A placeholder that resolved to a fallback value: `{source}`.
52    Fallback(FallbackSource<'p>),
53}
54
55/// A bidi isolation control.
56#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
57pub enum Isolation {
58    /// U+2066 LEFT-TO-RIGHT ISOLATE.
59    Lri,
60    /// U+2067 RIGHT-TO-LEFT ISOLATE.
61    Rli,
62    /// U+2068 FIRST STRONG ISOLATE.
63    Fsi,
64    /// U+2069 POP DIRECTIONAL ISOLATE.
65    Pdi,
66}
67
68impl Isolation {
69    /// The control character, as text.
70    pub const fn as_str(self) -> &'static str {
71        match self {
72            Isolation::Lri => "\u{2066}",
73            Isolation::Rli => "\u{2067}",
74            Isolation::Fsi => "\u{2068}",
75            Isolation::Pdi => "\u{2069}",
76        }
77    }
78}
79
80/// A formatted placeholder: its resolved value and how it formats.
81pub struct ExpressionPart<'p> {
82    pub(crate) value: &'p Value<'p>,
83    pub(crate) handler: Option<&'static dyn Function>,
84    pub(crate) cx: FnContext<'p>,
85    pub(crate) dir: Dir,
86    pub(crate) id: Option<&'p str>,
87}
88
89impl<'p> ExpressionPart<'p> {
90    /// The part's kind: the handler's `part_kind` (`"string"`, `"number"`,
91    /// …); for an unannotated value `"string"` or `"number"`.
92    pub fn kind(&self) -> &'p str {
93        match self.handler {
94            Some(h) => h.part_kind(),
95            None => unannotated::kind(self.value),
96        }
97    }
98
99    /// The catalog's locale.
100    pub fn locale(&self) -> &'p str {
101        self.cx.locale()
102    }
103
104    /// The value's direction after `u:dir`; `Auto` = unknown.
105    pub fn dir(&self) -> Dir {
106        self.dir
107    }
108
109    /// The `u:id` option.
110    pub fn id(&self) -> Option<&'p str> {
111        self.id
112    }
113
114    /// The resolved value.
115    pub fn value(&self) -> &'p Value<'p> {
116        self.value
117    }
118
119    /// Writes the formatted text (what string output shows).
120    pub fn write(&self, out: &mut dyn Sink) {
121        match self.handler {
122            Some(h) => h.format(&self.cx, self.value, out),
123            None => unannotated::format(self.value, self.cx.host(), out),
124        }
125    }
126
127    /// The sub-parts of the formatted text (a number's `minusSign`,
128    /// `integer`, `decimal`, `fraction`, …).
129    pub fn sub_parts(&self, out: &mut dyn SubPartSink) {
130        match self.handler {
131            Some(h) => h.format_parts(&self.cx, self.value, out),
132            None => unannotated::format_parts(self.value, self.cx.host(), out),
133        }
134    }
135}
136
137/// The value, its direction and `u:id`; the handler and the context are
138/// left out.
139impl core::fmt::Debug for ExpressionPart<'_> {
140    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
141        f.debug_struct("ExpressionPart")
142            .field("value", self.value)
143            .field("dir", &self.dir)
144            .field("id", &self.id)
145            .finish_non_exhaustive()
146    }
147}
148
149/// A markup placeholder.
150pub struct MarkupPart<'p> {
151    pub(crate) kind: MarkupKind,
152    pub(crate) name: &'p str,
153    pub(crate) id: Option<&'p str>,
154    pub(crate) options: OptionEntries<'p, 'p>,
155}
156
157impl<'p> MarkupPart<'p> {
158    /// Open, standalone or close.
159    pub fn kind(&self) -> MarkupKind {
160        self.kind
161    }
162
163    /// The markup name (NFC).
164    pub fn name(&self) -> &'p str {
165        self.name
166    }
167
168    /// The `u:id` option.
169    pub fn id(&self) -> Option<&'p str> {
170        self.id
171    }
172
173    /// The resolved options, `u:` options removed.
174    pub fn options(&self) -> MarkupOptions<'p> {
175        MarkupOptions {
176            entries: self.options,
177            at: 0,
178        }
179    }
180}
181
182/// The kind, the name, `u:id` and how many options it has.
183impl core::fmt::Debug for MarkupPart<'_> {
184    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
185        f.debug_struct("MarkupPart")
186            .field("kind", &self.kind)
187            .field("name", &self.name)
188            .field("id", &self.id)
189            .field("options", &self.options.len())
190            .finish()
191    }
192}
193
194/// The options of a [`MarkupPart`]: `(name, value)` in source order.
195pub struct MarkupOptions<'p> {
196    entries: OptionEntries<'p, 'p>,
197    at: usize,
198}
199
200/// How many options are left to iterate.
201impl core::fmt::Debug for MarkupOptions<'_> {
202    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
203        f.debug_struct("MarkupOptions")
204            .field("left", &self.entries.len().saturating_sub(self.at))
205            .finish()
206    }
207}
208
209impl<'p> Iterator for MarkupOptions<'p> {
210    type Item = (&'p str, &'p Value<'p>);
211
212    fn next(&mut self) -> Option<Self::Item> {
213        let e = self.entries.get(self.at)?;
214        self.at += 1;
215        Some((e.0, e.1.value))
216    }
217}
218
219/// What a fallback value shows (formatting.md, "Fallback Resolution").
220#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
221#[non_exhaustive]
222pub enum FallbackSource<'p> {
223    /// `$name`.
224    Variable(&'p str),
225    /// `|value|`, with `\` and `|` escaped.
226    Literal(&'p str),
227    /// `:ns:name`.
228    Function(&'p str),
229    /// `�` (a malformed record, or a name that could not be read).
230    Unknown,
231}
232
233impl FallbackSource<'_> {
234    /// Writes the representation, without the braces.
235    pub fn write(&self, out: &mut dyn Sink) {
236        match *self {
237            FallbackSource::Variable(n) => {
238                out.push_str("$");
239                out.push_str(n);
240            }
241            FallbackSource::Literal(v) => {
242                out.push_str("|");
243                write_escaped_literal(v, out);
244                out.push_str("|");
245            }
246            FallbackSource::Function(n) => {
247                out.push_str(":");
248                out.push_str(n);
249            }
250            FallbackSource::Unknown => out.push_str("\u{FFFD}"),
251        }
252    }
253}