Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added benchmarks/http/static/1k.bin
Binary file not shown.
Binary file added benchmarks/http/static/1m.bin
Binary file not shown.
Binary file added benchmarks/http/static/64k.bin
Binary file not shown.
14 changes: 12 additions & 2 deletions contrib/dyn_templates/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +19 to +21
tera1 = ["dep:tera1"]
tera = ["dep:tera"]
handlebars = ["dep:handlebars"]
minijinja = ["dep:minijinja"]
Expand All @@ -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 }

Expand All @@ -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"]
78 changes: 61 additions & 17 deletions contrib/dyn_templates/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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.
Comment on lines 46 to +48
///
/// 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};
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -122,7 +161,7 @@ impl Engines {
}

Some(Engines {
#[cfg(feature = "tera")]
#[cfg(any(feature = "tera1", feature = "tera"))]
tera: inner::<Tera>(_templates)?,
#[cfg(feature = "handlebars")]
handlebars: inner::<Handlebars<'static>>(_templates)?,
Expand All @@ -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<C: Serialize>(
Expand All @@ -141,7 +185,7 @@ impl Engines {
info: &TemplateInfo,
context: C,
) -> Option<String> {
#[cfg(feature = "tera")]
#[cfg(any(feature = "tera1", feature = "tera"))]
{
if info.engine_ext == Tera::EXT {
return Engine::render(&self.tera, name, context);
Expand All @@ -167,7 +211,7 @@ impl Engines {

/// Returns iterator over template (name, engine_extension).
pub(crate) fn templates(&self) -> impl Iterator<Item = (&str, &'static str)> {
#[cfg(feature = "tera")]
#[cfg(any(feature = "tera1", feature = "tera"))]
let tera = self.tera.get_template_names().map(|name| (name, Tera::EXT));

#[cfg(feature = "handlebars")]
Expand All @@ -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();
Expand Down
97 changes: 81 additions & 16 deletions contrib/dyn_templates/src/engine/tera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(<Tera as Engine>::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<std::borrow::Cow<'static, str>> {
use std::borrow::Cow;

let mut suffixes: Vec<Cow<'static, str>> = 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<Item = (&'a str, &'a Path)>) -> Option<Self> {
// 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::<Vec<_>>();

// 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) {
Expand All @@ -43,12 +103,17 @@ impl Engine for Tera {
}

fn render<C: Serialize>(&self, template: &str, context: C) -> Option<String> {
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()?;

Expand Down
15 changes: 14 additions & 1 deletion contrib/dyn_templates/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
//!
Expand Down Expand Up @@ -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)]
Expand Down
Loading
Loading