diff --git a/askama_derive/src/tests.rs b/askama_derive/src/tests.rs index 82dd5f56..b5aa4e45 100644 --- a/askama_derive/src/tests.rs +++ b/askama_derive/src/tests.rs @@ -44,7 +44,9 @@ fn compare_ex( ) { let generated = jinja_to_rust(jinja, fields, prefix); - let expected: TokenStream = expected.parse().unwrap(); + let expected: TokenStream = expected + .parse() + .expect("`TokenStream` failed to parse input"); let expected: syn::File = syn::parse_quote! { #[automatically_derived] impl askama::Template for Foo { @@ -159,7 +161,10 @@ struct Foo {{ {} }}"##, .join(","), ); - let generated = build_template(&syn::parse_str::(&jinja).unwrap()).unwrap(); + let generated = build_template( + &syn::parse_str::(&jinja).expect("`syn` failed to parse code"), + ) + .expect("`build_template` failed"); match syn::parse2(generated.clone()) { Ok(generated) => generated, Err(err) => panic!( @@ -1150,11 +1155,11 @@ fn test_concat() { fn extends_with_whitespace_control() { const CONTROL: &[&str] = &["", "\t", "-", "+", "~"]; - let expected = jinja_to_rust(r#"front {% extends "a.html" %} back"#, &[], ""); + let expected = jinja_to_rust(r#"{% extends "a.html" %} back"#, &[], ""); let expected = unparse(&expected); for front in CONTROL { for back in CONTROL { - let src = format!(r#"front {{%{front} extends "a.html" {back}%}} back"#); + let src = format!(r#"{{%{front} extends "a.html" {back}%}} back"#); let actual = jinja_to_rust(&src, &[], ""); let actual = unparse(&actual); assert_eq!(expected, actual, "source: {src:?}"); diff --git a/askama_parser/src/node.rs b/askama_parser/src/node.rs index 3b09cfc9..37ad661e 100644 --- a/askama_parser/src/node.rs +++ b/askama_parser/src/node.rs @@ -38,8 +38,27 @@ pub enum Node<'a> { impl<'a: 'l, 'l> Node<'a> { pub(super) fn parse_template(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Vec>> { - let mut p = parse_with_unexpected_fallback(Self::many, unexpected_tag); - let nodes = p.parse_next(i)?; + let mut nodes = vec![]; + let mut allow_extends = true; + while let Some(node) = parse_with_unexpected_fallback( + opt(move |i: &mut _| Self::one(i, allow_extends)), + unexpected_tag, + ) + .parse_next(i)? + { + if allow_extends { + match &*node { + // Since comments don't impact generated code, we allow them before `extends`. + Node::Comment(_) => {} + // If it only contains whitespace characters, it's fine too. + Node::Lit(lit) if lit.val.is_empty() => {} + // Everything else must not come before an `extends` block. + _ => allow_extends = false, + } + } + nodes.push(node); + } + if !i.is_empty() { opt(unexpected_tag).parse_next(i)?; return cut_error!( @@ -53,12 +72,15 @@ impl<'a: 'l, 'l> Node<'a> { } fn many(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Vec>> { - repeat( - 0.., - alt((Lit::parse, Comment::parse, Self::expr, Self::parse)), - ) - .map(|v: Vec<_>| v) - .parse_next(i) + repeat(0.., |i: &mut _| Self::one(i, false)).parse_next(i) + } + + fn one(i: &mut InputStream<'a, 'l>, allow_extends: bool) -> ParseResult<'a, Box> { + let node = alt((Lit::parse, Comment::parse, Self::expr, Self::parse)).parse_next(i)?; + if !allow_extends && let Node::Extends(node) = &*node { + return cut_error!("`extends` block must come first in a template", node.span()); + } + Ok(node) } fn parse(i: &mut InputStream<'a, 'l>) -> ParseResult<'a, Box> { diff --git a/askama_parser/src/tests.rs b/askama_parser/src/tests.rs index 1a42a125..7bf7ed54 100644 --- a/askama_parser/src/tests.rs +++ b/askama_parser/src/tests.rs @@ -1160,10 +1160,10 @@ fn extends_with_whitespace_control() { const CONTROL: &[&str] = &["", "\t", "-", "+", "~"]; let syntax = Syntax::default(); - let expected = Ast::from_str(r#"front {% extends "nothing" %} back"#, None, &syntax).unwrap(); + let expected = Ast::from_str(r#"{% extends "nothing" %} back"#, None, &syntax).unwrap(); for front in CONTROL { for back in CONTROL { - let src = format!(r#"front {{%{front} extends "nothing" {back}%}} back"#); + let src = format!(r#"{{%{front} extends "nothing" {back}%}} back"#); let actual = Ast::from_str(&src, None, &syntax).unwrap(); assert_eq!(expected.nodes(), actual.nodes(), "source: {src:?}"); } diff --git a/testing/tests/extend.rs b/testing/tests/extend.rs index c15d30e0..ce85d65b 100644 --- a/testing/tests/extend.rs +++ b/testing/tests/extend.rs @@ -25,3 +25,17 @@ fn test_macro_in_block_inheritance() { assert_eq!(A.render().unwrap(), "\n\n1 1\n2 2\n--> 3"); } + +// This test ensures that comments are allowed before `extends` block. +#[test] +fn test_comment_before_extend() { + #[derive(Template)] + #[template( + source = r##"{# comment #}{% extends "base.html" %}"##, + ext = "txt", + print = "ast" + )] + pub struct X { + title: &'static str, + } +} diff --git a/testing/tests/ui/blocks_below_top_level.stderr b/testing/tests/ui/blocks_below_top_level.stderr index ce1a084f..d7e932c3 100644 --- a/testing/tests/ui/blocks_below_top_level.stderr +++ b/testing/tests/ui/blocks_below_top_level.stderr @@ -1,5 +1,5 @@ -error: `extends` blocks are not allowed below top level - --> MyTemplate1.txt:3:2 +error: `extends` block must come first in a template + --> :3:2 " extends \"bla.txt\" %}\n{% endblock %}\n" --> tests/ui/blocks_below_top_level.rs:4:21 | diff --git a/testing/tests/ui/extend.rs b/testing/tests/ui/extend.rs new file mode 100644 index 00000000..871d501d --- /dev/null +++ b/testing/tests/ui/extend.rs @@ -0,0 +1,13 @@ +use askama::Template; + +#[derive(Template)] +#[template( + source = r##"bla +{% extends "base.html" %} +"##, + ext = "txt", + print = "ast" +)] +pub struct X; + +fn main() {} diff --git a/testing/tests/ui/extend.stderr b/testing/tests/ui/extend.stderr new file mode 100644 index 00000000..8d386702 --- /dev/null +++ b/testing/tests/ui/extend.stderr @@ -0,0 +1,10 @@ +error: `extends` block must come first in a template + --> :2:2 + " extends \"base.html\" %}\n" + --> tests/ui/extend.rs:5:14 + | +5 | source = r##"bla + | ______________^ +6 | | {% extends "base.html" %} +7 | | "##, + | |___^ diff --git a/testing/tests/ui/multiple_extends.stderr b/testing/tests/ui/multiple_extends.stderr index 8b196029..7e7ac496 100644 --- a/testing/tests/ui/multiple_extends.stderr +++ b/testing/tests/ui/multiple_extends.stderr @@ -1,5 +1,5 @@ -error: multiple extend blocks found - --> MyTemplate4.txt:3:2 +error: `extends` block must come first in a template + --> :3:2 " extends \"foo.html\" %}\n" --> tests/ui/multiple_extends.rs:4:21 |