mas_i18n/sprintf/
message.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 serde::{Deserialize, Serialize};
8
9/// Specifies how to format an argument.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TypeSpecifier {
12    /// `b`
13    BinaryNumber,
14
15    /// `c`
16    CharacterAsciiValue,
17
18    /// `i`
19    DecimalNumber,
20
21    /// `i`
22    IntegerNumber,
23
24    /// `e`
25    ScientificNotation,
26
27    /// `u`
28    UnsignedDecimalNumber,
29
30    /// `f`
31    FloatingPointNumber,
32
33    /// `g`
34    FloatingPointNumberWithSignificantDigits,
35
36    /// `o`
37    OctalNumber,
38
39    /// `s`
40    String,
41
42    /// `t`
43    TrueOrFalse,
44
45    /// `T`
46    TypeOfArgument,
47
48    /// `v`
49    PrimitiveValue,
50
51    /// `x`
52    HexadecimalNumberLowercase,
53
54    /// `X`
55    HexadecimalNumberUppercase,
56
57    /// `j`
58    Json,
59}
60
61impl TypeSpecifier {
62    /// Returns true if the type specifier is a numeric type, which should be
63    /// specially formatted with the zero
64    const fn is_numeric(self) -> bool {
65        matches!(
66            self,
67            Self::BinaryNumber
68                | Self::DecimalNumber
69                | Self::IntegerNumber
70                | Self::ScientificNotation
71                | Self::UnsignedDecimalNumber
72                | Self::FloatingPointNumber
73                | Self::FloatingPointNumberWithSignificantDigits
74                | Self::OctalNumber
75                | Self::HexadecimalNumberLowercase
76                | Self::HexadecimalNumberUppercase
77        )
78    }
79}
80
81impl std::fmt::Display for TypeSpecifier {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        let specifier = match self {
84            Self::BinaryNumber => 'b',
85            Self::CharacterAsciiValue => 'c',
86            Self::DecimalNumber => 'd',
87            Self::IntegerNumber => 'i',
88            Self::ScientificNotation => 'e',
89            Self::UnsignedDecimalNumber => 'u',
90            Self::FloatingPointNumber => 'f',
91            Self::FloatingPointNumberWithSignificantDigits => 'g',
92            Self::OctalNumber => 'o',
93            Self::String => 's',
94            Self::TrueOrFalse => 't',
95            Self::TypeOfArgument => 'T',
96            Self::PrimitiveValue => 'v',
97            Self::HexadecimalNumberLowercase => 'x',
98            Self::HexadecimalNumberUppercase => 'X',
99            Self::Json => 'j',
100        };
101        write!(f, "{specifier}")
102    }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum ArgumentReference {
107    Indexed(usize),
108    Named(String),
109}
110
111impl std::fmt::Display for ArgumentReference {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            ArgumentReference::Indexed(index) => write!(f, "{index}$"),
115            ArgumentReference::Named(name) => write!(f, "({name})"),
116        }
117    }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum PaddingSpecifier {
122    Zero,
123    Char(char),
124}
125
126impl std::fmt::Display for PaddingSpecifier {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        match self {
129            PaddingSpecifier::Zero => write!(f, "0"),
130            PaddingSpecifier::Char(c) => write!(f, "'{c}"),
131        }
132    }
133}
134
135impl PaddingSpecifier {
136    pub fn char(self) -> char {
137        match self {
138            PaddingSpecifier::Zero => '0',
139            PaddingSpecifier::Char(c) => c,
140        }
141    }
142
143    pub const fn is_zero(self) -> bool {
144        match self {
145            PaddingSpecifier::Zero => true,
146            PaddingSpecifier::Char(_) => false,
147        }
148    }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct Placeholder {
153    pub type_specifier: TypeSpecifier,
154    pub requested_argument: Option<ArgumentReference>,
155    pub plus_sign: bool,
156    pub padding_specifier: Option<PaddingSpecifier>,
157    pub left_align: bool,
158    pub width: Option<usize>,
159    pub precision: Option<usize>,
160}
161
162impl Placeholder {
163    pub fn padding_specifier_is_zero(&self) -> bool {
164        self.padding_specifier
165            .is_some_and(PaddingSpecifier::is_zero)
166    }
167
168    /// Whether it should be formatted as a number for the width argument
169    pub fn numeric_width(&self) -> Option<usize> {
170        self.width
171            .filter(|_| self.padding_specifier_is_zero() && self.type_specifier.is_numeric())
172    }
173}
174
175impl std::fmt::Display for Placeholder {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        write!(f, "%")?;
178        if let Some(argument) = &self.requested_argument {
179            write!(f, "{argument}")?;
180        }
181
182        if self.plus_sign {
183            write!(f, "+")?;
184        }
185
186        if let Some(padding_specifier) = &self.padding_specifier {
187            write!(f, "{padding_specifier}")?;
188        }
189
190        if self.left_align {
191            write!(f, "-")?;
192        }
193
194        if let Some(width) = self.width {
195            write!(f, "{width}")?;
196        }
197
198        if let Some(precision) = self.precision {
199            write!(f, ".{precision}")?;
200        }
201
202        write!(f, "{}", self.type_specifier)
203    }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct Message {
208    parts: Vec<Part>,
209}
210
211impl std::fmt::Display for Message {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        for part in &self.parts {
214            write!(f, "{part}")?;
215        }
216        Ok(())
217    }
218}
219
220impl FromIterator<Part> for Message {
221    fn from_iter<T: IntoIterator<Item = Part>>(iter: T) -> Self {
222        Self {
223            parts: iter.into_iter().collect(),
224        }
225    }
226}
227
228impl Message {
229    pub(crate) fn parts(&self) -> std::slice::Iter<'_, Part> {
230        self.parts.iter()
231    }
232
233    /// Create a message from a literal string, without any placeholders.
234    #[must_use]
235    pub fn from_literal(literal: String) -> Message {
236        Message {
237            parts: vec![Part::Text(literal)],
238        }
239    }
240}
241
242impl Serialize for Message {
243    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
244        let string = self.to_string();
245        serializer.serialize_str(&string)
246    }
247}
248
249impl<'de> Deserialize<'de> for Message {
250    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
251        let string = String::deserialize(deserializer)?;
252        string.parse().map_err(serde::de::Error::custom)
253    }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub(crate) enum Part {
258    Percent,
259    Text(String),
260    Placeholder(Placeholder),
261}
262
263impl From<Placeholder> for Part {
264    fn from(placeholder: Placeholder) -> Self {
265        Self::Placeholder(placeholder)
266    }
267}
268
269impl From<String> for Part {
270    fn from(text: String) -> Self {
271        Self::Text(text)
272    }
273}
274
275impl std::fmt::Display for Part {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        match self {
278            Part::Percent => write!(f, "%%"),
279            Part::Text(text) => write!(f, "{text}"),
280            Part::Placeholder(placeholder) => write!(f, "{placeholder}"),
281        }
282    }
283}