From 12a3ec5d1b873e614a6f18fb7e8623edd01cc004 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 2 Sep 2022 12:49:47 +0200 Subject: [PATCH 1/2] Added render! macro --- minijinja/src/context.rs | 98 ------------------- minijinja/src/environment.rs | 34 +++++++ minijinja/src/lib.rs | 4 +- minijinja/src/macros.rs | 180 +++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 100 deletions(-) delete mode 100644 minijinja/src/context.rs create mode 100644 minijinja/src/macros.rs diff --git a/minijinja/src/context.rs b/minijinja/src/context.rs deleted file mode 100644 index 002e0367..00000000 --- a/minijinja/src/context.rs +++ /dev/null @@ -1,98 +0,0 @@ -#[cfg(test)] -use similar_asserts::assert_eq; - -/// Hidden utility module for the [`context!`](crate::context!) macro. -#[doc(hidden)] -pub mod __context { - use crate::key::Key; - use crate::value::{RcType, Value, ValueMap, ValueRepr}; - - #[inline(always)] - pub fn make() -> ValueMap { - ValueMap::default() - } - - #[inline(always)] - pub fn add(ctx: &mut ValueMap, key: &'static str, value: Value) { - ctx.insert(Key::Str(key), value); - } - - #[inline(always)] - pub fn build(ctx: ValueMap) -> Value { - ValueRepr::Map(RcType::new(ctx)).into() - } -} - -/// Creates a template context with keys and values. -/// -/// ```rust -/// # use minijinja::context; -/// let ctx = context! { -/// name => "Peter", -/// location => "World", -/// }; -/// ``` -/// -/// Alternatively if the variable name matches the key name it can -/// be omitted: -/// -/// ```rust -/// # use minijinja::context; -/// let name = "Peter"; -/// let ctx = context! { name }; -/// ``` -/// -/// The return value is a [`Value`](crate::value::Value). -/// -/// Note that [`context!`](crate::context!) can also be used recursively if you need to -/// create nested objects: -/// -/// ```rust -/// # use minijinja::context; -/// let ctx = context! { -/// nav => vec![ -/// context!(path => "/", title => "Index"), -/// context!(path => "/downloads", title => "Downloads"), -/// context!(path => "/faq", title => "FAQ"), -/// ] -/// }; -/// ``` -#[macro_export] -macro_rules! context { - () => { - $crate::__context::build($crate::__context::make()) - }; - ( - $($key:ident $(=> $value:expr)?),* $(,)? - ) => {{ - let mut ctx = $crate::__context::make(); - $( - $crate::__context_pair!(ctx, $key $(, $value)?); - )* - $crate::__context::build(ctx) - }} -} - -#[macro_export] -#[doc(hidden)] -macro_rules! __context_pair { - ($ctx:ident, $key:ident) => {{ - $crate::__context_pair!($ctx, $key, $key); - }}; - ($ctx:ident, $key:ident, $value:expr) => { - $crate::__context::add( - &mut $ctx, - stringify!($key), - $crate::value::Value::from_serializable(&$value), - ); - }; -} - -#[test] -fn test_macro() { - use crate::value::Value; - let var1 = 23; - let ctx = context!(var1, var2 => 42); - assert_eq!(ctx.get_attr("var1").unwrap(), Value::from(23)); - assert_eq!(ctx.get_attr("var2").unwrap(), Value::from(42)); -} diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs index ec9ed9fb..12a5f6d8 100644 --- a/minijinja/src/environment.rs +++ b/minijinja/src/environment.rs @@ -461,6 +461,40 @@ impl<'source> Environment<'source> { }) } + /// Parses and renders a template from a string in one go. + /// + /// In some cases you really only need a template to be rendered once from + /// a string and returned. The internal name of the template is ``. + /// + /// ``` + /// # use minijinja::{Environment, context}; + /// let env = Environment::new(); + /// let rv = env.render_str("Hello {{ name }}", context! { name => "World" }); + /// println!("{}", rv.unwrap()); + /// ``` + pub fn render_str(&self, source: &str, ctx: S) -> Result { + // reduce total amount of code faling under mono morphization into + // this function, and share the rest in _eval. + self._render_str(source, Value::from_serializable(&ctx)) + } + + fn _render_str(&self, source: &str, root: Value) -> Result { + let name = ""; + let compiled = CompiledTemplate::from_name_and_source(name, source)?; + let mut output = String::new(); + let vm = Vm::new(self); + let blocks = &compiled.blocks; + let initial_auto_escape = self.get_initial_auto_escape(name); + vm.eval( + &compiled.instructions, + root, + blocks, + initial_auto_escape, + &mut output, + )?; + Ok(output) + } + /// Compiles an expression. /// /// This lets one compile an expression in the template language and diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs index 323e4991..d05e29e6 100644 --- a/minijinja/src/lib.rs +++ b/minijinja/src/lib.rs @@ -126,11 +126,11 @@ mod key; mod ast; mod compiler; -mod context; mod environment; mod error; mod instructions; mod lexer; +mod macros; mod parser; mod tokens; mod utils; @@ -159,7 +159,7 @@ pub use self::error::DebugInfo; #[cfg(feature = "source")] pub use self::source::Source; -pub use self::context::*; +pub use self::macros::__context; pub use self::vm::State; /// This module gives access to the low level machinery. diff --git a/minijinja/src/macros.rs b/minijinja/src/macros.rs new file mode 100644 index 00000000..9c8f870e --- /dev/null +++ b/minijinja/src/macros.rs @@ -0,0 +1,180 @@ +#[cfg(test)] +use similar_asserts::assert_eq; + +/// Hidden utility module for the [`context!`](crate::context!) macro. +#[doc(hidden)] +pub mod __context { + use crate::key::Key; + use crate::value::{RcType, Value, ValueMap, ValueRepr}; + use crate::Environment; + + #[inline(always)] + pub fn make() -> ValueMap { + ValueMap::default() + } + + #[inline(always)] + pub fn add(ctx: &mut ValueMap, key: &'static str, value: Value) { + ctx.insert(Key::Str(key), value); + } + + #[inline(always)] + pub fn build(ctx: ValueMap) -> Value { + ValueRepr::Map(RcType::new(ctx)).into() + } + + pub fn thread_local_env() -> Environment<'static> { + thread_local! { + static ENV: Environment<'static> = Environment::new() + } + ENV.with(|x| x.clone()) + } +} + +/// Creates a template context with keys and values. +/// +/// ```rust +/// # use minijinja::context; +/// let ctx = context! { +/// name => "Peter", +/// location => "World", +/// }; +/// ``` +/// +/// Alternatively if the variable name matches the key name it can +/// be omitted: +/// +/// ```rust +/// # use minijinja::context; +/// let name = "Peter"; +/// let ctx = context! { name }; +/// ``` +/// +/// The return value is a [`Value`](crate::value::Value). +/// +/// Note that [`context!`](crate::context!) can also be used recursively if you need to +/// create nested objects: +/// +/// ```rust +/// # use minijinja::context; +/// let ctx = context! { +/// nav => vec![ +/// context!(path => "/", title => "Index"), +/// context!(path => "/downloads", title => "Downloads"), +/// context!(path => "/faq", title => "FAQ"), +/// ] +/// }; +/// ``` +#[macro_export] +macro_rules! context { + () => { + $crate::__context::build($crate::__context::make()) + }; + ( + $($key:ident $(=> $value:expr)?),* $(,)? + ) => {{ + let mut ctx = $crate::__context::make(); + $( + $crate::__context_pair!(ctx, $key $(, $value)?); + )* + $crate::__context::build(ctx) + }} +} + +#[macro_export] +#[doc(hidden)] +macro_rules! __context_pair { + ($ctx:ident, $key:ident) => {{ + $crate::__context_pair!($ctx, $key, $key); + }}; + ($ctx:ident, $key:ident, $value:expr) => { + $crate::__context::add( + &mut $ctx, + stringify!($key), + $crate::value::Value::from_serializable(&$value), + ); + }; +} + +/// A macro similar to [`format!`] but that uses MiniJinja for rendering. +/// +/// This can be used to quickly render a MiniJinja template into a string +/// without having to create an environment first which can be useful in +/// some situations. Note however that the template is re-parsed every +/// time the [`render!`](crate::render) macro is called which is potentially +/// slow. +/// +/// There are two forms for this macro. The default form takes template +/// source and context variables, the extended form also lets you provide +/// a custom environment that should be used rather than a default one. +/// The context variables are passed the same way as with the +/// [`context!](crate::context) macro. +/// +/// # Example +/// +/// Passing context explicitly: +/// +/// ``` +/// # use minijinja::render; +/// println!("{}", render!("Hello {{ name }}!", name => "World")); +/// ``` +/// +/// Passing variables with the default name: +/// +/// ``` +/// # use minijinja::render; +/// let name = "World"; +/// println!("{}", render!("Hello {{ name }}!", name)); +/// ``` +/// +/// Passing an explicit environment: +/// +/// ``` +/// # use minijinja::{Environment, render}; +/// let env = Environment::new(); +/// println!("{}", render!(in env, "Hello {{ name }}!", name => "World")); +/// ``` +/// +/// # Panics +/// +/// This macro panics if the format string is an invalid template or the +/// template evaluation failed. +#[macro_export] +macro_rules! render { + ( + in $env:expr, + $tmpl:expr + $(, $key:ident $(=> $value:expr)?)* $(,)? + ) => { + ($env).render_str($tmpl, context! { $($key $(=> $value)? ,)* }) + .expect("failed to render expression") + }; + ( + $tmpl:expr + $(, $key:ident $(=> $value:expr)?)* $(,)? + ) => { + $crate::render!(in $crate::__context::thread_local_env(), $tmpl, $($key $(=> $value)? ,)*) + } +} + +#[test] +fn test_context() { + use crate::value::Value; + let var1 = 23; + let ctx = context!(var1, var2 => 42); + assert_eq!(ctx.get_attr("var1").unwrap(), Value::from(23)); + assert_eq!(ctx.get_attr("var2").unwrap(), Value::from(42)); +} + +#[test] +fn test_render() { + let env = crate::Environment::new(); + let rv = render!(in env, "Hello {{ name }}!", name => "World"); + assert_eq!(rv, "Hello World!"); + + let rv = render!("Hello {{ name }}!", name => "World"); + assert_eq!(rv, "Hello World!"); + + let rv = render!("Hello World!"); + assert_eq!(rv, "Hello World!"); +} From 5bbfb7d2cc4108090539291e58cdbe95965c6117 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 2 Sep 2022 13:17:10 +0200 Subject: [PATCH 2/2] Added example for render macro --- examples/README.md | 3 ++- examples/render-macro/Cargo.toml | 9 +++++++++ examples/render-macro/README.md | 9 +++++++++ examples/render-macro/src/main.rs | 5 +++++ minijinja/src/environment.rs | 8 ++++++++ minijinja/src/macros.rs | 2 +- 6 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 examples/render-macro/Cargo.toml create mode 100644 examples/render-macro/README.md create mode 100644 examples/render-macro/src/main.rs diff --git a/examples/README.md b/examples/README.md index 9ac1b899..6df94ca0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,5 +16,6 @@ the `cargo run` command. Alternatively you can do `cargo run -p example-name`. * [loader](loader): shows how to load templates dynamically at runtime. * [minimal](minimal): a Hello World example without default features. * [recursive-for](recursive-for): demonstrates the recursive for loop. +* [render-macro](render-macro): minimal Hello World example using the `render!` macro. * [render-template](render-template): CLI app that renders templates from string. -* [source](source): Demonstrates how to load templates from disk with the `source` feature. \ No newline at end of file +* [source](source): Demonstrates how to load templates from disk with the `source` feature. diff --git a/examples/render-macro/Cargo.toml b/examples/render-macro/Cargo.toml new file mode 100644 index 00000000..be883b05 --- /dev/null +++ b/examples/render-macro/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "render-macro" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +minijinja = { path = "../../minijinja" } diff --git a/examples/render-macro/README.md b/examples/render-macro/README.md new file mode 100644 index 00000000..52dfa758 --- /dev/null +++ b/examples/render-macro/README.md @@ -0,0 +1,9 @@ +# render-macro + +A simple example of how to render a single template from a string and render it +with the `render!` macro. + +```console +$ cargo run +Hello John! +``` diff --git a/examples/render-macro/src/main.rs b/examples/render-macro/src/main.rs new file mode 100644 index 00000000..ee9fffb3 --- /dev/null +++ b/examples/render-macro/src/main.rs @@ -0,0 +1,5 @@ +use minijinja::render; + +fn main() { + println!("{}", render!("Hello {{ name }}!", name => "John")); +} diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs index 12a5f6d8..0870b31c 100644 --- a/minijinja/src/environment.rs +++ b/minijinja/src/environment.rs @@ -445,6 +445,14 @@ impl<'source> Environment<'source> { /// This requires that the template has been loaded with /// [`add_template`](Environment::add_template) beforehand. If the template was /// not loaded an error of kind `TemplateNotFound` is returned. + /// + /// ``` + /// # use minijinja::{Environment, context}; + /// let mut env = Environment::new(); + /// env.add_template("hello.txt", "Hello {{ name }}!").unwrap(); + /// let tmpl = env.get_template("hello.txt").unwrap(); + /// println!("{}", tmpl.render(context!{ name => "World" }).unwrap()); + /// ``` pub fn get_template(&self, name: &str) -> Result, Error> { let compiled = match &self.templates { Source::Borrowed(ref map) => map diff --git a/minijinja/src/macros.rs b/minijinja/src/macros.rs index 9c8f870e..0e415b3b 100644 --- a/minijinja/src/macros.rs +++ b/minijinja/src/macros.rs @@ -146,7 +146,7 @@ macro_rules! render { $tmpl:expr $(, $key:ident $(=> $value:expr)?)* $(,)? ) => { - ($env).render_str($tmpl, context! { $($key $(=> $value)? ,)* }) + ($env).render_str($tmpl, $crate::context! { $($key $(=> $value)? ,)* }) .expect("failed to render expression") }; (