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