1use 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#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
19#[non_exhaustive]
20pub enum BidiStrategy {
21 #[default]
23 Default,
24 None,
26}
27
28#[non_exhaustive]
31pub struct FormatContext {
32 pub bidi: BidiStrategy,
34 pub host: &'static dyn Host,
36 pub time_zone: TimeZone,
39}
40
41impl FormatContext {
42 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#[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 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 pub fn catalog(&self) -> &'c Catalog {
89 self.catalog
90 }
91
92 #[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 #[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 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 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 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 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 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 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
220struct 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
259struct 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}