1use compact_str::ToCompactString;
2use nom::{
3 IResult, Parser,
4 bytes::complete::{tag, take_till, take_until, take_while},
5 sequence::delimited,
6};
7use tap::Pipe;
8use tinyvec::TinyVec;
9
10use crate::{
11 MiniStr,
12 error::{ResolverError, ResolverResult},
13 part::{TemplatePart, VariableRef},
14 selector,
15};
16pub(crate) type TinyTemplateParts = TinyVec<[TemplatePart; 5]>;
17
18#[derive(Debug, Clone, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Template {
30 Conditional(selector::Selector),
32 Parts(TinyTemplateParts),
34}
35
36impl Default for Template {
37 fn default() -> Self {
38 Self::Parts(Default::default())
39 }
40}
41
42#[allow(clippy::unnecessary_lazy_evaluations)]
43pub(crate) fn parse_template(input: &str) -> ResolverResult<TinyTemplateParts> {
44 let mut remaining = input;
45
46 core::iter::from_fn(|| {
47 (!remaining.is_empty()).then(|| ())?;
48
49 match parse_variable(remaining) {
50 Ok((next, var)) => {
51 remaining = next;
52 var
53 .pipe(TemplatePart::Variable)
54 .pipe(Ok)
55 .into()
56 }
57 Err(_) => parse_text(remaining)
58 .map(|(next, text)| {
59 remaining = next;
60 match text.is_empty() {
61 true => None,
62 _ => text
63 .pipe(MiniStr::from)
64 .pipe(TemplatePart::Text)
65 .into(),
66 }
67 })
68 .map_err(|e| {
69 e.to_compact_string()
70 .pipe(ResolverError::ParseError)
71 })
72 .transpose(),
73 }
74 })
75 .collect()
76}
77
78fn parse_variable(input: &str) -> IResult<&str, VariableRef> {
79 if input.starts_with("{{") {
81 return nom::error::Error::new(input, nom::error::ErrorKind::Verify)
82 .pipe(nom::Err::Error)
83 .pipe(Err);
84 }
85
86 let (input, content) =
87 delimited(tag("{"), take_until("}"), tag("}")).parse(input)?;
88 let content = content.trim();
89
90 match content.strip_prefix('$') {
91 Some(param) => (input, VariableRef::Parameter(param.trim().into())),
92 _ => (input, VariableRef::Variable(content.into())),
93 }
94 .pipe(Ok)
95}
96
97fn parse_delimited_braces(input: &str) -> IResult<&str, &str> {
98 let (input, braces) = take_while(|c| c == '{').parse(input)?;
100 let n = braces.len();
101
102 let closing_pattern = '}'
107 .pipe(core::iter::once)
108 .cycle()
109 .take(n)
110 .collect::<MiniStr>();
111
112 let (input, content) = closing_pattern
114 .pipe_deref(take_until)
115 .parse(input)?;
116
117 let (input, _) = closing_pattern
119 .pipe_deref(tag)
120 .parse(input)?;
121
122 Ok((input, content.trim_ascii()))
123}
124
125fn parse_text(input: &str) -> IResult<&str, &str> {
126 let (input, content) = take_till(|c| c == '{').parse(input)?;
127
128 match [input.starts_with("{{"), content.is_empty()]
129 .iter()
130 .any(|b| !b)
131 {
132 true => Ok((input, content)),
134 _ => parse_delimited_braces(input),
135 }
136}