From 9e58effe2aefbea3d8932d6ecfac1576239f55a9 Mon Sep 17 00:00:00 2001 From: Martyn Date: Fri, 28 Aug 2026 10:04:44 +0100 Subject: [PATCH] Updates rkt_dyn_templates to tera2 Move Tera v1 support to `tera1` feature flag and fix autoescape handling bug. AI-Tool: Claude Code --- benchmarks/http/static/1k.bin | Bin 0 -> 1024 bytes benchmarks/http/static/1m.bin | Bin 0 -> 1048576 bytes benchmarks/http/static/64k.bin | Bin 0 -> 65536 bytes contrib/dyn_templates/Cargo.toml | 14 ++- contrib/dyn_templates/src/engine/mod.rs | 78 +++++++++++--- contrib/dyn_templates/src/engine/tera.rs | 97 +++++++++++++++--- contrib/dyn_templates/src/lib.rs | 15 ++- contrib/dyn_templates/tests/templates.rs | 96 ++++++++++++++++- .../tests/templates/tera/htm_test.htm.tera | 5 + .../tests/templates/tera/xml_test.xml.tera | 5 + examples/chat/Cargo.toml | 2 +- examples/pastebin/Cargo.toml | 2 +- examples/todo/Cargo.toml | 2 +- scripts/test.sh | 3 + 14 files changed, 278 insertions(+), 41 deletions(-) create mode 100644 benchmarks/http/static/1k.bin create mode 100644 benchmarks/http/static/1m.bin create mode 100644 benchmarks/http/static/64k.bin create mode 100644 contrib/dyn_templates/tests/templates/tera/htm_test.htm.tera create mode 100644 contrib/dyn_templates/tests/templates/tera/xml_test.xml.tera diff --git a/benchmarks/http/static/1k.bin b/benchmarks/http/static/1k.bin new file mode 100644 index 0000000000000000000000000000000000000000..06d7405020018ddf3cacee90fd4af10487da3d20 GIT binary patch literal 1024 ScmZQz7zLvtFd70QH3R?z00031 literal 0 HcmV?d00001 diff --git a/benchmarks/http/static/1m.bin b/benchmarks/http/static/1m.bin new file mode 100644 index 0000000000000000000000000000000000000000..9e0f96a2a253b173cb45b41868209a5d043e1437 GIT binary patch literal 1048576 zcmeIuF#!Mo0K%a4Pi+Wah(KY$fB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ z0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VK zfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5 zV8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ T0|pEjFkrxd0RsjM82APT0Pp|- literal 0 HcmV?d00001 diff --git a/benchmarks/http/static/64k.bin b/benchmarks/http/static/64k.bin new file mode 100644 index 0000000000000000000000000000000000000000..c97c12f9b0a24bfc19c74a2b265a97c924137775 GIT binary patch literal 65536 zcmeIufdBvi0Dz$VsTV1P3IhfV7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA zz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEj zFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r z3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@ u0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFi-;k4*&rG literal 0 HcmV?d00001 diff --git a/contrib/dyn_templates/Cargo.toml b/contrib/dyn_templates/Cargo.toml index 228c9af54..452899873 100644 --- a/contrib/dyn_templates/Cargo.toml +++ b/contrib/dyn_templates/Cargo.toml @@ -16,6 +16,10 @@ rust-version.workspace = true workspace = true [features] +# `tera1` selects the deprecated Tera 1.x; `tera` selects Tera 2.x. Enabling +# both is not an error -- Cargo features must stay additive -- but `tera` +# takes precedence and Tera 1.x goes unused. +tera1 = ["dep:tera1"] tera = ["dep:tera"] handlebars = ["dep:handlebars"] minijinja = ["dep:minijinja"] @@ -25,7 +29,11 @@ walkdir = "2.4" notify = "8" normpath = "1" -tera = { version = "=1.20.1", optional = true } +# Pinned exactly: Tera 1.x autoescaping of discovered templates depends on +# `Renderer::new()` preferring a template's source path over its registered +# name. See `AUTOESCAPE_SUFFIXES` in `src/engine/tera.rs`. +tera1 = { package = "tera", version = "=1.20.1", optional = true } +tera = { version = "2.2.0", optional = true } handlebars = { version = "6.0", optional = true } @@ -42,4 +50,6 @@ default-features = false pretty_assertions = "1.4" [package.metadata.docs.rs] -all-features = true +# Not `all-features`: `tera1` and `tera` are alternatives, and enabling both +# would document the Tera 2.x reexport while pulling in an unused Tera 1.x. +features = ["tera", "handlebars", "minijinja"] diff --git a/contrib/dyn_templates/src/engine/mod.rs b/contrib/dyn_templates/src/engine/mod.rs index 8472f1d90..f057ffb7d 100644 --- a/contrib/dyn_templates/src/engine/mod.rs +++ b/contrib/dyn_templates/src/engine/mod.rs @@ -5,10 +5,12 @@ use rkt::serde::Serialize; use crate::template::TemplateInfo; -#[cfg(feature = "tera")] +#[cfg(any(feature = "tera1", feature = "tera"))] mod tera; -#[cfg(feature = "tera")] -use ::tera::Tera; +// Not `::tera::Tera`: the `tera1`/`tera` choice is made in `self::tera`, and +// this reexport follows it. +#[cfg(any(feature = "tera1", feature = "tera"))] +use self::tera::Tera; #[cfg(feature = "handlebars")] mod handlebars; @@ -21,7 +23,12 @@ mod minijinja; use ::minijinja::Environment; #[cfg_attr( - not(any(feature = "tera", feature = "handlebars", feature = "minijinja")), + not(any( + feature = "tera1", + feature = "tera", + feature = "handlebars", + feature = "minijinja" + )), allow(dead_code) )] pub(crate) trait Engine: Send + Sync + Sized + 'static { @@ -37,11 +44,13 @@ pub(crate) trait Engine: Send + Sync + Sized + 'static { /// types from the respective templating engine library. These types should be /// imported from the reexported crate at the root of `rkt_dyn_templates` to /// avoid version mismatches. For instance, when registering a Tera filter, the -/// [`tera::Value`] and [`tera::Result`] types are required. Import them from -/// `rkt_dyn_templates::tera`. The example below illustrates this: +/// [`tera::Value`] and result types are required. Import them from +/// `rkt_dyn_templates::tera`. The example below illustrates this. +/// +/// Tera 1.x, via the `tera1` feature: /// /// ```rust -/// # #[cfg(feature = "tera")] { +/// # #[cfg(all(feature = "tera1", not(feature = "tera")))] { /// use std::collections::HashMap; /// /// use rkt_dyn_templates::{Template, Engines}; @@ -65,16 +74,41 @@ pub(crate) trait Engine: Send + Sync + Sized + 'static { /// # } /// ``` /// +/// Tera 2.x, via the `tera` feature, where a filter is any function of the +/// right shape: +/// +/// ```rust +/// # #[cfg(feature = "tera")] { +/// use rkt_dyn_templates::{Template, Engines}; +/// use rkt_dyn_templates::tera::{Kwargs, State}; +/// +/// fn my_filter(value: i64, _: Kwargs, _: &State) -> i64 { +/// # /* +/// ... +/// # */ unimplemented!(); +/// } +/// +/// fn main() { +/// rkt::build() +/// // ... +/// .attach(Template::custom(|engines: &mut Engines| { +/// engines.tera.register_filter("my_filter", my_filter); +/// })) +/// // ... +/// # ; +/// } +/// # } +/// ``` +/// /// [`tera::Value`]: crate::tera::Value -/// [`tera::Result`]: crate::tera::Result /// pub struct Engines { /// A `Tera` templating engine. /// - /// This field is only available when the `tera` feature is enabled. When + /// This field is available when the `tera` or `tera1` feature is enabled. When /// calling methods on the `Tera` instance, ensure you use types imported /// from `rkt_dyn_templates::tera` to avoid version mismatches. - #[cfg(feature = "tera")] + #[cfg(any(feature = "tera1", feature = "tera"))] pub tera: Tera, /// The Handlebars templating engine. @@ -97,12 +131,17 @@ pub struct Engines { } #[cfg_attr( - not(any(feature = "tera", feature = "handlebars", feature = "minijinja")), + not(any( + feature = "tera1", + feature = "tera", + feature = "handlebars", + feature = "minijinja" + )), allow(dead_code) )] impl Engines { pub(crate) const ENABLED_EXTENSIONS: &'static [&'static str] = &[ - #[cfg(feature = "tera")] + #[cfg(any(feature = "tera1", feature = "tera"))] Tera::EXT, #[cfg(feature = "handlebars")] Handlebars::EXT, @@ -122,7 +161,7 @@ impl Engines { } Some(Engines { - #[cfg(feature = "tera")] + #[cfg(any(feature = "tera1", feature = "tera"))] tera: inner::(_templates)?, #[cfg(feature = "handlebars")] handlebars: inner::>(_templates)?, @@ -132,7 +171,12 @@ impl Engines { } #[cfg_attr( - not(any(feature = "tera", feature = "handlebars", feature = "minijinja")), + not(any( + feature = "tera1", + feature = "tera", + feature = "handlebars", + feature = "minijinja" + )), allow(unused_variables) )] pub(crate) fn render( @@ -141,7 +185,7 @@ impl Engines { info: &TemplateInfo, context: C, ) -> Option { - #[cfg(feature = "tera")] + #[cfg(any(feature = "tera1", feature = "tera"))] { if info.engine_ext == Tera::EXT { return Engine::render(&self.tera, name, context); @@ -167,7 +211,7 @@ impl Engines { /// Returns iterator over template (name, engine_extension). pub(crate) fn templates(&self) -> impl Iterator { - #[cfg(feature = "tera")] + #[cfg(any(feature = "tera1", feature = "tera"))] let tera = self.tera.get_template_names().map(|name| (name, Tera::EXT)); #[cfg(feature = "handlebars")] @@ -183,7 +227,7 @@ impl Engines { .templates() .map(|(name, _)| (name, Environment::EXT)); - #[cfg(not(feature = "tera"))] + #[cfg(not(any(feature = "tera1", feature = "tera")))] let tera = std::iter::empty(); #[cfg(not(feature = "handlebars"))] let handlebars = std::iter::empty(); diff --git a/contrib/dyn_templates/src/engine/tera.rs b/contrib/dyn_templates/src/engine/tera.rs index 3c3db25af..7e3bb55fc 100644 --- a/contrib/dyn_templates/src/engine/tera.rs +++ b/contrib/dyn_templates/src/engine/tera.rs @@ -2,29 +2,89 @@ use std::error::Error; use std::path::Path; use rkt::serde::Serialize; -use tera::{Context, Tera}; + +// Enabling both `tera1` and `tera` is not an error, because Cargo features +// have to stay additive. If both are enabled `tera`` has priority. +#[cfg(feature = "tera")] +pub(crate) use ::tera::{Context, Tera}; + +#[cfg(all(feature = "tera1", not(feature = "tera")))] +pub(crate) use ::tera1::{Context, Tera}; use crate::engine::Engine; +/// Builds the suffixes Tera uses to decide whether to escape a template. +/// +/// Each file type has two forms: +/// +/// * `.html.tera` for templates loaded from files. +/// * `.html` for templates added directly in code. +/// +/// Generating both from one list keeps their escaping rules in sync. +macro_rules! autoescape_suffixes { + ($engine_ext:literal; $($data_type:literal),+ $(,)?) => { + [$(concat!(".", $data_type, ".", $engine_ext),)+ $(concat!(".", $data_type),)+] + }; +} + +/// File extensions that Tera HTML-escapes. +/// +/// Rocket removes file extensions from registered template names. Tera 1 can +/// also check the original file path, while Tera 2 needs the matching template +/// names added separately. +const AUTOESCAPE_SUFFIXES: &[&str] = &autoescape_suffixes!("tera"; "html", "htm", "xml"); + +const _: () = { + // The engine extension above is a literal because `concat!` needs one. + assert!(matches!(::EXT.as_bytes(), b"tera")); +}; + +/// Tera 1.x: the static list is enough. A discovered template is matched by +/// its source path, a raw template by its registered name. +#[cfg(all(feature = "tera1", not(feature = "tera")))] +fn autoescape_suffixes(_files: &[(&Path, Option<&str>)]) -> Vec<&'static str> { + AUTOESCAPE_SUFFIXES.to_vec() +} + +/// Adds the names of file templates that Tera 2 should escape. +/// +/// Tera 2 checks template names, but Rocket's registered names have no file +/// extension. Use each template's file path to decide whether to add its name. +#[cfg(feature = "tera")] +fn autoescape_suffixes(files: &[(&Path, Option<&str>)]) -> Vec> { + use std::borrow::Cow; + + let mut suffixes: Vec> = AUTOESCAPE_SUFFIXES + .iter() + .copied() + .map(Cow::Borrowed) + .collect(); + + suffixes.extend(files.iter().filter_map(|(path, name)| { + let path = path.to_str()?; + let name = (*name)?; + AUTOESCAPE_SUFFIXES + .iter() + .any(|s| path.ends_with(s)) + .then(|| Cow::Owned(name.to_owned())) + })); + + suffixes +} + impl Engine for Tera { const EXT: &'static str = "tera"; fn init<'a>(templates: impl Iterator) -> Option { + // Collect into a tuple of (path, name) for Tera. If we register one at + // a time, it will complain about unregistered base templates. + let files = templates + .map(|(name, path)| (path, Some(name))) + .collect::>(); + // Create the Tera instance. let mut tera = Tera::default(); - let ext = [ - ".html.tera", - ".htm.tera", - ".xml.tera", - ".html", - ".htm", - ".xml", - ]; - tera.autoescape_on(ext.to_vec()); - - // Collect into a tuple of (name, path) for Tera. If we register one at - // a time, it will complain about unregistered base templates. - let files = templates.map(|(name, path)| (path, Some(name))); + tera.autoescape_on(autoescape_suffixes(&files)); // Finally try to tell Tera about all of the templates. if let Err(e) = tera.add_template_files(files) { @@ -43,12 +103,17 @@ impl Engine for Tera { } fn render(&self, template: &str, context: C) -> Option { - if self.get_template(template).is_err() { + #[cfg(all(feature = "tera1", not(feature = "tera")))] + let exists = self.get_template(template).is_ok(); + #[cfg(feature = "tera")] + let exists = self.contains_template(template); + + if !exists { error!(template, "requested template does not exist"); return None; }; - let tera_ctx = Context::from_serialize(context) + let tera_ctx = Context::from_serialize(&context) .map_err(|e| error!("Tera context error: {}.", e)) .ok()?; diff --git a/contrib/dyn_templates/src/lib.rs b/contrib/dyn_templates/src/lib.rs index 551a1c729..e114b74e6 100644 --- a/contrib/dyn_templates/src/lib.rs +++ b/contrib/dyn_templates/src/lib.rs @@ -16,6 +16,12 @@ //! features = ["handlebars", "tera", "minijinja"] //! ``` //! +//! Tera has two features: `tera` for Tera 2.x, and `tera1` for the +//! deprecated Tera 1.x. Enable one. They are not mutually exclusive -- +//! Cargo features must stay additive -- but `tera` wins if both end up +//! enabled, and `rkt_dyn_templates::tera` then refers to Tera 2.x. +//! Note that Tera 2 no longer escapes `/` in autoescaped templates. +//! //! 2. Write your templates inside of the [configurable] //! `${ROCKET_ROOT}/templates`. The filename _must_ end with an extension //! corresponding to an enabled engine. The second-to-last extension should @@ -28,7 +34,7 @@ //! | [MiniJinja] | `.j2` | `${ROCKET_ROOT}/templates/index.html.j2` | //! //! [configurable]: #configuration -//! [Tera]: https://docs.rs/crate/tera/1 +//! [Tera]: https://docs.rs/crate/tera/2 //! [Handlebars]: https://docs.rs/crate/handlebars/6 //! [MiniJinja]: https://docs.rs/minijinja/2 //! @@ -183,9 +189,16 @@ #[macro_use] extern crate rkt; +#[doc(inline)] +#[cfg(all(feature = "tera1", not(feature = "tera")))] +/// The tera templating engine library, reexported. +pub use tera1 as tera; + #[doc(inline)] #[cfg(feature = "tera")] /// The tera templating engine library, reexported. +/// +/// This is Tera 2.x, selected by the `tera` feature. pub use tera; #[doc(inline)] diff --git a/contrib/dyn_templates/tests/templates.rs b/contrib/dyn_templates/tests/templates.rs index 9bd08f00e..04e939143 100644 --- a/contrib/dyn_templates/tests/templates.rs +++ b/contrib/dyn_templates/tests/templates.rs @@ -1,7 +1,6 @@ #[macro_use] extern crate rkt; - use std::path::{Path, PathBuf}; use rkt::figment::value::Value; @@ -208,7 +207,7 @@ fn test_context_macro() { } } -#[cfg(feature = "tera")] +#[cfg(any(feature = "tera1", feature = "tera"))] mod tera_tests { use super::*; use pretty_assertions::assert_eq; @@ -217,8 +216,20 @@ mod tera_tests { use std::collections::HashMap; const UNESCAPED_EXPECTED: &str = "\nh_start\ntitle: _test_\nh_end\n\n\n