mas_i18n/sprintf/
parser.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
7#![allow(clippy::result_large_err)]
8
9use std::str::FromStr;
10
11use pest::{Parser, Span, error::ErrorVariant, iterators::Pair};
12
13use super::message::{
14    ArgumentReference, Message, PaddingSpecifier, Part, Placeholder, TypeSpecifier,
15};
16
17#[derive(pest_derive::Parser)]
18#[grammar = "sprintf/grammar.pest"]
19struct SprintfParser;
20
21pub type Error = pest::error::Error<Rule>;
22type Result<T, E = Error> = std::result::Result<T, E>;
23
24fn unexpected_rule_error(pair: &Pair<Rule>) -> Error {
25    Error::new_from_span(
26        ErrorVariant::CustomError {
27            message: format!("Unexpected rule: {:?}", pair.as_rule()),
28        },
29        pair.as_span(),
30    )
31}
32
33fn ensure_end_of_pairs(pairs: &mut pest::iterators::Pairs<Rule>, span: Span<'_>) -> Result<()> {
34    if pairs.next().is_none() {
35        Ok(())
36    } else {
37        Err(Error::new_from_span(
38            ErrorVariant::CustomError {
39                message: String::from("Expected end of pairs"),
40            },
41            span,
42        ))
43    }
44}
45
46fn next_pair<'i>(
47    pairs: &mut pest::iterators::Pairs<'i, Rule>,
48    span: Span<'i>,
49) -> Result<Pair<'i, Rule>> {
50    pairs.next().ok_or_else(|| {
51        Error::new_from_span(
52            ErrorVariant::CustomError {
53                message: String::from("Expected pair"),
54            },
55            span,
56        )
57    })
58}
59
60fn ensure_rule_type(pair: &Pair<Rule>, rule: Rule) -> Result<()> {
61    if pair.as_rule() == rule {
62        Ok(())
63    } else {
64        Err(unexpected_rule_error(pair))
65    }
66}
67
68fn interpret_ident(pair: &Pair<Rule>) -> Result<String> {
69    ensure_rule_type(pair, Rule::ident)?;
70    Ok(pair.as_str().to_owned())
71}
72
73fn interpret_number(pair: &Pair<Rule>) -> Result<usize> {
74    ensure_rule_type(pair, Rule::number)?;
75    pair.as_str().parse().map_err(|e| {
76        Error::new_from_span(
77            ErrorVariant::CustomError {
78                message: format!("Failed to parse number: {e}"),
79            },
80            pair.as_span(),
81        )
82    })
83}
84
85fn interpret_arg_named(pair: Pair<Rule>) -> Result<ArgumentReference> {
86    ensure_rule_type(&pair, Rule::arg_named)?;
87    let span = pair.as_span();
88    let mut pairs = pair.into_inner();
89
90    let ident = next_pair(&mut pairs, span)?;
91    let ident = interpret_ident(&ident)?;
92
93    ensure_end_of_pairs(&mut pairs, span)?;
94    Ok(ArgumentReference::Named(ident))
95}
96
97fn interpret_arg_indexed(pair: Pair<Rule>) -> Result<ArgumentReference> {
98    ensure_rule_type(&pair, Rule::arg_indexed)?;
99    let span = pair.as_span();
100    let mut pairs = pair.into_inner();
101
102    let number = next_pair(&mut pairs, span)?;
103    let number = interpret_number(&number)?;
104
105    ensure_end_of_pairs(&mut pairs, span)?;
106    Ok(ArgumentReference::Indexed(number))
107}
108
109fn interpret_padding_specifier(pair: &Pair<Rule>) -> Result<PaddingSpecifier> {
110    ensure_rule_type(pair, Rule::padding_specifier)?;
111    let specifier: Vec<char> = pair.as_str().chars().collect();
112
113    let specifier = match specifier[..] {
114        ['0'] => PaddingSpecifier::Zero,
115        ['\'', c] => PaddingSpecifier::Char(c),
116        ref specifier => {
117            return Err(Error::new_from_span(
118                ErrorVariant::CustomError {
119                    message: format!("Unexpected padding specifier: {specifier:?}"),
120                },
121                pair.as_span(),
122            ));
123        }
124    };
125
126    Ok(specifier)
127}
128
129fn interpret_width(pair: Pair<Rule>) -> Result<usize> {
130    ensure_rule_type(&pair, Rule::width)?;
131    let span = pair.as_span();
132    let mut pairs = pair.into_inner();
133
134    let number = next_pair(&mut pairs, span)?;
135    let number = interpret_number(&number)?;
136
137    ensure_end_of_pairs(&mut pairs, span)?;
138    Ok(number)
139}
140
141fn interpret_precision(pair: Pair<Rule>) -> Result<usize> {
142    ensure_rule_type(&pair, Rule::precision)?;
143    let span = pair.as_span();
144    let mut pairs = pair.into_inner();
145
146    let number = next_pair(&mut pairs, span)?;
147    let number = interpret_number(&number)?;
148
149    ensure_end_of_pairs(&mut pairs, span)?;
150    Ok(number)
151}
152
153fn interpret_type_specifier(pair: &Pair<Rule>) -> Result<TypeSpecifier> {
154    ensure_rule_type(pair, Rule::type_specifier)?;
155    let specifier: Vec<char> = pair.as_str().chars().collect();
156
157    let type_specifier = match specifier[..] {
158        ['b'] => TypeSpecifier::BinaryNumber,
159        ['c'] => TypeSpecifier::CharacterAsciiValue,
160        ['d'] => TypeSpecifier::DecimalNumber,
161        ['i'] => TypeSpecifier::IntegerNumber,
162        ['e'] => TypeSpecifier::ScientificNotation,
163        ['u'] => TypeSpecifier::UnsignedDecimalNumber,
164        ['f'] => TypeSpecifier::FloatingPointNumber,
165        ['g'] => TypeSpecifier::FloatingPointNumberWithSignificantDigits,
166        ['o'] => TypeSpecifier::OctalNumber,
167        ['s'] => TypeSpecifier::String,
168        ['t'] => TypeSpecifier::TrueOrFalse,
169        ['T'] => TypeSpecifier::TypeOfArgument,
170        ['v'] => TypeSpecifier::PrimitiveValue,
171        ['x'] => TypeSpecifier::HexadecimalNumberLowercase,
172        ['X'] => TypeSpecifier::HexadecimalNumberUppercase,
173        ['j'] => TypeSpecifier::Json,
174        _ => {
175            return Err(Error::new_from_span(
176                ErrorVariant::CustomError {
177                    message: String::from("Unexpected type specifier"),
178                },
179                pair.as_span(),
180            ));
181        }
182    };
183
184    Ok(type_specifier)
185}
186
187fn interpret_placeholder(pair: Pair<Rule>) -> Result<Placeholder> {
188    ensure_rule_type(&pair, Rule::placeholder)?;
189    let span = pair.as_span();
190    let mut pairs = pair.into_inner();
191    let mut current_pair = next_pair(&mut pairs, span)?;
192
193    let argument = if current_pair.as_rule() == Rule::arg_named {
194        let argument = interpret_arg_named(current_pair)?;
195        current_pair = next_pair(&mut pairs, span)?;
196        Some(argument)
197    } else if current_pair.as_rule() == Rule::arg_indexed {
198        let argument = interpret_arg_indexed(current_pair)?;
199        current_pair = next_pair(&mut pairs, span)?;
200        Some(argument)
201    } else {
202        None
203    };
204
205    let plus_sign = if current_pair.as_rule() == Rule::plus_sign {
206        current_pair = next_pair(&mut pairs, span)?;
207        true
208    } else {
209        false
210    };
211
212    let padding_specifier = if current_pair.as_rule() == Rule::padding_specifier {
213        let padding_specifier = interpret_padding_specifier(&current_pair)?;
214        current_pair = next_pair(&mut pairs, span)?;
215        Some(padding_specifier)
216    } else {
217        None
218    };
219
220    let left_align = if current_pair.as_rule() == Rule::left_align {
221        current_pair = next_pair(&mut pairs, span)?;
222        true
223    } else {
224        false
225    };
226
227    let width = if current_pair.as_rule() == Rule::width {
228        let width = interpret_width(current_pair)?;
229        current_pair = next_pair(&mut pairs, span)?;
230        Some(width)
231    } else {
232        None
233    };
234
235    let precision = if current_pair.as_rule() == Rule::precision {
236        let precision = interpret_precision(current_pair)?;
237        current_pair = next_pair(&mut pairs, span)?;
238        Some(precision)
239    } else {
240        None
241    };
242
243    let type_specifier = interpret_type_specifier(&current_pair)?;
244
245    ensure_end_of_pairs(&mut pairs, span)?;
246
247    Ok(Placeholder {
248        type_specifier,
249        requested_argument: argument,
250        plus_sign,
251        padding_specifier,
252        left_align,
253        width,
254        precision,
255    })
256}
257
258impl FromStr for Message {
259    type Err = Error;
260
261    fn from_str(input: &str) -> Result<Self, Self::Err> {
262        SprintfParser::parse(Rule::message, input)?
263            // Filter out the "end of input" rule
264            .filter(|pair| pair.as_rule() != Rule::EOI)
265            .map(|pair| match pair.as_rule() {
266                Rule::text => Ok(pair.as_str().to_owned().into()),
267                Rule::percent => Ok(Part::Percent),
268                Rule::placeholder => Ok(interpret_placeholder(pair)?.into()),
269                _ => Err(unexpected_rule_error(&pair)),
270            })
271            .collect()
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_parser() {
281        // Cases extracted from sprintf-js tests
282        let cases = [
283            "%%",
284            "%'_-5s",
285            "%'_5s",
286            "%+'_10d",
287            "%+.1f",
288            "%+010d",
289            "%+d",
290            "%+f",
291            "%+i",
292            "%-5s",
293            "%.1f",
294            "%.1g",
295            "%.1t",
296            "%.3g",
297            "%.6g",
298            "%0-5s",
299            "%02u",
300            "%05d",
301            "%05i",
302            "%05s",
303            "%2$s %3$s a %1$s",
304            "%2j",
305            "%5.1s",
306            "%5.5s",
307            "%5s",
308            "%8.3f",
309            "%T",
310            "%X",
311            "%b",
312            "%c",
313            "%d",
314            "%e",
315            "%f",
316            "%f %f",
317            "%f %s",
318            "%g",
319            "%i",
320            "%j",
321            "%o",
322            "%s",
323            "%t",
324            "%u",
325            "%v",
326            "%x",
327            "Hello %(who)s!",
328        ];
329
330        for case in cases {
331            let result: Result<Message> = case.parse();
332            assert!(result.is_ok(), "Failed to parse: {case}");
333            let message = result.unwrap();
334            assert_eq!(message.to_string(), *case);
335        }
336    }
337}