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_model::{Dir, MarkupKind};
5
6use crate::function::{FnContext, Function, OptionEntries};
7use crate::sink::{Sink, SubPartSink};
8use crate::text::write_escaped_literal;
9use crate::unannotated;
10use crate::value::Value;
11
12/// Receives the parts of a formatted message, in order.
13pub trait PartSink {
14    /// One part.
15    fn part(&mut self, part: Part<'_>);
16}
17
18/// A part of a formatted message. Concatenated, the parts are the string
19/// output.
20#[non_exhaustive]
21pub enum Part<'p> {
22    /// Literal text.
23    Text(&'p str),
24    /// A bidi isolation control (the Default Bidi Strategy).
25    BidiIsolation(Isolation),
26    /// A formatted placeholder.
27    Expression(ExpressionPart<'p>),
28    /// Markup (it writes no text).
29    Markup(MarkupPart<'p>),
30    /// A placeholder that resolved to a fallback value: `{source}`.
31    Fallback(FallbackSource<'p>),
32}
33
34/// A bidi isolation control.
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
36pub enum Isolation {
37    /// U+2066 LEFT-TO-RIGHT ISOLATE.
38    Lri,
39    /// U+2067 RIGHT-TO-LEFT ISOLATE.
40    Rli,
41    /// U+2068 FIRST STRONG ISOLATE.
42    Fsi,
43    /// U+2069 POP DIRECTIONAL ISOLATE.
44    Pdi,
45}
46
47impl Isolation {
48    /// The control character, as text.
49    pub const fn as_str(self) -> &'static str {
50        match self {
51            Isolation::Lri => "\u{2066}",
52            Isolation::Rli => "\u{2067}",
53            Isolation::Fsi => "\u{2068}",
54            Isolation::Pdi => "\u{2069}",
55        }
56    }
57}
58
59/// A formatted placeholder: its resolved value and how it formats.
60pub struct ExpressionPart<'p> {
61    pub(crate) value: &'p Value<'p>,
62    pub(crate) handler: Option<&'static dyn Function>,
63    pub(crate) cx: FnContext<'p>,
64    pub(crate) dir: Dir,
65    pub(crate) id: Option<&'p str>,
66}
67
68impl<'p> ExpressionPart<'p> {
69    /// The part's kind: the handler's `part_kind` (`"string"`, `"number"`,
70    /// …); for an unannotated value `"string"` or `"number"`.
71    pub fn kind(&self) -> &'p str {
72        match self.handler {
73            Some(h) => h.part_kind(),
74            None => unannotated::kind(self.value),
75        }
76    }
77
78    /// The catalog's locale.
79    pub fn locale(&self) -> &'p str {
80        self.cx.locale()
81    }
82
83    /// The value's direction after `u:dir`; `Auto` = unknown.
84    pub fn dir(&self) -> Dir {
85        self.dir
86    }
87
88    /// The `u:id` option.
89    pub fn id(&self) -> Option<&'p str> {
90        self.id
91    }
92
93    /// The resolved value.
94    pub fn value(&self) -> &'p Value<'p> {
95        self.value
96    }
97
98    /// Writes the formatted text (what string output shows).
99    pub fn write(&self, out: &mut dyn Sink) {
100        match self.handler {
101            Some(h) => h.format(&self.cx, self.value, out),
102            None => unannotated::format(self.value, self.cx.host(), out),
103        }
104    }
105
106    /// The sub-parts of the formatted text (a number's `minusSign`,
107    /// `integer`, `decimal`, `fraction`, …).
108    pub fn sub_parts(&self, out: &mut dyn SubPartSink) {
109        match self.handler {
110            Some(h) => h.format_parts(&self.cx, self.value, out),
111            None => unannotated::format_parts(self.value, self.cx.host(), out),
112        }
113    }
114}
115
116/// A markup placeholder.
117pub struct MarkupPart<'p> {
118    pub(crate) kind: MarkupKind,
119    pub(crate) name: &'p str,
120    pub(crate) id: Option<&'p str>,
121    pub(crate) options: OptionEntries<'p, 'p>,
122}
123
124impl<'p> MarkupPart<'p> {
125    /// Open, standalone or close.
126    pub fn kind(&self) -> MarkupKind {
127        self.kind
128    }
129
130    /// The markup name (NFC).
131    pub fn name(&self) -> &'p str {
132        self.name
133    }
134
135    /// The `u:id` option.
136    pub fn id(&self) -> Option<&'p str> {
137        self.id
138    }
139
140    /// The resolved options, `u:` options removed.
141    pub fn options(&self) -> MarkupOptions<'p> {
142        MarkupOptions {
143            entries: self.options,
144            at: 0,
145        }
146    }
147}
148
149/// The options of a [`MarkupPart`]: `(name, value)` in source order.
150pub struct MarkupOptions<'p> {
151    entries: OptionEntries<'p, 'p>,
152    at: usize,
153}
154
155impl<'p> Iterator for MarkupOptions<'p> {
156    type Item = (&'p str, &'p Value<'p>);
157
158    fn next(&mut self) -> Option<Self::Item> {
159        let e = self.entries.get(self.at)?;
160        self.at += 1;
161        Some((e.0, e.1.value))
162    }
163}
164
165/// What a fallback value shows (formatting.md, "Fallback Resolution").
166#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
167#[non_exhaustive]
168pub enum FallbackSource<'p> {
169    /// `$name`.
170    Variable(&'p str),
171    /// `|value|`, with `\` and `|` escaped.
172    Literal(&'p str),
173    /// `:ns:name`.
174    Function(&'p str),
175    /// `�` (a malformed record, or a name that could not be read).
176    Unknown,
177}
178
179impl FallbackSource<'_> {
180    /// Writes the representation, without the braces.
181    pub fn write(&self, out: &mut dyn Sink) {
182        match *self {
183            FallbackSource::Variable(n) => {
184                out.push_str("$");
185                out.push_str(n);
186            }
187            FallbackSource::Literal(v) => {
188                out.push_str("|");
189                write_escaped_literal(v, out);
190                out.push_str("|");
191            }
192            FallbackSource::Function(n) => {
193                out.push_str(":");
194                out.push_str(n);
195            }
196            FallbackSource::Unknown => out.push_str("\u{FFFD}"),
197        }
198    }
199}