spdlog_internal/pattern_parser/
parse.rs1use 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 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}