reinda_core/
template.rs

1use std::ops::Range;
2use bytes::Bytes;
3
4
5
6const FRAGMENT_START: &[u8] = b"{{: ";
7const FRAGMENT_END: &[u8] = b" :}}";
8
9/// Fragments longer than this are ignored. This is just to protect against
10/// random fragment start/end markers in large generated files.
11pub const MAX_FRAGMENT_LEN: usize = 256;
12
13
14/// A byte string that you can append to. Used for [`Template::render`].
15pub struct Appender<'a>(&'a mut Vec<u8>);
16
17impl Appender<'_> {
18    pub fn append(&mut self, s: &[u8]) {
19        self.0.extend_from_slice(s);
20    }
21}
22
23/// A parsed template.
24///
25/// # Template syntax
26///
27/// Our template syntax is super simple and is really just a glorified
28/// search-and-replace. The input is checked for "fragments" which have the
29/// syntax `{{: foo :}}`. The start token is actually `{{: ` (note the
30/// whitespace!). So `{{:foo:}}` is not recognized as fragment.
31///
32/// There are two additional constraints: the fragment must not contain a
33/// newline and must be shorter than [`MAX_FRAGMENT_LEN`]. If these conditions
34/// are not met, the fragment start token is ignored.
35///
36/// The string between the start and end tag is then trimmed (excess whitespace
37/// removed) and parsed into a [`Fragment`]. See that type's documentation for
38/// information about the existing kinds of fragments.
39#[derive(Debug)]
40pub struct Template {
41    raw: Bytes,
42    fragments: Vec<SpannedFragment>,
43}
44
45/// Error returned by the parsing functions.
46#[derive(Debug, thiserror::Error)]
47pub enum Error {
48    #[error("template fragment does not contain valid UTF8: {0:?}")]
49    NonUtf8TemplateFragment(Vec<u8>),
50    #[error("unknown template fragment specifier '{0}'")]
51    UnknownTemplateSpecifier(String),
52}
53
54/// A fragment with a span.
55#[derive(Debug)]
56struct SpannedFragment {
57    span: Range<usize>,
58    kind: Fragment,
59}
60
61/// A parsed template fragment.
62#[derive(Debug)]
63pub enum Fragment {
64    // TODO: one could avoid allocating those `String`s by storing `Byte`s
65    // instead. However, this would add more UTF8 checks and/or unsafe blocks.
66    // Not worth it for now.
67
68    /// Inserts the public path of another asset. Example:
69    /// `{{: path:bundle.js :}}`.
70    Path(String),
71
72    /// Includes another asset. Example: `{{: include:fonts.css :}}`.
73    Include(String),
74
75    /// Interpolates a runtime variable. Example: `{{: var:name :}}`.
76    Var(String),
77}
78
79impl Fragment {
80    fn parse(bytes: &[u8]) -> Result<Self, Error> {
81        let val = |s: &str| s[s.find(':').unwrap() + 1..].to_string();
82
83        let s = std::str::from_utf8(bytes)
84            .map_err(|_| Error::NonUtf8TemplateFragment(bytes.into()))?
85            .trim();
86
87        match () {
88            () if s.starts_with("path:") => Ok(Self::Path(val(s))),
89            () if s.starts_with("include:") => Ok(Self::Include(val(s))),
90            () if s.starts_with("var:") => Ok(Self::Var(val(s))),
91
92            _ => {
93                let specifier = s[..s.find(':').unwrap_or(s.len())].to_string();
94                Err(Error::UnknownTemplateSpecifier(specifier))
95            }
96        }
97    }
98
99    pub fn as_include(&self) -> Option<&str> {
100        match self {
101            Self::Include(p) => Some(p),
102            _ => None,
103        }
104    }
105}
106
107impl Template {
108    /// Parses the input byte string as template. Returns `Err` on parse error.
109    pub fn parse(input: Bytes) -> Result<Self, Error> {
110        let fragments = FragmentSpans::new(&input)
111            .map(|span| {
112                let kind = Fragment::parse(&input[span.clone()])?;
113                Ok(SpannedFragment { span, kind })
114            })
115            .collect::<Result<_, _>>()?;
116
117        Ok(Self {
118            raw: input,
119            fragments,
120        })
121    }
122
123    /// Returns `Some(out)` if this template does not have any fragments at all.
124    /// `out` is equal to the `input` that was passed to `parse`.
125    pub fn into_already_rendered(self) -> Result<Bytes, Self> {
126        if self.fragments.is_empty() {
127            Ok(self.raw)
128        } else {
129            Err(self)
130        }
131    }
132
133    /// Returns an iterator over all fragments.
134    pub fn fragments(&self) -> impl Iterator<Item = &Fragment> {
135        self.fragments.iter().map(|f| &f.kind)
136    }
137
138    /// Returns the raw input that was passed to `parse`.
139    pub fn raw_input(&self) -> &Bytes {
140        &self.raw
141    }
142
143    /// Renders the template using `replacer` to evaluate the fragments.
144    ///
145    /// # Replacing/evaluating fragments
146    ///
147    /// For each fragment in the `input` template, the `replacer` is called with
148    /// the parsed fragment. For example, the template string `foo {{: bar :}}
149    /// baz {{: config.data   :}}x` would lead to two calls to `replacer`, with
150    /// the following strings as first parameter:
151    ///
152    /// - `bar`
153    /// - `config.data`
154    ///
155    /// As you can see, excess whitespace is stripped before passing the string
156    /// within the fragment.
157    pub fn render<R, E>(self, mut replacer: R) -> Result<Bytes, E>
158    where
159        R: FnMut(Fragment, Appender) -> Result<(), E>,
160    {
161        if self.fragments.is_empty() {
162            return Ok(self.raw);
163        }
164
165        let mut out = Vec::new();
166        let mut last_fragment_end = 0;
167
168        for fragment in self.fragments {
169            // Add the part from the last fragment (or start) to the beginning
170            // of this fragment.
171            out.extend_from_slice(
172                &self.raw[last_fragment_end..fragment.span.start - FRAGMENT_START.len()]
173            );
174
175            // Evaluate the fragment.
176            replacer(fragment.kind, Appender(&mut out))?;
177
178            last_fragment_end = fragment.span.end +  FRAGMENT_END.len();
179        }
180
181        // Add the stuff after the last fragment.
182        out.extend_from_slice(&self.raw[last_fragment_end..]);
183
184        Ok(out.into())
185    }
186}
187
188
189/// An iterator over the spans of all template fragments in `input`, in order.
190///
191/// The iterator's item is the span (`Range<usize>`) of the fragment. The span
192/// excludes the fragment start and end token, but includes potential excess
193/// whitespace. Example:
194///
195/// ```text
196/// input:    b"a{{: kk   :}}b"
197/// indices:    0123456789012
198/// ```
199///
200/// For that input, one span would be yielded by the iterator: `5..9`
201///  (`input[5..9]` is `"kk  "`).
202pub struct FragmentSpans<'a> {
203    input: &'a [u8],
204    idx: usize,
205}
206
207impl<'a> FragmentSpans<'a> {
208    pub fn new(input: &'a [u8]) -> Self {
209        Self {
210            input,
211            idx: 0,
212        }
213    }
214}
215
216impl Iterator for FragmentSpans<'_> {
217    type Item = Range<usize>;
218    fn next(&mut self) -> Option<Self::Item> {
219        if self.idx >= self.input.len() {
220            return None;
221        }
222
223        while let Some(start_pos) = find(&self.input[self.idx..], FRAGMENT_START) {
224            // We have a fragment candidate. Now we need to make sure that it's
225            // actually a valid fragment.
226            let end_pos = self.input[self.idx + start_pos..]
227                .windows(FRAGMENT_END.len())
228                .take(MAX_FRAGMENT_LEN - FRAGMENT_END.len() + 1)
229                .take_while(|win| win[0] != b'\n')
230                .position(|win| win == FRAGMENT_END);
231
232            match end_pos {
233                // We haven't found a matching end marker: ignore this start marker.
234                None => {
235                    self.idx += start_pos + FRAGMENT_START.len();
236                }
237
238                // This is a real fragment.
239                Some(end_pos) => {
240                    let start = self.idx + start_pos;
241                    self.idx = start + end_pos + FRAGMENT_END.len();
242
243                    return Some(start + FRAGMENT_START.len()..start + end_pos);
244                }
245            }
246        }
247
248        self.idx = self.input.len();
249        None
250    }
251}
252
253fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
254    memchr::memmem::find(haystack, needle)
255}
256
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    fn dummy_replacer(f: Fragment, mut appender: Appender) -> Result<(), ()> {
263        match f {
264            Fragment::Include(p) => {
265                appender.append(b"i-");
266                appender.append(p.to_uppercase().as_bytes());
267            }
268            Fragment::Path(p) => {
269                appender.append(b"p-");
270                appender.append(&p.bytes().rev().collect::<Vec<_>>())
271            }
272            Fragment::Var(k) => {
273                appender.append(b"v-");
274                appender.append(k.to_lowercase().as_bytes())
275            }
276        }
277
278        Ok(())
279    }
280
281    fn render(input: &[u8]) -> Bytes {
282        let template = Template::parse(Bytes::copy_from_slice(input)).expect("failed to parse");
283        template.render(dummy_replacer).unwrap()
284    }
285
286    #[test]
287    fn render_no_fragments() {
288        let s = b"foo, bar, baz";
289        let res = render(s);
290        assert_eq!(res, s as &[_]);
291    }
292
293    #[test]
294    fn render_simple_fragments() {
295        assert_eq!(
296            render(b"{{: include:banana :}}"),
297            b"i-BANANA" as &[u8],
298        );
299        assert_eq!(
300            render(b"foo {{: path:cat :}}baz"),
301            b"foo p-tacbaz" as &[u8],
302        );
303        assert_eq!(
304            render(b"foo {{: include:cat :}}baz{{: var:DOG :}}"),
305            b"foo i-CATbazv-dog" as &[u8],
306        );
307    }
308
309    #[test]
310    fn render_ignored_fragments() {
311        assert_eq!(
312            render(b"x{{: a\nb :}}y"),
313            b"x{{: a\nb :}}y" as &[u8],
314        );
315        assert_eq!(
316            render(b"x{{: a\n {{: include:kiwi :}}y"),
317            b"x{{: a\n i-KIWIy" as &[u8],
318        );
319
320        let long = b"foo {:: \
321            abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy\
322            abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy\
323            abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy\
324            abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy\
325            abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy\
326            abcdefghijklmnopqrstuvwxyabcdefghijklmnopqrstuvwxy\
327            yo ::} bar\
328        " as &[u8];
329        assert_eq!(render(long), long);
330    }
331}