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