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, 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}