#![doc(html_logo_url = "https://gitlab.com/encre-org/pochoir/raw/main/.assets/logo.png")]
#![forbid(unsafe_code)]
#![warn(
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unused_qualifications,
rustdoc::private_doc_tests,
rustdoc::broken_intra_doc_links,
rustdoc::private_intra_doc_links,
clippy::unnecessary_wraps,
clippy::too_many_lines,
clippy::string_to_string,
clippy::explicit_iter_loop,
clippy::unnecessary_cast,
clippy::missing_errors_doc,
clippy::pedantic,
clippy::clone_on_ref_ptr,
clippy::non_ascii_literal,
clippy::dbg_macro,
clippy::map_err_ignore,
clippy::use_debug,
clippy::map_err_ignore,
clippy::use_self,
clippy::useless_let_if_seq,
clippy::verbose_file_reads,
clippy::panic,
clippy::unimplemented,
clippy::todo
)]
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
use std::{
borrow::{Borrow, Cow},
ops::{ControlFlow, DerefMut},
path::Path,
};
use error::{AutoError, AutoErrorOffset};
use pochoir_common::{Spanned, StreamParser};
use pochoir_lang::{Context, Value};
mod error;
pub mod escaping;
pub use error::{Error, Result};
pub use escaping::Escaping;
pub type BlockContext = Option<(char, usize)>;
pub trait TemplateCustomParsing {
fn each_char(
&self,
ch: char,
parser: &mut StreamParser,
block_context: BlockContext,
) -> ControlFlow<()>;
}
struct DummyCustomParsing;
impl TemplateCustomParsing for DummyCustomParsing {
fn each_char(
&self,
_ch: char,
_parser: &mut StreamParser,
_block_context: BlockContext,
) -> ControlFlow<()> {
ControlFlow::Continue(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TemplateBlock<'a> {
RawText(Cow<'a, str>),
Expr(Cow<'a, str>, bool),
Stmt(Cow<'a, str>),
}
impl<'a> TemplateBlock<'a> {
pub fn text<T: Into<Cow<'a, str>>>(text: T) -> Self {
Self::RawText(text.into())
}
pub fn deep_clone<'b>(self) -> TemplateBlock<'b> {
match self {
TemplateBlock::RawText(a) => TemplateBlock::RawText(Cow::Owned(a.into_owned())),
TemplateBlock::Expr(a, b) => TemplateBlock::Expr(Cow::Owned(a.into_owned()), b),
TemplateBlock::Stmt(a) => TemplateBlock::Stmt(Cow::Owned(a.into_owned())),
}
}
}
#[allow(clippy::too_many_lines)]
fn eval_stmt<'a, T: Borrow<Spanned<TemplateBlock<'a>>>>(
file_path: &Path,
stmt_expr: &str,
blocks: &[T],
context: &mut Context,
block_index: &mut usize,
result: &mut String,
file_offset: usize,
) -> Result<()> {
let mut parser = StreamParser::new(file_path, stmt_expr);
if parser.take_exact("if ").is_ok() {
parser.trim();
let is_let = parser.take_exact("let ").is_ok();
parser.trim();
let expr_index = parser.index();
let expr = parser.take_until_eoi().trim();
let mut if_branches = vec![(expr.to_string(), is_let, vec![])];
let mut else_branch = vec![];
let mut in_else = false;
let mut count_if = 0;
while *block_index < blocks.len() {
let block = blocks[*block_index].borrow();
*block_index += 1;
match &**block {
TemplateBlock::Stmt(s) if s.starts_with("if ") => {
if in_else {
else_branch.push(block);
} else {
if_branches.last_mut().unwrap().2.push(block);
}
count_if += 1;
}
TemplateBlock::Stmt(s) if s.starts_with("elif ") => {
if count_if > 1 {
if in_else {
else_branch.push(block);
} else {
if_branches.last_mut().unwrap().2.push(block);
}
} else if in_else {
return Err(Spanned::new(Error::ElifAfterElse)
.with_span(block.span().clone())
.with_file_path(file_path));
} else {
let val = s.strip_prefix("elif ").unwrap().trim();
let (is_let, val) = if let Some(val) = val.strip_prefix("let ") {
(true, val.trim_start())
} else {
(false, val)
};
if_branches.push((val.to_string(), is_let, vec![]));
}
}
TemplateBlock::Stmt(s) if s == "endif" => {
if count_if == 0 {
break;
}
if in_else {
else_branch.push(block);
} else {
if_branches.last_mut().unwrap().2.push(block);
}
count_if -= 1;
}
TemplateBlock::Stmt(s) if s == "else" => {
if count_if == 0 {
in_else = true;
} else if in_else {
else_branch.push(block);
} else {
if_branches.last_mut().unwrap().2.push(block);
}
}
_ if in_else => else_branch.push(block),
_ => if_branches.last_mut().unwrap().2.push(block),
}
}
let mut if_matched = None;
for (i, branch) in if_branches.iter().enumerate() {
let cond_result =
pochoir_lang::eval(file_path, &branch.0, context, file_offset + expr_index)
.auto_error()?;
if (branch.1 && cond_result != Value::Null) || cond_result == Value::Bool(true) {
if_matched = Some(i);
break;
}
}
if let Some(if_matched) = if_matched {
result.push_str(&render_template(&if_branches[if_matched].2, context)?);
} else {
result.push_str(&render_template(&else_branch, context)?);
}
} else if parser.take_exact("for ").is_ok() {
parser.trim();
let mut aliases = vec![];
loop {
parser.trim();
let mut first = true;
let alias = parser
.take_while(|(_, ch)| {
if first {
first = false;
ch.is_alphabetic() || ch == '_'
} else {
ch.is_alphanumeric() || ch == '_'
}
})
.trim();
aliases.push(alias);
if parser.take_exact(",").is_err() {
break;
}
}
parser.trim();
parser.take_exact("in ").auto_error_offset(file_offset)?;
parser.trim();
let expr_index = parser.index();
let expr = parser.take_until_eoi().trim();
let expr_end_index = parser.index();
let mut for_branch = vec![];
let mut count_for = 0;
while *block_index < blocks.len() {
let block = blocks[*block_index].borrow();
*block_index += 1;
match &**block {
TemplateBlock::Stmt(s) if s.starts_with("for ") => {
for_branch.push(block);
count_for += 1;
}
TemplateBlock::Stmt(s) if s == "endfor" => {
if count_for == 0 {
break;
}
for_branch.push(block);
count_for -= 1;
}
_ => for_branch.push(block),
}
}
let old_values = aliases
.iter()
.map(|a| context.get(*a).cloned())
.collect::<Vec<Option<Value>>>();
let interpreted_val =
pochoir_lang::eval(file_path, expr, context, file_offset + expr_index).auto_error()?;
if let Value::Array(array) = interpreted_val {
for item in array {
if aliases.len() == 1 {
context.insert(aliases[0], item);
} else {
match item {
Value::Array(array) => {
for (i, alias) in aliases.iter().enumerate() {
context
.insert(*alias, array.get(i).cloned().unwrap_or(Value::Null));
}
}
Value::Object(object) => {
for alias in &aliases {
context.insert(
*alias,
object.get(*alias).cloned().unwrap_or(Value::Null),
);
}
}
_ => {
for alias in &aliases {
context.insert(*alias, Value::Null);
}
}
}
}
result.push_str(&render_template(&for_branch, context)?);
}
} else if let Value::Range(start, end) = interpreted_val {
let range = match (start, end) {
(Some(start), Some(end)) => start..end,
_ => {
return Err(Spanned::new(Error::UnboundedRangeInForLoop)
.with_span(file_offset + expr_index..file_offset + expr_end_index)
.with_file_path(file_path));
}
};
for item in range {
if aliases.len() == 1 {
context.insert(aliases[0], item);
} else {
for alias in &aliases {
context.insert(*alias, Value::Null);
}
}
result.push_str(&render_template(&for_branch, context)?);
}
}
for (i, old_val) in old_values.into_iter().enumerate() {
if let Some(old_val) = old_val {
context.insert(aliases[i], old_val);
} else {
context.remove(aliases[i]);
}
}
} else if parser.take_exact("let ").is_ok() {
parser.trim();
let mut first = true;
let name = parser
.take_while(|(_, ch)| {
if first {
first = false;
ch.is_alphabetic() || ch == '_'
} else {
ch.is_alphanumeric() || ch == '_'
}
})
.trim();
parser.trim();
parser.take_exact("=").auto_error_offset(file_offset)?;
parser.trim();
let expr_index = parser.index();
let expr = parser.take_until_eoi().trim();
let val_evaluated =
pochoir_lang::eval(file_path, expr, context, file_offset + expr_index).auto_error()?;
context.insert((*name).to_string(), val_evaluated);
} else {
return Err(Spanned::new(Error::UnknownStatement(
stmt_expr[..stmt_expr
.find(char::is_whitespace)
.unwrap_or(stmt_expr.len())]
.to_string(),
))
.with_span(blocks[*block_index - 1].borrow().span().clone())
.with_file_path(file_path));
}
Ok(())
}
pub fn parse_template<P: AsRef<Path>>(
file_path: P,
value: &str,
custom_parsing: impl TemplateCustomParsing,
file_offset: usize,
) -> Result<Vec<Spanned<TemplateBlock<'_>>>> {
fn inner<'a>(
file_path: &Path,
value: &'a str,
custom_parsing: impl TemplateCustomParsing,
file_offset: usize,
) -> Result<Vec<Spanned<TemplateBlock<'a>>>> {
stream_parse_template(
file_path,
&mut StreamParser::new(file_path, value),
custom_parsing,
file_offset,
)
}
inner(file_path.as_ref(), value, custom_parsing, file_offset)
}
#[allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
pub fn stream_parse_template<'a, P: AsRef<Path>>(
file_path: P,
parser: &mut StreamParser<'a>,
custom_parsing: impl TemplateCustomParsing,
file_offset: usize,
) -> Result<Vec<Spanned<TemplateBlock<'a>>>> {
fn inner<'a>(
file_path: &Path,
parser: &mut StreamParser<'a>,
custom_parsing: impl TemplateCustomParsing,
file_offset: usize,
) -> Result<Vec<Spanned<TemplateBlock<'a>>>> {
let mut blocks = vec![];
let mut last_piece = parser.index();
let mut block_context = None;
let mut curly_before = false;
while let Ok(ch) = parser.take_next() {
if curly_before {
curly_before = false;
if ch == '{' {
block_context = Some(('}', parser.index()));
} else if ch == '%' || ch == '!' || ch == '#' {
block_context = Some((ch, parser.index()));
}
continue;
}
match ch {
'{' => {
if block_context.is_none() {
if last_piece != parser.index() - 1 {
let last_range = last_piece..parser.index() - 1;
blocks.push(
Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
parser
.get_range(last_range.clone())
.auto_error_offset(file_offset)?,
)))
.with_span(last_range)
.with_file_path(file_path),
);
last_piece = parser.index() - 1;
}
curly_before = true;
}
}
ch if block_context.is_some()
&& block_context.unwrap().0 == ch
&& parser.peek_exact("}") =>
{
let info = block_context.unwrap();
let block_ch = info.0;
let block_start = info.1;
let block_span = block_start..parser.index() - 1;
parser.take_exact("}").unwrap();
block_context = None;
last_piece = parser.index();
let block = parser
.get_range(block_span.clone())
.auto_error_offset(file_offset)?;
let block_left_trimmed = block.trim_start();
let block_trimmed = block_left_trimmed.trim_end();
let left_space = block.len() - block_left_trimmed.len();
let right_space = block_left_trimmed.len() - block_trimmed.len();
let block_span = file_offset + block_span.start + left_space
..file_offset + block_span.end - right_space;
if block_ch == '}' {
blocks.push(
Spanned::new(TemplateBlock::Expr(Cow::Borrowed(block_trimmed), true))
.with_span(block_span)
.with_file_path(file_path),
);
} else if block_ch == '%' {
blocks.push(
Spanned::new(TemplateBlock::Stmt(Cow::Borrowed(block_trimmed)))
.with_span(block_span)
.with_file_path(file_path),
);
} else if block_ch == '!' {
blocks.push(
Spanned::new(TemplateBlock::Expr(Cow::Borrowed(block_trimmed), false))
.with_span(block_span)
.with_file_path(file_path),
);
} else if block_ch == '#' {
}
}
_ => {
if matches!(
custom_parsing.each_char(ch, parser, block_context),
ControlFlow::Break(())
) {
break;
}
}
}
}
let end = parser.index();
if last_piece < end {
let mut reused_last = false;
if let Some(TemplateBlock::RawText(ref mut text)) =
blocks.last_mut().map(DerefMut::deref_mut)
{
*text += parser
.get_range(last_piece..end)
.auto_error_offset(file_offset)?;
reused_last = true;
}
if !reused_last {
blocks.push(
Spanned::new(TemplateBlock::RawText(Cow::Borrowed(
parser
.get_range(last_piece..end)
.auto_error_offset(file_offset)?,
)))
.with_span(last_piece..end)
.with_file_path(file_path),
);
}
}
Ok(blocks)
}
inner(file_path.as_ref(), parser, custom_parsing, file_offset)
}
pub fn render_template<'a, T: Borrow<Spanned<TemplateBlock<'a>>>>(
blocks: &[T],
context: &mut Context,
) -> Result<String> {
let mut result = String::new();
let mut block_index = 0;
while block_index < blocks.len() {
let block = blocks[block_index].borrow();
block_index += 1;
match &**block {
TemplateBlock::RawText(text) => {
result.push_str(text);
}
TemplateBlock::Expr(expr, escape) => {
let expr_evaluated =
pochoir_lang::eval(block.file_path(), expr, context, block.span().start)
.auto_error()?
.to_string();
if *escape {
result.push_str(&Escaping::Html.escape(&expr_evaluated));
} else {
result.push_str(&expr_evaluated);
}
}
TemplateBlock::Stmt(stmt_expr) => {
eval_stmt(
block.file_path(),
stmt_expr,
blocks,
context,
&mut block_index,
&mut result,
block.span().start,
)?;
}
}
}
Ok(result)
}
pub fn process_template<P: AsRef<Path>>(
file_path: P,
value: &str,
context: &mut Context,
file_offset: usize,
) -> Result<String> {
let blocks = parse_template(file_path, value, DummyCustomParsing, file_offset)?;
render_template(&blocks, context)
}
#[cfg(test)]
mod tests {
use super::*;
use pochoir_lang::{object, IntoValue};
use pretty_assertions::assert_eq;
#[test]
fn only_static() {
let html = r#"<div class="bg-red-500"><p></p>
<img src="/path/to/file.png">
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>"#;
assert_eq!(
process_template("index.html", html, &mut Context::new(), 0),
Ok(r#"<div class="bg-red-500"><p></p>
<img src="/path/to/file.png">
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>"#
.to_string())
);
}
#[test]
fn expr() {
let html = "<p>Hello {{ msg }}!</p>";
let mut context = Context::new();
context.insert("msg", "world");
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("<p>Hello world!</p>".to_string())
);
let html = r#"<p {{ attr_key }}="{{ attr_value }}">Hello world!</p>"#;
let mut context = Context::new();
context.insert("attr_key", "id");
context.insert("attr_value", "bg-red-500");
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"<p id="bg-red-500">Hello world!</p>"#.to_string()),
);
}
#[test]
fn if_stmt() {
let html = r#"<div class="bg-red-500"><p></p>
<img src="/path/to/file.png">
{{ hello }}
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>
{% if checked %}
<input type="checkbox" checked />
{% else %}
<input type="checkbox" />
{% endif %}"#;
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert("checked", true);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"<div class="bg-red-500"><p></p>
<img src="/path/to/file.png">
Hello world!
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>
<input type="checkbox" checked />
"#
.to_string())
);
}
#[test]
fn for_stmt() {
let html = "<ol>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ol>";
let mut context = Context::new();
context.insert("items", vec!["Wash dishes", "Feed the dog"]);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("<ol>
<li>Wash dishes</li>
<li>Feed the dog</li>
</ol>"
.to_string())
);
}
#[test]
fn for_with_destructuring() {
let html = r#"<ul>
{% for index, item in enumerate(items) %}
<li style="{% if index == 1 || index == len(items) %}color: red;{% else %}color: blue;{% endif %}">{{ item }}</li>
{% endfor %}
</ul>"#;
assert_eq!(
process_template(
"index.html",
html,
&mut Context::from_iter([(
"items".to_string(),
Value::Array(vec![
"Living room".into_value(),
"Kitchen".into_value(),
"Bathroom".into_value()
])
)]),
0,
)
.unwrap(),
r#"<ul>
<li style="color: red;">Living room</li>
<li style="color: blue;">Kitchen</li>
<li style="color: red;">Bathroom</li>
</ul>"#
.to_string(),
);
let html = "<ul>
{% for city, street in locations %}
<li>{{ street }} (in {{ city }})</li>
{% endfor %}
</ul>";
assert_eq!(
process_template(
"index.html",
html,
&mut Context::from_iter([(
"locations".to_string(),
vec![
object! { "city" => "London", "street" => "10 High Street" },
object! { "city" => "New York", "street" => "898 Brandywine Rd." },
]
.into_value()
)]),
0,
)
.unwrap(),
"<ul>
<li>10 High Street (in London)</li>
<li>898 Brandywine Rd. (in New York)</li>
</ul>"
.to_string(),
);
}
#[test]
fn comments() {
let html = "<p>{# Some comment which won't appear in the final HTML #}</p>";
assert_eq!(
process_template("index.html", html, &mut Context::new(), 0),
Ok("<p></p>".to_string()),
);
}
#[test]
fn if_with_expr_inside() {
let html = r#"<div class="bg-red-500"><p></p>
<img src="/path/to/file.png">
{{ hello }}
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>
{% if checked %}
{{ hello }}
<input type="checkbox" checked />
{% else %}
<input type="checkbox" />
{% endif %}"#;
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert("checked", true);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"<div class="bg-red-500"><p></p>
<img src="/path/to/file.png">
Hello world!
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>
Hello world!
<input type="checkbox" checked />
"#
.to_string())
);
}
#[test]
fn for_with_range() {
let html = "{% for item in 1..=4 %}{{ item }}{% endfor %}";
assert_eq!(
process_template("index.html", html, &mut Context::new(), 0),
Ok("1234".to_string())
);
}
#[test]
fn for_with_unbounded_range() {
let html = "{% for item in 1.. %}{{ item }}{% endfor %}";
assert_eq!(
process_template("index.html", html, &mut Context::new(), 0),
Err(Spanned::new(Error::UnboundedRangeInForLoop)
.with_span(15..18)
.with_file_path("index.html")),
);
}
#[test]
fn for_with_enumerate() {
let html = "<ol>
{% for item in enumerate(items) %}
<li>{{ item[0] }}. {{ item[1] }}</li>
{% endfor %}
</ol>";
let mut context = Context::new();
context.insert("items", vec!["Wash dishes", "Feed the dog"]);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("<ol>
<li>1. Wash dishes</li>
<li>2. Feed the dog</li>
</ol>"
.to_string())
);
let html = "<ol>
{% for item in enumerate(items, 0) %}
<li>{{ item[0] + 1 }}. {{ item[1] }}</li>
{% endfor %}
</ol>";
let mut context = Context::new();
context.insert("items", vec!["Wash dishes", "Feed the dog"]);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("<ol>
<li>1. Wash dishes</li>
<li>2. Feed the dog</li>
</ol>"
.to_string()),
);
}
#[test]
fn let_in_templates() {
let html = "{% let a = 42 + 2 %}";
let mut context = Context::new();
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(String::new())
);
assert_eq!(context.get("a"), Some(&Value::Number(44.0)));
}
#[test]
fn lang_expressions_in_templates() {
let html = "<h1>{{ word_count(content) < 100 ? 'Short' : 'Long' }} article</h1><p>{{ content }}</p>";
let mut context = Context::new();
context.insert("content", "Lorem ipsum.");
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("<h1>Short article</h1><p>Lorem ipsum.</p>".to_string())
);
}
#[test]
fn expressions_are_escaped() {
let html = r#"<p class="{{ attribute }}">{{ msg }}</p>"#;
let mut context = Context::new();
context.insert("attribute", "\" onblur=\"alert('1')");
context.insert("msg", "<script>alert('1')</script>");
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"<p class="" onblur="alert('1')"><script>alert('1')</script></p>"#.to_string()),
);
}
#[test]
fn expressions_can_contain_curly_brackets() {
let html = "abcd{efgh{";
assert_eq!(
process_template("index.html", html, &mut Context::new(), 0),
Ok("abcd{efgh{".to_string())
);
}
#[test]
fn expressions_can_be_unescaped() {
let html = r#"<p test="{! msg !}">{! msg !}</p>"#;
let mut context = Context::new();
context.insert("msg", "<script>alert('1')</script>");
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(
r#"<p test="<script>alert('1')</script>"><script>alert('1')</script></p>"#
.to_string()
),
);
}
#[test]
fn expressions_with_functions() {
let html = r##"<a href="#{{ title |> slugify() }}">Link</a>"##;
let mut context = Context::new();
context.insert(
"title",
"# My new blog post ;-) with special characters \u{a9} `\\/)=???",
);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r##"<a href="#my-new-blog-post-with-special-characters-c">Link</a>"##.to_string()),
);
}
#[test]
fn render_structure() {
#[derive(serde::Serialize)]
struct MyData {
foo: String,
num: usize,
my_bool: bool,
my_vec: Vec<String>,
}
let html = "<div>Welcome to {{ foo }}! The magic number is {{ num }}. It is {{ my_bool }}. The message is {{ my_vec }}.</div>";
let mut context = Context::from_serialize(MyData {
foo: "bar".to_string(),
num: 41,
my_bool: true,
my_vec: vec!["Hello".to_string(), "world!".to_string()],
})
.unwrap();
context.insert("num", 42);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("<div>Welcome to bar! The magic number is 42. It is true. The message is [Hello, world!].</div>".to_string()),
);
}
#[test]
fn render_object() {
#[derive(serde::Serialize)]
struct MyData {
foo: String,
num: usize,
my_bool: bool,
my_vec: Vec<String>,
}
let html = "<div>Extract from data.num: {{ data.num }}. Full data: {{ data }}</div>";
let mut context = Context::new();
context.insert_serialize(
"data",
MyData {
foo: "bar".to_string(),
num: 42,
my_bool: true,
my_vec: vec!["Hello".to_string(), "world!".to_string()],
},
);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("<div>Extract from data.num: 42. Full data: {foo: bar, num: 42, my_bool: true, my_vec: [Hello, world!]}</div>".to_string()),
);
}
#[test]
fn if_statements() {
let html = r#"{% if already_shown %}
<h1 class="before{% if primary %} text-primary{% endif %} after">Hello Mr. {{ name }}!</h1>
<h1 class="before {% if primary %}text-primary {%else%}text-secondary {% endif %}after">Hello Mr. {{ name }} nice to see you!</h1>
{% else %}
<h1>Good bye.</h1>
{% endif %}"#;
let mut context = Context::new();
context.insert("already_shown", true);
context.insert("primary", true);
context.insert("name", "Thomas");
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"
<h1 class="before text-primary after">Hello Mr. Thomas!</h1>
<h1 class="before text-primary after">Hello Mr. Thomas nice to see you!</h1>
"#
.to_string())
);
*context.get_mut("primary").unwrap() = false.into_value();
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"
<h1 class="before after">Hello Mr. Thomas!</h1>
<h1 class="before text-secondary after">Hello Mr. Thomas nice to see you!</h1>
"#
.to_string())
);
}
#[test]
fn else_if_statements() {
let html = "{% if grade > 18 %}
Good!
{% elif grade > 15 %}
Not bad
{% elif grade > 10 %}
OK
{% elif grade > 5 %}
Bad
{% elif grade > 1 %}
Oops
{% elif grade == 0 %}
Awful
{% elif grade < 0 %}
What!?
{% endif %}";
let mut context = Context::new();
context.insert("grade", 12);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("
OK
"
.to_string())
);
}
#[test]
fn if_let_statements() {
let html = "{% if let alphabet = maybe_alphabet %}
There is an alphabet
{% else %}
Letters don't exist!
{% endif %}";
let mut context = Context::new();
context.insert("maybe_alphabet", Value::Null);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("
Letters don't exist!
"
.to_string())
);
let html = "{% if let list = list %}{% for a in list %}{{ a }}{% endfor %}{% endif %}";
let mut context = Context::new();
context.insert("list", vec!["a", "b", "c"]);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("abc".to_string())
);
let mut context = Context::new();
context.insert("list", Value::Null);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(String::new())
);
}
#[test]
fn for_statements() {
#[derive(serde::Serialize)]
struct Recipe {
name: &'static str,
is_vegan: bool,
}
let html = r#"{% for recipe in recipes %}
<h3>{{ recipe.name }}</h3>
{% if recipe.is_vegan %}<div class="text-green-500">This recipe is vegan!</div>{% else %}<div class="text-red-500">This recipe is <b>not</b> vegan.</div>{% endif %}
{% endfor %}"#;
let mut context = Context::new();
context.insert_serialize(
"recipes",
vec![
Recipe {
name: "Vegan Pancakes",
is_vegan: true,
},
Recipe {
name: "Egg Salad",
is_vegan: false,
},
],
);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"
<h3>Vegan Pancakes</h3>
<div class="text-green-500">This recipe is vegan!</div>
<h3>Egg Salad</h3>
<div class="text-red-500">This recipe is <b>not</b> vegan.</div>
"#
.to_string())
);
}
#[test]
fn nested_if_for_statements() {
let html = "
{% if len(restaurants) > 0 %}
{% for restaurant in restaurants %}
<li>{{ restaurant }}</li>
{% endfor %}
{% else %}
No restaurants yet
{% endif %}";
let mut context = Context::new();
context.insert_serialize("restaurants", ["un\u{b7}cooked"]);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("
<li>un\u{b7}cooked</li>
"
.to_string()),
);
}
#[test]
fn nested_for_if_statements() {
let html = r#"{% for checked in checkboxes %}
{% if checked %}
<input type="checkbox" checked />
{% else %}
<input type="checkbox" />
{% endif %}
{% endfor %}"#;
let mut context = Context::new();
context.insert_serialize("checkboxes", [true, true, true, false, true, false]);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok(r#"
<input type="checkbox" checked />
<input type="checkbox" checked />
<input type="checkbox" checked />
<input type="checkbox" />
<input type="checkbox" checked />
<input type="checkbox" />
"#
.to_string())
);
}
#[test]
fn nested_if_else_if_statements() {
let html = "{% if grade > 18 %}
Good!
{% if scholarship %}
You are an honor to our school.
{% else %}
Keep it up!
{% endif %}
{% elif grade > 15 %}
Not bad
{% elif grade > 10 %}
OK
{% elif grade > 5 %}
Bad
{% elif grade > 1 %}
Oops
{% if scholarship %}
Out!
{% endif %}
{% elif grade == 0 %}
Awful
{% elif grade < 0 %}
What!?
{% endif %}";
let mut context = Context::new();
context.insert("grade", 2);
context.insert("scholarship", true);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("
Oops
Out!
"
.to_string())
);
let mut context = Context::new();
context.insert("grade", 19);
context.insert("scholarship", false);
assert_eq!(
process_template("index.html", html, &mut context, 0),
Ok("
Good!
Keep it up!
"
.to_string())
);
}
}