Skip to main content

carta_core/template/
mod.rs

1//! A small string-template engine: variables, conditionals, loops, pipes, and partials over a
2//! [`Value`] context. The language uses `$`-delimited directives; see `parse` for the grammar and
3//! the surrounding module docs for whitespace handling.
4//!
5//! The engine is format-agnostic and does no I/O: partial inclusion is delegated to a caller-supplied
6//! resolver, so the same parsed [`Template`] renders identically whether partials come from disk, an
7//! embedded set, or a test fixture.
8
9mod node;
10mod parse;
11mod pipe;
12mod render;
13#[cfg(test)]
14mod tests;
15
16use std::collections::BTreeMap;
17use std::fmt;
18
19pub use node::Template;
20
21/// A value a template can interpolate. Maps are ordered, so iteration and `pairs` are deterministic
22/// and key-sorted.
23#[derive(Debug, Clone, PartialEq)]
24pub enum Value {
25    /// A string, inserted as-is.
26    Str(String),
27    /// A sequence; iterated by `$for$`, concatenated (no separator) when interpolated directly.
28    List(Vec<Value>),
29    /// A keyed record; fields reached with `$x.field$`, enumerated with the `pairs` pipe.
30    Map(BTreeMap<String, Value>),
31    /// A boolean; renders bare as `true`/`false`, and is the one non-empty value that is still falsy
32    /// (when `false`) in a conditional.
33    Bool(bool),
34}
35
36impl Value {
37    /// Build a map value from string key/value pairs.
38    #[must_use]
39    pub fn map(entries: impl IntoIterator<Item = (String, Value)>) -> Value {
40        Value::Map(entries.into_iter().collect())
41    }
42
43    /// Whether this value is true in a conditional. An empty string and a list with no truthy
44    /// element are false; a map is true by mere presence; a `Bool` follows its flag — the one value
45    /// that is non-empty yet still false when `false`.
46    #[must_use]
47    pub fn is_truthy(&self) -> bool {
48        match self {
49            Value::Str(s) => !s.is_empty(),
50            Value::List(items) => items.iter().any(Value::is_truthy),
51            Value::Map(_) => true,
52            Value::Bool(b) => *b,
53        }
54    }
55}
56
57/// A template that could not be processed: either a parse failure (an unterminated directive, an
58/// unmatched `$if$`/`$for$`, an unknown pipe, …) or a render failure (a referenced partial that
59/// cannot be resolved).
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct TemplateError {
62    message: String,
63}
64
65impl TemplateError {
66    fn new(message: impl Into<String>) -> Self {
67        Self {
68            message: message.into(),
69        }
70    }
71}
72
73impl fmt::Display for TemplateError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(&self.message)
76    }
77}
78
79impl std::error::Error for TemplateError {}