diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9f4bee17..627a7530 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -28,7 +28,7 @@ jobs: for PKG in \ examples/actix-web-app examples/axum-app examples/poem-app examples/rocket-app examples/salvo-app examples/warp-app fuzzing \ rinja rinja_derive rinja_derive_standalone rinja_parser \ - testing testing-alloc testing-no-std + testing testing-alloc testing-no-std testing-renamed do cd "$PKG" echo "Testing: $PKG" @@ -118,7 +118,7 @@ jobs: for PKG in \ examples/actix-web-app examples/axum-app examples/poem-app examples/rocket-app examples/salvo-app examples/warp-app fuzzing \ rinja rinja_derive rinja_derive_standalone rinja_parser \ - testing testing-alloc testing-no-std + testing testing-alloc testing-no-std testing-renamed do cd "$PKG" cargo sort --check --check-format --grouped @@ -161,7 +161,7 @@ jobs: package: [ examples/actix-web-app, examples/axum-app, examples/poem-app, examples/rocket-app, examples/salvo-app, examples/warp-app, fuzzing, rinja, rinja_derive, rinja_derive_standalone, rinja_parser, - testing, testing-alloc, testing-no-std, + testing, testing-alloc, testing-no-std, testing-renamed, ] runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index ddaa1b60..57b393a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "rinja_parser", "testing", "testing-alloc", - "testing-no-std" + "testing-no-std", + "testing-renamed" ] resolver = "2" diff --git a/book/src/creating_templates.md b/book/src/creating_templates.md index 510906e5..0e45f2e7 100644 --- a/book/src/creating_templates.md +++ b/book/src/creating_templates.md @@ -30,7 +30,7 @@ code generation process takes some options that can be specified through the `template()` attribute. The following sub-attributes are currently recognized: -* `path` (as `path = "foo.html"`): sets the path to the template file. The +* `path` (e.g. `path = "foo.html"`): sets the path to the template file. The path is interpreted as relative to the configured template directories (by default, this is a `templates` directory next to your `Cargo.toml`). The file name extension is used to infer an escape mode (see below). In @@ -43,7 +43,7 @@ recognized: struct HelloTemplate<'a> { ... } ``` -* `source` (as `source = "{{ foo }}"`): directly sets the template source. +* `source` (e.g. `source = "{{ foo }}"`): directly sets the template source. This can be useful for test cases or short templates. The generated path is undefined, which generally makes it impossible to refer to this template from other templates. If `source` is specified, `ext` must also @@ -56,10 +56,10 @@ recognized: } ``` -* `in_doc` (as `in_doc = true`): +* `in_doc` (e.g. `in_doc = true`): please see the section ["documentation as template code"](#documentation-as-template-code). -* `ext` (as `ext = "txt"`): lets you specify the content type as a file +* `ext` (e.g. `ext = "txt"`): lets you specify the content type as a file extension. This is used to infer an escape mode (see below), and some web framework integrations use it to determine the content type. Cannot be used together with `path`. @@ -71,7 +71,7 @@ recognized: } ``` -* `print` (as `print = "code"`): enable debugging by printing nothing +* `print` (e.g. `print = "code"`): enable debugging by printing nothing (`none`), the parsed syntax tree (`ast`), the generated code (`code`) or `all` for both. The requested data will be printed to stdout at compile time. @@ -81,7 +81,7 @@ recognized: struct HelloTemplate<'a> { ... } ``` -* `block` (as `block = "block_name"`): renders the block by itself. +* `block` (e.g. `block = "block_name"`): renders the block by itself. Expressions outside of the block are not required by the struct, and inheritance is also supported. This can be useful when you need to decompose your template for partial rendering, without needing to @@ -92,7 +92,7 @@ recognized: struct HelloTemplate<'a> { ... } ``` -* `escape` (as `escape = "none"`): override the template's extension used for +* `escape` (e.g. `escape = "none"`): override the template's extension used for the purpose of determining the escaper for this template. See the section on configuring custom escapers for more information. ```rust @@ -101,7 +101,7 @@ recognized: struct HelloTemplate<'a> { ... } ``` -* `syntax` (as `syntax = "foo"`): set the syntax name for a parser defined +* `syntax` (e.g. `syntax = "foo"`): set the syntax name for a parser defined in the configuration file. The default syntax , "default", is the one provided by Rinja. ```rust @@ -110,7 +110,7 @@ recognized: struct HelloTemplate<'a> { ... } ``` -* `config` (as `config = "config_file_path"`): set the path for the config file +* `config` (e.g. `config = "config_file_path"`): set the path for the config file to be used. The path is interpreted as relative to your crate root. ```rust #[derive(Template)] @@ -118,6 +118,33 @@ recognized: struct HelloTemplate<'a> { ... } ``` +* `rinja` (e.g. `rinja = rinja`): + If you are using rinja in a subproject, a library or a [macro][book-macro], it might be + necessary to specify the [path][book-tree] where to find the module `rinja`: + + [book-macro]: https://doc.rust-lang.org/book/ch19-06-macros.html + [book-tree]: https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html + + ```rust,ignore + #[doc(hidden)] + use rinja as __rinja; + + #[macro_export] + macro_rules! new_greeter { + ($name:ident) => { + #[derive(Debug, $crate::rinja::Template)] + #[template( + ext = "txt", + source = "Hello, world!", + rinja = $crate::__rinja + )] + struct $name; + } + } + + new_greeter!(HelloWorld); + assert_eq!(HelloWorld.to_string(), Ok("Hello, world.")); + ``` ## Templating `enum`s You can add derive `Template`s for `struct`s and `enum`s. @@ -200,8 +227,8 @@ enum AreaWithBlocks { ## Documentation as template code [#documentation-as-template-code]: #documentation-as-template-code -As an alternative to supplying the code template code in an external file (as `path` argument), -or as a string (as `source` argument), you can also enable the `"code-in-doc"` feature. +As an alternative to supplying the code template code in an external file (e.g. `path` argument), +or as a string (e.g. `source` argument), you can also enable the `"code-in-doc"` feature. With this feature, you can specify the template code directly in the documentation of the template item. diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 5b7d9195..4cd60df5 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -27,10 +27,6 @@ pub(crate) fn template_to_string( heritage: Option<&Heritage<'_, '_>>, tmpl_kind: TmplKind, ) -> Result { - if tmpl_kind == TmplKind::Struct { - buf.write("const _: () = { extern crate rinja as rinja;"); - } - let generator = Generator::new( input, contexts, @@ -49,7 +45,6 @@ pub(crate) fn template_to_string( if tmpl_kind == TmplKind::Struct { impl_everything(input.ast, buf); - buf.write("};"); } Ok(size_hint) } diff --git a/rinja_derive/src/input.rs b/rinja_derive/src/input.rs index 253840bc..5e20edb2 100644 --- a/rinja_derive/src/input.rs +++ b/rinja_derive/src/input.rs @@ -12,7 +12,7 @@ use proc_macro2::Span; use rustc_hash::FxBuildHasher; use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{Attribute, Expr, ExprLit, Ident, Lit, LitBool, LitStr, Meta, Token}; +use syn::{Attribute, Expr, ExprLit, ExprPath, Ident, Lit, LitBool, LitStr, Meta, Token}; use crate::config::{Config, SyntaxAndCache}; use crate::{CompileError, FileInfo, MsgValidEscapers, OnceMap}; @@ -329,9 +329,21 @@ impl AnyTemplateArgs { has_default_impl: needs_default_impl > 0, }) } + + pub(crate) fn take_crate_name(&mut self) -> Option { + match self { + AnyTemplateArgs::Struct(template_args) => template_args.crate_name.take(), + AnyTemplateArgs::Enum { enum_args, .. } => { + if let Some(PartialTemplateArgs { crate_name, .. }) = enum_args { + crate_name.take() + } else { + None + } + } + } + } } -#[derive(Debug)] pub(crate) struct TemplateArgs { pub(crate) source: (Source, Option), block: Option, @@ -341,6 +353,7 @@ pub(crate) struct TemplateArgs { ext_span: Option, syntax: Option, config: Option, + crate_name: Option, pub(crate) whitespace: Option, pub(crate) template_span: Option, pub(crate) config_span: Option, @@ -389,6 +402,7 @@ impl TemplateArgs { ext_span: args.ext.as_ref().map(|value| value.span()), syntax: args.syntax.map(|value| value.value()), config: args.config.as_ref().map(|value| value.value()), + crate_name: args.crate_name, whitespace: args.whitespace, template_span: Some(args.template.span()), config_span: args.config.as_ref().map(|value| value.span()), @@ -405,6 +419,7 @@ impl TemplateArgs { ext_span: None, syntax: None, config: None, + crate_name: None, whitespace: None, template_span: None, config_span: None, @@ -676,6 +691,7 @@ pub(crate) struct PartialTemplateArgs { pub(crate) syntax: Option, pub(crate) config: Option, pub(crate) whitespace: Option, + pub(crate) crate_name: Option, } #[derive(Clone)] @@ -735,6 +751,7 @@ const _: () = { syntax: None, config: None, whitespace: None, + crate_name: None, }; let mut has_data = false; @@ -776,6 +793,12 @@ const _: () = { None => unreachable!("not possible in syn::Meta::NameValue(…)"), }; + if ident == "rinja" { + ensure_only_once(ident, &mut this.crate_name)?; + this.crate_name = Some(get_exprpath(ident, pair.value)?); + continue; + } + let value = get_lit(ident, pair.value)?; if ident == "path" { @@ -918,6 +941,21 @@ const _: () = { } } + fn get_exprpath(name: &Ident, mut expr: Expr) -> Result { + loop { + match expr { + Expr::Path(path) => return Ok(path), + Expr::Group(group) => expr = *group.expr, + v => { + return Err(CompileError::no_file_info( + format!("template attribute `{name}` expects a path or identifier"), + Some(v.span()), + )); + } + } + } + } + fn ensure_source_only_once( name: &Ident, source: &Option, diff --git a/rinja_derive/src/integration.rs b/rinja_derive/src/integration.rs index 182872b9..fbb52db6 100644 --- a/rinja_derive/src/integration.rs +++ b/rinja_derive/src/integration.rs @@ -250,7 +250,6 @@ pub(crate) fn build_template_enum( unreachable!(); }; - buf.write("const _: () = { extern crate rinja as rinja;"); impl_everything(enum_ast, buf); let enum_id = &enum_ast.ident; @@ -368,8 +367,7 @@ pub(crate) fn build_template_enum( buf.write(format_args!( "\ const SIZE_HINT: rinja::helpers::core::primitive::usize = {biggest_size_hint}usize;\ - }}\ - }};", + }}", )); Ok(biggest_size_hint) } diff --git a/rinja_derive/src/lib.rs b/rinja_derive/src/lib.rs index 0a0925fd..27e6ac50 100644 --- a/rinja_derive/src/lib.rs +++ b/rinja_derive/src/lib.rs @@ -28,8 +28,8 @@ use parser::{Parsed, strip_common}; use proc_macro::TokenStream as TokenStream12; #[cfg(feature = "__standalone")] use proc_macro2::TokenStream as TokenStream12; -use proc_macro2::{Span, TokenStream}; -use quote::quote_spanned; +use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree}; +use quote::{quote, quote_spanned}; use rustc_hash::FxBuildHasher; /// The `Template` derive macro and its `template()` attribute. @@ -113,6 +113,37 @@ use rustc_hash::FxBuildHasher; /// /// Set the syntax name for a parser defined in the configuration file. /// The default syntax, `"default"`, is the one provided by Rinja. +/// +/// ### rinja +/// +/// E.g. `rinja = rinja` +/// +/// If you are using rinja in a subproject, a library or a [macro][book-macro], it might be +/// necessary to specify the [path][book-tree] where to find the module `rinja`: +/// +/// [book-macro]: https://doc.rust-lang.org/book/ch19-06-macros.html +/// [book-tree]: https://doc.rust-lang.org/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html +/// +/// ```rust,ignore +/// #[doc(hidden)] +/// use rinja as __rinja; +/// +/// #[macro_export] +/// macro_rules! new_greeter { +/// ($name:ident) => { +/// #[derive(Debug, $crate::rinja::Template)] +/// #[template( +/// ext = "txt", +/// source = "Hello, world!", +/// rinja = $crate::__rinja +/// )] +/// struct $name; +/// } +/// } +/// +/// new_greeter!(HelloWorld); +/// assert_eq!(HelloWorld.to_string(), Ok("Hello, world.")); +/// ``` #[allow(clippy::useless_conversion)] // To be compatible with both `TokenStream`s #[cfg_attr( not(feature = "__standalone"), @@ -124,32 +155,57 @@ pub fn derive_template(input: TokenStream12) -> TokenStream12 { Ok(ast) => ast, Err(err) => { let msgs = err.into_iter().map(|err| err.to_string()); - return compile_error(msgs, Span::call_site()).into(); + let ts = quote! { + span => + const _: () = { + extern crate core; + #(core::compile_error!(#msgs);)* + }; + }; + return ts.into(); } }; let mut buf = Buffer::new(); - if let Err(CompileError { msg, span }) = build_template(&mut buf, &ast) { - let mut ts = compile_error(std::iter::once(msg), span.unwrap_or(ast.ident.span())); + let mut args = AnyTemplateArgs::new(&ast); + let crate_name = args + .as_mut() + .map(|a| a.take_crate_name()) + .unwrap_or_default(); + + let result = args.and_then(|args| build_template(&mut buf, &ast, args)); + let ts = if let Err(CompileError { msg, span }) = result { + let mut ts = quote_spanned! { + span.unwrap_or(ast.ident.span()) => + rinja::helpers::core::compile_error!(#msg); + }; buf.clear(); if build_skeleton(&mut buf, &ast).is_ok() { let source: TokenStream = buf.into_string().parse().unwrap(); ts.extend(source); } - ts.into() + ts } else { buf.into_string().parse().unwrap() - } -} + }; -fn compile_error(msgs: impl Iterator, span: Span) -> TokenStream { - quote_spanned! { - span => - const _: () = { - extern crate rinja as rinja; - #(rinja::helpers::core::compile_error!(#msgs);)* - }; - } + let ts = TokenTree::Group(Group::new(Delimiter::None, ts)); + let ts = if let Some(crate_name) = crate_name { + quote! { + const _: () = { + use #crate_name as rinja; + #ts + }; + } + } else { + quote! { + const _: () = { + extern crate rinja; + #ts + }; + } + }; + ts.into() } fn build_skeleton(buf: &mut Buffer, ast: &syn::DeriveInput) -> Result { @@ -172,9 +228,10 @@ fn build_skeleton(buf: &mut Buffer, ast: &syn::DeriveInput) -> Result Result { let err_span; - let mut result = match AnyTemplateArgs::new(ast)? { + let mut result = match args { AnyTemplateArgs::Struct(item) => { err_span = item.source.1.or(item.template_span); build_template_item(buf, ast, None, &item, TmplKind::Struct) diff --git a/rinja_derive/src/tests.rs b/rinja_derive/src/tests.rs index 9d1c32b8..6aa7ee8f 100644 --- a/rinja_derive/src/tests.rs +++ b/rinja_derive/src/tests.rs @@ -7,11 +7,14 @@ use console::style; use prettyplease::unparse; use similar::{Algorithm, ChangeTag, TextDiffConfig}; +use crate::AnyTemplateArgs; use crate::integration::Buffer; +#[track_caller] fn build_template(ast: &syn::DeriveInput) -> Result { let mut buf = Buffer::new(); - crate::build_template(&mut buf, ast)?; + let args = AnyTemplateArgs::new(ast)?; + crate::build_template(&mut buf, ast, args)?; Ok(buf.into_string()) } @@ -23,49 +26,45 @@ fn compare(jinja: &str, expected: &str, fields: &[(&str, &str)], size_hint: usiz let expected: proc_macro2::TokenStream = expected.parse().unwrap(); let expected: syn::File = syn::parse_quote! { - const _: () = { - extern crate rinja as rinja; - - impl rinja::Template for Foo { - fn render_into_with_values( - &self, - __rinja_writer: &mut RinjaW, - __rinja_values: &dyn rinja::Values, - ) -> rinja::Result<()> - where - RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized, - { - #[allow(unused_imports)] - use rinja::{ - filters::{AutoEscape as _, WriteWritable as _}, - helpers::{ResultConverter as _, core::fmt::Write as _}, - }; - #expected - rinja::Result::Ok(()) - } - const SIZE_HINT: rinja::helpers::core::primitive::usize = #size_hint; + impl rinja::Template for Foo { + fn render_into_with_values( + &self, + __rinja_writer: &mut RinjaW, + __rinja_values: &dyn rinja::Values, + ) -> rinja::Result<()> + where + RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized, + { + #[allow(unused_imports)] + use rinja::{ + filters::{AutoEscape as _, WriteWritable as _}, + helpers::{ResultConverter as _, core::fmt::Write as _}, + }; + #expected + rinja::Result::Ok(()) } + const SIZE_HINT: rinja::helpers::core::primitive::usize = #size_hint; + } - /// Implement the [`format!()`][rinja::helpers::std::format] trait for [`Foo`] - /// - /// Please be aware of the rendering performance notice in the [`Template`][rinja::Template] trait. - impl rinja::helpers::core::fmt::Display for Foo { - #[inline] - fn fmt(&self, f: &mut rinja::helpers::core::fmt::Formatter<'_>) -> rinja::helpers::core::fmt::Result { - rinja::Template::render_into(self, f).map_err(|_| rinja::helpers::core::fmt::Error) - } + /// Implement the [`format!()`][rinja::helpers::std::format] trait for [`Foo`] + /// + /// Please be aware of the rendering performance notice in the [`Template`][rinja::Template] trait. + impl rinja::helpers::core::fmt::Display for Foo { + #[inline] + fn fmt(&self, f: &mut rinja::helpers::core::fmt::Formatter<'_>) -> rinja::helpers::core::fmt::Result { + rinja::Template::render_into(self, f).map_err(|_| rinja::helpers::core::fmt::Error) } + } - impl rinja::filters::FastWritable for Foo { - #[inline] - fn write_into(&self, dest: &mut RinjaW) -> rinja::Result<()> - where - RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized, - { - rinja::Template::render_into(self, dest) - } + impl rinja::filters::FastWritable for Foo { + #[inline] + fn write_into(&self, dest: &mut RinjaW) -> rinja::Result<()> + where + RinjaW: rinja::helpers::core::fmt::Write + ?rinja::helpers::core::marker::Sized, + { + rinja::Template::render_into(self, dest) } - }; + } }; let expected = unparse(&expected); diff --git a/testing-renamed/.rustfmt.toml b/testing-renamed/.rustfmt.toml new file mode 120000 index 00000000..a7ef950c --- /dev/null +++ b/testing-renamed/.rustfmt.toml @@ -0,0 +1 @@ +../.rustfmt.toml \ No newline at end of file diff --git a/testing-renamed/Cargo.toml b/testing-renamed/Cargo.toml new file mode 100644 index 00000000..7ad2c234 --- /dev/null +++ b/testing-renamed/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rinja_testing-renamed" +version = "0.3.5" +authors = ["rinja-rs developers"] +edition = "2021" +rust-version = "1.81" +publish = false + +[dev-dependencies] +some_name = { package = "rinja", path = "../rinja", version = "0.3.5", default-features = false } + +assert_matches = "1.5.0" diff --git a/testing-renamed/LICENSE-APACHE b/testing-renamed/LICENSE-APACHE new file mode 120000 index 00000000..965b606f --- /dev/null +++ b/testing-renamed/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/testing-renamed/LICENSE-MIT b/testing-renamed/LICENSE-MIT new file mode 120000 index 00000000..76219eb7 --- /dev/null +++ b/testing-renamed/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/testing-renamed/_typos.toml b/testing-renamed/_typos.toml new file mode 120000 index 00000000..2264b5db --- /dev/null +++ b/testing-renamed/_typos.toml @@ -0,0 +1 @@ +../_typos.toml \ No newline at end of file diff --git a/testing-renamed/clippy.toml b/testing-renamed/clippy.toml new file mode 120000 index 00000000..85f6167c --- /dev/null +++ b/testing-renamed/clippy.toml @@ -0,0 +1 @@ +../clippy.toml \ No newline at end of file diff --git a/testing-renamed/deny.toml b/testing-renamed/deny.toml new file mode 120000 index 00000000..a65e17bb --- /dev/null +++ b/testing-renamed/deny.toml @@ -0,0 +1 @@ +../deny.toml \ No newline at end of file diff --git a/testing-renamed/tests/hello-world.rs b/testing-renamed/tests/hello-world.rs new file mode 100644 index 00000000..d078e6aa --- /dev/null +++ b/testing-renamed/tests/hello-world.rs @@ -0,0 +1,54 @@ +use std::fmt; + +use assert_matches::assert_matches; +use some_name::Template; + +pub(crate) mod some { + pub(crate) mod deeply { + pub(crate) mod nested { + pub(crate) mod path { + pub(crate) mod with { + pub(crate) use some_name; + } + } + } + } +} + +#[test] +fn hello_world() { + #[derive(Template)] + #[template( + ext = "html", + source = "Hello {%- if let Some(user) = user? -%} , {{ user }} {%- endif -%}!", + rinja = some::deeply::nested::path::with::some_name + )] + struct Hello<'a> { + user: Result, fmt::Error>, + } + + let tmpl = Hello { user: Ok(None) }; + let mut cursor = String::new(); + assert_matches!(tmpl.render_into(&mut cursor), Ok(())); + assert_eq!(cursor, "Hello!"); + + let tmpl = Hello { + user: Ok(Some("user")), + }; + let mut cursor = String::new(); + assert_matches!(tmpl.render_into(&mut cursor), Ok(())); + assert_eq!(cursor, "Hello, user!"); + + let tmpl = Hello { + user: Ok(Some("")), + }; + let mut cursor = String::new(); + assert_matches!(tmpl.render_into(&mut cursor), Ok(())); + assert_eq!(cursor, "Hello, <user>!"); + + let tmpl = Hello { + user: Err(fmt::Error), + }; + let mut cursor = String::new(); + assert_matches!(tmpl.render_into(&mut cursor), Err(some_name::Error::Fmt)); +} diff --git a/testing-renamed/tomlfmt.toml b/testing-renamed/tomlfmt.toml new file mode 120000 index 00000000..053191cc --- /dev/null +++ b/testing-renamed/tomlfmt.toml @@ -0,0 +1 @@ +../tomlfmt.toml \ No newline at end of file