mas_i18n/sprintf/
formatter.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::fmt::Formatter;
8
9use pad::{Alignment, PadStr};
10use serde::Serialize;
11use serde_json::{Value, ser::PrettyFormatter};
12use thiserror::Error;
13
14use super::{ArgumentList, Message};
15use crate::sprintf::message::{
16    ArgumentReference, PaddingSpecifier, Part, Placeholder, TypeSpecifier,
17};
18
19macro_rules! format_placeholder {
20    ($value:expr, $type:literal, $placeholder:expr) => {
21        format_step_plus_sign!($value, $type, $placeholder, "",)
22    };
23    ($value:expr, $placeholder:expr) => {
24        format_placeholder!($value, "", $placeholder)
25    };
26}
27
28macro_rules! format_step_plus_sign {
29    ($value:expr, $type:literal, $placeholder:expr, $modifiers:expr, $($argk:ident = $argv:expr),* $(,)?) => {{
30        if $placeholder.plus_sign {
31            format_step_zero!(
32                $value,
33                $type,
34                $placeholder,
35                concat!($modifiers, "+"),
36                $($argk = $argv),*
37            )
38        } else {
39            format_step_zero!(
40                $value,
41                $type,
42                $placeholder,
43                $modifiers,
44                $($argk = $argv),*
45            )
46        }
47    }};
48}
49
50macro_rules! format_step_zero {
51    ($value:expr, $type:literal, $placeholder:expr, $modifiers:expr, $($argk:ident = $argv:expr),* $(,)?) => {{
52        if $placeholder.padding_specifier_is_zero() {
53            format_step_width!(
54                $value,
55                $type,
56                $placeholder,
57                concat!($modifiers, "0"),
58                $($argk = $argv),*
59            )
60        } else {
61            format_step_width!(
62                $value,
63                $type,
64                $placeholder,
65                $modifiers,
66                $($argk = $argv),*
67            )
68        }
69    }};
70}
71
72macro_rules! format_step_width {
73    ($value:expr, $type:literal, $placeholder:expr, $modifiers:expr, $($argk:ident = $argv:expr),* $(,)?) => {{
74        if let Some(width) = $placeholder.numeric_width() {
75            format_step_precision!(
76                $value,
77                $type,
78                $placeholder,
79                concat!($modifiers, "width$"),
80                width = width,
81                $($argk = $argv),*
82            )
83        } else {
84            format_step_precision!(
85                $value,
86                $type,
87                $placeholder,
88                $modifiers,
89                $($argk = $argv),*
90            )
91        }
92    }};
93}
94
95macro_rules! format_step_precision {
96    ($value:expr, $type:literal, $placeholder:expr, $modifiers:expr, $($argk:ident = $argv:expr),* $(,)?) => {{
97        if let Some(precision) = $placeholder.precision {
98            format_end!(
99                $value,
100                $type,
101                $placeholder,
102                concat!($modifiers, ".precision$"),
103                precision = precision,
104                $($argk = $argv),*
105            )
106        } else {
107            format_end!(
108                $value,
109                $type,
110                $placeholder,
111                $modifiers,
112                $($argk = $argv),*
113            )
114        }
115    }};
116}
117
118macro_rules! format_end {
119    ($value:expr, $type:literal, $placeholder:expr, $modifiers:expr, $($argk:ident = $argv:expr),* $(,)?) => {
120        format!(concat!("{value:", $modifiers, $type, "}"), value = $value, $($argk = $argv),*)
121    };
122}
123
124#[derive(Debug)]
125pub enum ValueType {
126    String,
127    Number,
128    Float,
129    Null,
130    Bool,
131    Array,
132    Object,
133}
134
135impl ValueType {
136    fn of_value(value: &Value) -> Self {
137        match value {
138            Value::String(_) => Self::String,
139            Value::Number(_) => Self::Number,
140            Value::Null => Self::Null,
141            Value::Bool(_) => Self::Bool,
142            Value::Array(_) => Self::Array,
143            Value::Object(_) => Self::Object,
144        }
145    }
146}
147
148#[derive(Debug, Error)]
149pub enum FormatError {
150    #[error("Can't format a {value_type:?} as a %{type_specifier}")]
151    InvalidTypeSpecifier {
152        type_specifier: TypeSpecifier,
153        value_type: ValueType,
154    },
155
156    #[error("Unsupported type specifier %{type_specifier}")]
157    UnsupportedTypeSpecifier { type_specifier: TypeSpecifier },
158
159    #[error("Unexpected number type")]
160    NumberIsNotANumber,
161
162    #[error("Unknown named argument {name}")]
163    UnknownNamedArgument { name: String },
164
165    #[error("Unknown indexed argument {index}")]
166    UnknownIndexedArgument { index: usize },
167
168    #[error("Not enough arguments")]
169    NotEnoughArguments,
170
171    #[error("Can't serialize value")]
172    Serialize(#[from] serde_json::Error),
173
174    #[error("Can't convert value to UTF-8")]
175    InvalidUtf8(#[from] std::string::FromUtf8Error),
176}
177
178fn find_value<'a>(
179    arguments: &'a ArgumentList,
180    requested_argument: Option<&ArgumentReference>,
181    current_index: usize,
182) -> Result<&'a Value, FormatError> {
183    match requested_argument {
184        Some(ArgumentReference::Named(name)) => arguments
185            .get_by_name(name)
186            .ok_or(FormatError::UnknownNamedArgument { name: name.clone() }),
187
188        Some(ArgumentReference::Indexed(index)) => arguments
189            .get_by_index(*index - 1)
190            .ok_or(FormatError::UnknownIndexedArgument { index: *index }),
191
192        None => arguments
193            .get_by_index(current_index)
194            .ok_or(FormatError::NotEnoughArguments),
195    }
196}
197
198/// An approximation of JS's Number.prototype.toPrecision
199fn to_precision(number: f64, mut placeholder: Placeholder) -> String {
200    // If the precision is not set, then we just format the number as normal
201    let Some(precision) = placeholder.precision else {
202        return format_placeholder!(number, &placeholder);
203    };
204
205    // This treats NaN, Infinity, -Infinity and zero without any special handling
206    if !number.is_normal() {
207        return format_placeholder!(number, &placeholder);
208    }
209
210    // This tells us how many numbers are before the decimal point
211    // This lossy cast is fine because we only care about the order of magnitude,
212    // and special cases are handled above
213    #[allow(clippy::cast_possible_truncation)]
214    let log10 = number.abs().log10().floor() as i64;
215    let precision_i64 = precision.try_into().unwrap_or(i64::MAX);
216    // We can fit the number in the precision, so we just format it as normal
217    if log10 > 0 && log10 <= precision_i64 || number.abs() < 10.0 {
218        // We remove the number of digits before the decimal point from the precision
219        placeholder.precision = Some(precision - 1 - log10.try_into().unwrap_or(0usize));
220        format_placeholder!(number, &placeholder)
221    } else {
222        // Else in scientific notation there is always one digit before the decimal
223        // point
224        placeholder.precision = Some(precision - 1);
225        format_placeholder!(number, "e", &placeholder)
226    }
227}
228
229#[allow(clippy::too_many_lines, clippy::match_same_arms)]
230fn format_value(value: &Value, placeholder: &Placeholder) -> Result<String, FormatError> {
231    match (value, &placeholder.type_specifier) {
232        (Value::Number(number), ts @ TypeSpecifier::BinaryNumber) => {
233            if let Some(number) = number.as_u64() {
234                Ok(format_placeholder!(number, "b", placeholder))
235            } else if let Some(number) = number.as_i64() {
236                Ok(format_placeholder!(number, "b", placeholder))
237            } else {
238                Err(FormatError::InvalidTypeSpecifier {
239                    type_specifier: *ts,
240                    value_type: ValueType::Float,
241                })
242            }
243        }
244        (v, ts @ TypeSpecifier::BinaryNumber) => Err(FormatError::InvalidTypeSpecifier {
245            type_specifier: *ts,
246            value_type: ValueType::of_value(v),
247        }),
248
249        (Value::String(string), TypeSpecifier::CharacterAsciiValue) if string.len() == 1 => {
250            Ok(format_placeholder!(string, placeholder))
251        }
252        (Value::Number(n), TypeSpecifier::CharacterAsciiValue) => {
253            if let Some(character) = n
254                .as_u64()
255                .and_then(|n| u32::try_from(n).ok())
256                .and_then(|n| char::try_from(n).ok())
257            {
258                Ok(format_placeholder!(character, placeholder))
259            } else {
260                Err(FormatError::InvalidTypeSpecifier {
261                    type_specifier: TypeSpecifier::CharacterAsciiValue,
262                    value_type: ValueType::Number,
263                })
264            }
265        }
266        (v, ts @ TypeSpecifier::CharacterAsciiValue) => Err(FormatError::InvalidTypeSpecifier {
267            type_specifier: *ts,
268            value_type: ValueType::of_value(v),
269        }),
270
271        (
272            Value::Number(number),
273            ts @ (TypeSpecifier::DecimalNumber | TypeSpecifier::IntegerNumber),
274        ) => {
275            if let Some(number) = number.as_u64() {
276                Ok(format_placeholder!(number, placeholder))
277            } else if let Some(number) = number.as_i64() {
278                Ok(format_placeholder!(number, placeholder))
279            } else {
280                Err(FormatError::InvalidTypeSpecifier {
281                    type_specifier: *ts,
282                    value_type: ValueType::Float,
283                })
284            }
285        }
286        (v, ts @ (TypeSpecifier::DecimalNumber | TypeSpecifier::IntegerNumber)) => {
287            Err(FormatError::InvalidTypeSpecifier {
288                type_specifier: *ts,
289                value_type: ValueType::of_value(v),
290            })
291        }
292
293        (Value::Number(number), ts @ TypeSpecifier::UnsignedDecimalNumber) => {
294            if let Some(number) = number.as_u64() {
295                Ok(format_placeholder!(number, placeholder))
296            } else if let Some(number) = number.as_i64() {
297                // Truncate to a i32 and then u32 to mimic JS's behaviour
298                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
299                let number = number as i32 as u32;
300                Ok(format_placeholder!(number, placeholder))
301            } else {
302                Err(FormatError::InvalidTypeSpecifier {
303                    type_specifier: *ts,
304                    value_type: ValueType::Float,
305                })
306            }
307        }
308        (v, ts @ TypeSpecifier::UnsignedDecimalNumber) => Err(FormatError::InvalidTypeSpecifier {
309            type_specifier: *ts,
310            value_type: ValueType::of_value(v),
311        }),
312
313        (Value::Number(number), TypeSpecifier::ScientificNotation) => {
314            if let Some(number) = number.as_u64() {
315                Ok(format_placeholder!(number, "e", placeholder))
316            } else if let Some(number) = number.as_i64() {
317                Ok(format_placeholder!(number, "e", placeholder))
318            } else if let Some(number) = number.as_f64() {
319                Ok(format_placeholder!(number, "e", placeholder))
320            } else {
321                // This should never happen
322                Err(FormatError::NumberIsNotANumber)
323            }
324        }
325        (v, ts @ TypeSpecifier::ScientificNotation) => Err(FormatError::InvalidTypeSpecifier {
326            type_specifier: *ts,
327            value_type: ValueType::of_value(v),
328        }),
329
330        (Value::Number(number), TypeSpecifier::FloatingPointNumber) => {
331            if let Some(number) = number.as_u64() {
332                Ok(format_placeholder!(number, placeholder))
333            } else if let Some(number) = number.as_i64() {
334                Ok(format_placeholder!(number, placeholder))
335            } else if let Some(number) = number.as_f64() {
336                Ok(format_placeholder!(number, placeholder))
337            } else {
338                // This should never happen
339                Err(FormatError::NumberIsNotANumber)
340            }
341        }
342        (v, ts @ TypeSpecifier::FloatingPointNumber) => Err(FormatError::InvalidTypeSpecifier {
343            type_specifier: *ts,
344            value_type: ValueType::of_value(v),
345        }),
346
347        (Value::Number(number), TypeSpecifier::FloatingPointNumberWithSignificantDigits) => {
348            if let Some(number) = number.as_f64() {
349                Ok(to_precision(number, placeholder.clone()))
350            } else {
351                // This might happen if the integer is too big to be represented as a f64
352                Err(FormatError::NumberIsNotANumber)
353            }
354        }
355        (v, ts @ TypeSpecifier::FloatingPointNumberWithSignificantDigits) => {
356            Err(FormatError::InvalidTypeSpecifier {
357                type_specifier: *ts,
358                value_type: ValueType::of_value(v),
359            })
360        }
361
362        (Value::Number(number), ts @ TypeSpecifier::OctalNumber) => {
363            if let Some(number) = number.as_u64() {
364                Ok(format_placeholder!(number, "o", placeholder))
365            } else if let Some(number) = number.as_i64() {
366                // Truncate to a i32 and then u32 to mimic JS's behaviour
367                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
368                let number = number as i32 as u32;
369                Ok(format_placeholder!(number, "o", placeholder))
370            } else {
371                Err(FormatError::InvalidTypeSpecifier {
372                    type_specifier: *ts,
373                    value_type: ValueType::Float,
374                })
375            }
376        }
377        (v, ts @ TypeSpecifier::OctalNumber) => Err(FormatError::InvalidTypeSpecifier {
378            type_specifier: *ts,
379            value_type: ValueType::of_value(v),
380        }),
381
382        (Value::String(string), TypeSpecifier::String) => {
383            Ok(format_placeholder!(string, placeholder))
384        }
385        (Value::Number(number), TypeSpecifier::String) => {
386            let string = format!("{number}");
387            Ok(format_placeholder!(string, placeholder))
388        }
389        (v, ts @ TypeSpecifier::String) => Err(FormatError::InvalidTypeSpecifier {
390            type_specifier: *ts,
391            value_type: ValueType::of_value(v),
392        }),
393
394        (Value::Bool(boolean), TypeSpecifier::TrueOrFalse) => {
395            Ok(format_placeholder!(boolean, placeholder))
396        }
397        (v, ts @ TypeSpecifier::TrueOrFalse) => Err(FormatError::InvalidTypeSpecifier {
398            type_specifier: *ts,
399            value_type: ValueType::of_value(v),
400        }),
401
402        (v, TypeSpecifier::TypeOfArgument) => match v {
403            Value::String(_) => Ok("string".to_owned()),
404            Value::Number(_) => Ok("number".to_owned()),
405            Value::Null => Ok("null".to_owned()),
406            Value::Bool(_) => Ok("boolean".to_owned()),
407            Value::Array(_) => Ok("array".to_owned()),
408            Value::Object(_) => Ok("object".to_owned()),
409        },
410
411        // Unimplemented
412        (_v, TypeSpecifier::PrimitiveValue) => Err(FormatError::UnsupportedTypeSpecifier {
413            type_specifier: placeholder.type_specifier,
414        }),
415
416        (Value::Number(n), ts @ TypeSpecifier::HexadecimalNumberLowercase) => {
417            if let Some(number) = n.as_u64() {
418                Ok(format_placeholder!(number, "x", placeholder))
419            } else if let Some(number) = n.as_i64() {
420                // Truncate to a i32 and then u32 to mimic JS's behaviour
421                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
422                let number = number as i32 as u32;
423                Ok(format_placeholder!(number, "x", placeholder))
424            } else {
425                Err(FormatError::InvalidTypeSpecifier {
426                    type_specifier: *ts,
427                    value_type: ValueType::Float,
428                })
429            }
430        }
431        (v, ts @ TypeSpecifier::HexadecimalNumberLowercase) => {
432            Err(FormatError::InvalidTypeSpecifier {
433                type_specifier: *ts,
434                value_type: ValueType::of_value(v),
435            })
436        }
437
438        (Value::Number(n), ts @ TypeSpecifier::HexadecimalNumberUppercase) => {
439            if let Some(number) = n.as_u64() {
440                Ok(format_placeholder!(number, "X", placeholder))
441            } else if let Some(number) = n.as_i64() {
442                // Truncate to a i32 and then u32 to mimic JS's behaviour
443                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
444                let number = number as i32 as u32;
445                Ok(format_placeholder!(number, "X", placeholder))
446            } else {
447                Err(FormatError::InvalidTypeSpecifier {
448                    type_specifier: *ts,
449                    value_type: ValueType::Float,
450                })
451            }
452        }
453        (v, ts @ TypeSpecifier::HexadecimalNumberUppercase) => {
454            Err(FormatError::InvalidTypeSpecifier {
455                type_specifier: *ts,
456                value_type: ValueType::of_value(v),
457            })
458        }
459
460        (value, TypeSpecifier::Json) => {
461            let mut json = Vec::new();
462            if let Some(width) = placeholder.width {
463                let indent = b" ".repeat(width);
464                let mut serializer = serde_json::Serializer::with_formatter(
465                    &mut json,
466                    PrettyFormatter::with_indent(indent.as_slice()),
467                );
468                value.serialize(&mut serializer)?;
469            } else {
470                let mut serializer = serde_json::Serializer::new(&mut json);
471                value.serialize(&mut serializer)?;
472            }
473            let json = String::from_utf8(json)?;
474            Ok(format_placeholder!(json, placeholder))
475        }
476    }
477}
478
479pub enum FormattedMessagePart<'a> {
480    /// A literal text part of the message. It should not be escaped.
481    Text(&'a str),
482    /// A placeholder part of the message. It should be escaped.
483    Placeholder(String),
484}
485
486impl FormattedMessagePart<'_> {
487    fn len(&self) -> usize {
488        match self {
489            FormattedMessagePart::Text(text) => text.len(),
490            FormattedMessagePart::Placeholder(placeholder) => placeholder.len(),
491        }
492    }
493}
494
495impl std::fmt::Display for FormattedMessagePart<'_> {
496    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
497        match self {
498            FormattedMessagePart::Text(text) => write!(f, "{text}"),
499            FormattedMessagePart::Placeholder(placeholder) => write!(f, "{placeholder}"),
500        }
501    }
502}
503
504pub struct FormattedMessage<'a> {
505    parts: Vec<FormattedMessagePart<'a>>,
506    total_len: usize,
507}
508
509impl FormattedMessage<'_> {
510    /// Returns the length of the formatted message (not the number of parts).
511    #[must_use]
512    pub fn len(&self) -> usize {
513        self.total_len
514    }
515
516    /// Returns `true` if the formatted message is empty.
517    #[must_use]
518    pub fn is_empty(&self) -> bool {
519        self.total_len == 0
520    }
521
522    /// Returns the list of parts of the formatted message.
523    #[must_use]
524    pub fn parts(&self) -> &[FormattedMessagePart<'_>] {
525        &self.parts
526    }
527}
528
529impl std::fmt::Display for FormattedMessage<'_> {
530    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
531        for part in &self.parts {
532            write!(f, "{part}")?;
533        }
534        Ok(())
535    }
536}
537
538impl Message {
539    /// Format the message with the given arguments.
540    ///
541    /// # Errors
542    ///
543    /// Returns an error if the message can't be formatted with the given
544    /// arguments.
545    pub fn format(&self, arguments: &ArgumentList) -> Result<String, FormatError> {
546        self.format_(arguments).map(|fm| fm.to_string())
547    }
548
549    #[doc(hidden)]
550    pub fn format_(&self, arguments: &ArgumentList) -> Result<FormattedMessage<'_>, FormatError> {
551        let mut parts = Vec::with_capacity(self.parts().len());
552
553        // Holds the current index of the placeholder we are formatting, which is used
554        // by non-named, non-indexed placeholders
555        let mut current_placeholder = 0usize;
556        // Compute the total length of the formatted message
557        let mut total_len = 0usize;
558        for part in self.parts() {
559            let formatted = match part {
560                Part::Percent => FormattedMessagePart::Text("%"),
561                Part::Text(text) => FormattedMessagePart::Text(text),
562                Part::Placeholder(placeholder) => {
563                    let value = find_value(
564                        arguments,
565                        placeholder.requested_argument.as_ref(),
566                        current_placeholder,
567                    )?;
568
569                    let formatted = format_value(value, placeholder)?;
570
571                    // Do the extra padding which std::fmt can't really do
572                    let formatted = if let Some(width) = placeholder.width {
573                        let spacer = placeholder
574                            .padding_specifier
575                            .map_or(' ', PaddingSpecifier::char);
576
577                        let alignment = if placeholder.left_align {
578                            Alignment::Left
579                        } else {
580                            Alignment::Right
581                        };
582
583                        formatted.pad(width, spacer, alignment, false)
584                    } else {
585                        formatted
586                    };
587
588                    current_placeholder += 1;
589                    FormattedMessagePart::Placeholder(formatted)
590                }
591            };
592            total_len += formatted.len();
593            parts.push(formatted);
594        }
595
596        Ok(FormattedMessage { parts, total_len })
597    }
598}