From 357d7a4dd1e5d75d8f111a736ed07a5e99e64982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 28 Aug 2024 02:06:29 +0200 Subject: [PATCH 01/21] derive: fix SPDX syntax --- rinja_derive/Cargo.toml | 2 +- rinja_derive_standalone/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rinja_derive/Cargo.toml b/rinja_derive/Cargo.toml index 014e7cf6..bd5dc8d2 100644 --- a/rinja_derive/Cargo.toml +++ b/rinja_derive/Cargo.toml @@ -4,7 +4,7 @@ version = "0.3.2" description = "Procedural macro package for Rinja" homepage = "https://github.com/rinja-rs/rinja" repository = "https://github.com/rinja-rs/rinja" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" workspace = ".." readme = "README.md" edition = "2021" diff --git a/rinja_derive_standalone/Cargo.toml b/rinja_derive_standalone/Cargo.toml index 79e2545f..abd6917d 100644 --- a/rinja_derive_standalone/Cargo.toml +++ b/rinja_derive_standalone/Cargo.toml @@ -4,7 +4,7 @@ version = "0.3.2" description = "Procedural macro package for Rinja" homepage = "https://github.com/rinja-rs/rinja" repository = "https://github.com/rinja-rs/rinja" -license = "MIT/Apache-2.0" +license = "MIT OR Apache-2.0" readme = "README.md" edition = "2021" rust-version = "1.71" From 02079329a9d580ce62aa02e27fa4098e630e11a7 Mon Sep 17 00:00:00 2001 From: malteneuss Date: Mon, 26 Aug 2024 11:56:22 +0200 Subject: [PATCH 02/21] Suggest disable html escaping in templates in templates section --- book/src/template_syntax.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index 689e979b..1a5b28b2 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -743,6 +743,11 @@ let t = RenderInPlace { s1: SectionOne { a: "a", b: "b" } }; assert_eq!(t.render().unwrap(), "Section 1: A=a\nB=b") ``` +**Note that if your inner template** like `SectionOne` **renders HTML content, you may want to +disable escaping** when injecting it into an outer template, e.g. `{{ s1|escape("none") }}`; +otherwise it will render the HTML content literally. +Askama [escapes HTML variables](#html-escaping) by default. + See the example [render in place](https://github.com/rinja-rs/rinja/blob/master/testing/tests/render_in_place.rs) using a vector of templates in a for block. From a187606e6a3ea78ee0835e398c862e0252d73f65 Mon Sep 17 00:00:00 2001 From: malteneuss Date: Mon, 26 Aug 2024 14:19:29 +0200 Subject: [PATCH 03/21] Replace escape("none") with safe --- book/src/template_syntax.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index 1a5b28b2..c79a89b0 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -744,8 +744,8 @@ assert_eq!(t.render().unwrap(), "Section 1: A=a\nB=b") ``` **Note that if your inner template** like `SectionOne` **renders HTML content, you may want to -disable escaping** when injecting it into an outer template, e.g. `{{ s1|escape("none") }}`; -otherwise it will render the HTML content literally. +disable escaping** when injecting it into an outer template, e.g. `{{ s1|safe }}`. +Otherwise it will render the HTML content literally, because Askama [escapes HTML variables](#html-escaping) by default. See the example From d8711f1c9763f154577b0867de1799a045805564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 28 Aug 2024 03:19:56 +0200 Subject: [PATCH 04/21] book: replace `askama` with `rinja` and less bold text --- book/src/template_syntax.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index c79a89b0..9c414d18 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -743,10 +743,10 @@ let t = RenderInPlace { s1: SectionOne { a: "a", b: "b" } }; assert_eq!(t.render().unwrap(), "Section 1: A=a\nB=b") ``` -**Note that if your inner template** like `SectionOne` **renders HTML content, you may want to -disable escaping** when injecting it into an outer template, e.g. `{{ s1|safe }}`. +Note that if your inner template like `SectionOne` renders HTML content, then you may want to +disable escaping when injecting it into an outer template, e.g. `{{ s1|safe }}`. Otherwise it will render the HTML content literally, because -Askama [escapes HTML variables](#html-escaping) by default. +rinja [escapes HTML variables](#html-escaping) by default. See the example [render in place](https://github.com/rinja-rs/rinja/blob/master/testing/tests/render_in_place.rs) From 98b60d3d8b5afb6dcbbcd5f9d64c3734d2025f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 28 Aug 2024 18:41:09 +0200 Subject: [PATCH 05/21] parser: debug output for `Syntax`/`InnerSyntax` are swapped --- rinja_parser/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs index cb17b3dc..5e7ce1fd 100644 --- a/rinja_parser/src/lib.rs +++ b/rinja_parser/src/lib.rs @@ -762,14 +762,14 @@ impl Default for InnerSyntax<'static> { } } -impl<'a> fmt::Debug for InnerSyntax<'a> { +impl<'a> fmt::Debug for Syntax<'a> { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt_syntax("Syntax", self, f) } } -impl<'a> fmt::Debug for Syntax<'a> { +impl<'a> fmt::Debug for InnerSyntax<'a> { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt_syntax("InnerSyntax", self, f) From 785aea366895e5a138afd0c51b34fabc5e2244de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Wed, 28 Aug 2024 15:43:08 +0200 Subject: [PATCH 06/21] fuzz: add minified corpus after 1,000,000,000 runs --- .github/workflows/rust.yml | 28 +++++++++++++++++++++++++++ .gitmodules | 4 ++++ fuzzing/fuzz/.gitignore | 2 -- fuzzing/fuzz/artifacts/.gitattributes | 2 ++ fuzzing/fuzz/corpus | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 .gitmodules delete mode 100644 fuzzing/fuzz/.gitignore create mode 100644 fuzzing/fuzz/artifacts/.gitattributes create mode 160000 fuzzing/fuzz/corpus diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5bbf5e0e..1a14127d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -7,6 +7,10 @@ on: schedule: - cron: "32 4 * * 5" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: Test: strategy: @@ -151,3 +155,27 @@ jobs: cargo sort --check --check-format --grouped cd - > /dev/null done + + Fuzz: + strategy: + matrix: + fuzz_target: + - all + - filters + - html + - parser + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@master + 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 + - uses: Swatinem/rust-cache@v2 + - run: cargo fuzz run ${{ matrix.fuzz_target }} --jobs 4 -- -max_total_time=90 + working-directory: fuzzing + env: + RUSTFLAGS: '-Ctarget-feature=-crt-static' diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..14295a5f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "fuzzing/fuzz/corpus"] + path = fuzzing/fuzz/corpus + url = https://github.com/rinja-rs/fuzzing-corpus.git + branch = main diff --git a/fuzzing/fuzz/.gitignore b/fuzzing/fuzz/.gitignore deleted file mode 100644 index 8fa697f9..00000000 --- a/fuzzing/fuzz/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/artifacts/ -/corpus/ diff --git a/fuzzing/fuzz/artifacts/.gitattributes b/fuzzing/fuzz/artifacts/.gitattributes new file mode 100644 index 00000000..c6e54c93 --- /dev/null +++ b/fuzzing/fuzz/artifacts/.gitattributes @@ -0,0 +1,2 @@ +* -text -diff binary +.gitattributes text diff diff --git a/fuzzing/fuzz/corpus b/fuzzing/fuzz/corpus new file mode 160000 index 00000000..dde68840 --- /dev/null +++ b/fuzzing/fuzz/corpus @@ -0,0 +1 @@ +Subproject commit dde688407c37aeb0d36792561b71a39501c32be7 From acd6a48557888bf87b5809737a708596739c114d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Fri, 30 Aug 2024 17:01:13 +0200 Subject: [PATCH 07/21] ci: fix a few typos and add check This PR adds `_typos.toml`, which lets `typos` ignore generated data. Some filler texts are "fixed", too. It does not matter what the actual text is. --- .github/workflows/rust.yml | 6 +++ _typos.toml | 26 +++++++++++ book/src/configuration.md | 2 +- book/src/template_syntax.md | 2 +- examples/actix-web-app/src/main.rs | 6 +-- rinja/benches/escape.rs | 67 +++-------------------------- rinja/benches/strings.inc | 69 ++++++++++++++++++++++++++++++ rinja/benches/to-json.rs | 66 +++------------------------- rinja_derive/src/config.rs | 2 +- rinja_derive/src/input.rs | 2 +- 10 files changed, 120 insertions(+), 128 deletions(-) create mode 100644 _typos.toml create mode 100644 rinja/benches/strings.inc diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1a14127d..84219e5e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -179,3 +179,9 @@ jobs: working-directory: fuzzing env: RUSTFLAGS: '-Ctarget-feature=-crt-static' + + Typos: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000..4175b71b --- /dev/null +++ b/_typos.toml @@ -0,0 +1,26 @@ +[default] +locale = "en-us" + +[files] +extend-exclude = [ + # generated files + "book/ethicalads-theme.css", + "fuzzing/fuzz/artifacts/", + "fuzzing/fuzz/corpus/", + "target/", + "rinja_parser/tests/*.txt", + # fillter texts + "rinja/benches/strings.inc", + # too many false positives + "testing/tests/gen_ws_tests.py", +] + +[default.extend-words] +# French words +exemple = "exemple" +existant = "existant" +# used in tests +Ba = "Ba" +fo = "fo" +Fo = "Fo" +sur = "sur" diff --git a/book/src/configuration.md b/book/src/configuration.md index 5c80b972..bc45e411 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -54,7 +54,7 @@ Hello To be noted, if one of the trimmed characters is a newline, then the only character remaining will be a newline. -If you want this to be the default behaviour, you can set `whitespace` to +If you want this to be the default behavior, you can set `whitespace` to `"minimize"`. To be noted: you can also configure `whitespace` directly into the `template` diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index 9c414d18..eb708a9a 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -196,7 +196,7 @@ struct MyTemplate { However, since we'll need to define this function every time we create an instance of `MyTemplate`, it's probably not the most ideal way to associate -some behaviour for our template. +some behavior for our template. ### Static functions diff --git a/examples/actix-web-app/src/main.rs b/examples/actix-web-app/src/main.rs index e232abf6..1f9722c1 100644 --- a/examples/actix-web-app/src/main.rs +++ b/examples/actix-web-app/src/main.rs @@ -55,7 +55,7 @@ enum Error { /// /// The same type is used by actix-web as part of the URL, and in rinja to select what content to /// show, and also as an HTML attribute in ` Result { } } -/// The is first page your user hits does not contain language infomation, so we redirect them +/// The is first page your user hits does not contain language information, so we redirect them /// to a URL that does contain the default language. #[get("/")] async fn start_handler(req: HttpRequest) -> Result { @@ -139,7 +139,7 @@ async fn index_handler( // `{% if lang !=`, the former to select the text of a specific language, e.g. in the ``; // and the latter to display references to all other available languages except the currently // selected one. - // The field `name` will contain the value of the query paramater of the same name. + // The field `name` will contain the value of the query parameter of the same name. // In `IndexHandlerQuery` we annotated the field with `#[serde(default)]`, so if the value is // absent, an empty string is selected by default, which is visible to the user an empty // `<input type="text" />` element. diff --git a/rinja/benches/escape.rs b/rinja/benches/escape.rs index a8e55f7b..83c7783b 100644 --- a/rinja/benches/escape.rs +++ b/rinja/benches/escape.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rinja::filters::{escape, Html}; criterion_main!(benches); @@ -9,66 +9,11 @@ fn functions(c: &mut Criterion) { } fn escaping(b: &mut criterion::Bencher<'_>) { - let string_long = r#" - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat tellus sit - amet ornare fermentum. Etiam nec erat ante. In at metus a orci mollis scelerisque. - Sed eget ultrices turpis, at sollicitudin erat. Integer hendrerit nec magna quis - venenatis. Vivamus non dolor hendrerit, vulputate velit sed, varius nunc. Quisque - in pharetra mi. Sed ullamcorper nibh malesuada commodo porttitor. Ut scelerisque - sodales felis quis dignissim. Morbi aliquam finibus justo, sit amet consectetur - mauris efficitur sit amet. Donec posuere turpis felis, eu lacinia magna accumsan - quis. Fusce egestas lacus vel fermentum tincidunt. Phasellus a nulla eget lectus - placerat commodo at eget nisl. Fusce cursus dui quis purus accumsan auctor. - Donec iaculis felis quis metus consectetur porttitor. -<p> - Etiam nibh mi, <b>accumsan</b> quis purus sed, posuere fermentum lorem. In pulvinar porta - maximus. Fusce tincidunt lacinia tellus sit amet tincidunt. Aliquam lacus est, pulvinar - non metus a, <b>facilisis</b> ultrices quam. Nulla feugiat leo in cursus eleifend. Suspendisse - eget nisi ac justo sagittis interdum id a ipsum. Nulla mauris justo, scelerisque ac - rutrum vitae, consequat vel ex. -</p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p> -<p> - Sed sollicitudin <b>sem</b> mauris, at rutrum nibh egestas vel. Ut eu nisi tellus. Praesent dignissim - orci elementum, mattis turpis eget, maximus ante. Suspendisse luctus eu felis a tempor. Morbi - ac risus vitae sem molestie ullamcorper. Curabitur ligula augue, sollicitudin quis maximus vel, - facilisis sed nibh. Aenean auctor magna sem, id rutrum metus convallis quis. Nullam non arcu - dictum, lobortis erat quis, rhoncus est. Suspendisse venenatis, mi sed venenatis vehicula, - tortor dolor egestas lectus, et efficitur turpis odio non augue. Integer velit sapien, dictum - non egestas vitae, hendrerit sed quam. Phasellus a nunc eu erat varius imperdiet. Etiam id - sollicitudin turpis, vitae molestie orci. Quisque ornare magna quis metus rhoncus commodo. - Phasellus non mauris velit. -</p> -<p> - Etiam dictum tellus ipsum, nec varius quam ornare vel. Cras vehicula diam nec sollicitudin - ultricies. Pellentesque rhoncus sagittis nisl id facilisis. Nunc viverra convallis risus ut - luctus. Aliquam vestibulum <b>efficitur massa</b>, id tempus nisi posuere a. Aliquam scelerisque - elit justo. Nullam a ante felis. Cras vitae lorem eu nisi feugiat hendrerit. Maecenas vitae - suscipit leo, lacinia dignissim lacus. Sed eget volutpat mi. In eu bibendum neque. Pellentesque - finibus velit a fermentum rhoncus. Maecenas leo purus, eleifend eu lacus a, condimentum sagittis - justo. -</p>"#; - let string_short = "Lorem ipsum dolor sit amet,<foo>bar&foo\"bar\\foo/bar"; - let empty = ""; - let no_escape = "Lorem ipsum dolor sit amet,"; - let no_escape_long = r#" -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin scelerisque eu urna in aliquet. -Phasellus ac nulla a urna sagittis consequat id quis est. Nullam eu ex eget erat accumsan dictum -ac lobortis urna. Etiam fermentum ut quam at dignissim. Curabitur vestibulum luctus tellus, sit -amet lobortis augue tempor faucibus. Nullam sed felis eget odio elementum euismod in sit amet massa. -Vestibulum sagittis purus sit amet eros auctor, sit amet pharetra purus dapibus. Donec ornare metus -vel dictum porta. Etiam ut nisl nisi. Nullam rutrum porttitor mi. Donec aliquam ac ipsum eget -hendrerit. Cras faucibus, eros ut pharetra imperdiet, est tellus aliquet felis, eget convallis -lacus ipsum eget quam. Vivamus orci lorem, maximus ac mi eget, bibendum vulputate massa. In -vestibulum dui hendrerit, vestibulum lacus sit amet, posuere erat. Vivamus euismod massa diam, -vulputate euismod lectus vestibulum nec. Donec sit amet massa magna. Nunc ipsum nulla, euismod -quis lacus at, gravida maximus elit. Duis tristique, nisl nullam. - "#; - b.iter(|| { - format!("{}", escape(string_long, Html).unwrap()); - format!("{}", escape(string_short, Html).unwrap()); - format!("{}", escape(empty, Html).unwrap()); - format!("{}", escape(no_escape, Html).unwrap()); - format!("{}", escape(no_escape_long, Html).unwrap()); + for &s in black_box(STRINGS) { + format!("{}", escape(s, Html).unwrap()); + } }); } + +const STRINGS: &[&str] = include!("strings.inc"); diff --git a/rinja/benches/strings.inc b/rinja/benches/strings.inc new file mode 100644 index 00000000..9f90ba77 --- /dev/null +++ b/rinja/benches/strings.inc @@ -0,0 +1,69 @@ +{ + const STRING_LONG: &str = r#" + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat tellus sit + amet ornare fermentum. Etiam nec erat ante. In at metus a orci mollis scelerisque. + Sed eget ultrices turpis, at sollicitudin erat. Integer hendrerit nec magna quis + venenatis. Vivamus non dolor hendrerit, vulputate velit sed, varius nunc. Quisque + in pharetra mi. Sed ullamcorper nibh malesuada commodo porttitor. Ut scelerisque + sodales felis quis dignissim. Morbi aliquam finibus justo, sit amet consectetur + mauris efficitur sit amet. Donec posuere turpis felis, eu lacinia magna accumsan + quis. Fusce egestas lacus vel fermentum tincidunt. Phasellus a nulla eget lectus + placerat commodo at eget nisl. Fusce cursus dui quis purus accumsan auctor. + Donec iaculis felis quis metus consectetur porttitor. +<p> + Etiam nibh mi, <b>accumsan</b> quis purus sed, posuere fermentum lorem. In pulvinar porta + maximus. Fusce tincidunt lacinia tellus sit amet tincidunt. Aliquam lacus est, pulvinar + non metus a, <b>facilisis</b> ultrices quam. Nulla feugiat leo in cursus eleifend. Suspendisse + eget nisi ac justo sagittis interdum id a ipsum. Nulla mauris justo, scelerisque ac + rutrum vitae, consequat vel ex. +</p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p> +<p> + Sed sollicitudin <b>sem</b> mauris, at rutrum nibh egestas vel. Ut eu nisi tellus. Praesent dignissim + orci elementum, mattis turpis eget, maximus ante. Suspendisse luctus eu felis a tempor. Morbi + ac risus vitae sem molestie ullamcorper. Curabitur ligula augue, sollicitudin quis maximus vel, + facilisis sed nibh. Aenean auctor magna sem, id rutrum metus convallis quis. Nullam non arcu + dictum, lobortis erat quis, rhoncus est. Suspendisse venenatis, mi sed venenatis vehicula, + tortor dolor egestas lectus, et efficitur turpis odio non augue. Integer velit sapien, dictum + non egestas vitae, hendrerit sed quam. Phasellus a nunc eu erat varius imperdiet. Etiam id + sollicitudin turpis, vitae molestie orci. Quisque ornare magna quis metus rhoncus commodo. + Phasellus non mauris velit. +</p> +<p> + Etiam dictum tellus ipsum, nec varius quam ornare vel. Cras vehicula diam nec sollicitudin + ultricies. Pellentesque rhoncus sagittis nisl id facilisis. Nunc viverra convallis risus ut + luctus. Aliquam vestibulum <b>efficitur massa</b>, id tempus nisi posuere a. Aliquam scelerisque + elit justo. Nullam a ante felis. Cras vitae lorem eu nisi feugiat hendrerit. Maecenas vitae + suscipit leo, lacinia dignissim lacus. Sed eget volutpat mi. In eu bibendum neque. Pellentesque + finibus velit a fermentum rhoncus. Maecenas leo purus, eleifend eu lacus a, condimentum sagittis + justo. +</p>"#; + + const STRING_SHORT: &str = "Lorem ipsum dolor sit amet,<foo>bar&foo\"bar\\foo/bar"; + + const EMPTY: &str = ""; + + const NO_ESCAPE: &str = "Lorem ipsum dolor sit amet,"; + + const NO_ESCAPE_LONG: &str = r#" +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin scelerisque eu urna in aliquet. +Phasellus ac nulla a urna sagittis consequat id quis est. Nullam eu ex eget erat accumsan dictum +ac lobortis urna. Etiam fermentum ut quam at dignissim. Curabitur vestibulum luctus tellus, sit +amet lobortis augue tempor faucibus. Nullam sed felis eget odio elementum euismod in sit amet massa. +Vestibulum sagittis purus sit amet eros auctor, sit amet pharetra purus dapibus. Donec ornare metus +vel dictum porta. Etiam ut nisl nisi. Nullam rutrum porttitor mi. Donec aliquam ac ipsum eget +hendrerit. Cras faucibus, eros ut pharetra imperdiet, est tellus aliquet felis, eget convallis +lacus ipsum eget quam. Vivamus orci lorem, maximus ac mi eget, bibendum vulputate massa. In +vestibulum dui hendrerit, vestibulum lacus sit amet, posuere erat. Vivamus euismod massa diam, +vulputate euismod lectus vestibulum nec. Donec sit amet massa magna. Nunc ipsum nulla, euismod +quis lacus at, gravida maximus elit. Duis tristique, nisl nullam. + "#; + + const STRINGS: &[&str] = &[ + STRING_LONG, + STRING_SHORT, + EMPTY, + NO_ESCAPE, + NO_ESCAPE_LONG, + ]; + STRINGS +} diff --git a/rinja/benches/to-json.rs b/rinja/benches/to-json.rs index 78f31908..8d83a149 100644 --- a/rinja/benches/to-json.rs +++ b/rinja/benches/to-json.rs @@ -1,4 +1,4 @@ -use criterion::{criterion_group, criterion_main, Criterion}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; use rinja::Template; criterion_main!(benches); @@ -18,7 +18,7 @@ fn escape_json(b: &mut criterion::Bencher<'_>) { b.iter(|| { let mut len = 0; - for &s in STRINGS { + for &s in black_box(STRINGS) { len += Tmpl(s).to_string().len(); } len @@ -32,7 +32,7 @@ fn escape_json_pretty(b: &mut criterion::Bencher<'_>) { b.iter(|| { let mut len = 0; - for &s in STRINGS { + for &s in black_box(STRINGS) { len += Tmpl(s).to_string().len(); } len @@ -46,7 +46,7 @@ fn escape_json_for_html(b: &mut criterion::Bencher<'_>) { b.iter(|| { let mut len = 0; - for &s in STRINGS { + for &s in black_box(STRINGS) { len += Tmpl(s).to_string().len(); } len @@ -60,65 +60,11 @@ fn escape_json_for_html_pretty(b: &mut criterion::Bencher<'_>) { b.iter(|| { let mut len = 0; - for &s in STRINGS { + for &s in black_box(STRINGS) { len += Tmpl(s).to_string().len(); } len }); } -const STRINGS: &[&str] = &[STRING_LONG, STRING_SHORT, EMPTY, NO_ESCAPE, NO_ESCAPE_LONG]; -const STRING_LONG: &str = r#" -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat tellus sit -amet ornare fermentum. Etiam nec erat ante. In at metus a orci mollis scelerisque. -Sed eget ultrices turpis, at sollicitudin erat. Integer hendrerit nec magna quis -venenatis. Vivamus non dolor hendrerit, vulputate velit sed, varius nunc. Quisque -in pharetra mi. Sed ullamcorper nibh malesuada commodo porttitor. Ut scelerisque -sodales felis quis dignissim. Morbi aliquam finibus justo, sit amet consectetur -mauris efficitur sit amet. Donec posuere turpis felis, eu lacinia magna accumsan -quis. Fusce egestas lacus vel fermentum tincidunt. Phasellus a nulla eget lectus -placerat commodo at eget nisl. Fusce cursus dui quis purus accumsan auctor. -Donec iaculis felis quis metus consectetur porttitor. -<p> -Etiam nibh mi, <b>accumsan</b> quis purus sed, posuere fermentum lorem. In pulvinar porta -maximus. Fusce tincidunt lacinia tellus sit amet tincidunt. Aliquam lacus est, pulvinar -non metus a, <b>facilisis</b> ultrices quam. Nulla feugiat leo in cursus eleifend. Suspendisse -eget nisi ac justo sagittis interdum id a ipsum. Nulla mauris justo, scelerisque ac -rutrum vitae, consequat vel ex. -</p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p></p> -<p> -Sed sollicitudin <b>sem</b> mauris, at rutrum nibh egestas vel. Ut eu nisi tellus. Praesent dignissim -orci elementum, mattis turpis eget, maximus ante. Suspendisse luctus eu felis a tempor. Morbi -ac risus vitae sem molestie ullamcorper. Curabitur ligula augue, sollicitudin quis maximus vel, -facilisis sed nibh. Aenean auctor magna sem, id rutrum metus convallis quis. Nullam non arcu -dictum, lobortis erat quis, rhoncus est. Suspendisse venenatis, mi sed venenatis vehicula, -tortor dolor egestas lectus, et efficitur turpis odio non augue. Integer velit sapien, dictum -non egestas vitae, hendrerit sed quam. Phasellus a nunc eu erat varius imperdiet. Etiam id -sollicitudin turpis, vitae molestie orci. Quisque ornare magna quis metus rhoncus commodo. -Phasellus non mauris velit. -</p> -<p> -Etiam dictum tellus ipsum, nec varius quam ornare vel. Cras vehicula diam nec sollicitudin -ultricies. Pellentesque rhoncus sagittis nisl id facilisis. Nunc viverra convallis risus ut -luctus. Aliquam vestibulum <b>efficitur massa</b>, id tempus nisi posuere a. Aliquam scelerisque -elit justo. Nullam a ante felis. Cras vitae lorem eu nisi feugiat hendrerit. Maecenas vitae -suscipit leo, lacinia dignissim lacus. Sed eget volutpat mi. In eu bibendum neque. Pellentesque -finibus velit a fermentum rhoncus. Maecenas leo purus, eleifend eu lacus a, condimentum sagittis -justo. -</p>"#; -const STRING_SHORT: &str = "Lorem ipsum dolor sit amet,<foo>bar&foo\"bar\\foo/bar"; -const EMPTY: &str = ""; -const NO_ESCAPE: &str = "Lorem ipsum dolor sit amet,"; -const NO_ESCAPE_LONG: &str = r#" -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin scelerisque eu urna in aliquet. -Phasellus ac nulla a urna sagittis consequat id quis est. Nullam eu ex eget erat accumsan dictum -ac lobortis urna. Etiam fermentum ut quam at dignissim. Curabitur vestibulum luctus tellus, sit -amet lobortis augue tempor faucibus. Nullam sed felis eget odio elementum euismod in sit amet massa. -Vestibulum sagittis purus sit amet eros auctor, sit amet pharetra purus dapibus. Donec ornare metus -vel dictum porta. Etiam ut nisl nisi. Nullam rutrum porttitor mi. Donec aliquam ac ipsum eget -hendrerit. Cras faucibus, eros ut pharetra imperdiet, est tellus aliquet felis, eget convallis -lacus ipsum eget quam. Vivamus orci lorem, maximus ac mi eget, bibendum vulputate massa. In -vestibulum dui hendrerit, vestibulum lacus sit amet, posuere erat. Vivamus euismod massa diam, -vulputate euismod lectus vestibulum nec. Donec sit amet massa magna. Nunc ipsum nulla, euismod -quis lacus at, gravida maximus elit. Duis tristique, nisl nullam. -"#; +const STRINGS: &[&str] = include!("strings.inc"); diff --git a/rinja_derive/src/config.rs b/rinja_derive/src/config.rs index b01500a8..871f893c 100644 --- a/rinja_derive/src/config.rs +++ b/rinja_derive/src/config.rs @@ -337,7 +337,7 @@ impl RawConfig<'_> { #[cfg_attr(feature = "config", derive(Deserialize))] #[cfg_attr(feature = "config", serde(field_identifier, rename_all = "lowercase"))] pub(crate) enum WhitespaceHandling { - /// The default behaviour. It will leave the whitespace characters "as is". + /// The default behavior. It will leave the whitespace characters "as is". #[default] Preserve, /// It'll remove all the whitespace characters before and after the jinja block. diff --git a/rinja_derive/src/input.rs b/rinja_derive/src/input.rs index 4f99d4e9..5b0115a3 100644 --- a/rinja_derive/src/input.rs +++ b/rinja_derive/src/input.rs @@ -433,7 +433,7 @@ impl TemplateArgs { } } -/// Try to find the souce in the comment, in a `rinja` code block. +/// Try to find the source in the comment, in a `rinja` code block. /// /// This is only done if no path or source was given in the `#[template]` attribute. fn source_from_docs( From 9f9057c88fe379b593d1bc30e7397308d3efb3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Sun, 8 Sep 2024 02:03:50 +0200 Subject: [PATCH 08/21] Fix clippy warning --- rinja/benches/escape.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rinja/benches/escape.rs b/rinja/benches/escape.rs index 83c7783b..18125939 100644 --- a/rinja/benches/escape.rs +++ b/rinja/benches/escape.rs @@ -11,7 +11,7 @@ fn functions(c: &mut Criterion) { fn escaping(b: &mut criterion::Bencher<'_>) { b.iter(|| { for &s in black_box(STRINGS) { - format!("{}", escape(s, Html).unwrap()); + let _ = black_box(format!("{}", escape(s, Html).unwrap())); } }); } From c38007cf25720997957e4e96b403535ad02ec0c8 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez <guillaume1.gomez@gmail.com> Date: Tue, 10 Sep 2024 02:39:59 +0200 Subject: [PATCH 09/21] Improve error message for unclosed items --- rinja_parser/src/node.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index 1e8efa9e..eb7b1126 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -318,6 +318,22 @@ impl Whitespace { } } +fn check_block_start<'a>( + i: &'a str, + start: &'a str, + s: &State<'_>, + node: &str, + expected: &str, +) -> ParseResult<'a> { + if i.is_empty() { + return Err(nom::Err::Failure(ErrorContext::new( + format!("expected `{expected}` to terminate `{node}` node, found nothing"), + start, + ))); + } + s.tag_block_start(i) +} + #[derive(Debug, PartialEq)] pub struct Loop<'a> { pub ws1: Ws, @@ -376,7 +392,7 @@ impl<'a> Loop<'a> { cut(tuple(( |i| content(i, s), cut(tuple(( - |i| s.tag_block_start(i), + |i| check_block_start(i, start, s, "for", "endfor"), opt(Whitespace::parse), opt(else_block), end_node("for", "endfor"), @@ -449,7 +465,7 @@ impl<'a> Macro<'a> { let mut end = cut(tuple(( |i| Node::many(i, s), cut(tuple(( - |i| s.tag_block_start(i), + |i| check_block_start(i, start_s, s, "macro", "endmacro"), opt(Whitespace::parse), end_node("macro", "endmacro"), cut(preceded( @@ -525,7 +541,7 @@ impl<'a> FilterBlock<'a> { let mut end = cut(tuple(( |i| Node::many(i, s), cut(tuple(( - |i| s.tag_block_start(i), + |i| check_block_start(i, start_s, s, "filter", "endfilter"), opt(Whitespace::parse), end_node("filter", "endfilter"), opt(Whitespace::parse), @@ -645,7 +661,7 @@ impl<'a> Match<'a> { cut(tuple(( opt(|i| When::r#match(i, s)), cut(tuple(( - ws(|i| s.tag_block_start(i)), + ws(|i| check_block_start(i, start, s, "match", "endmatch")), opt(Whitespace::parse), end_node("match", "endmatch"), opt(Whitespace::parse), @@ -704,7 +720,7 @@ impl<'a> BlockDef<'a> { let mut end = cut(tuple(( |i| Node::many(i, s), cut(tuple(( - |i| s.tag_block_start(i), + |i| check_block_start(i, start_s, s, "block", "endblock"), opt(Whitespace::parse), end_node("block", "endblock"), cut(tuple(( @@ -893,7 +909,7 @@ impl<'a> If<'a> { |i| Node::many(i, s), many0(|i| Cond::parse(i, s)), cut(tuple(( - |i| s.tag_block_start(i), + |i| check_block_start(i, start, s, "if", "endif"), opt(Whitespace::parse), end_node("if", "endif"), opt(Whitespace::parse), From a60c2e7db976717e99d8ca7dd8368d61b7942300 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez <guillaume1.gomez@gmail.com> Date: Tue, 10 Sep 2024 02:40:12 +0200 Subject: [PATCH 10/21] Add regression test for unclosed items --- testing/tests/ui/end-block.rs | 61 +++++++++++++++++++++++++++++++ testing/tests/ui/end-block.stderr | 55 ++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 testing/tests/ui/end-block.rs create mode 100644 testing/tests/ui/end-block.stderr diff --git a/testing/tests/ui/end-block.rs b/testing/tests/ui/end-block.rs new file mode 100644 index 00000000..0e7bd1ee --- /dev/null +++ b/testing/tests/ui/end-block.rs @@ -0,0 +1,61 @@ +use rinja::Template; + +#[derive(Template)] +#[template( + ext = "html", + source = "{% if x %}{% if x %}{% endif %}", +)] +struct EndIf { + x: bool, +} + +#[derive(Template)] +#[template( + ext = "html", + source = "{% if x %}", +)] +struct EndIf2 { + x: bool, +} + +#[derive(Template)] +#[template( + ext = "html", + source = "{% match x %}", +)] +struct EndMatch { + x: bool, +} + +#[derive(Template)] +#[template( + ext = "html", + source = "{% for a in x %}", +)] +struct EndFor { + x: [u32; 2], +} + +#[derive(Template)] +#[template( + ext = "html", + source = "{% macro bla %}", +)] +struct EndMacro; + +#[derive(Template)] +#[template( + ext = "html", + source = "{% filter bla %}", +)] +struct EndFilter; + +#[derive(Template)] +#[template( + ext = "html", + source = "{% block bla %}", +)] +struct EndBlock; + +fn main() { +} diff --git a/testing/tests/ui/end-block.stderr b/testing/tests/ui/end-block.stderr new file mode 100644 index 00000000..1c474101 --- /dev/null +++ b/testing/tests/ui/end-block.stderr @@ -0,0 +1,55 @@ +error: expected `endif` to terminate `if` node, found nothing + --> <source attribute>:1:2 + " if x %}{% if x %}{% endif %}" + --> tests/ui/end-block.rs:6:14 + | +6 | source = "{% if x %}{% if x %}{% endif %}", + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected `endif` to terminate `if` node, found nothing + --> <source attribute>:1:2 + " if x %}" + --> tests/ui/end-block.rs:15:14 + | +15 | source = "{% if x %}", + | ^^^^^^^^^^^^ + +error: expected `endmatch` to terminate `match` node, found nothing + --> <source attribute>:1:2 + " match x %}" + --> tests/ui/end-block.rs:24:14 + | +24 | source = "{% match x %}", + | ^^^^^^^^^^^^^^^ + +error: expected `endfor` to terminate `for` node, found nothing + --> <source attribute>:1:2 + " for a in x %}" + --> tests/ui/end-block.rs:33:14 + | +33 | source = "{% for a in x %}", + | ^^^^^^^^^^^^^^^^^^ + +error: expected `endmacro` to terminate `macro` node, found nothing + --> <source attribute>:1:2 + " macro bla %}" + --> tests/ui/end-block.rs:42:14 + | +42 | source = "{% macro bla %}", + | ^^^^^^^^^^^^^^^^^ + +error: expected `endfilter` to terminate `filter` node, found nothing + --> <source attribute>:1:2 + " filter bla %}" + --> tests/ui/end-block.rs:49:14 + | +49 | source = "{% filter bla %}", + | ^^^^^^^^^^^^^^^^^^ + +error: expected `endblock` to terminate `block` node, found nothing + --> <source attribute>:1:2 + " block bla %}" + --> tests/ui/end-block.rs:56:14 + | +56 | source = "{% block bla %}", + | ^^^^^^^^^^^^^^^^^ From f9932a03ffb1db38a877bad47e9531690fd20a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Mon, 9 Sep 2024 23:24:27 +0200 Subject: [PATCH 11/21] parser: `When::match()` matches `else` case --- rinja_parser/src/node.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index eb7b1126..9d27ec49 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -175,7 +175,7 @@ pub struct When<'a> { } impl<'a> When<'a> { - fn r#match(i: &'a str, s: &State<'_>) -> ParseResult<'a, WithSpan<'a, Self>> { + fn r#else(i: &'a str, s: &State<'_>) -> ParseResult<'a, WithSpan<'a, Self>> { let start = i; let mut p = tuple(( |i| s.tag_block_start(i), @@ -658,15 +658,15 @@ impl<'a> Match<'a> { cut(tuple(( ws(many0(ws(value((), |i| Comment::parse(i, s))))), many0(|i| When::when(i, s)), - cut(tuple(( - opt(|i| When::r#match(i, s)), + cut(pair( + opt(|i| When::r#else(i, s)), cut(tuple(( ws(|i| check_block_start(i, start, s, "match", "endmatch")), opt(Whitespace::parse), end_node("match", "endmatch"), opt(Whitespace::parse), ))), - ))), + )), ))), ))), )); From 7b4f1dc907e0329c6c434e1d69ced35b747ab46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Tue, 10 Sep 2024 00:00:31 +0200 Subject: [PATCH 12/21] parser: add optional `{% endwhen %}` --- rinja_parser/src/node.rs | 33 ++++++++++++++++++++++++++++++++- testing/tests/matches.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index 9d27ec49..d9d501f3 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -204,6 +204,33 @@ impl<'a> When<'a> { #[allow(clippy::self_named_constructors)] fn when(i: &'a str, s: &State<'_>) -> ParseResult<'a, WithSpan<'a, Self>> { let start = i; + let endwhen = map( + consumed(ws(pair( + delimited( + |i| s.tag_block_start(i), + opt(Whitespace::parse), + ws(keyword("endwhen")), + ), + cut(tuple(( + opt(Whitespace::parse), + |i| s.tag_block_end(i), + many0(value((), ws(|i| Comment::parse(i, s)))), + ))), + ))), + |(span, (pws, _))| { + // A comment node is used to pass the whitespace suppressing information to the + // generator. This way we don't have to fix up the next `when` node or the closing + // `endmatch`. Any whitespaces after `endwhen` are to be suppressed. Actually, they + // don't wind up in the AST anyway. + Node::Comment(WithSpan::new( + Comment { + ws: Ws(pws, Some(Whitespace::Suppress)), + content: "", + }, + span, + )) + }, + ); let mut p = tuple(( |i| s.tag_block_start(i), opt(Whitespace::parse), @@ -213,9 +240,13 @@ impl<'a> When<'a> { opt(Whitespace::parse), |i| s.tag_block_end(i), cut(|i| Node::many(i, s)), + opt(endwhen), ))), )); - let (i, (_, pws, _, (target, nws, _, nodes))) = p(i)?; + let (i, (_, pws, _, (target, nws, _, mut nodes, endwhen))) = p(i)?; + if let Some(endwhen) = endwhen { + nodes.push(endwhen); + } Ok(( i, WithSpan::new( diff --git a/testing/tests/matches.rs b/testing/tests/matches.rs index 6102b237..65ac0d24 100644 --- a/testing/tests/matches.rs +++ b/testing/tests/matches.rs @@ -265,3 +265,38 @@ fn test_match_with_patterns() { let s = MatchPatterns { n: 12 }; assert_eq!(s.render().unwrap(), "12"); } + +#[derive(Template)] +#[template(in_doc = true, ext = "html")] +/// ```rinja +/// {% match result %} +/// {% when Some(Ok(s)) -%} +/// good: {{s}} +/// {%- endwhen +%} +/// {# This is not good: #} +/// {%+ when Some(Err(s)) -%} +/// bad: {{s}} +/// {%- endwhen +%} +/// {%+ else -%} +/// unprocessed +/// {% endmatch %} +/// ``` +struct EndWhen<'a> { + result: Option<Result<&'a str, &'a str>>, +} + +#[test] +fn test_end_when() { + let tmpl = EndWhen { + result: Some(Ok("msg")), + }; + assert_eq!(tmpl.to_string(), "good: msg"); + + let tmpl = EndWhen { + result: Some(Err("msg")), + }; + assert_eq!(tmpl.to_string(), "bad: msg"); + + let tmpl = EndWhen { result: None }; + assert_eq!(tmpl.to_string(), "unprocessed\n"); +} From f1d4c1ba1eee1a7d5557b28670fbb604fe134c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Tue, 10 Sep 2024 00:03:52 +0200 Subject: [PATCH 13/21] book: add `{% endwhen %}` --- book/src/template_syntax.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index eb708a9a..55531598 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -619,6 +619,24 @@ You can also match against multiple alternative patterns at once: {% endmatch %} ``` +For better interoperability with linters and auto-formatters like [djLint], +you can also use a optional `{% endwhen %}` node to close a `{% when %}` case: + +```jinja +{% match number %} + {% when 0 | 2 | 4 | 6 | 8 %} + even + {% endwhen %} + {% when 1 | 3 | 5 | 7 | 9 %} + odd + {% endwhen %} + {% else } + unknown +{% endmatch %} +``` + +[djLint]: <https://github.com/djlint/djlint> + ### Referencing and dereferencing variables If you need to put something behind a reference or to dereference it, you From 1948d632414b70602ba6cb31508801ec94164abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Tue, 10 Sep 2024 03:16:44 +0200 Subject: [PATCH 14/21] parser: add context information for unknown tags --- rinja_parser/src/lib.rs | 25 +- rinja_parser/src/node.rs | 546 ++++++++++++++++--------- rinja_parser/src/tests.rs | 8 +- testing/tests/ui/rinja-block.stderr | 2 +- testing/tests/ui/unexpected-tag.rs | 55 +++ testing/tests/ui/unexpected-tag.stderr | 47 +++ 6 files changed, 481 insertions(+), 202 deletions(-) create mode 100644 testing/tests/ui/unexpected-tag.rs create mode 100644 testing/tests/ui/unexpected-tag.stderr diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs index 5e7ce1fd..fe01c712 100644 --- a/rinja_parser/src/lib.rs +++ b/rinja_parser/src/lib.rs @@ -13,10 +13,10 @@ use std::{fmt, str}; use nom::branch::alt; use nom::bytes::complete::{escaped, is_not, tag, take_till, take_while_m_n}; use nom::character::complete::{anychar, char, one_of, satisfy}; -use nom::combinator::{complete, consumed, cut, eof, fail, map, not, opt, recognize, value}; +use nom::combinator::{consumed, cut, fail, map, not, opt, recognize, value}; use nom::error::{ErrorKind, FromExternalError}; use nom::multi::{many0_count, many1}; -use nom::sequence::{delimited, pair, preceded, terminated, tuple}; +use nom::sequence::{delimited, pair, preceded, tuple}; use nom::{AsChar, InputTakeAtPosition}; pub mod expr; @@ -110,21 +110,18 @@ impl<'a> Ast<'a> { file_path: Option<Arc<Path>>, syntax: &Syntax<'_>, ) -> Result<Self, ParseError> { - let parse = |i: &'a str| Node::many(i, &State::new(syntax)); - let (input, message) = match complete(terminated(parse, cut(eof)))(src) { - Ok(("", nodes)) => return Ok(Self { nodes }), - Ok(_) => unreachable!("eof() is not eof?"), - Err(nom::Err::Incomplete(_)) => unreachable!("complete() is not complete?"), + match Node::parse_template(src, &State::new(syntax)) { + Ok(("", nodes)) => Ok(Self { nodes }), + Ok(_) | Err(nom::Err::Incomplete(_)) => unreachable!(), Err( nom::Err::Error(ErrorContext { input, message, .. }) | nom::Err::Failure(ErrorContext { input, message, .. }), - ) => (input, message), - }; - Err(ParseError { - message, - offset: src.len() - input.len(), - file_path, - }) + ) => Err(ParseError { + message, + offset: src.len() - input.len(), + file_path, + }), + } } pub fn nodes(&self) -> &[Node<'a>] { diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index d9d501f3..03ba6830 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -35,13 +35,38 @@ pub enum Node<'a> { } impl<'a> Node<'a> { - pub(super) fn many(i: &'a str, s: &State<'_>) -> ParseResult<'a, Vec<Self>> { - complete(many0(alt(( + pub(super) fn parse_template(i: &'a str, s: &State<'_>) -> ParseResult<'a, Vec<Self>> { + let (i, result) = match complete(|i| Self::many(i, s))(i) { + Ok((i, result)) => (i, result), + Err(err) => { + if let nom::Err::Error(err) | nom::Err::Failure(err) = &err { + if err.message.is_none() { + opt(|i| unexpected_tag(i, s))(err.input)?; + } + } + return Err(err); + } + }; + let (i, _) = opt(|i| unexpected_tag(i, s))(i)?; + let (i, is_eof) = opt(eof)(i)?; + if is_eof.is_none() { + return Err(nom::Err::Failure(ErrorContext::new( + "cannot parse entire template\n\ + you should never encounter this error\n\ + please report this error to <https://github.com/rinja-rs/rinja/issues>", + i, + ))); + } + Ok((i, result)) + } + + fn many(i: &'a str, s: &State<'_>) -> ParseResult<'a, Vec<Self>> { + many0(alt(( map(|i| Lit::parse(i, s), Self::Lit), map(|i| Comment::parse(i, s), Self::Comment), |i| Self::expr(i, s), |i| Self::parse(i, s), - ))))(i) + )))(i) } fn parse(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { @@ -77,15 +102,15 @@ impl<'a> Node<'a> { "break" => |i, s| Self::r#break(i, s), "continue" => |i, s| Self::r#continue(i, s), "filter" => |i, s| wrap(Self::FilterBlock, FilterBlock::parse(i, s)), - _ => return fail(i), + _ => return fail(start), }; let (i, node) = s.nest(j, |i| func(i, s))?; - let (i, closed) = cut(alt(( - value(true, |i| s.tag_block_end(i)), - value(false, ws(eof)), - )))(i)?; + let (i, closed) = cut_node( + None, + alt((value(true, |i| s.tag_block_end(i)), value(false, ws(eof)))), + )(i)?; match closed { true => Ok((i, node)), false => Err(ErrorContext::unclosed("block", s.syntax.block_end, start).into()), @@ -128,16 +153,22 @@ impl<'a> Node<'a> { let start = i; let (i, (pws, expr)) = preceded( |i| s.tag_expr_start(i), - cut(pair( - opt(Whitespace::parse), - ws(|i| Expr::parse(i, s.level.get())), - )), + cut_node( + None, + pair( + opt(Whitespace::parse), + ws(|i| Expr::parse(i, s.level.get())), + ), + ), )(i)?; - let (i, (nws, closed)) = cut(pair( - opt(Whitespace::parse), - alt((value(true, |i| s.tag_expr_end(i)), value(false, ws(eof)))), - ))(i)?; + let (i, (nws, closed)) = cut_node( + None, + pair( + opt(Whitespace::parse), + alt((value(true, |i| s.tag_expr_end(i)), value(false, ws(eof)))), + ), + )(i)?; match closed { true => Ok((i, Self::Expr(Ws(pws, nws), expr))), false => Err(ErrorContext::unclosed("expression", s.syntax.expr_end, start).into()), @@ -167,6 +198,48 @@ impl<'a> Node<'a> { } } +fn cut_node<'a, O>( + kind: Option<&'static str>, + inner: impl FnMut(&'a str) -> ParseResult<'a, O>, +) -> impl FnMut(&'a str) -> ParseResult<'a, O> { + let mut inner = cut(inner); + move |i: &'a str| { + let result = inner(i); + if let Err(nom::Err::Failure(err) | nom::Err::Error(err)) = &result { + if err.message.is_none() { + opt(|i| unexpected_raw_tag(kind, i))(err.input)?; + } + } + result + } +} + +fn unexpected_tag<'a>(i: &'a str, s: &State<'_>) -> ParseResult<'a, ()> { + value( + (), + tuple(( + |i| s.tag_block_start(i), + opt(Whitespace::parse), + |i| unexpected_raw_tag(None, i), + )), + )(i) +} + +fn unexpected_raw_tag<'a>(kind: Option<&'static str>, i: &'a str) -> ParseResult<'a, ()> { + let (_, tag) = ws(identifier)(i)?; + let msg = match tag { + "end" | "elif" | "else" | "when" => match kind { + Some(kind) => { + format!("node `{tag}` was not expected in the current context: `{kind}` block") + } + None => format!("node `{tag}` was not expected in the current context"), + }, + tag if tag.starts_with("end") => format!("unexpected closing tag `{tag}`"), + tag => format!("unknown node `{tag}`"), + }; + Err(nom::Err::Failure(ErrorContext::new(msg, i))) +} + #[derive(Debug, PartialEq)] pub struct When<'a> { pub ws: Ws, @@ -181,11 +254,14 @@ impl<'a> When<'a> { |i| s.tag_block_start(i), opt(Whitespace::parse), ws(keyword("else")), - cut(tuple(( - opt(Whitespace::parse), - |i| s.tag_block_end(i), - cut(|i| Node::many(i, s)), - ))), + cut_node( + Some("match-else"), + tuple(( + opt(Whitespace::parse), + |i| s.tag_block_end(i), + cut_node(Some("match-else"), |i| Node::many(i, s)), + )), + ), )); let (i, (_, pws, _, (nws, _, nodes))) = p(i)?; Ok(( @@ -211,11 +287,14 @@ impl<'a> When<'a> { opt(Whitespace::parse), ws(keyword("endwhen")), ), - cut(tuple(( - opt(Whitespace::parse), - |i| s.tag_block_end(i), - many0(value((), ws(|i| Comment::parse(i, s)))), - ))), + cut_node( + Some("match-endwhen"), + tuple(( + opt(Whitespace::parse), + |i| s.tag_block_end(i), + many0(value((), ws(|i| Comment::parse(i, s)))), + )), + ), ))), |(span, (pws, _))| { // A comment node is used to pass the whitespace suppressing information to the @@ -235,13 +314,16 @@ impl<'a> When<'a> { |i| s.tag_block_start(i), opt(Whitespace::parse), ws(keyword("when")), - cut(tuple(( - separated_list1(char('|'), ws(|i| Target::parse(i, s))), - opt(Whitespace::parse), - |i| s.tag_block_end(i), - cut(|i| Node::many(i, s)), - opt(endwhen), - ))), + cut_node( + Some("match-when"), + tuple(( + separated_list1(char('|'), ws(|i| Target::parse(i, s))), + opt(Whitespace::parse), + |i| s.tag_block_end(i), + cut_node(Some("match-when"), |i| Node::many(i, s)), + opt(endwhen), + )), + ), )); let (i, (_, pws, _, (target, nws, _, mut nodes, endwhen))) = p(i)?; if let Some(endwhen) = endwhen { @@ -278,12 +360,12 @@ impl<'a> Cond<'a> { preceded(ws(keyword("else")), opt(|i| CondTest::parse(i, s))), preceded( ws(keyword("elif")), - cut(map(|i| CondTest::parse_cond(i, s), Some)), + cut_node(Some("if-elif"), map(|i| CondTest::parse_cond(i, s), Some)), ), )), opt(Whitespace::parse), - cut(|i| s.tag_block_end(i)), - cut(|i| Node::many(i, s)), + cut_node(Some("if"), |i| s.tag_block_end(i)), + cut_node(Some("if"), |i| Node::many(i, s)), ))(i)?; Ok(( i, @@ -308,7 +390,10 @@ pub struct CondTest<'a> { impl<'a> CondTest<'a> { fn parse(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { - preceded(ws(keyword("if")), cut(|i| Self::parse_cond(i, s)))(i) + preceded( + ws(keyword("if")), + cut_node(Some("if"), |i| Self::parse_cond(i, s)), + )(i) } fn parse_cond(i: &'a str, s: &State<'_>) -> ParseResult<'a, Self> { @@ -389,53 +474,72 @@ impl<'a> Loop<'a> { let start = i; let if_cond = preceded( ws(keyword("if")), - cut(ws(|i| Expr::parse(i, s.level.get()))), + cut_node(Some("for-if"), ws(|i| Expr::parse(i, s.level.get()))), ); let else_block = |i| { let mut p = preceded( ws(keyword("else")), - cut(tuple(( - opt(Whitespace::parse), - delimited( - |i| s.tag_block_end(i), - |i| Node::many(i, s), - |i| s.tag_block_start(i), - ), - opt(Whitespace::parse), - ))), + cut_node( + Some("for-else"), + tuple(( + opt(Whitespace::parse), + delimited( + |i| s.tag_block_end(i), + |i| Node::many(i, s), + |i| s.tag_block_start(i), + ), + opt(Whitespace::parse), + )), + ), ); let (i, (pws, nodes, nws)) = p(i)?; Ok((i, (pws, nodes, nws))) }; - let mut p = tuple(( - opt(Whitespace::parse), - ws(keyword("for")), - cut(tuple(( - ws(|i| Target::parse(i, s)), - ws(keyword("in")), - cut(tuple(( - ws(|i| Expr::parse(i, s.level.get())), - opt(if_cond), - opt(Whitespace::parse), - |i| s.tag_block_end(i), - cut(tuple(( - |i| content(i, s), - cut(tuple(( + let body_and_end = |i| { + let (i, (body, (_, pws, else_block, _, nws))) = cut_node( + Some("for"), + tuple(( + |i| content(i, s), + cut_node( + Some("for"), + tuple(( |i| check_block_start(i, start, s, "for", "endfor"), opt(Whitespace::parse), opt(else_block), end_node("for", "endfor"), opt(Whitespace::parse), - ))), - ))), - ))), - ))), + )), + ), + )), + )(i)?; + Ok((i, (body, pws, else_block, nws))) + }; + + let mut p = tuple(( + opt(Whitespace::parse), + ws(keyword("for")), + cut_node( + Some("for"), + tuple(( + ws(|i| Target::parse(i, s)), + ws(keyword("in")), + cut_node( + Some("for"), + tuple(( + ws(|i| Expr::parse(i, s.level.get())), + opt(if_cond), + opt(Whitespace::parse), + |i| s.tag_block_end(i), + body_and_end, + )), + ), + )), + ), )); - let (i, (pws1, _, (var, _, (iter, cond, nws1, _, (body, (_, pws2, else_block, _, nws2)))))) = - p(i)?; - let (nws3, else_block, pws3) = else_block.unwrap_or_default(); + let (i, (pws1, _, (var, _, (iter, cond, nws1, _, (body, pws2, else_block, nws2))))) = p(i)?; + let (nws3, else_nodes, pws3) = else_block.unwrap_or_default(); Ok(( i, WithSpan::new( @@ -446,7 +550,7 @@ impl<'a> Loop<'a> { cond, body, ws2: Ws(pws2, nws3), - else_nodes: else_block, + else_nodes, ws3: Ws(pws3, nws2), }, start, @@ -478,12 +582,15 @@ impl<'a> Macro<'a> { let mut start = tuple(( opt(Whitespace::parse), ws(keyword("macro")), - cut(tuple(( - ws(identifier), - opt(ws(parameters)), - opt(Whitespace::parse), - |i| s.tag_block_end(i), - ))), + cut_node( + Some("macro"), + tuple(( + ws(identifier), + opt(ws(parameters)), + opt(Whitespace::parse), + |i| s.tag_block_end(i), + )), + ), )); let (j, (pws1, _, (name, params, nws1, _))) = start(i)?; if is_rust_keyword(name) { @@ -493,21 +600,30 @@ impl<'a> Macro<'a> { ))); } - let mut end = cut(tuple(( - |i| Node::many(i, s), - cut(tuple(( - |i| check_block_start(i, start_s, s, "macro", "endmacro"), - opt(Whitespace::parse), - end_node("macro", "endmacro"), - cut(preceded( - opt(|before| { - let (after, end_name) = ws(identifier)(before)?; - check_end_name(before, after, name, end_name, "macro") - }), - opt(Whitespace::parse), - )), - ))), - ))); + let mut end = cut_node( + Some("macro"), + tuple(( + |i| Node::many(i, s), + cut_node( + Some("macro"), + tuple(( + |i| check_block_start(i, start_s, s, "macro", "endmacro"), + opt(Whitespace::parse), + end_node("macro", "endmacro"), + cut_node( + Some("macro"), + preceded( + opt(|before| { + let (after, end_name) = ws(identifier)(before)?; + check_end_name(before, after, name, end_name, "macro") + }), + opt(Whitespace::parse), + ), + ), + )), + ), + )), + ); let (i, (contents, (_, pws2, _, nws2))) = end(j)?; Ok(( @@ -541,14 +657,19 @@ impl<'a> FilterBlock<'a> { let mut start = tuple(( opt(Whitespace::parse), ws(keyword("filter")), - cut(tuple(( - ws(identifier), - opt(|i| Expr::arguments(i, s.level.get(), false)), - many0(|i| filter(i, &mut level).map(|(j, (name, params))| (j, (name, params, i)))), - ws(|i| Ok((i, ()))), - opt(Whitespace::parse), - |i| s.tag_block_end(i), - ))), + cut_node( + Some("filter"), + tuple(( + ws(identifier), + opt(|i| Expr::arguments(i, s.level.get(), false)), + many0(|i| { + filter(i, &mut level).map(|(j, (name, params))| (j, (name, params, i))) + }), + ws(|i| Ok((i, ()))), + opt(Whitespace::parse), + |i| s.tag_block_end(i), + )), + ), )); let (i, (pws1, _, (filter_name, params, extra_filters, _, nws1, _))) = start(i)?; @@ -569,15 +690,21 @@ impl<'a> FilterBlock<'a> { }; } - let mut end = cut(tuple(( - |i| Node::many(i, s), - cut(tuple(( - |i| check_block_start(i, start_s, s, "filter", "endfilter"), - opt(Whitespace::parse), - end_node("filter", "endfilter"), - opt(Whitespace::parse), - ))), - ))); + let mut end = cut_node( + Some("filter"), + tuple(( + |i| Node::many(i, s), + cut_node( + Some("filter"), + tuple(( + |i| check_block_start(i, start_s, s, "filter", "endfilter"), + opt(Whitespace::parse), + end_node("filter", "endfilter"), + opt(Whitespace::parse), + )), + ), + )), + ); let (i, (nodes, (_, pws2, _, nws2))) = end(i)?; Ok(( @@ -608,11 +735,14 @@ impl<'a> Import<'a> { let mut p = tuple(( opt(Whitespace::parse), ws(keyword("import")), - cut(tuple(( - ws(str_lit_without_prefix), - ws(keyword("as")), - cut(pair(ws(identifier), opt(Whitespace::parse))), - ))), + cut_node( + Some("import"), + tuple(( + ws(str_lit_without_prefix), + ws(keyword("as")), + cut_node(Some("import"), pair(ws(identifier), opt(Whitespace::parse))), + )), + ), )); let (i, (pws, _, (path, _, (scope, nws)))) = p(i)?; Ok(( @@ -643,12 +773,15 @@ impl<'a> Call<'a> { let mut p = tuple(( opt(Whitespace::parse), ws(keyword("call")), - cut(tuple(( - opt(tuple((ws(identifier), ws(tag("::"))))), - ws(identifier), - opt(ws(|nested| Expr::arguments(nested, s.level.get(), true))), - opt(Whitespace::parse), - ))), + cut_node( + Some("call"), + tuple(( + opt(tuple((ws(identifier), ws(tag("::"))))), + ws(identifier), + opt(ws(|nested| Expr::arguments(nested, s.level.get(), true))), + opt(Whitespace::parse), + )), + ), )); let (i, (pws, _, (scope, name, args, nws))) = p(i)?; let scope = scope.map(|(scope, _)| scope); @@ -682,24 +815,38 @@ impl<'a> Match<'a> { let mut p = tuple(( opt(Whitespace::parse), ws(keyword("match")), - cut(tuple(( - ws(|i| Expr::parse(i, s.level.get())), - opt(Whitespace::parse), - |i| s.tag_block_end(i), - cut(tuple(( - ws(many0(ws(value((), |i| Comment::parse(i, s))))), - many0(|i| When::when(i, s)), - cut(pair( - opt(|i| When::r#else(i, s)), - cut(tuple(( - ws(|i| check_block_start(i, start, s, "match", "endmatch")), - opt(Whitespace::parse), - end_node("match", "endmatch"), - opt(Whitespace::parse), - ))), - )), - ))), - ))), + cut_node( + Some("match"), + tuple(( + ws(|i| Expr::parse(i, s.level.get())), + opt(Whitespace::parse), + |i| s.tag_block_end(i), + cut_node( + Some("match"), + tuple(( + ws(many0(ws(value((), |i| Comment::parse(i, s))))), + many0(|i| When::when(i, s)), + cut_node( + Some("match"), + tuple(( + opt(|i| When::r#else(i, s)), + cut_node( + Some("match"), + tuple(( + ws(|i| { + check_block_start(i, start, s, "match", "endmatch") + }), + opt(Whitespace::parse), + end_node("match", "endmatch"), + opt(Whitespace::parse), + )), + ), + )), + ), + )), + ), + )), + ), )); let (i, (pws1, _, (expr, nws1, _, (_, mut arms, (else_arm, (_, pws2, _, nws2)))))) = p(i)?; @@ -742,27 +889,39 @@ impl<'a> BlockDef<'a> { let mut start = tuple(( opt(Whitespace::parse), ws(keyword("block")), - cut(tuple((ws(identifier), opt(Whitespace::parse), |i| { - s.tag_block_end(i) - }))), + cut_node( + Some("block"), + tuple((ws(identifier), opt(Whitespace::parse), |i| { + s.tag_block_end(i) + })), + ), )); let (i, (pws1, _, (name, nws1, _))) = start(i)?; - let mut end = cut(tuple(( - |i| Node::many(i, s), - cut(tuple(( - |i| check_block_start(i, start_s, s, "block", "endblock"), - opt(Whitespace::parse), - end_node("block", "endblock"), - cut(tuple(( - opt(|before| { - let (after, end_name) = ws(identifier)(before)?; - check_end_name(before, after, name, end_name, "block") - }), - opt(Whitespace::parse), - ))), - ))), - ))); + let mut end = cut_node( + Some("block"), + tuple(( + |i| Node::many(i, s), + cut_node( + Some("block"), + tuple(( + |i| check_block_start(i, start_s, s, "block", "endblock"), + opt(Whitespace::parse), + end_node("block", "endblock"), + cut_node( + Some("block"), + tuple(( + opt(|before| { + let (after, end_name) = ws(identifier)(before)?; + check_end_name(before, after, name, end_name, "block") + }), + opt(Whitespace::parse), + )), + ), + )), + ), + )), + ); let (i, (nodes, (_, pws2, _, (_, nws2)))) = end(i)?; Ok(( @@ -868,11 +1027,14 @@ impl<'a> Raw<'a> { let mut p = tuple(( opt(Whitespace::parse), ws(keyword("raw")), - cut(tuple(( - opt(Whitespace::parse), - |i| s.tag_block_end(i), - consumed(skip_till(Splitter1::new(s.syntax.block_start), endraw)), - ))), + cut_node( + Some("raw"), + tuple(( + opt(Whitespace::parse), + |i| s.tag_block_end(i), + consumed(skip_till(Splitter1::new(s.syntax.block_start), endraw)), + )), + ), )); let (_, (pws1, _, (nws1, _, (contents, (i, (_, pws2, _, nws2, _)))))) = p(i)?; @@ -896,14 +1058,17 @@ impl<'a> Let<'a> { let mut p = tuple(( opt(Whitespace::parse), ws(alt((keyword("let"), keyword("set")))), - cut(tuple(( - ws(|i| Target::parse(i, s)), - opt(preceded( - ws(char('=')), - ws(|i| Expr::parse(i, s.level.get())), + cut_node( + Some("let"), + tuple(( + ws(|i| Target::parse(i, s)), + opt(preceded( + ws(char('=')), + ws(|i| Expr::parse(i, s.level.get())), + )), + opt(Whitespace::parse), )), - opt(Whitespace::parse), - ))), + ), )); let (i, (pws, _, (var, val, nws))) = p(i)?; @@ -933,20 +1098,29 @@ impl<'a> If<'a> { let mut p = tuple(( opt(Whitespace::parse), |i| CondTest::parse(i, s), - cut(tuple(( - opt(Whitespace::parse), - |i| s.tag_block_end(i), - cut(tuple(( - |i| Node::many(i, s), - many0(|i| Cond::parse(i, s)), - cut(tuple(( - |i| check_block_start(i, start, s, "if", "endif"), - opt(Whitespace::parse), - end_node("if", "endif"), - opt(Whitespace::parse), - ))), - ))), - ))), + cut_node( + Some("if"), + tuple(( + opt(Whitespace::parse), + |i| s.tag_block_end(i), + cut_node( + Some("if"), + tuple(( + |i| Node::many(i, s), + many0(|i| Cond::parse(i, s)), + cut_node( + Some("if"), + tuple(( + |i| check_block_start(i, start, s, "if", "endif"), + opt(Whitespace::parse), + end_node("if", "endif"), + opt(Whitespace::parse), + )), + ), + )), + ), + )), + ), )); let (i, (pws1, cond, (nws1, _, (nodes, elifs, (_, pws2, _, nws2))))) = p(i)?; @@ -985,7 +1159,10 @@ impl<'a> Include<'a> { let mut p = tuple(( opt(Whitespace::parse), ws(keyword("include")), - cut(pair(ws(str_lit_without_prefix), opt(Whitespace::parse))), + cut_node( + Some("include"), + pair(ws(str_lit_without_prefix), opt(Whitespace::parse)), + ), )); let (i, (pws, _, (path, nws))) = p(i)?; Ok(( @@ -1013,7 +1190,10 @@ impl<'a> Extends<'a> { let (i, (pws, _, (path, nws))) = tuple(( opt(Whitespace::parse), ws(keyword("extends")), - cut(pair(ws(str_lit_without_prefix), opt(Whitespace::parse))), + cut_node( + Some("extends"), + pair(ws(str_lit_without_prefix), opt(Whitespace::parse)), + ), ))(i)?; match (pws, nws) { (None, None) => Ok((i, WithSpan::new(Self { path }, start))), @@ -1079,7 +1259,7 @@ impl<'a> Comment<'a> { let start = i; let (i, (pws, content)) = pair( preceded(|i| s.tag_comment_start(i), opt(Whitespace::parse)), - recognize(cut(|i| content(i, s))), + recognize(cut_node(Some("comment"), |i| content(i, s))), )(i)?; let mut nws = None; diff --git a/rinja_parser/src/tests.rs b/rinja_parser/src/tests.rs index 92ddbc4f..46358bae 100644 --- a/rinja_parser/src/tests.rs +++ b/rinja_parser/src/tests.rs @@ -913,10 +913,10 @@ fn test_parse_tuple() { fn test_missing_space_after_kw() { let syntax = Syntax::default(); let err = Ast::from_str("{%leta=b%}", None, &syntax).unwrap_err(); - assert!(matches!( - &*err.to_string(), - "failed to parse template source near offset 0", - )); + assert_eq!( + err.to_string(), + "unknown node `leta`\nfailed to parse template source near offset 2", + ); } #[test] diff --git a/testing/tests/ui/rinja-block.stderr b/testing/tests/ui/rinja-block.stderr index 2f309e56..976951ab 100644 --- a/testing/tests/ui/rinja-block.stderr +++ b/testing/tests/ui/rinja-block.stderr @@ -1,4 +1,4 @@ -error: failed to parse template source +error: unknown node `fail` --> <source attribute>:2:6 " fail %}\n{% endif %}" --> tests/ui/rinja-block.rs:5:1 diff --git a/testing/tests/ui/unexpected-tag.rs b/testing/tests/ui/unexpected-tag.rs new file mode 100644 index 00000000..940b9cda --- /dev/null +++ b/testing/tests/ui/unexpected-tag.rs @@ -0,0 +1,55 @@ +use rinja::Template; + +#[derive(Template)] +#[template(in_doc = true, ext = "html")] +/// ```rinja +/// {% end %} +/// ``` +struct UnexpectedEnd; + +#[derive(Template)] +#[template(in_doc = true, ext = "html")] +/// ```rinja +/// {% for i in 0..10 %} +/// i = {{i}} +/// {% elif %} +/// what? +/// {% endfor %} +/// ``` +struct UnexpectedElif; + +#[derive(Template)] +#[template(in_doc = true, ext = "html")] +/// ```rinja +/// {% block meta %} +/// then +/// {% else %} +/// else +/// {% endblock meta %} +/// ``` +struct UnexpectedElse; + +#[derive(Template)] +#[template(in_doc = true, ext = "html")] +/// ```rinja +/// {% when condition %} +/// true +/// {% endwhen %} +/// ``` +struct UnexpectedWhen; + +#[derive(Template)] +#[template(in_doc = true, ext = "html")] +/// ```rinja +/// {% let var %}value{% endlet %} +/// ``` +struct UnexpectedEndLet; + +#[derive(Template)] +#[template(in_doc = true, ext = "html")] +/// ```rinja +/// {% syntax error %} +/// ``` +struct Unexpected; + +fn main() {} diff --git a/testing/tests/ui/unexpected-tag.stderr b/testing/tests/ui/unexpected-tag.stderr new file mode 100644 index 00000000..bac5bc6d --- /dev/null +++ b/testing/tests/ui/unexpected-tag.stderr @@ -0,0 +1,47 @@ +error: node `end` was not expected in the current context + --> <source attribute>:1:2 + " end %}" + --> tests/ui/unexpected-tag.rs:5:1 + | +5 | /// ```rinja + | ^^^^^^^^^^^^ + +error: node `elif` was not expected in the current context: `for` block + --> <source attribute>:3:2 + " elif %}\n what?\n{% endfor %}" + --> tests/ui/unexpected-tag.rs:12:1 + | +12 | /// ```rinja + | ^^^^^^^^^^^^ + +error: node `else` was not expected in the current context: `block` block + --> <source attribute>:3:2 + " else %}\n else\n{% endblock meta %}" + --> tests/ui/unexpected-tag.rs:23:1 + | +23 | /// ```rinja + | ^^^^^^^^^^^^ + +error: node `when` was not expected in the current context + --> <source attribute>:1:2 + " when condition %}\n true\n{% endwhen %}" + --> tests/ui/unexpected-tag.rs:34:1 + | +34 | /// ```rinja + | ^^^^^^^^^^^^ + +error: unexpected closing tag `endlet` + --> <source attribute>:1:20 + " endlet %}" + --> tests/ui/unexpected-tag.rs:43:1 + | +43 | /// ```rinja + | ^^^^^^^^^^^^ + +error: unknown node `syntax` + --> <source attribute>:1:2 + " syntax error %}" + --> tests/ui/unexpected-tag.rs:50:1 + | +50 | /// ```rinja + | ^^^^^^^^^^^^ From d6e61b3323c96bd664a69583a4f831efd5a910ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Tue, 10 Sep 2024 16:13:57 +0200 Subject: [PATCH 15/21] book: typo --- book/src/template_syntax.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md index 55531598..30cce324 100644 --- a/book/src/template_syntax.md +++ b/book/src/template_syntax.md @@ -620,7 +620,7 @@ You can also match against multiple alternative patterns at once: ``` For better interoperability with linters and auto-formatters like [djLint], -you can also use a optional `{% endwhen %}` node to close a `{% when %}` case: +you can also use an optional `{% endwhen %}` node to close a `{% when %}` case: ```jinja {% match number %} From ee73116c7dac3e638ebba26bc45ea8130613ebbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Wed, 11 Sep 2024 02:53:11 +0200 Subject: [PATCH 16/21] parser: fix clippy warning --- rinja_parser/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs index 5e7ce1fd..5d196fa6 100644 --- a/rinja_parser/src/lib.rs +++ b/rinja_parser/src/lib.rs @@ -312,7 +312,7 @@ fn skip_till<'a, 'b, O>( } } -fn keyword<'a>(k: &'a str) -> impl FnMut(&'a str) -> ParseResult<'_> { +fn keyword<'a>(k: &'a str) -> impl FnMut(&'a str) -> ParseResult<'a> { move |i: &'a str| -> ParseResult<'a> { let (j, v) = identifier(i)?; if k == v { Ok((j, v)) } else { fail(i) } From 5eb3cf017a0181144dc10fa3888195e456b182d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Wed, 11 Sep 2024 03:08:52 +0200 Subject: [PATCH 17/21] Implement `pluralize` filter --- book/src/filters.md | 28 +++++ rinja/src/filters/builtin.rs | 211 +++++++++++++++++++++++++++++++++- rinja/src/filters/mod.rs | 2 +- rinja_derive/src/generator.rs | 24 ++++ 4 files changed, 263 insertions(+), 2 deletions(-) diff --git a/book/src/filters.md b/book/src/filters.md index 892cbdcc..f2b604e8 100644 --- a/book/src/filters.md +++ b/book/src/filters.md @@ -35,6 +35,7 @@ Enable it with Cargo features (see below for more information). * [`linebreaksbr`][#linebreaksbr] * [`lower|lowercase`][#lower] * [`paragraphbreaks`][#paragraphbreaks] + * [`pluralize`][#pluralize] * [`ref`][#ref] * [`safe`][#safe] * [`title`][#title] @@ -311,6 +312,33 @@ Output: hello ``` +### `pluralize` +[#pluralize]: #pluralize + +Select a singular or plural version of a word, depending on the input value. + +If the value of `self.count` is +1 or -1, then "cat" is returned, otherwise "cats": + +```jinja +cat{{ count|pluralize }} +``` + +You can override the default empty singular suffix, e.g. to spell "doggo" for a single dog: + +```jinja +dog{{ count|pluralize("go") }} +``` + +If the word cannot be declined by simply adding a suffix, +then you can also override singular and the plural, too: + +```jinja +{{ count|pluralize("mouse", "mice") }} +``` + +More complex languages that know multiple plurals might be impossible to implement with this filter, +though. + ### ref [#ref]: #ref diff --git a/rinja/src/filters/builtin.rs b/rinja/src/filters/builtin.rs index 5af2708d..e18c2dfd 100644 --- a/rinja/src/filters/builtin.rs +++ b/rinja/src/filters/builtin.rs @@ -182,7 +182,7 @@ pub fn linebreaks(s: impl fmt::Display) -> Result<HtmlSafeOutput<String>, fmt::E /// Converts all newlines in a piece of plain text to HTML line breaks /// -/// ```rust +/// ``` /// # #[cfg(feature = "code-in-doc")] { /// # use rinja::Template; /// /// ```jinja @@ -528,6 +528,215 @@ pub fn title(s: impl fmt::Display) -> Result<String, fmt::Error> { Ok(output) } +/// For a value of `±1` by default an empty string `""` is returned, otherwise `"s"`. +/// +/// # Examples +/// +/// ## With default arguments +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// I have {{dogs}} dog{{dogs|pluralize}} and {{cats}} cat{{cats|pluralize}}. +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Pets { +/// dogs: i8, +/// cats: i8, +/// } +/// +/// assert_eq!( +/// Pets { dogs: 0, cats: 0 }.to_string(), +/// "I have 0 dogs and 0 cats." +/// ); +/// assert_eq!( +/// Pets { dogs: 1, cats: 1 }.to_string(), +/// "I have 1 dog and 1 cat." +/// ); +/// assert_eq!( +/// Pets { dogs: -1, cats: 99 }.to_string(), +/// "I have -1 dog and 99 cats." +/// ); +/// # } +/// ``` +/// +/// ## Overriding the singular case +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// I have {{dogs}} dog{{ dogs|pluralize("go") }}. +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Dog { +/// dogs: i8, +/// } +/// +/// assert_eq!( +/// Dog { dogs: 0 }.to_string(), +/// "I have 0 dogs." +/// ); +/// assert_eq!( +/// Dog { dogs: 1 }.to_string(), +/// "I have 1 doggo." +/// ); +/// # } +/// ``` +/// +/// ## Overriding singular and plural cases +/// +/// ``` +/// # #[cfg(feature = "code-in-doc")] { +/// # use rinja::Template; +/// /// ```jinja +/// /// I have {{mice}} {{ mice|pluralize("mouse", "mice") }}. +/// /// ``` +/// #[derive(Template)] +/// #[template(ext = "html", in_doc = true)] +/// struct Mice { +/// mice: i8, +/// } +/// +/// assert_eq!( +/// Mice { mice: 42 }.to_string(), +/// "I have 42 mice." +/// ); +/// assert_eq!( +/// Mice { mice: 1 }.to_string(), +/// "I have 1 mouse." +/// ); +/// # } +/// ``` +#[inline] +pub fn pluralize<C, S, P>(count: C, singular: S, plural: P) -> Result<Pluralize<S, P>, C::Error> +where + C: PluralizeCount, +{ + match count.is_singular()? { + true => Ok(Pluralize::Singular(singular)), + false => Ok(Pluralize::Plural(plural)), + } +} + +/// An integer that can have the value `+1` and maybe `-1`. +pub trait PluralizeCount { + /// A possible error that can occur while checking the value. + type Error: Into<Error>; + + /// Returns `true` if and only if the value is `±1`. + fn is_singular(&self) -> Result<bool, Self::Error>; +} + +const _: () = { + // implement PluralizeCount for a list of reference wrapper types to PluralizeCount + macro_rules! impl_pluralize_count_for_ref { + ($T:ident => $($ty:ty)*) => { $( + impl<T: PluralizeCount + ?Sized> PluralizeCount for $ty { + type Error = <T as PluralizeCount>::Error; + + #[inline] + fn is_singular(&self) -> Result<bool, Self::Error> { + <T as PluralizeCount>::is_singular(self) + } + } + )* }; + } + + impl_pluralize_count_for_ref! { + T => + &T + Box<T> + std::cell::Ref<'_, T> + std::cell::RefMut<'_, T> + std::pin::Pin<&T> + std::rc::Rc<T> + std::sync::Arc<T> + std::sync::MutexGuard<'_, T> + std::sync::RwLockReadGuard<'_, T> + std::sync::RwLockWriteGuard<'_, T> + } + + /// implement PluralizeCount for unsigned integer types + macro_rules! impl_pluralize_for_unsigned_int { + ($($ty:ty)*) => { $( + impl PluralizeCount for $ty { + type Error = Infallible; + + #[inline] + fn is_singular(&self) -> Result<bool, Self::Error> { + Ok(*self == 1) + } + } + )* }; + } + + impl_pluralize_for_unsigned_int!(u8 u16 u32 u64 u128 usize); + + /// implement PluralizeCount for signed integer types + macro_rules! impl_pluralize_for_signed_int { + ($($ty:ty)*) => { $( + impl PluralizeCount for $ty { + type Error = Infallible; + + #[inline] + fn is_singular(&self) -> Result<bool, Self::Error> { + Ok(*self == 1 || *self == -1) + } + } + )* }; + } + + impl_pluralize_for_signed_int!(i8 i16 i32 i64 i128 isize); + + /// implement PluralizeCount for non-zero integer types + macro_rules! impl_pluralize_for_non_zero { + ($($ty:ident)*) => { $( + impl PluralizeCount for std::num::$ty { + type Error = Infallible; + + #[inline] + fn is_singular(&self) -> Result<bool, Self::Error> { + self.get().is_singular() + } + } + )* }; + } + + impl_pluralize_for_non_zero! { + NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize + NonZeroU8 NonZeroU16 NonZeroU32 NonZeroU64 NonZeroU128 NonZeroUsize + } +}; + +pub enum Pluralize<S, P> { + Singular(S), + Plural(P), +} + +impl<S: fmt::Display, P: fmt::Display> fmt::Display for Pluralize<S, P> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Pluralize::Singular(value) => write!(f, "{value}"), + Pluralize::Plural(value) => write!(f, "{value}"), + } + } +} + +impl<S: FastWritable, P: FastWritable> FastWritable for Pluralize<S, P> { + #[inline] + fn write_into<W: fmt::Write + ?Sized>(&self, dest: &mut W) -> fmt::Result { + match self { + Pluralize::Singular(value) => value.write_into(dest), + Pluralize::Plural(value) => value.write_into(dest), + } + } +} + fn try_to_string(s: impl fmt::Display) -> Result<String, fmt::Error> { let mut result = String::new(); write!(result, "{s}")?; diff --git a/rinja/src/filters/mod.rs b/rinja/src/filters/mod.rs index 24555aad..af4ff2e4 100644 --- a/rinja/src/filters/mod.rs +++ b/rinja/src/filters/mod.rs @@ -21,7 +21,7 @@ pub use builtin::filesizeformat; pub use builtin::{abs, into_f64, into_isize}; pub use builtin::{ capitalize, center, fmt, format, indent, join, linebreaks, linebreaksbr, lower, lowercase, - paragraphbreaks, title, trim, truncate, upper, uppercase, wordcount, + paragraphbreaks, pluralize, title, trim, truncate, upper, uppercase, wordcount, PluralizeCount, }; #[cfg(feature = "urlencode")] pub use builtin::{urlencode, urlencode_strict}; diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 920c19eb..7b3ee4d3 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -1538,6 +1538,7 @@ impl<'a> Generator<'a> { "linebreaks" | "linebreaksbr" | "paragraphbreaks" => { return self._visit_linebreaks_filter(ctx, buf, name, args, filter); } + "pluralize" => return self._visit_pluralize_filter(ctx, buf, args, filter), "ref" => return self._visit_ref_filter(ctx, buf, args, filter), "safe" => return self._visit_safe_filter(ctx, buf, args, filter), _ => {} @@ -1553,6 +1554,29 @@ impl<'a> Generator<'a> { Ok(DisplayWrap::Unwrapped) } + fn _visit_pluralize_filter<T>( + &mut self, + ctx: &Context<'_>, + buf: &mut Buffer, + args: &[WithSpan<'_, Expr<'_>>], + node: &WithSpan<'_, T>, + ) -> Result<DisplayWrap, CompileError> { + buf.write(format_args!("{CRATE}::filters::pluralize(")); + self._visit_args(ctx, buf, args)?; + match args.len() { + 1 => buf.write(r#", "", "s""#), + 2 => buf.write(r#", "s""#), + 3 => {} + _ => { + return Err( + ctx.generate_error("unexpected argument(s) in `pluralize` filter", node) + ); + } + } + buf.write(")?"); + Ok(DisplayWrap::Unwrapped) + } + fn _visit_linebreaks_filter<T>( &mut self, ctx: &Context<'_>, From ecd0e6e84e33be51cf4c670bf82af87a3422fcc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= <rene.kijewski@fu-berlin.de> Date: Wed, 11 Sep 2024 01:24:31 +0200 Subject: [PATCH 18/21] parser: make `{#-#}` a syntax error --- rinja_parser/src/node.rs | 70 +++++++++++++----------- rinja_parser/src/tests.rs | 1 + testing/tests/ui/ambiguous-ws-raw.rs | 16 ++++++ testing/tests/ui/ambiguous-ws-raw.stderr | 26 +++++++++ 4 files changed, 80 insertions(+), 33 deletions(-) create mode 100644 testing/tests/ui/ambiguous-ws-raw.rs create mode 100644 testing/tests/ui/ambiguous-ws-raw.stderr diff --git a/rinja_parser/src/node.rs b/rinja_parser/src/node.rs index 03ba6830..b309d20b 100644 --- a/rinja_parser/src/node.rs +++ b/rinja_parser/src/node.rs @@ -2,8 +2,10 @@ use std::str; use nom::branch::alt; use nom::bytes::complete::{tag, take_till}; -use nom::character::complete::char; -use nom::combinator::{complete, consumed, cut, eof, fail, map, not, opt, peek, recognize, value}; +use nom::character::complete::{anychar, char}; +use nom::combinator::{ + complete, consumed, cut, eof, fail, map, map_opt, not, opt, peek, recognize, value, +}; use nom::multi::{many0, separated_list0, separated_list1}; use nom::sequence::{delimited, pair, preceded, tuple}; @@ -426,11 +428,16 @@ pub enum Whitespace { impl Whitespace { fn parse(i: &str) -> ParseResult<'_, Self> { - alt(( - value(Self::Preserve, char('+')), - value(Self::Suppress, char('-')), - value(Self::Minimize, char('~')), - ))(i) + map_opt(anychar, Self::parse_char)(i) + } + + fn parse_char(c: char) -> Option<Self> { + match c { + '+' => Some(Self::Preserve), + '-' => Some(Self::Suppress), + '~' => Some(Self::Minimize), + _ => None, + } } } @@ -1226,12 +1233,12 @@ impl<'a> Comment<'a> { ))(i) } - fn content<'a>(mut i: &'a str, s: &State<'_>) -> ParseResult<'a, ()> { + fn content<'a>(mut i: &'a str, s: &State<'_>) -> ParseResult<'a> { let mut depth = 0usize; + let start = i; loop { - let start = i; let splitter = Splitter2::new(s.syntax.comment_start, s.syntax.comment_end); - let (_, tag) = opt(skip_till(splitter, |i| tag(i, s)))(i)?; + let (k, tag) = opt(skip_till(splitter, |i| tag(i, s)))(i)?; let Some((j, tag)) = tag else { return Err( ErrorContext::unclosed("comment", s.syntax.comment_end, start).into(), @@ -1249,7 +1256,7 @@ impl<'a> Comment<'a> { }, Tag::Close => match depth.checked_sub(1) { Some(new_depth) => depth = new_depth, - None => return Ok((j, ())), + None => return Ok((j, &start[..start.len() - k.len()])), }, } i = j; @@ -1257,31 +1264,28 @@ impl<'a> Comment<'a> { } let start = i; - let (i, (pws, content)) = pair( - preceded(|i| s.tag_comment_start(i), opt(Whitespace::parse)), - recognize(cut_node(Some("comment"), |i| content(i, s))), + let (i, content) = preceded( + |i| s.tag_comment_start(i), + cut_node(Some("comment"), |i| content(i, s)), )(i)?; - let mut nws = None; - if let Some(content) = content.strip_suffix(s.syntax.comment_end) { - nws = match content.chars().last() { - Some('-') => Some(Whitespace::Suppress), - Some('+') => Some(Whitespace::Preserve), - Some('~') => Some(Whitespace::Minimize), - _ => None, - } - }; - - Ok(( - i, - WithSpan::new( - Self { - ws: Ws(pws, nws), - content, - }, + let mut ws = Ws(None, None); + if content.len() == 1 && matches!(content, "-" | "+" | "~") { + return Err(nom::Err::Failure(ErrorContext::new( + format!( + "ambiguous whitespace stripping\n\ + use `{}{content} {content}{}` to apply the same whitespace stripping on both \ + sides", + s.syntax.comment_start, s.syntax.comment_end, + ), start, - ), - )) + ))); + } else if content.len() >= 2 { + ws.0 = Whitespace::parse_char(content.chars().next().unwrap_or_default()); + ws.1 = Whitespace::parse_char(content.chars().next_back().unwrap_or_default()); + } + + Ok((i, WithSpan::new(Self { ws, content }, start))) } } diff --git a/rinja_parser/src/tests.rs b/rinja_parser/src/tests.rs index 46358bae..0c2f9bc9 100644 --- a/rinja_parser/src/tests.rs +++ b/rinja_parser/src/tests.rs @@ -721,6 +721,7 @@ fn test_odd_calls() { #[test] fn test_parse_comments() { + #[track_caller] fn one_comment_ws(source: &str, ws: Ws) { let s = &Syntax::default(); let mut nodes = Ast::from_str(source, None, s).unwrap().nodes; diff --git a/testing/tests/ui/ambiguous-ws-raw.rs b/testing/tests/ui/ambiguous-ws-raw.rs new file mode 100644 index 00000000..a3efd734 --- /dev/null +++ b/testing/tests/ui/ambiguous-ws-raw.rs @@ -0,0 +1,16 @@ +use rinja::Template; + +#[derive(Template)] +#[template(source = r#"X{#-#}Y"#, ext = "html")] +struct Suppress; + +#[derive(Template)] +#[template(source = r#"X{#+#}Y"#, ext = "html")] +struct Preserve; + +#[derive(Template)] +#[template(source = r#"X{#~#}Y"#, ext = "html")] +struct Minimize; + +fn main() { +} diff --git a/testing/tests/ui/ambiguous-ws-raw.stderr b/testing/tests/ui/ambiguous-ws-raw.stderr new file mode 100644 index 00000000..06477bfa --- /dev/null +++ b/testing/tests/ui/ambiguous-ws-raw.stderr @@ -0,0 +1,26 @@ +error: ambiguous whitespace stripping + use `{#- -#}` to apply the same whitespace stripping on both sides + --> <source attribute>:1:1 + "{#-#}Y" + --> tests/ui/ambiguous-ws-raw.rs:4:21 + | +4 | #[template(source = r#"X{#-#}Y"#, ext = "html")] + | ^^^^^^^^^^^^ + +error: ambiguous whitespace stripping + use `{#+ +#}` to apply the same whitespace stripping on both sides + --> <source attribute>:1:1 + "{#+#}Y" + --> tests/ui/ambiguous-ws-raw.rs:8:21 + | +8 | #[template(source = r#"X{#+#}Y"#, ext = "html")] + | ^^^^^^^^^^^^ + +error: ambiguous whitespace stripping + use `{#~ ~#}` to apply the same whitespace stripping on both sides + --> <source attribute>:1:1 + "{#~#}Y" + --> tests/ui/ambiguous-ws-raw.rs:12:21 + | +12 | #[template(source = r#"X{#~#}Y"#, ext = "html")] + | ^^^^^^^^^^^^ From d85a3cb08e3008aa277fc12ad89e3207c209277b Mon Sep 17 00:00:00 2001 From: Guillaume Gomez <guillaume1.gomez@gmail.com> Date: Wed, 11 Sep 2024 17:25:18 +0200 Subject: [PATCH 19/21] Fix invalid condition optimization --- rinja_derive/src/generator.rs | 5 ++++- rinja_derive/src/tests.rs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/rinja_derive/src/generator.rs b/rinja_derive/src/generator.rs index 920c19eb..7a6fe4e2 100644 --- a/rinja_derive/src/generator.rs +++ b/rinja_derive/src/generator.rs @@ -445,7 +445,10 @@ impl<'a> Generator<'a> { EvaluatedResult::AlwaysTrue, WithSpan::new(Expr::BoolLit(true), ""), ), - EvaluatedResult::Unknown => (EvaluatedResult::Unknown, expr), + EvaluatedResult::Unknown => ( + EvaluatedResult::Unknown, + WithSpan::new(Expr::Unary("!", Box::new(expr)), span), + ), } } Expr::Unary(_, _) => (EvaluatedResult::Unknown, WithSpan::new(expr, span)), diff --git a/rinja_derive/src/tests.rs b/rinja_derive/src/tests.rs index c39b9844..2ef24157 100644 --- a/rinja_derive/src/tests.rs +++ b/rinja_derive/src/tests.rs @@ -393,6 +393,24 @@ match ( &[("y", "u32")], 3, ); + + // Ensure that the `!` is kept . + compare( + "{% if y is defined && !y %}bla{% endif %}", + r#"if *(&(!self.y) as &::core::primitive::bool) { + writer.write_str("bla")?; +}"#, + &[("y", "bool")], + 3, + ); + compare( + "{% if y is defined && !(y) %}bla{% endif %}", + r#"if *(&(!(self.y)) as &::core::primitive::bool) { + writer.write_str("bla")?; +}"#, + &[("y", "bool")], + 3, + ); } #[test] From 1a58b357436ed70a886fda0d45e3f62e52cc8247 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez <guillaume1.gomez@gmail.com> Date: Wed, 11 Sep 2024 19:56:04 +0200 Subject: [PATCH 20/21] Add more tests for `!` operator --- rinja_derive/src/tests.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rinja_derive/src/tests.rs b/rinja_derive/src/tests.rs index 2ef24157..57e36ec4 100644 --- a/rinja_derive/src/tests.rs +++ b/rinja_derive/src/tests.rs @@ -407,6 +407,22 @@ match ( "{% if y is defined && !(y) %}bla{% endif %}", r#"if *(&(!(self.y)) as &::core::primitive::bool) { writer.write_str("bla")?; +}"#, + &[("y", "bool")], + 3, + ); + compare( + "{% if y is not defined || !y %}bla{% endif %}", + r#"if *(&(!self.y) as &::core::primitive::bool) { + writer.write_str("bla")?; +}"#, + &[("y", "bool")], + 3, + ); + compare( + "{% if y is not defined || !(y) %}bla{% endif %}", + r#"if *(&(!(self.y)) as &::core::primitive::bool) { + writer.write_str("bla")?; }"#, &[("y", "bool")], 3, From 447729f753b1a1854be917f464c32d1b48713b4c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez <guillaume1.gomez@gmail.com> Date: Wed, 11 Sep 2024 20:33:52 +0200 Subject: [PATCH 21/21] Update crates version to 0.3.3 --- examples/actix-web-app/Cargo.toml | 4 ++-- rinja/Cargo.toml | 4 ++-- rinja_actix/Cargo.toml | 4 ++-- rinja_axum/Cargo.toml | 4 ++-- rinja_derive/Cargo.toml | 4 ++-- rinja_derive_standalone/Cargo.toml | 4 ++-- rinja_parser/Cargo.toml | 2 +- rinja_rocket/Cargo.toml | 4 ++-- rinja_warp/Cargo.toml | 4 ++-- testing/Cargo.toml | 6 +++--- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/actix-web-app/Cargo.toml b/examples/actix-web-app/Cargo.toml index 50151bac..d14de4a2 100644 --- a/examples/actix-web-app/Cargo.toml +++ b/examples/actix-web-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-app" -version = "0.3.2" +version = "0.3.3" edition = "2021" license = "MIT OR Apache-2.0" publish = false @@ -10,7 +10,7 @@ publish = false # and actix-web as your web-framework. # rinja_actix makes it easy to use rinja templates as `Responder` of an actix-web request. # The rendered template is simply the response of your handler! -rinja_actix = { version = "0.3.2", path = "../../rinja_actix" } +rinja_actix = { version = "0.3.3", path = "../../rinja_actix" } actix-web = { version = "4.8.0", default-features = false, features = ["macros"] } tokio = { version = "1.38.0", features = ["sync", "rt-multi-thread"] } diff --git a/rinja/Cargo.toml b/rinja/Cargo.toml index 8d20aad1..9f0a0bb4 100644 --- a/rinja/Cargo.toml +++ b/rinja/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja" -version = "0.3.2" +version = "0.3.3" description = "Type-safe, compiled Jinja-like templates for Rust" documentation = "https://docs.rs/rinja" keywords = ["markup", "template", "jinja2", "html"] @@ -37,7 +37,7 @@ with-rocket = ["rinja_derive/with-rocket"] with-warp = ["rinja_derive/with-warp"] [dependencies] -rinja_derive = { version = "=0.3.2", path = "../rinja_derive" } +rinja_derive = { version = "=0.3.3", path = "../rinja_derive" } humansize = { version = "2", optional = true } num-traits = { version = "0.2.6", optional = true } diff --git a/rinja_actix/Cargo.toml b/rinja_actix/Cargo.toml index b7330946..1f2c536e 100644 --- a/rinja_actix/Cargo.toml +++ b/rinja_actix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_actix" -version = "0.3.2" +version = "0.3.3" description = "Actix-Web integration for Rinja templates" documentation = "https://docs.rs/rinja" keywords = ["markup", "template", "jinja2", "html"] @@ -17,7 +17,7 @@ all-features = true rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"] [dependencies] -rinja = { version = "0.3.2", path = "../rinja", default-features = false, features = ["with-actix-web"] } +rinja = { version = "0.3.3", path = "../rinja", default-features = false, features = ["with-actix-web"] } actix-web = { version = "4", default-features = false } diff --git a/rinja_axum/Cargo.toml b/rinja_axum/Cargo.toml index 99160af0..36ff642a 100644 --- a/rinja_axum/Cargo.toml +++ b/rinja_axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_axum" -version = "0.3.2" +version = "0.3.3" edition = "2021" rust-version = "1.71" description = "Axum integration for Rinja templates" @@ -17,7 +17,7 @@ all-features = true rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"] [dependencies] -rinja = { version = "0.3.2", path = "../rinja", default-features = false, features = ["with-axum"] } +rinja = { version = "0.3.3", path = "../rinja", default-features = false, features = ["with-axum"] } axum-core = "0.4" http = "1.0" diff --git a/rinja_derive/Cargo.toml b/rinja_derive/Cargo.toml index bd5dc8d2..7ba71f54 100644 --- a/rinja_derive/Cargo.toml +++ b/rinja_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_derive" -version = "0.3.2" +version = "0.3.3" description = "Procedural macro package for Rinja" homepage = "https://github.com/rinja-rs/rinja" repository = "https://github.com/rinja-rs/rinja" @@ -30,7 +30,7 @@ with-rocket = [] with-warp = [] [dependencies] -parser = { package = "rinja_parser", version = "=0.3.2", path = "../rinja_parser" } +parser = { package = "rinja_parser", version = "=0.3.3", path = "../rinja_parser" } basic-toml = { version = "0.1.1", optional = true } pulldown-cmark = { version = "0.12.0", optional = true, default-features = false } diff --git a/rinja_derive_standalone/Cargo.toml b/rinja_derive_standalone/Cargo.toml index abd6917d..ec5b8772 100644 --- a/rinja_derive_standalone/Cargo.toml +++ b/rinja_derive_standalone/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_derive_standalone" -version = "0.3.2" +version = "0.3.3" description = "Procedural macro package for Rinja" homepage = "https://github.com/rinja-rs/rinja" repository = "https://github.com/rinja-rs/rinja" @@ -30,7 +30,7 @@ with-rocket = [] with-warp = [] [dependencies] -parser = { package = "rinja_parser", version = "=0.3.2", path = "../rinja_parser" } +parser = { package = "rinja_parser", version = "=0.3.3", path = "../rinja_parser" } basic-toml = { version = "0.1.1", optional = true } pulldown-cmark = { version = "0.12.0", optional = true, default-features = false } diff --git a/rinja_parser/Cargo.toml b/rinja_parser/Cargo.toml index b09baa0b..e6d65bd4 100644 --- a/rinja_parser/Cargo.toml +++ b/rinja_parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_parser" -version = "0.3.2" +version = "0.3.3" description = "Parser for Rinja templates" documentation = "https://docs.rs/rinja" keywords = ["markup", "template", "jinja2", "html"] diff --git a/rinja_rocket/Cargo.toml b/rinja_rocket/Cargo.toml index 1ab65704..30a58473 100644 --- a/rinja_rocket/Cargo.toml +++ b/rinja_rocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_rocket" -version = "0.3.2" +version = "0.3.3" description = "Rocket integration for Rinja templates" documentation = "https://docs.rs/rinja" keywords = ["markup", "template", "jinja2", "html"] @@ -17,7 +17,7 @@ all-features = true rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"] [dependencies] -rinja = { version = "0.3.2", path = "../rinja", default-features = false, features = ["with-rocket"] } +rinja = { version = "0.3.3", path = "../rinja", default-features = false, features = ["with-rocket"] } rocket = { version = "0.5", default-features = false } diff --git a/rinja_warp/Cargo.toml b/rinja_warp/Cargo.toml index dba9a123..438f83bd 100644 --- a/rinja_warp/Cargo.toml +++ b/rinja_warp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_warp" -version = "0.3.2" +version = "0.3.3" description = "Warp integration for Rinja templates" documentation = "https://docs.rs/rinja" keywords = ["markup", "template", "jinja2", "html"] @@ -17,7 +17,7 @@ all-features = true rustdoc-args = ["--generate-link-to-definition", "--cfg=docsrs"] [dependencies] -rinja = { version = "0.3.2", path = "../rinja", default-features = false, features = ["with-warp"] } +rinja = { version = "0.3.3", path = "../rinja", default-features = false, features = ["with-warp"] } warp = { version = "0.3", default-features = false } diff --git a/testing/Cargo.toml b/testing/Cargo.toml index d9195042..61349801 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rinja_testing" -version = "0.3.2" +version = "0.3.3" authors = ["rinja-rs developers"] workspace = ".." edition = "2021" @@ -13,12 +13,12 @@ code-in-doc = ["rinja/code-in-doc"] serde_json = ["dep:serde_json", "rinja/serde_json"] [dependencies] -rinja = { path = "../rinja", version = "0.3.2" } +rinja = { path = "../rinja", version = "0.3.3" } serde_json = { version = "1.0", optional = true } [dev-dependencies] -rinja = { path = "../rinja", version = "0.3.2", features = ["code-in-doc", "serde_json"] } +rinja = { path = "../rinja", version = "0.3.3", features = ["code-in-doc", "serde_json"] } criterion = "0.5" phf = { version = "0.11", features = ["macros" ] }