use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
enum Node {
Text(String),
Field(Vec<String>), If(Vec<String>, Vec<Node>, Vec<Node>), Range(Vec<String>, Vec<Node>),
Template(String), }
pub struct Template {
name: String,
root: Vec<Node>,
defines: HashMap<String, Vec<Node>>,
}
#[allow(non_snake_case)]
pub fn New(name: impl Into<String>) -> Template {
Template { name: name.into(), root: Vec::new(), defines: HashMap::new() }
}
impl Template {
pub fn Parse(mut self, src: impl AsRef<str>) -> (Self, crate::errors::error) {
let src = src.as_ref();
match parse(src) {
Ok((root, defines)) => {
self.root = root;
for (k, v) in defines { self.defines.insert(k, v); }
(self, crate::errors::nil)
}
Err(e) => (self, crate::errors::New(&e)),
}
}
pub fn Name(&self) -> &str { &self.name }
pub fn Execute(&self, out: &mut String, data: &Value) -> crate::errors::error {
render(&self.root, out, data, self)
}
pub fn ExecuteTemplate(&self, out: &mut String, name: &str, data: &Value) -> crate::errors::error {
match self.defines.get(name) {
Some(nodes) => render(nodes, out, data, self),
None => crate::errors::New(&format!("template: no template {:?} associated with template {:?}", name, self.name)),
}
}
}
fn parse(src: &str) -> Result<(Vec<Node>, HashMap<String, Vec<Node>>), String> {
let tokens = tokenize(src)?;
let mut p = Parser { toks: tokens, pos: 0, defines: HashMap::new() };
let root = p.parse_nodes(&[])?;
Ok((root, p.defines))
}
#[derive(Debug, Clone)]
enum Token {
Text(String),
Action(String), }
fn tokenize(src: &str) -> Result<Vec<Token>, String> {
let mut out = Vec::new();
let mut i = 0;
let bytes = src.as_bytes();
while i < bytes.len() {
if i + 1 < bytes.len() && bytes[i] == b'{' && bytes[i + 1] == b'{' {
if let Some(end) = src[i + 2..].find("}}") {
let inner = src[i + 2..i + 2 + end].trim();
i = i + 2 + end + 2;
if inner.starts_with("/*") && inner.ends_with("*/") {
continue;
}
out.push(Token::Action(inner.to_string()));
continue;
} else {
return Err("unclosed action".to_string());
}
}
let start = i;
while i < bytes.len() && !(i + 1 < bytes.len() && bytes[i] == b'{' && bytes[i + 1] == b'{') {
i += 1;
}
if i > start {
out.push(Token::Text(src[start..i].to_string()));
}
}
Ok(out)
}
struct Parser {
toks: Vec<Token>,
pos: usize,
defines: HashMap<String, Vec<Node>>,
}
impl Parser {
fn parse_nodes(&mut self, stop: &[&str]) -> Result<Vec<Node>, String> {
let mut out = Vec::new();
while self.pos < self.toks.len() {
match &self.toks[self.pos] {
Token::Text(s) => {
out.push(Node::Text(s.clone()));
self.pos += 1;
}
Token::Action(a) => {
let kw = a.split_whitespace().next().unwrap_or("");
if stop.contains(&kw) { return Ok(out); }
let action = a.clone();
self.pos += 1;
if let Some(node) = self.parse_action(&action)? {
out.push(node);
}
}
}
}
if !stop.is_empty() {
return Err(format!("unexpected end; looking for {:?}", stop));
}
Ok(out)
}
fn parse_action(&mut self, a: &str) -> Result<Option<Node>, String> {
let a = a.trim();
if a == "end" || a == "else" {
return Err(format!("unexpected {{{{{}}}}}", a));
}
if let Some(rest) = a.strip_prefix("if ") {
let path = parse_path(rest.trim())?;
let then = self.parse_nodes(&["end", "else"])?;
let mut else_ = Vec::new();
if self.pos < self.toks.len() {
if let Token::Action(kw) = &self.toks[self.pos] {
if kw.trim() == "else" {
self.pos += 1;
else_ = self.parse_nodes(&["end"])?;
}
}
}
self.expect_action("end")?;
return Ok(Some(Node::If(path, then, else_)));
}
if let Some(rest) = a.strip_prefix("range ") {
let path = parse_path(rest.trim())?;
let body = self.parse_nodes(&["end"])?;
self.expect_action("end")?;
return Ok(Some(Node::Range(path, body)));
}
if let Some(rest) = a.strip_prefix("define ") {
let name = parse_string_lit(rest.trim())?;
let body = self.parse_nodes(&["end"])?;
self.expect_action("end")?;
self.defines.insert(name, body);
return Ok(None); }
if let Some(rest) = a.strip_prefix("template ") {
let rest = rest.trim();
let name = parse_string_lit(rest)?;
return Ok(Some(Node::Template(name)));
}
let path = parse_path(a)?;
Ok(Some(Node::Field(path)))
}
fn expect_action(&mut self, kw: &str) -> Result<(), String> {
if self.pos >= self.toks.len() {
return Err(format!("expected {{{{{}}}}} at end of input", kw));
}
match &self.toks[self.pos] {
Token::Action(a) if a.trim() == kw => { self.pos += 1; Ok(()) }
other => Err(format!("expected {{{{{}}}}}, got {:?}", kw, other)),
}
}
}
fn parse_path(s: &str) -> Result<Vec<String>, String> {
let s = s.trim();
if s == "." { return Ok(vec![]); }
if !s.starts_with('.') {
return Err(format!("expected path starting with '.', got {:?}", s));
}
Ok(s[1..].split('.').map(|p| p.to_string()).collect())
}
fn parse_string_lit(s: &str) -> Result<String, String> {
let s = s.trim();
if !s.starts_with('"') {
return Err(format!("expected \"name\", got {:?}", s));
}
let end = s[1..].find('"').ok_or("unterminated string literal")?;
Ok(s[1..1 + end].to_string())
}
fn render(nodes: &[Node], out: &mut String, scope: &Value, tmpl: &Template) -> crate::errors::error {
for node in nodes {
match node {
Node::Text(s) => out.push_str(s),
Node::Field(path) => {
let v = resolve(scope, path);
write_value(out, &v);
}
Node::If(path, then, else_) => {
let v = resolve(scope, path);
if truthy(&v) {
let e = render(then, out, scope, tmpl);
if e != crate::errors::nil { return e; }
} else {
let e = render(else_, out, scope, tmpl);
if e != crate::errors::nil { return e; }
}
}
Node::Range(path, body) => {
let v = resolve(scope, path);
match &v {
Value::Array(items) => {
for item in items {
let e = render(body, out, item, tmpl);
if e != crate::errors::nil { return e; }
}
}
Value::Object(map) => {
for (_k, val) in map {
let e = render(body, out, val, tmpl);
if e != crate::errors::nil { return e; }
}
}
Value::Null => { }
_ => return crate::errors::New(&format!("range over non-collection: {}", v)),
}
}
Node::Template(name) => {
match tmpl.defines.get(name) {
Some(body) => {
let e = render(body, out, scope, tmpl);
if e != crate::errors::nil { return e; }
}
None => return crate::errors::New(&format!("template: no template {:?} associated", name)),
}
}
}
}
crate::errors::nil
}
fn resolve(v: &Value, path: &[String]) -> Value {
let mut cur = v.clone();
for p in path {
cur = match cur {
Value::Object(map) => map.get(p).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
}
cur
}
fn truthy(v: &Value) -> bool {
match v {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(o) => !o.is_empty(),
}
}
fn write_value(out: &mut String, v: &Value) {
match v {
Value::Null => {}
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => out.push_str(&n.to_string()),
Value::String(s) => out.push_str(s),
Value::Array(_) | Value::Object(_) => out.push_str(&v.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn exec(src: &str, data: &Value) -> String {
let (t, err) = New("t").Parse(src);
assert!(err == crate::errors::nil, "parse error: {}", err);
let mut out = String::new();
let e = t.Execute(&mut out, data);
assert!(e == crate::errors::nil, "exec error: {}", e);
out
}
#[test]
fn plain_text() {
assert_eq!(exec("hello", &json!({})), "hello");
}
#[test]
fn field_lookup() {
assert_eq!(exec("Hi {{.name}}!", &json!({"name": "alice"})), "Hi alice!");
}
#[test]
fn nested_field() {
assert_eq!(exec("{{.user.name}}", &json!({"user": {"name": "bob"}})), "bob");
}
#[test]
fn if_truthy() {
assert_eq!(exec("{{if .x}}Y{{else}}N{{end}}", &json!({"x": true})), "Y");
assert_eq!(exec("{{if .x}}Y{{else}}N{{end}}", &json!({"x": false})), "N");
assert_eq!(exec("{{if .x}}Y{{else}}N{{end}}", &json!({})), "N");
}
#[test]
fn range_over_array() {
assert_eq!(exec("{{range .}}[{{.}}]{{end}}", &json!([1, 2, 3])), "[1][2][3]");
}
#[test]
fn define_and_template() {
let src = r#"{{define "item"}}<{{.}}>{{end}}{{range .}}{{template "item"}}{{end}}"#;
assert_eq!(exec(src, &json!(["a", "b"])), "<a><b>");
}
#[test]
fn comment_stripped() {
assert_eq!(exec("A{{/* skip */}}B", &json!({})), "AB");
}
}