glossa_dsl/
template.rs

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/// Core template representation
19///
20/// ## Variants
21/// - Conditional: Enables branching logic based on parameters
22/// - Parts: Direct template content (text + variables)
23///
24/// ## Serialization
25/// - Derives Serialize/Deserialize with serde feature
26/// - Uses compact binary representation with bincode
27#[derive(Debug, Clone, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Template {
30  /// Conditional template branch
31  Conditional(selector::Selector),
32  /// Linear template segments
33  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  // => escaped text, not variable
80  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  // Count opening braces
99  let (input, braces) = take_while(|c| c == '{').parse(input)?;
100  let n = braces.len();
101
102  // "{{" => n=2
103  // "{{{" => n=3
104  // if n=2 => "}}"
105  //    n=3 => "}}}"
106  let closing_pattern = '}'
107    .pipe(core::iter::once)
108    .cycle()
109    .take(n)
110    .collect::<MiniStr>();
111
112  // Extract content until closing pattern
113  let (input, content) = closing_pattern
114    .pipe_deref(take_until)
115    .parse(input)?;
116
117  // Verify and consume closing braces
118  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    // input.not_starts_with("{{") or content.is_not_empty()
133    true => Ok((input, content)),
134    _ => parse_delimited_braces(input),
135  }
136}