From 119dfb579c8456bbae2fdd8587530a61b1aec89d Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 23 Apr 2026 13:16:13 +0200 Subject: [PATCH 01/10] Drop impl Hash for Value, we dont need it --- tera/src/value/mod.rs | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/tera/src/value/mod.rs b/tera/src/value/mod.rs index fd95faf..e19ddd4 100644 --- a/tera/src/value/mod.rs +++ b/tera/src/value/mod.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fmt; use std::fmt::Formatter; -use std::hash::{Hash, Hasher}; +use std::hash::Hash; use std::sync::Arc; use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer}; @@ -296,27 +296,6 @@ impl Ord for Value { } } -impl Hash for Value { - fn hash(&self, state: &mut H) { - match &self.inner { - ValueInner::Undefined | ValueInner::None => 0.hash(state), - ValueInner::Bool(v) => v.hash(state), - ValueInner::U64(_) - | ValueInner::I64(_) - | ValueInner::U128(_) - | ValueInner::I128(_) - | ValueInner::F64(_) => self.as_number().hash(state), - ValueInner::Bytes(v) => v.hash(state), - ValueInner::String(v) => v.as_str().hash(state), - ValueInner::Array(v) => v.hash(state), - ValueInner::Map(v) => v.iter().for_each(|(k, v)| { - k.hash(state); - v.hash(state); - }), - } - } -} - impl Value { pub fn none() -> Self { Value { From 267f6d928ba91b12fce9e3e0ab6bd7ff254c3f21 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 23 Apr 2026 13:28:38 +0200 Subject: [PATCH 02/10] Handle unicode feature in Value::{len,reverse} --- tera/src/value/mod.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tera/src/value/mod.rs b/tera/src/value/mod.rs index e19ddd4..f5e18c7 100644 --- a/tera/src/value/mod.rs +++ b/tera/src/value/mod.rs @@ -637,7 +637,16 @@ impl Value { ValueInner::Map(v) => Some(v.len()), ValueInner::Array(v) => Some(v.len()), ValueInner::Bytes(v) => Some(v.len()), - ValueInner::String(v) => Some(v.as_str().chars().count()), + ValueInner::String(v) => { + #[cfg(feature = "unicode")] + { + Some(v.as_str().graphemes(true).count()) + } + #[cfg(not(feature = "unicode"))] + { + Some(v.as_str().chars().count()) + } + } _ => None, } } @@ -650,7 +659,13 @@ impl Value { Ok(Self::from(rev)) } ValueInner::Bytes(v) => Ok(Self::from(v.iter().rev().copied().collect::>())), - ValueInner::String(v) => Ok(Self::from(String::from_iter(v.as_str().chars().rev()))), + ValueInner::String(v) => { + #[cfg(feature = "unicode")] + let reversed: String = v.as_str().graphemes(true).rev().collect(); + #[cfg(not(feature = "unicode"))] + let reversed: String = v.as_str().chars().rev().collect(); + Ok(Self::from(reversed)) + } _ => Err(Error::message(format!( "Value of type {} cannot be reversed", self.name() @@ -1126,3 +1141,25 @@ impl> FunctionResult for I { Ok(self.into()) } } + +#[cfg(test)] +mod tests { + use super::*; + + // "école" with é = 'e' + U+0301 + #[cfg(not(feature = "unicode"))] + #[test] + fn len_and_reverse_use_chars() { + let v = Value::from("e\u{0301}cole"); + assert_eq!(v.len(), Some(6)); + assert_eq!(v.reverse().unwrap().as_str(), Some("eloc\u{0301}e")); + } + + #[cfg(feature = "unicode")] + #[test] + fn len_and_reverse_use_graphemes() { + let v = Value::from("e\u{0301}cole"); + assert_eq!(v.len(), Some(5)); + assert_eq!(v.reverse().unwrap().as_str(), Some("eloce\u{0301}")); + } +} From b94ef0d24e41698c526092753c1b5ab6364f10ac Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 23 Apr 2026 13:40:27 +0200 Subject: [PATCH 03/10] Handle NaN comp for f64 comparison --- tera/src/value/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tera/src/value/mod.rs b/tera/src/value/mod.rs index f5e18c7..f832645 100644 --- a/tera/src/value/mod.rs +++ b/tera/src/value/mod.rs @@ -219,6 +219,7 @@ impl PartialEq for Value { (ValueInner::String(v), ValueInner::String(v2)) => v.as_str() == v2.as_str(), (ValueInner::Map(v), ValueInner::Map(v2)) => v == v2, // Then the numbers + (ValueInner::F64(a), ValueInner::F64(b)) => (a.is_nan() && b.is_nan()) || a == b, // First if there's a float we need to convert to float (ValueInner::F64(v), _) => Some(*v) == other.as_f64(), (_, ValueInner::F64(v)) => Some(*v) == self.as_f64(), @@ -250,6 +251,7 @@ impl PartialOrd for Value { (ValueInner::Bytes(v), ValueInner::Bytes(v2)) => v.partial_cmp(v2), (ValueInner::String(v), ValueInner::String(v2)) => v.as_str().partial_cmp(v2.as_str()), // Then the numbers + (ValueInner::F64(a), ValueInner::F64(b)) => Some(a.total_cmp(b)), // First if there's a float we need to convert to float (ValueInner::F64(v), _) => v.partial_cmp(&other.as_f64()?), (_, ValueInner::F64(v)) => v.partial_cmp(&self.as_f64()?), From 968289a4ca092171b04567345a61dde2ee63b2b6 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Thu, 23 Apr 2026 14:30:14 +0200 Subject: [PATCH 04/10] fix int filter --- tera/src/filters.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tera/src/filters.rs b/tera/src/filters.rs index 0dfd507..2e6f7d5 100644 --- a/tera/src/filters.rs +++ b/tera/src/filters.rs @@ -331,10 +331,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) From c0b18ec5da879e2eed88a5c569fe98b4b32efe83 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 24 Apr 2026 13:17:51 +0200 Subject: [PATCH 05/10] Fix short-circuiting bytecode optimization --- MIGRATION.md | 1 + tera/src/parsing/instructions.rs | 27 ++++++++++++++++++- .../success/short_circuit_path.txt | 1 + .../success/short_circuit_path.txt | 4 +++ ...r__compiler_ok@short_circuit_path.txt.snap | 22 +++++++++++++++ ...__rendering_ok@short_circuit_path.txt.snap | 9 +++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 tera/src/snapshot_tests/compiler_inputs/success/short_circuit_path.txt create mode 100644 tera/src/snapshot_tests/rendering_inputs/success/short_circuit_path.txt create mode 100644 tera/src/snapshot_tests/snapshots/tera__snapshot_tests__compiler__compiler_ok@short_circuit_path.txt.snap create mode 100644 tera/src/snapshot_tests/snapshots/tera__snapshot_tests__rendering__rendering_ok@short_circuit_path.txt.snap diff --git a/MIGRATION.md b/MIGRATION.md index 1aa08ee..0c1394e 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/src/parsing/instructions.rs b/tera/src/parsing/instructions.rs index 42d83d1..9a5751b 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/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 0000000..8d3c2bb --- /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_inputs/success/short_circuit_path.txt b/tera/src/snapshot_tests/rendering_inputs/success/short_circuit_path.txt new file mode 100644 index 0000000..11f21f9 --- /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 0000000..bd77a63 --- /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_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 0000000..0fc679d --- /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 From 43aae78e421fe0b4f20cd1bdbae49de3fe5f395d Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 24 Apr 2026 15:09:52 +0200 Subject: [PATCH 06/10] Rust API cleanup --- tera/src/args.rs | 34 +++++++++++++++++++++++- tera/src/errors.rs | 7 ++++- tera/src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++++++++- tera/src/template.rs | 3 ++- tera/src/tera.rs | 31 +++++++++++++++++----- tera/src/value/mod.rs | 2 +- 6 files changed, 126 insertions(+), 11 deletions(-) diff --git a/tera/src/args.rs b/tera/src/args.rs index 3b0dd48..67e15e2 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; diff --git a/tera/src/errors.rs b/tera/src/errors.rs index 75a62e8..e2fa52c 100644 --- a/tera/src/errors.rs +++ b/tera/src/errors.rs @@ -67,6 +67,7 @@ impl ReportError { } #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ErrorKind { /// Generic error Msg(String), @@ -193,7 +194,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 +210,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/lib.rs b/tera/src/lib.rs index ffec951..ffc933b 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/template.rs b/tera/src/template.rs index e40c9e4..00db854 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 1ea5573..783075a 100644 --- a/tera/src/tera.rs +++ b/tera/src/tera.rs @@ -32,15 +32,12 @@ 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, @@ -868,11 +865,33 @@ impl Tera { /// Get a template by name, resolving fallback prefixes if needed. #[inline] + #[doc(hidden)] pub fn get_template(&self, template_name: &str) -> Option<&Template> { self.resolve_template_name(template_name) .map(|resolved| &self.templates[resolved]) } + /// Returns an iterator over the names of all registered templates in an + /// unspecified order. + /// + /// # Example + /// + /// ```rust + /// use tera::Tera; + /// + /// let mut tera = Tera::default(); + /// tera.add_raw_template("foo", "{{ hello }}"); + /// tera.add_raw_template("another-one.html", "contents go here"); + /// + /// let names: Vec<_> = tera.get_template_names().collect(); + /// assert_eq!(names.len(), 2); + /// assert!(names.contains(&"foo")); + /// assert!(names.contains(&"another-one.html")); + /// ``` + pub fn get_template_names(&self) -> impl Iterator { + self.templates.keys().map(|s| s.as_str()) + } + /// Get a template by name, returning an error if not found. Used internally. #[inline] pub(crate) fn must_get_template(&self, template_name: &str) -> TeraResult<&Template> { diff --git a/tera/src/value/mod.rs b/tera/src/value/mod.rs index f832645..dea22c2 100644 --- a/tera/src/value/mod.rs +++ b/tera/src/value/mod.rs @@ -56,7 +56,7 @@ pub(crate) fn format_map(map: &Map, f: &mut impl std::io::Write) -> std::io::Res } #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum StringKind { +pub(crate) enum StringKind { Normal, Safe, } From b433d9708669c3bf22c9e8e819ca98b95b2d20ce Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 24 Apr 2026 21:36:15 +0200 Subject: [PATCH 07/10] Use Cow for delimiters --- tera/src/delimiters.rs | 36 ++++++++++++++------------- tera/src/parsing/lexer.rs | 18 +++++++------- tera/src/snapshot_tests/rendering.rs | 12 ++++----- tera/src/tera.rs | 37 ++++++++++++++++------------ 4 files changed, 55 insertions(+), 48 deletions(-) diff --git a/tera/src/delimiters.rs b/tera/src/delimiters.rs index 2d50a97..b662ea1 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/parsing/lexer.rs b/tera/src/parsing/lexer.rs index 6422167..1cc04a3 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/snapshot_tests/rendering.rs b/tera/src/snapshot_tests/rendering.rs index e4394f2..18aaf3b 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/tera.rs b/tera/src/tera.rs index 783075a..a36c101 100644 --- a/tera/src/tera.rs +++ b/tera/src/tera.rs @@ -138,12 +138,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(); /// ``` @@ -683,8 +683,12 @@ impl Tera { let mut inserted: Vec<(String, Option