spdlog_internal/pattern_parser/
parse.rs

1use nom::{error::Error as NomError, Parser};
2
3use super::{helper, Error, Result};
4
5#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
6pub struct Template<'a> {
7    pub tokens: Vec<TemplateToken<'a>>,
8}
9
10impl<'a> Template<'a> {
11    pub fn parse(template: &'a str) -> Result<Self> {
12        let mut parser = Self::parser();
13
14        let (_, parsed_template) = parser.parse(template).map_err(|err| {
15            let err = match err {
16                // The "complete" combinator should transform `Incomplete` into `Error`
17                nom::Err::Incomplete(..) => unreachable!(),
18                nom::Err::Error(err) | nom::Err::Failure(err) => err,
19            };
20            Error::Parse(NomError::new(err.input.into(), err.code))
21        })?;
22
23        Ok(parsed_template)
24    }
25}
26
27impl<'a> Template<'a> {
28    #[must_use]
29    fn parser() -> impl Parser<&'a str, Output = Template<'a>, Error = NomError<&'a str>> {
30        let token_parser = TemplateToken::parser();
31        nom::combinator::complete(nom::multi::many0(token_parser).and(nom::combinator::eof))
32            .map(|(tokens, _)| Self { tokens })
33    }
34
35    #[must_use]
36    fn parser_without_style_range(
37    ) -> impl Parser<&'a str, Output = Template<'a>, Error = NomError<&'a str>> {
38        let token_parser = TemplateToken::parser_without_style_range();
39        nom::combinator::complete(nom::multi::many0(token_parser).and(nom::combinator::eof))
40            .map(|(tokens, _)| Self { tokens })
41    }
42}
43
44#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
45pub enum TemplateToken<'a> {
46    Literal(TemplateLiteral),
47    Formatter(TemplateFormatterToken<'a>),
48    StyleRange(TemplateStyleRange<'a>),
49}
50
51impl<'a> TemplateToken<'a> {
52    #[must_use]
53    fn parser() -> impl Parser<&'a str, Output = TemplateToken<'a>, Error = NomError<&'a str>> {
54        let style_range_parser = TemplateStyleRange::parser();
55        let other_parser = Self::parser_without_style_range();
56
57        nom::combinator::map(style_range_parser, Self::StyleRange).or(other_parser)
58    }
59
60    #[must_use]
61    fn parser_without_style_range(
62    ) -> impl Parser<&'a str, Output = TemplateToken<'a>, Error = NomError<&'a str>> {
63        let literal_parser = TemplateLiteral::parser();
64        let formatter_parser = TemplateFormatterToken::parser();
65
66        nom::combinator::map(literal_parser, Self::Literal)
67            .or(nom::combinator::map(formatter_parser, Self::Formatter))
68    }
69}
70
71#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
72pub struct TemplateLiteral {
73    pub literal: String,
74}
75
76impl TemplateLiteral {
77    #[must_use]
78    fn parser<'a>() -> impl Parser<&'a str, Output = Self, Error = NomError<&'a str>> {
79        let literal_char_parser = nom::combinator::value('{', nom::bytes::complete::tag("{{"))
80            .or(nom::combinator::value('}', nom::bytes::complete::tag("}}")))
81            .or(nom::character::complete::none_of("{"));
82        nom::multi::many1(literal_char_parser).map(|literal_chars| Self {
83            literal: literal_chars.into_iter().collect(),
84        })
85    }
86}
87
88#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
89pub struct TemplateFormatterToken<'a> {
90    pub has_custom_prefix: bool,
91    pub placeholder: &'a str,
92}
93
94impl<'a> TemplateFormatterToken<'a> {
95    #[must_use]
96    fn parser(
97    ) -> impl Parser<&'a str, Output = TemplateFormatterToken<'a>, Error = NomError<&'a str>> {
98        let open_paren = nom::character::complete::char('{');
99        let close_paren = nom::character::complete::char('}');
100        let formatter_prefix = nom::character::complete::char('$');
101        let formatter_placeholder = nom::combinator::recognize((
102            nom::combinator::opt(formatter_prefix),
103            nom::branch::alt((
104                nom::character::complete::alpha1,
105                nom::bytes::complete::tag("_"),
106            )),
107            nom::multi::many0_count(nom::branch::alt((
108                nom::character::complete::alphanumeric1,
109                nom::bytes::complete::tag("_"),
110            ))),
111        ));
112
113        nom::sequence::delimited(open_paren, formatter_placeholder, close_paren).map(
114            move |placeholder: &str| match placeholder.strip_prefix('$') {
115                Some(placeholder) => Self {
116                    has_custom_prefix: true,
117                    placeholder,
118                },
119                None => Self {
120                    has_custom_prefix: false,
121                    placeholder,
122                },
123            },
124        )
125    }
126}
127
128#[cfg_attr(test, derive(Debug, Eq, PartialEq))]
129pub struct TemplateStyleRange<'a> {
130    pub body: Template<'a>,
131}
132
133impl<'a> TemplateStyleRange<'a> {
134    #[must_use]
135    fn parser() -> impl Parser<&'a str, Output = TemplateStyleRange<'a>, Error = NomError<&'a str>>
136    {
137        nom::bytes::complete::tag("{^")
138            .and(helper::take_until_unbalanced('{', '}'))
139            .and(nom::bytes::complete::tag("}"))
140            .map(|((_, body), _)| body)
141            .and_then(Template::parser_without_style_range())
142            .map(|body| Self { body })
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    mod template_parsing {
151        use super::*;
152
153        fn parse_template_str(template: &str) -> nom::IResult<&str, Template<'_>> {
154            Template::parser().parse(template)
155        }
156
157        #[test]
158        fn test_parse_basic() {
159            assert_eq!(
160                parse_template_str(r#"hello"#),
161                Ok((
162                    "",
163                    Template {
164                        tokens: vec![TemplateToken::Literal(TemplateLiteral {
165                            literal: String::from("hello"),
166                        }),],
167                    }
168                ))
169            );
170        }
171
172        #[test]
173        fn test_parse_empty() {
174            assert_eq!(
175                parse_template_str(""),
176                Ok(("", Template { tokens: Vec::new() },))
177            );
178        }
179
180        #[test]
181        fn test_parse_escape_literal() {
182            assert_eq!(
183                parse_template_str(r#"hello {{name}}"#),
184                Ok((
185                    "",
186                    Template {
187                        tokens: vec![TemplateToken::Literal(TemplateLiteral {
188                            literal: String::from("hello {name}"),
189                        }),],
190                    }
191                ))
192            );
193        }
194
195        #[test]
196        fn test_parse_escape_literal_at_beginning() {
197            assert_eq!(
198                parse_template_str(r#"{{name}}"#),
199                Ok((
200                    "",
201                    Template {
202                        tokens: vec![TemplateToken::Literal(TemplateLiteral {
203                            literal: String::from("{name}"),
204                        }),],
205                    }
206                ))
207            );
208        }
209
210        #[test]
211        fn test_parse_formatter_basic() {
212            assert_eq!(
213                parse_template_str(r#"hello {full}!{$custom}"#),
214                Ok((
215                    "",
216                    Template {
217                        tokens: vec![
218                            TemplateToken::Literal(TemplateLiteral {
219                                literal: String::from("hello "),
220                            }),
221                            TemplateToken::Formatter(TemplateFormatterToken {
222                                has_custom_prefix: false,
223                                placeholder: "full"
224                            }),
225                            TemplateToken::Literal(TemplateLiteral {
226                                literal: String::from("!"),
227                            }),
228                            TemplateToken::Formatter(TemplateFormatterToken {
229                                has_custom_prefix: true,
230                                placeholder: "custom",
231                            }),
232                        ],
233                    }
234                ))
235            );
236
237            assert_eq!(
238                parse_template_str(r#"hello {not_exists}!{$custom}"#),
239                Ok((
240                    "",
241                    Template {
242                        tokens: vec![
243                            TemplateToken::Literal(TemplateLiteral {
244                                literal: String::from("hello "),
245                            }),
246                            TemplateToken::Formatter(TemplateFormatterToken {
247                                has_custom_prefix: false,
248                                placeholder: "not_exists",
249                            }),
250                            TemplateToken::Literal(TemplateLiteral {
251                                literal: String::from("!"),
252                            }),
253                            TemplateToken::Formatter(TemplateFormatterToken {
254                                has_custom_prefix: true,
255                                placeholder: "custom",
256                            }),
257                        ],
258                    }
259                ))
260            );
261        }
262
263        #[test]
264        fn test_parse_literal_single_close_paren() {
265            assert_eq!(
266                parse_template_str(r#"hello name}"#),
267                Ok((
268                    "",
269                    Template {
270                        tokens: vec![TemplateToken::Literal(TemplateLiteral {
271                            literal: String::from("hello name}"),
272                        }),],
273                    }
274                ))
275            );
276        }
277
278        #[test]
279        fn test_parse_formatter_invalid_name() {
280            assert!(parse_template_str(r#"hello {name{}!"#).is_err());
281        }
282
283        #[test]
284        fn test_parse_formatter_missing_close_paren() {
285            assert!(parse_template_str(r#"hello {name"#).is_err());
286        }
287
288        #[test]
289        fn test_parse_formatter_duplicate_close_paren() {
290            assert_eq!(
291                parse_template_str(r#"hello {time}}"#),
292                Ok((
293                    "",
294                    Template {
295                        tokens: vec![
296                            TemplateToken::Literal(TemplateLiteral {
297                                literal: String::from("hello "),
298                            }),
299                            TemplateToken::Formatter(TemplateFormatterToken {
300                                has_custom_prefix: false,
301                                placeholder: "time",
302                            }),
303                            TemplateToken::Literal(TemplateLiteral {
304                                literal: String::from("}"),
305                            }),
306                        ],
307                    }
308                ))
309            );
310        }
311
312        #[test]
313        fn test_parse_style_range_basic() {
314            assert_eq!(
315                parse_template_str(r#"hello {^world}"#),
316                Ok((
317                    "",
318                    Template {
319                        tokens: vec![
320                            TemplateToken::Literal(TemplateLiteral {
321                                literal: String::from("hello "),
322                            }),
323                            TemplateToken::StyleRange(TemplateStyleRange {
324                                body: Template {
325                                    tokens: vec![TemplateToken::Literal(TemplateLiteral {
326                                        literal: String::from("world"),
327                                    }),],
328                                },
329                            }),
330                        ],
331                    }
332                ))
333            );
334
335            assert_eq!(
336                parse_template_str(r#"hello {^world {level} {$c_pat} {{escape}}}"#),
337                Ok((
338                    "",
339                    Template {
340                        tokens: vec![
341                            TemplateToken::Literal(TemplateLiteral {
342                                literal: String::from("hello "),
343                            }),
344                            TemplateToken::StyleRange(TemplateStyleRange {
345                                body: Template {
346                                    tokens: vec![
347                                        TemplateToken::Literal(TemplateLiteral {
348                                            literal: String::from("world "),
349                                        }),
350                                        TemplateToken::Formatter(TemplateFormatterToken {
351                                            has_custom_prefix: false,
352                                            placeholder: "level",
353                                        }),
354                                        TemplateToken::Literal(TemplateLiteral {
355                                            literal: String::from(" "),
356                                        }),
357                                        TemplateToken::Formatter(TemplateFormatterToken {
358                                            has_custom_prefix: true,
359                                            placeholder: "c_pat",
360                                        }),
361                                        TemplateToken::Literal(TemplateLiteral {
362                                            literal: String::from(" {escape}"),
363                                        }),
364                                    ],
365                                },
366                            }),
367                        ],
368                    }
369                ))
370            );
371        }
372
373        #[test]
374        fn test_parse_style_range_nested() {
375            assert!(parse_template_str(r#"hello {^ hello {^ world } }"#).is_err());
376        }
377    }
378}