From 8c02e48cdb52f077dc0bc62f84b0dc0c5a414428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sat, 16 Aug 2025 04:01:59 +0200 Subject: [PATCH] Implement feature `"nightly-spans"` --- .github/workflows/rust.yml | 31 ++++- askama/Cargo.toml | 1 + askama_derive/Cargo.toml | 9 +- askama_derive/src/generator.rs | 52 ++++--- askama_derive/src/heritage.rs | 5 + askama_derive/src/input.rs | 4 +- askama_derive/src/lib.rs | 4 + askama_derive/src/spans.rs | 152 +++++++++++++++++++-- askama_macros/Cargo.toml | 1 + testing-alloc/Cargo.toml | 3 + testing-alloc/templates/hello-world.html | 1 + testing-alloc/tests/hello-world.rs | 32 +++-- testing-no-std/Cargo.toml | 3 + testing-no-std/templates/hello-world.html | 1 + testing-no-std/tests/hello-world.rs | 32 +++-- testing-renamed/Cargo.toml | 3 + testing-renamed/templates/hello-world.html | 1 + testing-renamed/tests/hello-world.rs | 35 +++-- testing/Cargo.toml | 1 + testing/tests/no_implicit_prelude.rs | 26 +++- 20 files changed, 312 insertions(+), 85 deletions(-) create mode 100644 testing-alloc/templates/hello-world.html create mode 100644 testing-no-std/templates/hello-world.html create mode 100644 testing-renamed/templates/hello-world.html diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 97df44e5..f533a47b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -145,7 +145,7 @@ jobs: #################################################################################################### # STEP 2: INTERMEDIATE -# ["Test", "Package", "MSRV"] +# ["Test", "Package", "Nightly", "MSRV"] #################################################################################################### Test: @@ -168,8 +168,8 @@ jobs: with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v2 - - run: cargo test --all-features - - run: cargo test --all-targets --all-features + - run: cargo test --features full + - run: cargo test --all-targets --features full Package: needs: ["Rustfmt", "Docs", "Audit", "Book", "Typos", "Jinja2-Assumptions", "DevSkim", "CargoSort"] @@ -193,6 +193,23 @@ jobs: - run: cd ${{ matrix.package }} && cargo test --all-targets - run: cd ${{ matrix.package }} && cargo clippy --all-targets -- -D warnings + Nightly: + needs: ["Rustfmt", "Docs", "Audit", "Book", "Typos", "Jinja2-Assumptions", "DevSkim", "CargoSort"] + strategy: + matrix: + package: [ + askama, askama_derive, askama_escape, askama_macros, askama_parser, + testing, testing-alloc, testing-no-std, testing-renamed, + ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 + - run: cd ${{ matrix.package }} && cargo test --all-features + MSRV: needs: ["Rustfmt", "Docs", "Audit", "Book", "Typos", "Jinja2-Assumptions", "DevSkim", "CargoSort"] runs-on: ubuntu-latest @@ -203,7 +220,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: "1.88.0" - - run: cargo check --lib -p askama --all-features + - run: cargo check --lib -p askama --features full #################################################################################################### # STEP 2: SLOW @@ -211,7 +228,7 @@ jobs: #################################################################################################### Fuzz: - needs: ["Test", "Package", "MSRV"] + needs: ["Test", "Package", "Nightly", "MSRV"] strategy: matrix: fuzz_target: @@ -231,7 +248,7 @@ jobs: with: toolchain: nightly components: rust-src - - run: curl --location --silent --show-error --fail https://github.com/cargo-bins/cargo-quickinstall/releases/download/cargo-fuzz-0.12.0/cargo-fuzz-0.12.0-x86_64-unknown-linux-gnu.tar.gz | tar -xzvvf - -C $HOME/.cargo/bin + - run: curl --location --silent --show-error --fail https://github.com/cargo-bins/cargo-quickinstall/releases/download/cargo-fuzz-0.13.1/cargo-fuzz-0.13.1-x86_64-unknown-linux-gnu.tar.gz | tar -xzvvf - -C $HOME/.cargo/bin - uses: Swatinem/rust-cache@v2 - run: cargo fuzz run ${{ matrix.fuzz_target }} --jobs 4 -- -max_total_time=600 working-directory: fuzzing @@ -239,7 +256,7 @@ jobs: RUSTFLAGS: '-Ctarget-feature=-crt-static' Cluster-Fuzz: - needs: ["Test", "Package", "MSRV"] + needs: ["Test", "Package", "Nightly", "MSRV"] runs-on: ubuntu-latest permissions: actions: read diff --git a/askama/Cargo.toml b/askama/Cargo.toml index a71b744a..7ff5d68e 100644 --- a/askama/Cargo.toml +++ b/askama/Cargo.toml @@ -57,6 +57,7 @@ blocks = ["askama_macros?/blocks"] code-in-doc = ["askama_macros?/code-in-doc"] config = ["askama_macros?/config"] derive = ["dep:askama_macros", "dep:askama_macros"] +nightly-spans = ["askama_macros/nightly-spans"] serde_json = ["std", "askama_macros?/serde_json", "dep:serde", "dep:serde_json"] std = [ "alloc", diff --git a/askama_derive/Cargo.toml b/askama_derive/Cargo.toml index 4bc13913..dc13ad46 100644 --- a/askama_derive/Cargo.toml +++ b/askama_derive/Cargo.toml @@ -56,8 +56,15 @@ default = [ alloc = [] blocks = ["syn/full"] code-in-doc = ["dep:pulldown-cmark"] -config = ["external-sources", "dep:basic-toml", "dep:serde", "dep:serde_derive", "parser/config"] +config = [ + "external-sources", + "dep:basic-toml", + "dep:serde", + "dep:serde_derive", + "parser/config", +] external-sources = [] +nightly-spans = [] proc-macro = ["proc-macro2/proc-macro"] serde_json = [] std = ["alloc"] diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index 5d2c1e82..b09a7d84 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -174,40 +174,49 @@ impl<'a, 'h> Generator<'a, 'h> { TmplKind::Block(trait_name) => field_new(trait_name, span), }; - let mut full_paths = TokenStream::new(); + let mut paths_ts = TokenStream::new(); + if let Some(full_config_path) = &self.input.config.full_config_path { let full_config_path = self.rel_path(full_config_path).display().to_string(); - full_paths = quote_spanned!(span=> + paths_ts.extend(quote_spanned!(span => const _: &[askama::helpers::core::primitive::u8] = - askama::helpers::core::include_bytes!(#full_config_path); - ); + askama::helpers::core::include_bytes!(#full_config_path); + )); } // Make sure the compiler understands that the generated code depends on the template files. let mut paths = self .contexts - .keys() - .map(|path| -> &Path { path }) - .collect::>(); - paths.sort(); - let paths = paths - .into_iter() - .filter(|path| { + .iter() + .map(|(path, _ctx)| { + ( + &***path, + #[cfg(not(feature = "external-sources"))] + (), + #[cfg(feature = "external-sources")] + _ctx, + ) + }) + .filter(|&(path, _)| { // Skip the fake path of templates defined in rust source. match self.input.source { #[cfg(feature = "external-sources")] Source::Path(_) => true, - Source::Source(_) => **path != *self.input.path, + Source::Source(_) => *path != *self.input.path, } }) - .fold(TokenStream::new(), |mut acc, path| { - let path = self.rel_path(path).display().to_string(); - acc.extend(quote_spanned!(span=> - const _: &[askama::helpers::core::primitive::u8] = - askama::helpers::core::include_bytes!(#path); - )); - acc - }); + .collect::>(); + paths.sort_by_key(|&(path, _)| path); + for (path, _ctx) in paths { + let path = self.rel_path(path).display().to_string(); + paths_ts.extend(quote_spanned!(span=> + const _: &[askama::helpers::core::primitive::u8] = + askama::helpers::core::include_bytes!(#path); + )); + + #[cfg(all(feature = "external-sources", feature = "nightly-spans"))] + _ctx.resolve_path(&path); + } let mut content = Buffer::new(); let size_hint = self.impl_template_inner(ctx, &mut content)?; @@ -238,8 +247,7 @@ impl<'a, 'h> Generator<'a, 'h> { helpers::{ResultConverter as _, core::fmt::Write as _}, }; - #full_paths - #paths + #paths_ts #content askama::Result::Ok(()) } diff --git a/askama_derive/src/heritage.rs b/askama_derive/src/heritage.rs index eac7e3f5..55fad307 100644 --- a/askama_derive/src/heritage.rs +++ b/askama_derive/src/heritage.rs @@ -177,6 +177,11 @@ impl<'a> Context<'a> { pub(crate) fn file_info_of(&self, node: Span) -> Option> { self.path.map(|path| FileInfo::of(node, path, self.parsed)) } + + #[cfg(all(feature = "external-sources", feature = "nightly-spans"))] + pub(crate) fn resolve_path(&self, path: &str) { + self.literal.resolve_path(path); + } } fn ensure_top( diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs index d4920958..fc69b53a 100644 --- a/askama_derive/src/input.rs +++ b/askama_derive/src/input.rs @@ -455,7 +455,7 @@ impl TemplateArgs { source: match args.source { #[cfg(feature = "external-sources")] Some(PartialTemplateArgsSource::Path(s)) => { - (Source::Path(s.value().into()), SourceSpan::Path(s.span())) + (Source::Path(s.value().into()), SourceSpan::from_path(s)?) } Some(PartialTemplateArgsSource::Source(s)) => { let (source, span) = SourceSpan::from_source(s)?; @@ -463,7 +463,7 @@ impl TemplateArgs { } #[cfg(feature = "code-in-doc")] Some(PartialTemplateArgsSource::InDoc(span, source)) => { - (source, SourceSpan::Span(span)) + (source, SourceSpan::CodeInDoc(span)) } None => { return Err(CompileError::no_file_info( diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs index de9956a4..d9488c0c 100644 --- a/askama_derive/src/lib.rs +++ b/askama_derive/src/lib.rs @@ -1,6 +1,10 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #![deny(elided_lifetimes_in_paths)] #![deny(unreachable_pub)] +#![cfg_attr( + all(feature = "external-sources", feature = "nightly-spans"), + feature(proc_macro_def_site, proc_macro_expand) +)] extern crate proc_macro; diff --git a/askama_derive/src/spans.rs b/askama_derive/src/spans.rs index 3ab6f4b0..17d871f2 100644 --- a/askama_derive/src/spans.rs +++ b/askama_derive/src/spans.rs @@ -11,38 +11,53 @@ use crate::spans::rustc_literal_escaper::unescape; #[allow(private_interfaces)] // don't look behind the curtain #[derive(Clone, Debug)] pub(crate) enum SourceSpan { + Empty(Span), Source(SpannedSource), - // TODO: transclude source file - Path(Span), + #[cfg(feature = "external-sources")] + Path(SpannedPath), // TODO: implement for "code-in-doc" #[cfg_attr(not(feature = "code-in-doc"), allow(dead_code))] - Span(Span), - Empty(Span), + CodeInDoc(Span), } impl SourceSpan { + pub(crate) fn empty() -> SourceSpan { + Self::Empty(Span::call_site()) + } + pub(crate) fn from_source(source: LitStr) -> Result<(String, Self), CompileError> { - let (source, span) = SpannedSource::from_source(source)?; + let (source, span) = SpannedSource::new(source)?; Ok((source, Self::Source(span))) } + #[cfg(feature = "external-sources")] + pub(crate) fn from_path(config: LitStr) -> Result { + Ok(Self::Path(SpannedPath::new(config)?)) + } + pub(crate) fn config_span(&self) -> Span { match self { - SourceSpan::Source(literal) => literal.config_span(), - SourceSpan::Path(span) | SourceSpan::Span(span) | Self::Empty(span) => *span, + SourceSpan::Source(v) => v.config_span(), + #[cfg(feature = "external-sources")] + SourceSpan::Path(v) => v.config_span(), + SourceSpan::CodeInDoc(span) | Self::Empty(span) => *span, } } pub(crate) fn content_subspan(&self, bytes: Range) -> Option { match self { - Self::Source(source) => source.content_subspan(bytes), - Self::Path(_) | Self::Span(_) => None, - Self::Empty(_) => None, + Self::Source(v) => v.content_subspan(bytes), + #[cfg(feature = "external-sources")] + SourceSpan::Path(v) => v.content_subspan(bytes), + Self::CodeInDoc(_) | Self::Empty(_) => None, } } - pub(crate) fn empty() -> SourceSpan { - Self::Empty(Span::call_site()) + #[cfg(all(feature = "external-sources", feature = "nightly-spans"))] + pub(crate) fn resolve_path(&self, path: &str) { + if let Self::Path(v) = self { + v.resolve_path(path); + } } } @@ -76,7 +91,7 @@ impl SpannedSource { } } - fn from_source(source: LitStr) -> Result<(String, Self), CompileError> { + fn new(source: LitStr) -> Result<(String, Self), CompileError> { let literal = source.token(); let unparsed = literal.to_string(); let result = if unparsed.starts_with('r') { @@ -134,3 +149,114 @@ impl SpannedSource { Ok((source, Self { literal, positions })) } } + +#[cfg(feature = "external-sources")] +#[cfg_attr(not(feature = "nightly-spans"), derive(Debug, Clone))] +struct SpannedPath { + config: Span, + #[cfg(feature = "nightly-spans")] + literal: std::cell::Cell>, +} + +#[cfg(feature = "external-sources")] +impl SpannedPath { + fn new(config: LitStr) -> Result { + Ok(Self { + config: config.span(), + #[cfg(feature = "nightly-spans")] + literal: std::cell::Cell::new(None), + }) + } + + #[inline] + fn config_span(&self) -> Span { + self.config + } +} + +#[cfg(all(feature = "external-sources", not(feature = "nightly-spans")))] +impl SpannedPath { + #[inline] + fn content_subspan(&self, _: Range) -> Option { + None + } +} + +#[cfg(all(feature = "external-sources", feature = "nightly-spans"))] +const _: () = { + use std::cell::Cell; + use std::fmt; + + use proc_macro::TokenStream as TokenStream1; + use proc_macro2::{TokenStream as TokenStream2, TokenTree}; + use quote::quote_spanned; + + impl fmt::Debug for SpannedPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SpannedPath") + .field("config", &self.config) + .field("span", &self.literal_span()) + .finish() + } + } + + impl Clone for SpannedPath { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + literal: Cell::new(self.literal()), + } + } + } + + impl SpannedPath { + fn content_subspan(&self, bytes: Range) -> Option { + let literal = self.literal.take()?; + let span = literal.subspan(bytes); + self.literal.set(Some(literal)); + span + } + + fn literal_span(&self) -> Option { + let literal = self.literal.take()?; + let span = literal.span(); + self.literal.set(Some(literal)); + Some(span) + } + + fn literal(&self) -> Option { + let literal = self.literal.take()?; + self.literal.set(Some(literal.clone())); + Some(literal) + } + + fn resolve_path(&self, path: &str) { + if proc_macro::is_available() + && let Ok(stream) = TokenStream1::from(quote_spanned! { + // In token expansion, `extern crate some_name` does not work. Only crates that + // were imported output _before_ the `#[derive(Template)]` invocation can be + // used. + // + // In the macro expansion, using an identifier that was not defined will emit + // an error `Diagnostic`. We cannot un-emit a `Diagnostic`, so this would be a + // hard compilation error. + // + // At `call_site()`, macro `include_str!` may not exist (`#[no_implicit_prelude]`), + // or may be shadowed. The symbol `askama` may not exist or be shadowed, too. + // + // At `def_site()`, the we know that the macro exists. We do not know if `core` + // or `::core` exists, but the unprefixed macro `include_str!` does exist, and + // it cannot be shadowed from outside of this function call. + // + // + proc_macro::Span::def_site().into() => include_str!(#path) + }) + .expand_expr() + && let Some(TokenTree::Literal(literal)) = + TokenStream2::from(stream).into_iter().next() + { + self.literal.set(Some(literal)); + } + } + } +}; diff --git a/askama_macros/Cargo.toml b/askama_macros/Cargo.toml index 10763dd0..25277e61 100644 --- a/askama_macros/Cargo.toml +++ b/askama_macros/Cargo.toml @@ -32,6 +32,7 @@ alloc = ["askama_derive/alloc"] blocks = ["askama_derive/blocks"] code-in-doc = ["askama_derive/code-in-doc"] config = ["askama_derive/config"] +nightly-spans = ["askama_derive/nightly-spans"] serde_json = ["askama_derive/serde_json"] std = ["askama_derive/std"] urlencode = ["askama_derive/urlencode"] diff --git a/testing-alloc/Cargo.toml b/testing-alloc/Cargo.toml index 69a80d5a..1ac72433 100644 --- a/testing-alloc/Cargo.toml +++ b/testing-alloc/Cargo.toml @@ -10,3 +10,6 @@ publish = false askama = { path = "../askama", version = "0.14.0", default-features = false, features = ["alloc", "derive"] } assert_matches = "1.5.0" + +[features] +nightly-spans = ["askama/nightly-spans"] diff --git a/testing-alloc/templates/hello-world.html b/testing-alloc/templates/hello-world.html new file mode 100644 index 00000000..a29011e9 --- /dev/null +++ b/testing-alloc/templates/hello-world.html @@ -0,0 +1 @@ +Hello {%- if let Some(user) = user? -%} , {{ user }} {%- endif -%}! diff --git a/testing-alloc/tests/hello-world.rs b/testing-alloc/tests/hello-world.rs index fec311b7..027a565c 100644 --- a/testing-alloc/tests/hello-world.rs +++ b/testing-alloc/tests/hello-world.rs @@ -7,7 +7,7 @@ use askama::Template; use assert_matches::assert_matches; #[test] -fn hello_world() { +fn test_source() { #[derive(Template)] #[template( ext = "html", @@ -17,30 +17,40 @@ fn hello_world() { user: Result, CustomError>, } + test_common(|user| Hello { user }); +} + +#[test] +fn test_path() { + #[derive(Template)] + #[template(path = "hello-world.html")] + struct Hello<'a> { + user: Result, CustomError>, + } + + test_common(|user| Hello { user }); +} + +#[track_caller] +fn test_common<'a, T: Template + 'a>(hello: fn(Result, CustomError>) -> T) { let mut buffer = [0; 32]; - let tmpl = Hello { user: Ok(None) }; + let tmpl = hello(Ok(None)); let mut cursor = Cursor::new(&mut buffer); assert_matches!(tmpl.render_into(&mut cursor), Ok(())); assert_eq!(cursor.finalize(), Ok("Hello!")); - let tmpl = Hello { - user: Ok(Some("user")), - }; + let tmpl = hello(Ok(Some("user"))); let mut cursor = Cursor::new(&mut buffer); assert_matches!(tmpl.render_into(&mut cursor), Ok(())); assert_eq!(cursor.finalize(), Ok("Hello, user!")); - let tmpl = Hello { - user: Ok(Some("")), - }; + let tmpl = hello(Ok(Some(""))); let mut cursor = Cursor::new(&mut buffer); assert_matches!(tmpl.render_into(&mut cursor), Ok(())); assert_eq!(cursor.finalize(), Ok("Hello, <user>!")); - let tmpl = Hello { - user: Err(CustomError), - }; + let tmpl = hello(Err(CustomError)); let mut cursor = Cursor::new(&mut buffer); let err = match tmpl.render_into(&mut cursor) { Err(askama::Error::Custom(err)) => err, diff --git a/testing-no-std/Cargo.toml b/testing-no-std/Cargo.toml index 63ad917c..e0be540d 100644 --- a/testing-no-std/Cargo.toml +++ b/testing-no-std/Cargo.toml @@ -10,3 +10,6 @@ publish = false askama = { path = "../askama", version = "0.14.0", default-features = false, features = ["derive"] } assert_matches = "1.5.0" + +[features] +nightly-spans = ["askama/nightly-spans"] diff --git a/testing-no-std/templates/hello-world.html b/testing-no-std/templates/hello-world.html new file mode 100644 index 00000000..a29011e9 --- /dev/null +++ b/testing-no-std/templates/hello-world.html @@ -0,0 +1 @@ +Hello {%- if let Some(user) = user? -%} , {{ user }} {%- endif -%}! diff --git a/testing-no-std/tests/hello-world.rs b/testing-no-std/tests/hello-world.rs index 0f131ce2..2379a315 100644 --- a/testing-no-std/tests/hello-world.rs +++ b/testing-no-std/tests/hello-world.rs @@ -7,7 +7,7 @@ use askama::Template; use assert_matches::assert_matches; #[test] -fn hello_world() { +fn test_source() { #[derive(Template)] #[template( ext = "html", @@ -17,30 +17,40 @@ fn hello_world() { user: Result, fmt::Error>, } + test_common(|user| Hello { user }) +} + +#[test] +fn test_path() { + #[derive(Template)] + #[template(path = "hello-world.html")] + struct Hello<'a> { + user: Result, fmt::Error>, + } + + test_common(|user| Hello { user }) +} + +#[track_caller] +fn test_common<'a, T: Template + 'a>(hello: fn(Result, fmt::Error>) -> T) { let mut buffer = [0; 32]; - let tmpl = Hello { user: Ok(None) }; + let tmpl = hello(Ok(None)); let mut cursor = Cursor::new(&mut buffer); assert_matches!(tmpl.render_into(&mut cursor), Ok(())); assert_eq!(cursor.finalize(), Ok("Hello!")); - let tmpl = Hello { - user: Ok(Some("user")), - }; + let tmpl = hello(Ok(Some("user"))); let mut cursor = Cursor::new(&mut buffer); assert_matches!(tmpl.render_into(&mut cursor), Ok(())); assert_eq!(cursor.finalize(), Ok("Hello, user!")); - let tmpl = Hello { - user: Ok(Some("")), - }; + let tmpl = hello(Ok(Some(""))); let mut cursor = Cursor::new(&mut buffer); assert_matches!(tmpl.render_into(&mut cursor), Ok(())); assert_eq!(cursor.finalize(), Ok("Hello, <user>!")); - let tmpl = Hello { - user: Err(fmt::Error), - }; + let tmpl = hello(Err(fmt::Error)); let mut cursor = Cursor::new(&mut buffer); assert_matches!(tmpl.render_into(&mut cursor), Err(askama::Error::Fmt)); } diff --git a/testing-renamed/Cargo.toml b/testing-renamed/Cargo.toml index 2d08fcce..705347d3 100644 --- a/testing-renamed/Cargo.toml +++ b/testing-renamed/Cargo.toml @@ -10,3 +10,6 @@ publish = false some_name = { package = "askama", path = "../askama", version = "0.14.0", default-features = false, features = ["derive"] } assert_matches = "1.5.0" + +[features] +nightly-spans = ["some_name/nightly-spans"] diff --git a/testing-renamed/templates/hello-world.html b/testing-renamed/templates/hello-world.html new file mode 100644 index 00000000..a29011e9 --- /dev/null +++ b/testing-renamed/templates/hello-world.html @@ -0,0 +1 @@ +Hello {%- if let Some(user) = user? -%} , {{ user }} {%- endif -%}! diff --git a/testing-renamed/tests/hello-world.rs b/testing-renamed/tests/hello-world.rs index 50e2277b..e890d643 100644 --- a/testing-renamed/tests/hello-world.rs +++ b/testing-renamed/tests/hello-world.rs @@ -17,7 +17,7 @@ pub(crate) mod some { } #[test] -fn hello_world() { +fn test_source() { #[derive(Template)] #[template( ext = "html", @@ -28,28 +28,41 @@ fn hello_world() { user: Result, fmt::Error>, } - let tmpl = Hello { user: Ok(None) }; + test_common(|user| Hello { user }); +} + +#[test] +fn test_path() { + #[derive(Template)] + #[template( + path = "hello-world.html", + askama = some::deeply::nested::path::with::some_name + )] + struct Hello<'a> { + user: Result, fmt::Error>, + } + + test_common(|user| Hello { user }); +} + +#[track_caller] +fn test_common<'a, T: Template + 'a>(hello: fn(Result, fmt::Error>) -> T) { + let tmpl = hello(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 tmpl = hello(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 tmpl = hello(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 tmpl = hello(Err(fmt::Error)); let mut cursor = String::new(); assert_matches!(tmpl.render_into(&mut cursor), Err(some_name::Error::Fmt)); } diff --git a/testing/Cargo.toml b/testing/Cargo.toml index 066afb17..a3e0e164 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -31,6 +31,7 @@ trybuild = "1.0.100" default = ["blocks", "code-in-doc", "serde_json"] blocks = ["askama/blocks"] code-in-doc = ["askama/code-in-doc"] +nightly-spans = ["askama/nightly-spans"] serde_json = ["dep:serde_json", "askama/serde_json"] [lints.rust] diff --git a/testing/tests/no_implicit_prelude.rs b/testing/tests/no_implicit_prelude.rs index a9c12ef9..4bafc112 100644 --- a/testing/tests/no_implicit_prelude.rs +++ b/testing/tests/no_implicit_prelude.rs @@ -2,14 +2,26 @@ use ::askama::Template; -#[derive(Template)] -#[template(path = "hello.html")] -struct HelloTemplate<'a> { - name: &'a str, -} - #[test] -fn main() { +fn test_source() { + #[derive(Template)] + #[template(ext = "html", source = "Hello, {{ name }}!")] + struct HelloTemplate<'a> { + name: &'a str, + } + + let hello = HelloTemplate { name: "world" }; + ::std::assert_eq!("Hello, world!", hello.render().unwrap()); +} + +#[test] +fn test_path() { + #[derive(Template)] + #[template(path = "hello.html")] + struct HelloTemplate<'a> { + name: &'a str, + } + let hello = HelloTemplate { name: "world" }; ::std::assert_eq!("Hello, world!", hello.render().unwrap()); }