diff --git a/MIGRATION.md b/MIGRATION.md index 1aa08eeb..0c1394e3 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -11,6 +11,7 @@ A lot of things have changed for the better. * `{{ hey }}` should error if hey is undefined * `{{ existing.hey }}` should error if hey is undefined but existing is * `{{ hey or 1 }}` should print 1 + * `{{ false and user.name }}` will not evaluate `user.name` and print `false` * `{% if hey or true %}` should be truthy * `{% if hey.other or true %}` should error if `hey` is not defined (currently truthy) * `{{ hey.other or 1 }}` should error if `hey` is not defined (currently prints "true") diff --git a/tera-contrib/src/dates.rs b/tera-contrib/src/dates.rs index 9a27fd0b..b7496db3 100644 --- a/tera-contrib/src/dates.rs +++ b/tera-contrib/src/dates.rs @@ -28,7 +28,9 @@ fn parse_to_zoned(val: &Value, tz: Option) -> TeraResult { )) }) } else if let Some(Number::Integer(ts)) = val.as_number() { - Timestamp::new(ts as i64, 0) + let ts = i64::try_from(ts) + .map_err(|_| tera::Error::message(format!("Invalid timestamp: {ts}")))?; + Timestamp::new(ts, 0) .map(|t| t.to_zoned(default_tz)) .map_err(|e| tera::Error::message(format!("Invalid timestamp: {e}"))) } else { diff --git a/tera-contrib/src/format.rs b/tera-contrib/src/format.rs index a11b4eeb..b7787bb9 100644 --- a/tera-contrib/src/format.rs +++ b/tera-contrib/src/format.rs @@ -23,11 +23,16 @@ pub fn format(val: Value, kwargs: Kwargs, _: &State) -> TeraResult { formatx::formatx!(&fmt_str, s) .map_err(|e| Error::message(format!("format error: {}", e))) } - ValueKind::I64 | ValueKind::I128 | ValueKind::U64 | ValueKind::U128 => { + ValueKind::I64 | ValueKind::I128 | ValueKind::U64 => { let n = val.as_i128().unwrap(); formatx::formatx!(&fmt_str, n) .map_err(|e| Error::message(format!("format error: {}", e))) } + ValueKind::U128 => { + let n = val.as_u128().unwrap(); + formatx::formatx!(&fmt_str, n) + .map_err(|e| Error::message(format!("format error: {}", e))) + } ValueKind::F64 => { let n = val.as_number().unwrap(); let f = n.as_float(); diff --git a/tera/src/args.rs b/tera/src/args.rs index 3b0dd488..d5bd5331 100644 --- a/tera/src/args.rs +++ b/tera/src/args.rs @@ -7,7 +7,39 @@ use crate::errors::{Error, TeraResult}; use crate::value::number::Number; use crate::value::{Key, Map, ValueInner}; -pub trait ArgFromValue<'k> { +mod private { + use super::{Map, Number, Value}; + use std::borrow::Cow; + + pub trait Sealed {} + + impl Sealed for bool {} + impl Sealed for f32 {} + impl Sealed for f64 {} + impl Sealed for u8 {} + impl Sealed for u16 {} + impl Sealed for u32 {} + impl Sealed for u64 {} + impl Sealed for u128 {} + impl Sealed for usize {} + impl Sealed for i8 {} + impl Sealed for i16 {} + impl Sealed for i32 {} + impl Sealed for i64 {} + impl Sealed for i128 {} + impl Sealed for isize {} + impl Sealed for String {} + impl Sealed for &str {} + impl<'a> Sealed for Cow<'a, str> {} + impl Sealed for Value {} + impl Sealed for &Value {} + impl Sealed for Number {} + impl Sealed for Map {} + impl Sealed for Vec {} +} + +#[doc(hidden)] +pub trait ArgFromValue<'k>: private::Sealed { type Output; fn from_value(value: &'k Value) -> TeraResult; @@ -135,9 +167,15 @@ impl<'k> ArgFromValue<'k> for Number { type Output = Number; fn from_value(value: &'k Value) -> TeraResult { - value - .as_number() - .ok_or_else(|| Error::invalid_arg_type("Number", value.name())) + if let Some(n) = value.as_number() { + Ok(n) + } else if value.is_number() { + Err(Error::message(format!( + "Number `{value}` is out of range for i128" + ))) + } else { + Err(Error::invalid_arg_type("Number", value.name())) + } } } diff --git a/tera/src/delimiters.rs b/tera/src/delimiters.rs index 2d50a970..b662ea16 100644 --- a/tera/src/delimiters.rs +++ b/tera/src/delimiters.rs @@ -1,33 +1,35 @@ +use std::borrow::Cow; + use crate::errors::{Error, TeraResult}; /// This allows customizing the delimiters used for blocks, variables, and comments in case /// you want to template files that contains text like `{{`, like LaTeX. /// Delimiters need to be 2 ASCII characters. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Delimiters { /// Start delimiter for blocks, default: `{%` - pub block_start: &'static str, + pub block_start: Cow<'static, str>, /// End delimiter for blocks, default: `%}` - pub block_end: &'static str, + pub block_end: Cow<'static, str>, /// Start delimiter for variables, default: `{{` - pub variable_start: &'static str, + pub variable_start: Cow<'static, str>, /// End delimiter for variables, default: `}}` - pub variable_end: &'static str, + pub variable_end: Cow<'static, str>, /// Start delimiter for comments, default: `{#` - pub comment_start: &'static str, + pub comment_start: Cow<'static, str>, /// End delimiter for comments, default: `#}` - pub comment_end: &'static str, + pub comment_end: Cow<'static, str>, } impl Default for Delimiters { fn default() -> Self { Self { - block_start: "{%", - block_end: "%}", - variable_start: "{{", - variable_end: "}}", - comment_start: "{#", - comment_end: "#}", + block_start: "{%".into(), + block_end: "%}".into(), + variable_start: "{{".into(), + variable_end: "}}".into(), + comment_start: "{#".into(), + comment_end: "#}".into(), } } } @@ -93,16 +95,16 @@ mod tests { fn errors_on_invalid_delimiters() { let inputs = vec![ Delimiters { - block_start: "", + block_start: "".into(), ..Delimiters::default() }, Delimiters { - block_start: "[[[", + block_start: "[[[".into(), ..Delimiters::default() }, Delimiters { - block_start: "[[", - comment_start: "[[", + block_start: "[[".into(), + comment_start: "[[".into(), ..Delimiters::default() }, ]; diff --git a/tera/src/errors.rs b/tera/src/errors.rs index 75a62e87..68feb58e 100644 --- a/tera/src/errors.rs +++ b/tera/src/errors.rs @@ -7,7 +7,7 @@ use std::error::Error as StdError; use crate::utils::Span; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Note { +pub(crate) struct Note { pub(crate) filename: String, pub(crate) source: String, pub(crate) span: Span, @@ -23,7 +23,7 @@ pub struct ReportError { } impl ReportError { - pub fn new(message: String, filename: &str, source: &str, span: &Span) -> Self { + pub(crate) fn new(message: String, filename: &str, source: &str, span: &Span) -> Self { Self { message, filename: filename.to_string(), @@ -34,7 +34,7 @@ impl ReportError { } /// Create a ReportError without filename/source - must call set_source before generating report - pub fn new_without_source(message: String, span: &Span) -> Self { + pub(crate) fn new_without_source(message: String, span: &Span) -> Self { Self { message, filename: String::new(), @@ -44,12 +44,12 @@ impl ReportError { } } - pub fn set_source(&mut self, filename: &str, source: &str) { + pub(crate) fn set_source(&mut self, filename: &str, source: &str) { self.filename = filename.to_string(); self.source = source.to_string(); } - pub fn add_note(&mut self, filename: &str, source: &str, span: &Span) { + pub(crate) fn add_note(&mut self, filename: &str, source: &str, span: &Span) { self.notes.push(Note { filename: filename.to_string(), source: source.to_string(), @@ -61,12 +61,29 @@ impl ReportError { generate_report(self) } - pub fn unexpected_end_of_input(span: &Span) -> Self { + pub fn message(&self) -> &str { + &self.message + } + + pub fn span(&self) -> &Span { + &self.span + } + + pub fn filename(&self) -> &str { + &self.filename + } + + pub fn source(&self) -> &str { + &self.source + } + + pub(crate) fn unexpected_end_of_input(span: &Span) -> Self { Self::new_without_source("Unexpected end of input".to_string(), span) } } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ErrorKind { /// Generic error Msg(String), @@ -193,7 +210,7 @@ impl fmt::Display for ErrorKind { #[derive(Debug)] pub struct Error { - pub kind: ErrorKind, + pub(crate) kind: ErrorKind, // If the error comes from some third party libs, TODO we need that? pub(crate) source: Option>, } @@ -209,6 +226,10 @@ impl Error { Self { kind, source: None } } + pub fn kind(&self) -> &ErrorKind { + &self.kind + } + /// Creates generic error with a source pub fn chain(value: impl ToString, source: impl Into>) -> Self { Self { diff --git a/tera/src/filters.rs b/tera/src/filters.rs index 0dfd5074..2cbd9c94 100644 --- a/tera/src/filters.rs +++ b/tera/src/filters.rs @@ -282,15 +282,14 @@ pub(crate) fn as_str(val: Value, _: Kwargs, _: &State) -> String { pub(crate) fn int(val: Value, kwargs: Kwargs, _: &State) -> TeraResult { let base = kwargs.get::("base")?.unwrap_or(10); - let handle_f64 = |v: f64| { - if let Some(i) = Number::Float(v).as_integer() { - Ok(i.into()) - } else { - Err(Error::message(format!( - "The float {v} would have to be truncated to convert to an int" - ))) - } - }; + let handle_f64 = + |v: f64| { + Number::Float(v).as_integer().map(Into::into).ok_or_else(|| { + Error::message(format!( + "The float {v} cannot be converted to an int (non-integer or out of i128 range)" + )) + }) + }; match val.kind() { ValueKind::String => { @@ -331,10 +330,7 @@ pub(crate) fn int(val: Value, kwargs: Kwargs, _: &State) -> TeraResult { let v = val.as_i128().unwrap(); Ok(v.into()) } - ValueKind::U128 => { - let v = val.as_i128().unwrap() as u128; - Ok(v.into()) - } + ValueKind::U128 => Ok(val), ValueKind::F64 => { let v = val.as_f64().unwrap(); handle_f64(v) diff --git a/tera/src/lib.rs b/tera/src/lib.rs index ffec951a..ffc933bc 100644 --- a/tera/src/lib.rs +++ b/tera/src/lib.rs @@ -1,3 +1,62 @@ +//! # Tera +//! +//! A powerful, fast and easy-to-use template engine for Rust +//! +//! This crate provides an implementation of the Tera template engine, which is designed for use in +//! Rust applications. Inspired by [Jinja2] and [Django] templates, Tera provides a familiar and +//! expressive syntax for creating dynamic HTML, XML, and other text-based documents. It supports +//! template inheritance, variable interpolation, conditionals, loops, filters, and custom +//! functions, enabling developers to build complex applications with ease. +//! +//! See the [site](http://keats.github.io/tera/) for more information and to get started. +//! +//! ## Features +//! +//! - High-performance template rendering +//! - Safe and sandboxed execution environment +//! - Template inheritance and includes +//! - Expressive and familiar syntax +//! - Extensible with custom filters and functions +//! - Automatic escaping of HTML/XML by default +//! - Template caching and auto-reloading for efficient development +//! - Built-in support for JSON and other data formats +//! - Comprehensive error messages and debugging information +//! +//! ## Example +//! +//! ```rust +//! use tera::Tera; +//! +//! // Create a new Tera instance and add a template from a string +//! let mut tera = Tera::new(); +//! tera.register_filter("do_nothing", do_nothing_filter); +//! tera.load_from_glob("examples/basic/templates/**/*")?; +//! // Prepare the context with some data +//! let mut context = tera::Context::new(); +//! context.insert("name", "World"); +//! +//! // Render the template with the given context +//! let rendered = tera.render("hello", &context)?; +//! assert_eq!(rendered, "Hello, World!"); +//! ``` +//! +//! ## Getting Started +//! +//! Add the following to your Cargo.toml file: +//! +//! ```toml +//! [dependencies] +//! tera = "2" +//! ``` +//! +//! Then, consult the official documentation and examples to learn more about using Tera in your +//! Rust projects. +//! +//! [Jinja2]: http://jinja.pocoo.org/ +//! [Django]: https://docs.djangoproject.com/en/3.1/topics/templates/ + +//#![deny(missing_docs)] + mod args; mod components; mod context; @@ -24,7 +83,6 @@ pub use delimiters::Delimiters; pub use errors::{Error, ErrorKind, TeraResult}; pub use filters::Filter; pub use functions::Function; -pub use parsing::parser::Parser; pub use tests::Test; pub use utils::escape_html; pub use value::number::Number; diff --git a/tera/src/parsing/instructions.rs b/tera/src/parsing/instructions.rs index 42d83d13..9a5751b6 100644 --- a/tera/src/parsing/instructions.rs +++ b/tera/src/parsing/instructions.rs @@ -205,6 +205,24 @@ impl Chunk { // Map from old instruction index to new instruction index // +1 to handle jumps that target one-past-the-end (i.e., chunk.len()) let mut index_map: Vec = vec![0; old_instructions.len() + 1]; + + // We don't fuse instructions with jumps. + // `{{ false and user.name }}` emits JumpIfFalseOrPop targeting + // the WriteTop; if that WriteTop were folded into WritePath, the jump would + // execute the path load it was meant to skip. + let mut is_jump_target: Vec = vec![false; old_instructions.len()]; + for (instr, _) in &old_instructions { + if let Instruction::Jump(t) + | Instruction::PopJumpIfFalse(t) + | Instruction::JumpIfFalseOrPop(t) + | Instruction::JumpIfTrueOrPop(t) + | Instruction::Iterate(t) = instr + && *t < is_jump_target.len() + { + is_jump_target[*t] = true; + } + } + let mut i = 0; // Placeholder for mem::replace - cheapest instruction (no heap allocation) @@ -229,6 +247,10 @@ impl Chunk { // Collect consecutive LoadAttr instructions while j < old_instructions.len() { + // Don't absorb a jump target into the fusion + if is_jump_target[j] { + break; + } if matches!(&old_instructions[j].0, Instruction::LoadAttr(_)) { // Map the consumed LoadAttr to the same position as the first instruction index_map[j] = optimized.len(); @@ -247,8 +269,11 @@ impl Chunk { } } - // Check if followed by WriteTop + // Check if followed by WriteTop. Skip fusion when WriteTop is a jump + // target: short-circuit `and`/`or` land their jump on WriteTop, and + // fusing into WritePath would make the skipped path execute anyway. let has_write = j < old_instructions.len() + && !is_jump_target[j] && matches!(&old_instructions[j].0, Instruction::WriteTop); if has_write { diff --git a/tera/src/parsing/lexer.rs b/tera/src/parsing/lexer.rs index 64221676..1cc04a31 100644 --- a/tera/src/parsing/lexer.rs +++ b/tera/src/parsing/lexer.rs @@ -14,7 +14,7 @@ fn memstr(haystack: &[u8], needle: &[u8]) -> Option { /// Will try to go over `-? {name} -?{block_end}`. /// Returns None if the name doesn't match the tag or the (offset, ws) tuple for the end of the tag -fn skip_tag(block_str: &str, name: &str, block_end: &'static str) -> Option<(usize, bool)> { +fn skip_tag(block_str: &str, name: &str, block_end: &str) -> Option<(usize, bool)> { let mut ptr = block_str; if let Some(rest) = ptr.strip_prefix('-') { @@ -40,7 +40,7 @@ fn skip_tag(block_str: &str, name: &str, block_end: &'static str) -> Option<(usi } /// We want to find the next time we see any start marker (variable, block, or comment) -fn find_start_marker(tpl: &str, delimiters: Delimiters) -> Option { +fn find_start_marker(tpl: &str, delimiters: &Delimiters) -> Option { let var_start = delimiters.variable_start.as_bytes(); let block_start = delimiters.block_start.as_bytes(); let comment_start = delimiters.comment_start.as_bytes(); @@ -418,7 +418,7 @@ fn basic_tokenize( let ws = check_ws_start!(); if let Some((mut offset, end_ws_start_tag)) = - skip_tag(rest, "raw", delimiters.block_end) + skip_tag(rest, "raw", &delimiters.block_end) { let body_start_offset = offset; // Then we see whether we find the start of the tag @@ -432,7 +432,7 @@ fn basic_tokenize( let start_ws_end_tag = rest.as_bytes().get(offset + 1) == Some(&b'-'); if let Some((endraw, ws_end)) = - skip_tag(&rest[offset..], "endraw", delimiters.block_end) + skip_tag(&rest[offset..], "endraw", &delimiters.block_end) { let mut result = &rest[body_start_offset..body_end_offset]; // Then we trim the inner body of the raw tag as needed directly here @@ -483,7 +483,7 @@ fn basic_tokenize( _ => {} } - let text = match find_start_marker(rest, delimiters) { + let text = match find_start_marker(rest, &delimiters) { Some(start) => advance!(start), None => advance!(rest.len()), }; @@ -512,13 +512,13 @@ fn basic_tokenize( State::Tag => { // Check for whitespace control: -{block_end} if rest.get(..1) == Some("-") - && rest.get(1..3) == Some(delimiters.block_end) + && rest.get(1..3) == Some(delimiters.block_end.as_ref()) { stack.pop(); advance!(3); return Some(Ok((Token::TagEnd(true), make_span!(start_loc)))); } - if rest.get(..2) == Some(delimiters.block_end) { + if rest.get(..2) == Some(delimiters.block_end.as_ref()) { stack.pop(); advance!(2); return Some(Ok((Token::TagEnd(false), make_span!(start_loc)))); @@ -527,13 +527,13 @@ fn basic_tokenize( State::Variable => { // Check for whitespace control: -{variable_end} if rest.get(..1) == Some("-") - && rest.get(1..3) == Some(delimiters.variable_end) + && rest.get(1..3) == Some(delimiters.variable_end.as_ref()) { stack.pop(); advance!(3); return Some(Ok((Token::VariableEnd(true), make_span!(start_loc)))); } - if rest.get(..2) == Some(delimiters.variable_end) { + if rest.get(..2) == Some(delimiters.variable_end.as_ref()) { stack.pop(); advance!(2); return Some(Ok(( diff --git a/tera/src/parsing/parser.rs b/tera/src/parsing/parser.rs index 06510bb6..c7eceef2 100644 --- a/tera/src/parsing/parser.rs +++ b/tera/src/parsing/parser.rs @@ -202,12 +202,7 @@ impl<'a> Parser<'a> { // If we don't have a colon (eg `::-1`), parse an expr first // If there are no colons after, it's just the normal subscript expr if !matches!(self.next, Some(Ok((Token::Colon, _)))) { - let sub_expr = self.parse_expression(0)?; - // Something like [-1] is a slice - if matches!(sub_expr, Expression::UnaryOperation(..)) { - slice = true; - } - start = Some(sub_expr) + start = Some(self.parse_expression(0)?); } // Now, it could be slice indexing pattern if there is a `:` diff --git a/tera/src/snapshot_tests/compiler_inputs/success/short_circuit_path.txt b/tera/src/snapshot_tests/compiler_inputs/success/short_circuit_path.txt new file mode 100644 index 00000000..8d3c2bb7 --- /dev/null +++ b/tera/src/snapshot_tests/compiler_inputs/success/short_circuit_path.txt @@ -0,0 +1 @@ +{{ false and user.name }}{{ true or user.name }}{{ some_bool and product.name }} diff --git a/tera/src/snapshot_tests/rendering.rs b/tera/src/snapshot_tests/rendering.rs index e4394f27..18aaf3ba 100644 --- a/tera/src/snapshot_tests/rendering.rs +++ b/tera/src/snapshot_tests/rendering.rs @@ -287,12 +287,12 @@ Age: << age >> let mut tera = Tera::default(); tera.set_delimiters(Delimiters { - block_start: "<%", - block_end: "%>", - variable_start: "<<", - variable_end: ">>", - comment_start: "<#", - comment_end: "#>", + block_start: "<%".into(), + block_end: "%>".into(), + variable_start: "<<".into(), + variable_end: ">>".into(), + comment_start: "<#".into(), + comment_end: "#>".into(), }) .unwrap(); tera.add_raw_template("custom_delimiters.txt", tpl).unwrap(); diff --git a/tera/src/snapshot_tests/rendering_inputs/errors/slicing_non_containers.txt b/tera/src/snapshot_tests/rendering_inputs/errors/slicing_non_containers.txt index c2d5278c..394aa90b 100644 --- a/tera/src/snapshot_tests/rendering_inputs/errors/slicing_non_containers.txt +++ b/tera/src/snapshot_tests/rendering_inputs/errors/slicing_non_containers.txt @@ -1 +1 @@ -{{ one[-1] }} \ No newline at end of file +{{ one[-1:] }} \ No newline at end of file diff --git a/tera/src/snapshot_tests/rendering_inputs/success/short_circuit_path.txt b/tera/src/snapshot_tests/rendering_inputs/success/short_circuit_path.txt new file mode 100644 index 00000000..11f21f97 --- /dev/null +++ b/tera/src/snapshot_tests/rendering_inputs/success/short_circuit_path.txt @@ -0,0 +1,4 @@ +{{ false and product.name }} +{{ true or product.name }} +{{ false and data.names }} +{{ some_bool and product.name }} diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__compiler__compiler_ok@short_circuit_path.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__compiler__compiler_ok@short_circuit_path.txt.snap new file mode 100644 index 00000000..bd77a63b --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__compiler__compiler_ok@short_circuit_path.txt.snap @@ -0,0 +1,22 @@ +--- +source: tera/src/snapshot_tests/compiler.rs +expression: compiler.chunk +input_file: tera/src/snapshot_tests/compiler_inputs/success/short_circuit_path.txt +--- +=== short_circuit_path.txt === +0000 LoadConst(Bool(false)) +0001 JumpIfFalseOrPop(4) +0002 LoadName("user") +0003 LoadAttr("name") +0004 WriteTop +0005 LoadConst(Bool(true)) +0006 JumpIfTrueOrPop(9) +0007 LoadName("user") +0008 LoadAttr("name") +0009 WriteTop +0010 LoadName("some_bool") +0011 JumpIfFalseOrPop(14) +0012 LoadName("product") +0013 LoadAttr("name") +0014 WriteTop +0015 WriteText("\n") diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_errors@slicing_non_containers.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_errors@slicing_non_containers.txt.snap index b589727f..5a200bd4 100644 --- a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_errors@slicing_non_containers.txt.snap +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_errors@slicing_non_containers.txt.snap @@ -6,5 +6,5 @@ input_file: tera/src/snapshot_tests/rendering_inputs/errors/slicing_non_containe error: Slicing can only be used on arrays or strings, not on `i64`. --> slicing_non_containers.txt:1:3 | -1 | {{ one[-1] }} +1 | {{ one[-1:] }} | ^^^ diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap index 9be6e119..da1f1638 100644 --- a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@indexing.txt.snap @@ -5,7 +5,7 @@ input_file: tera/src/snapshot_tests/rendering_inputs/success/indexing.txt --- 1 1 -[3] +3 [1, 2] [1, 2] [2] diff --git a/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@short_circuit_path.txt.snap b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@short_circuit_path.txt.snap new file mode 100644 index 00000000..0fc679d3 --- /dev/null +++ b/tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@short_circuit_path.txt.snap @@ -0,0 +1,9 @@ +--- +source: tera/src/snapshot_tests/rendering.rs +expression: "&normalized_out" +input_file: tera/src/snapshot_tests/rendering_inputs/success/short_circuit_path.txt +--- +false +true +false +Moto G diff --git a/tera/src/template.rs b/tera/src/template.rs index e40c9e4e..00db854e 100644 --- a/tera/src/template.rs +++ b/tera/src/template.rs @@ -1,10 +1,11 @@ +use crate::HashMap; use crate::delimiters::Delimiters; use crate::errors::{Error, ErrorKind, TeraResult}; use crate::parsing::ast::ComponentDefinition; +use crate::parsing::parser::Parser; use crate::parsing::{Chunk, Compiler}; use crate::tera::Tera; use crate::utils::Span; -use crate::{HashMap, Parser}; use std::collections::HashSet; #[derive(Debug, PartialEq, Clone)] diff --git a/tera/src/tera.rs b/tera/src/tera.rs index 1ea5573a..ef11c628 100644 --- a/tera/src/tera.rs +++ b/tera/src/tera.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::collections::HashSet; use std::fmt; use std::fs::File; @@ -32,21 +33,18 @@ pub type EscapeFn = fn(&[u8], &mut dyn Write) -> std::io::Result<()>; pub struct Tera { /// The glob used to load templates if there was one. /// Only used if the `glob_fs` feature is turned on - #[doc(hidden)] #[allow(dead_code)] - glob: Option, - #[doc(hidden)] - pub templates: HashMap, + pub(crate) glob: Option, + pub(crate) templates: HashMap, /// Which extensions does Tera automatically autoescape on. /// Defaults to [".html", ".htm", ".xml"] - #[doc(hidden)] - autoescape_suffixes: Vec<&'static str>, + pub(crate) autoescape_suffixes: Vec<&'static str>, #[doc(hidden)] pub(crate) escape_fn: EscapeFn, global_context: Context, - pub(crate) filters: HashMap<&'static str, StoredFilter>, - pub(crate) tests: HashMap<&'static str, StoredTest>, - pub(crate) functions: HashMap<&'static str, StoredFunction>, + pub(crate) filters: HashMap, StoredFilter>, + pub(crate) tests: HashMap, StoredTest>, + pub(crate) functions: HashMap, StoredFunction>, pub(crate) components: HashMap, /// Custom delimiters for template syntax delimiters: Delimiters, @@ -141,12 +139,12 @@ impl Tera { /// /// let mut tera = Tera::new(); /// tera.set_delimiters(Delimiters { - /// block_start: "<%", - /// block_end: "%>", - /// variable_start: "<<", - /// variable_end: ">>", - /// comment_start: "<#", - /// comment_end: "#>", + /// block_start: "<%".into(), + /// block_end: "%>".into(), + /// variable_start: "<<".into(), + /// variable_end: ">>".into(), + /// comment_start: "<#".into(), + /// comment_end: "#>".into(), /// }).unwrap(); /// tera.add_raw_template("example", "<< name >>").unwrap(); /// ``` @@ -223,13 +221,16 @@ impl Tera { /// let mut tera = Tera::default(); /// tera.register_filter("double", |x: i64, _: Kwargs, _: &State| x * 2); /// ``` - pub fn register_filter(&mut self, name: &'static str, filter: Func) - where + pub fn register_filter( + &mut self, + name: impl Into>, + filter: Func, + ) where Func: Filter + for<'a> Filter<>::Output, Res>, Arg: for<'a> ArgFromValue<'a>, Res: FunctionResult, { - self.filters.insert(name, StoredFilter::new(filter)); + self.filters.insert(name.into(), StoredFilter::new(filter)); } /// Register a test with Tera. @@ -241,24 +242,25 @@ impl Tera { /// let mut tera = Tera::default(); /// tera.register_test("odd", |x: i64, _: Kwargs, _: &State| x % 2 != 0); /// ``` - pub fn register_test(&mut self, name: &'static str, test: Func) + pub fn register_test(&mut self, name: impl Into>, test: Func) where Func: Test + for<'a> Test<>::Output, Res>, Arg: for<'a> ArgFromValue<'a>, Res: TestResult, { - self.tests.insert(name, StoredTest::new(test)); + self.tests.insert(name.into(), StoredTest::new(test)); } /// Register a function with Tera. /// /// If a function with that name already exists, it will be overwritten - pub fn register_function(&mut self, name: &'static str, func: Func) + pub fn register_function(&mut self, name: impl Into>, func: Func) where Func: Function, Res: FunctionResult, { - self.functions.insert(name, StoredFunction::new(func)); + self.functions + .insert(name.into(), StoredFunction::new(func)); } /// Register filters, tests, and functions from another [`Tera`] instance. @@ -268,19 +270,19 @@ impl Tera { pub fn register_from(&mut self, other: &Tera) { for (name, filter) in &other.filters { if !self.filters.contains_key(name) { - self.filters.insert(name, filter.clone()); + self.filters.insert(name.clone(), filter.clone()); } } for (name, test) in &other.tests { if !self.tests.contains_key(name) { - self.tests.insert(name, test.clone()); + self.tests.insert(name.clone(), test.clone()); } } for (name, function) in &other.functions { if !self.functions.contains_key(name) { - self.functions.insert(name, function.clone()); + self.functions.insert(name.clone(), function.clone()); } } } @@ -686,8 +688,12 @@ impl Tera { let mut inserted: Vec<(String, Option