use std::borrow::Cow;
use std::collections::BTreeMap;
use crate::{Block, SelPart, Selector, Template};
use eyre::{bail, eyre, Result, WrapErr};
use serde::Serialize;
use serde_json::{to_value, Value};
struct Scope {
content: Value,
more: BTreeMap<String, Value>,
}
fn number_non_zero(n: &serde_json::Number) -> bool {
if let Some(n) = n.as_f64() {
n != 0.0
} else if let Some(n) = n.as_i64() {
n != 0
} else if let Some(n) = n.as_u64() {
n != 0
} else {
panic!("Funky number {}", n)
}
}
impl Scope {
fn get(&self, sel: &Selector) -> Result<&Value> {
let mut sels = &sel.items[..];
if sel.items.is_empty() {
bail!("Empty selector {:?}", sel);
}
let mut place = match &sel.items[0] {
SelPart::Map(key) => {
let k: &str = &key;
if let Some(place) = self.more.get(k) {
sels = &sels[1..];
place
} else {
&self.content
}
}
SelPart::Array(_) => &self.content,
};
for i in sels {
match i {
SelPart::Map(k) => {
let k: &str = &k;
place = place
.as_object()
.ok_or_else(|| eyre!("Expected object"))?
.get(k)
.ok_or_else(|| eyre!("Key not found"))?;
}
SelPart::Array(idx) => {
place = place
.as_array()
.ok_or_else(|| eyre!("Expected array"))?
.get(*idx)
.ok_or_else(|| eyre!("OOB"))?;
}
}
}
Ok(place)
}
fn get_to_str(&self, sel: &Selector) -> Result<Cow<'_, str>> {
let place = self.get(sel)?;
Ok(match place {
Value::Null => Cow::Borrowed(""),
Value::Bool(n) => Cow::Borrowed(if *n { "true" } else { "false" }),
Value::Number(n) => Cow::Owned(n.to_string()),
Value::String(s) => Cow::Borrowed(&s),
Value::Array(_) => {
bail!("Got array, expected null|bool|number|string")
}
Value::Object(_) => {
bail!("Got object, expected null|bool|number|string")
}
})
}
fn is_truthy(&self, sel: &Selector) -> bool {
self.get(sel)
.map(|x| match x {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => number_non_zero(n),
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(o) => !o.is_empty(),
})
.unwrap_or(false)
}
fn assert_not_in(&self, key: &str) -> Result<()> {
if let Value::Object(o) = &self.content {
if o.contains_key(key) {
bail!("Found {}, expected none", key);
}
}
if self.more.contains_key(key) {
bail!("Found {}, expected none", key);
}
Ok(())
}
fn set_extra(&mut self, key: &str, value: serde_json::Value) {
if let Some(ptr) = self.more.get_mut(key) {
*ptr = value;
} else {
self.more.insert(key.to_owned(), value);
}
}
fn remove_extra(&mut self, key: &str) {
self.more.remove(key);
}
}
fn render_in(parts: &[Block<'_>], scope: &mut Scope, str: &mut String) -> Result<()> {
use Block::*;
for i in parts {
match i {
HtmlLit(s) => str.push_str(&s),
PutUnescaped(sel) => {
let s = scope.get_to_str(sel)?;
str.push_str(&s);
}
PutEscaped(sel) => {
let s = scope.get_to_str(sel)?;
for c in s.chars() {
match c {
'&' => str.push_str("&"),
'<' => str.push_str("<"),
'>' => str.push_str(">"),
'"' => str.push_str("""),
'\'' => str.push_str("'"),
'/' => str.push_str("/"),
_ => str.push(c),
}
}
}
If {
sel,
if_true,
if_false,
} => {
let add = if scope.is_truthy(sel) {
if_true
} else {
if_false
};
render_in(add, scope, str)?;
}
For {
name,
iter_over,
each,
} => {
let vals = scope.get(iter_over)?;
scope.assert_not_in(name)?;
let vals = match vals {
Value::Array(a) => a.clone(),
other => bail!("Expected array, got {}", other),
};
for v in vals {
scope.set_extra(name, v);
render_in(each, scope, str)?;
}
scope.remove_extra(name);
}
Comment => {}
}
}
Ok(())
}
impl<'t> Template<'t> {
pub fn render<T: Serialize>(&self, content: T) -> Result<String> {
let content = to_value(content).context("Couldnt convert `content` to json value")?;
let mut scope = Scope {
content,
more: BTreeMap::new(),
};
let mut str = String::with_capacity(20_000);
render_in(&self.parts, &mut scope, &mut str)?;
Ok(str)
}
}
#[cfg(test)]
mod tests {
use crate::*;
use render::Scope;
use serde_json::json;
use smallvec::smallvec;
#[test]
fn basic_tests() {
let mut tpl = Template {
parts: vec![
Block::HtmlLit(Cow::Borrowed("Start\n")),
],
};
assert_eq!(tpl.render(json!({})).unwrap(), "Start\n");
tpl.parts.push(Block::PutEscaped(Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("a"))],
}));
assert!(tpl.render(json!({})).is_err());
assert_eq!(
tpl.render(json!({ "a": "Hello\n" })).unwrap(),
"Start\nHello\n"
);
assert_eq!(tpl.render(json!({ "a": 32 })).unwrap(), "Start\n32");
assert!(tpl.render(json!({ "a": vec![1, 2, 3] })).is_err());
assert_eq!(
tpl.render(json!({ "a": Option::<()>::None })).unwrap(),
"Start\n"
);
assert_eq!(
tpl.render(json!({ "a": "<hello>" })).unwrap(),
"Start\n<hello>"
);
tpl.parts.push(Block::PutUnescaped(Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("b"))],
}));
assert!(tpl.render(json!({ "a": "Hello\n" })).is_err());
assert_eq!(
tpl.render(json!({
"a": "<hello>",
"b": "<hello>",
}))
.unwrap(),
"Start\n<hello><hello>"
);
}
#[test]
fn nested() {
let tpl = Template {
parts: vec![
Block::PutUnescaped(Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("b"))],
}),
Block::HtmlLit(Cow::Borrowed("*")),
Block::PutUnescaped(Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("a"))],
}),
Block::HtmlLit(Cow::Borrowed("*")),
Block::PutUnescaped(Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("c")),
SelPart::Map(Cow::Borrowed("d"))
],
}),
],
};
assert_eq!(
tpl.render(json!({
"b": "B",
"a": "A",
"c": {
"d": "D"
}
}))
.unwrap(),
"B*A*D"
);
assert_eq!(
tpl.render(json!({
"b": "B",
"d": ["1","2","3"],
"c": {
"d": "D",
"e": {
"f": "j"
}
},
"a": 1,
}))
.unwrap(),
"B*1*D"
);
}
#[test]
fn arrays_basic() {
let tpl = Template {
parts: vec![Block::PutUnescaped(Selector {
items: smallvec![SelPart::Array(1)],
})],
};
assert_eq!(tpl.render(json!(["abc", "def", "ghi"])).unwrap(), "def");
assert!(tpl.render(json!(["d"])).is_err());
assert!(tpl.render(json!(["x", [1, 2], "y"])).is_err());
}
#[test]
fn arrays_nested() {
let tpl = Template {
parts: vec![Block::PutUnescaped(Selector {
items: smallvec![
SelPart::Array(3),
SelPart::Map(Cow::Borrowed("0")),
SelPart::Array(2)
],
})],
};
assert_eq!(
tpl.render(json!([
{},
{},
{},
{
"x": "fd",
"g": "fds",
"0": ["d", "d", "ghj"]
}
]))
.unwrap(),
"ghj"
);
assert!(tpl.render(json!({})).is_err());
assert!(tpl.render(json!({"a": 1, "b": 2})).is_err());
assert!(tpl.render(json!([{}, {}, {}, {}, {},])).is_err());
assert!(tpl
.render(json!([
0,
0,
0,
{
"0": "0"
}
]))
.is_err());
assert!(tpl
.render(json!([
0,0,0,
{
"0": [0,1]
}
]))
.is_err());
assert_eq!(
tpl.render(json!([
0,0,0,
{
"0": [0,1,2]
}
]))
.unwrap(),
"2"
);
assert!(tpl
.render(json!([
0,0,0,
{
"0": [0,1,[1,2]]
}
]))
.is_err(),);
}
#[test]
fn sel_get() {
let scope = Scope {
content: json!({
"a": "b",
"c": "d",
}),
more: maplit::btreemap! {
"i".to_owned() => json!({"j": "k"})
},
};
assert_eq!(
scope
.get(&Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("a"))],
})
.unwrap(),
&json!("b"),
);
assert_eq!(
scope
.get(&Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("i"))],
})
.unwrap(),
&json!({"j": "k"}),
);
assert!(scope
.get(&Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("j"))]
})
.is_err());
assert!(scope
.get(&Selector {
items: smallvec![SelPart::Array(0)]
})
.is_err())
}
#[test]
fn sel_get_array_root() {
let scope = Scope {
content: json!([
"0",
{
"b": "c",
"d": [1,2],
},
1,
]),
more: maplit::btreemap! {
"i".to_owned() => json!({}),
"j".to_owned() => json!({
"a": "c",
"e": [111,8,1]
})
},
};
assert_eq!(
scope
.get(&Selector {
items: smallvec![SelPart::Array(0)]
})
.unwrap(),
&json!("0")
);
assert_eq!(
scope
.get(&Selector {
items: smallvec![SelPart::Array(1), SelPart::Map(Cow::Borrowed("d"))]
})
.unwrap(),
&json!([1, 2])
);
assert_eq!(
scope
.get(&Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("i"))],
})
.unwrap(),
&json!({}),
);
assert_eq!(
scope
.get(&Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("j")),
SelPart::Map(Cow::Borrowed("e")),
SelPart::Array(1)
],
})
.unwrap(),
&json!(8),
);
assert!(scope
.get(&Selector {
items: smallvec![SelPart::Array(3)]
})
.is_err());
}
#[test]
fn for_loop() {
let tpl = Template {
parts: vec![
Block::HtmlLit(Cow::Borrowed("<ul>")),
Block::For {
name: Cow::Borrowed("i"),
iter_over: Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("items"))],
},
each: vec![
Block::HtmlLit(Cow::Borrowed("<li>")),
Block::PutEscaped(Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("i"))],
}),
Block::HtmlLit(Cow::Borrowed("</li>")),
],
},
Block::HtmlLit(Cow::Borrowed("</ul>")),
],
};
assert!(tpl.render(json!({})).is_err());
assert!(tpl.render(json!({"items": "hello"})).is_err());
assert!(tpl.render(json!({"items": [1,2,3, {"x": "y"}]})).is_err());
assert_eq!(
tpl.render(json!({"items": ["one", "two", "three"]}))
.unwrap(),
"<ul><li>one</li><li>two</li><li>three</li></ul>"
);
assert_eq!(
tpl.render(json!(
{
"items": ["one", "two", "three"],
"more-items": [5,3,1]
}))
.unwrap(),
"<ul><li>one</li><li>two</li><li>three</li></ul>"
);
assert_eq!(
tpl.render(json!(
{
"items": ["one", "two", "three"],
"more-items": [5,3,1]
}))
.unwrap(),
"<ul><li>one</li><li>two</li><li>three</li></ul>"
);
}
#[test]
fn for_if() {
let tpl1 = Template {
parts: vec![
Block::HtmlLit(Cow::Borrowed("<ul>")),
Block::For {
name: Cow::Borrowed("i"),
iter_over: Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("items"))],
},
each: vec![
Block::HtmlLit(Cow::Borrowed("<li>")),
Block::If {
sel: Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("i")),
SelPart::Map(Cow::Borrowed("bold"))
],
},
if_true: vec![Block::HtmlLit(Cow::Borrowed("<b>"))],
if_false: vec![],
},
Block::PutEscaped(Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("i")),
SelPart::Map(Cow::Borrowed("name"))
],
}),
Block::If {
sel: Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("i")),
SelPart::Map(Cow::Borrowed("bold"))
],
},
if_true: vec![Block::HtmlLit(Cow::Borrowed("</b>"))],
if_false: vec![],
},
Block::HtmlLit(Cow::Borrowed("</li>")),
],
},
Block::HtmlLit(Cow::Borrowed("</ul>")),
],
};
let tpl2 = Template {
parts: vec![
Block::HtmlLit(Cow::Borrowed("<ul>")),
Block::For {
name: Cow::Borrowed("i"),
iter_over: Selector {
items: smallvec![SelPart::Map(Cow::Borrowed("items"))],
},
each: vec![
Block::HtmlLit(Cow::Borrowed("<li>")),
Block::If {
sel: Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("i")),
SelPart::Map(Cow::Borrowed("bold"))
],
},
if_true: vec![
Block::HtmlLit(Cow::Borrowed("<b>")),
Block::PutEscaped(Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("i")),
SelPart::Map(Cow::Borrowed("name"))
],
}),
Block::HtmlLit(Cow::Borrowed("</b>")),
],
if_false: vec![Block::PutEscaped(Selector {
items: smallvec![
SelPart::Map(Cow::Borrowed("i")),
SelPart::Map(Cow::Borrowed("name"))
],
})],
},
Block::HtmlLit(Cow::Borrowed("</li>")),
],
},
Block::HtmlLit(Cow::Borrowed("</ul>")),
],
};
for tpl in &[tpl1, tpl2] {
assert!(tpl.render(json!({})).is_err());
assert!(tpl.render(json!({"items": "hello"})).is_err());
assert!(tpl.render(json!({"items": [1,2,3, {"x": "y"}]})).is_err());
assert!(tpl
.render(json!({"items": ["one", "two", "three"]}))
.is_err());
assert_eq!(
tpl.render(json!({"items": [
{ "name": "one", "bold": false},
{ "name": "two", "bold": false},
{ "name": "three", "bold": false},
]}))
.unwrap(),
"<ul><li>one</li><li>two</li><li>three</li></ul>"
);
assert_eq!(
tpl.render(json!({"items": [
{ "name": "one", "bold": true},
{ "name": "two", "bold": false},
{ "name": "three", "bold": true},
]}))
.unwrap(),
"<ul><li><b>one</b></li><li>two</li><li><b>three</b></li></ul>"
);
assert_eq!(
tpl.render(json!({"items": [
{ "name": "one", "bold": {}},
{ "name": "two", "bold": 0},
{ "name": "three", "bold": []},
{ "name": "four", "bold": ""},
{ "name": "five", "bold": Option::<()>::None }
]}))
.unwrap(),
"<ul><li>one</li><li>two</li><li>three</li><li>four</li><li>five</li></ul>"
);
assert_eq!(
tpl.render(json!({"items": [
{ "name": "one", "bold": {"a": "one"}},
{ "name": "two", "bold": 32},
{ "name": "three", "bold": [{}, {}]},
{ "name": "four", "bold": " "},
{ "name": "five", "bold": -2.3 }
]}))
.unwrap(),
"<ul><li><b>one</b></li><li><b>two</b></li><li><b>three</b></li><li><b>four</b></li><li><b>five</b></li></ul>"
);
}
}
}