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
// `` 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.
-
- Etiam nibh mi, accumsan quis purus sed, posuere fermentum lorem. In pulvinar porta
- maximus. Fusce tincidunt lacinia tellus sit amet tincidunt. Aliquam lacus est, pulvinar
- non metus a, facilisis 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.
-
-
- Sed sollicitudin sem 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.
-
-
- 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 efficitur massa, 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.
-
"#;
- let string_short = "Lorem ipsum dolor sit amet,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.
+
+ Etiam nibh mi, accumsan quis purus sed, posuere fermentum lorem. In pulvinar porta
+ maximus. Fusce tincidunt lacinia tellus sit amet tincidunt. Aliquam lacus est, pulvinar
+ non metus a, facilisis 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.
+
+
+ Sed sollicitudin sem 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.
+
+
+ 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 efficitur massa, 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.
+
"#;
+
+ const STRING_SHORT: &str = "Lorem ipsum dolor sit amet,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.
-
-Etiam nibh mi, accumsan quis purus sed, posuere fermentum lorem. In pulvinar porta
-maximus. Fusce tincidunt lacinia tellus sit amet tincidunt. Aliquam lacus est, pulvinar
-non metus a, facilisis 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.
-
-
-Sed sollicitudin sem 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.
-
-
-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 efficitur massa, 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.
-
"#;
-const STRING_SHORT: &str = "Lorem ipsum dolor sit amet,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?=
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
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
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
+ --> :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
+ --> :1:2
+ " if x %}"
+ --> tests/ui/end-block.rs:15:14
+ |
+15 | source = "{% if x %}",
+ | ^^^^^^^^^^^^
+
+error: expected `endmatch` to terminate `match` node, found nothing
+ --> :1:2
+ " match x %}"
+ --> tests/ui/end-block.rs:24:14
+ |
+24 | source = "{% match x %}",
+ | ^^^^^^^^^^^^^^^
+
+error: expected `endfor` to terminate `for` node, found nothing
+ --> :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
+ --> :1:2
+ " macro bla %}"
+ --> tests/ui/end-block.rs:42:14
+ |
+42 | source = "{% macro bla %}",
+ | ^^^^^^^^^^^^^^^^^
+
+error: expected `endfilter` to terminate `filter` node, found nothing
+ --> :1:2
+ " filter bla %}"
+ --> tests/ui/end-block.rs:49:14
+ |
+49 | source = "{% filter bla %}",
+ | ^^^^^^^^^^^^^^^^^^
+
+error: expected `endblock` to terminate `block` node, found nothing
+ --> :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?=
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?=
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>,
+}
+
+#[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?=
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]:
+
### 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?=
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>,
syntax: &Syntax<'_>,
) -> Result {
- 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> {
- complete(many0(alt((
+ pub(super) fn parse_template(i: &'a str, s: &State<'_>) -> ParseResult<'a, Vec> {
+ 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 ",
+ i,
+ )));
+ }
+ Ok((i, result))
+ }
+
+ fn many(i: &'a str, s: &State<'_>) -> ParseResult<'a, Vec> {
+ 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`
--> :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
+ --> :1:2
+ " end %}"
+ --> tests/ui/unexpected-tag.rs:5:1
+ |
+5 | /// ```rinja
+ | ^^^^^^^^^^^^
+
+error: node `elif` was not expected in the current context: `for` block
+ --> :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
+ --> :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
+ --> :1:2
+ " when condition %}\n true\n{% endwhen %}"
+ --> tests/ui/unexpected-tag.rs:34:1
+ |
+34 | /// ```rinja
+ | ^^^^^^^^^^^^
+
+error: unexpected closing tag `endlet`
+ --> :1:20
+ " endlet %}"
+ --> tests/ui/unexpected-tag.rs:43:1
+ |
+43 | /// ```rinja
+ | ^^^^^^^^^^^^
+
+error: unknown node `syntax`
+ --> :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?=
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?=
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?=
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, 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 {
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(count: C, singular: S, plural: P) -> Result, 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;
+
+ /// Returns `true` if and only if the value is `±1`.
+ fn is_singular(&self) -> Result;
+}
+
+const _: () = {
+ // implement PluralizeCount for a list of reference wrapper types to PluralizeCount
+ macro_rules! impl_pluralize_count_for_ref {
+ ($T:ident => $($ty:ty)*) => { $(
+ impl PluralizeCount for $ty {
+ type Error = ::Error;
+
+ #[inline]
+ fn is_singular(&self) -> Result {
+ ::is_singular(self)
+ }
+ }
+ )* };
+ }
+
+ impl_pluralize_count_for_ref! {
+ T =>
+ &T
+ Box
+ std::cell::Ref<'_, T>
+ std::cell::RefMut<'_, T>
+ std::pin::Pin<&T>
+ std::rc::Rc
+ std::sync::Arc
+ 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 {
+ 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 {
+ 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 {
+ self.get().is_singular()
+ }
+ }
+ )* };
+ }
+
+ impl_pluralize_for_non_zero! {
+ NonZeroI8 NonZeroI16 NonZeroI32 NonZeroI64 NonZeroI128 NonZeroIsize
+ NonZeroU8 NonZeroU16 NonZeroU32 NonZeroU64 NonZeroU128 NonZeroUsize
+ }
+};
+
+pub enum Pluralize {
+ Singular(S),
+ Plural(P),
+}
+
+impl fmt::Display for Pluralize {
+ #[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 FastWritable for Pluralize {
+ #[inline]
+ fn write_into(&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 {
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(
+ &mut self,
+ ctx: &Context<'_>,
+ buf: &mut Buffer,
+ args: &[WithSpan<'_, Expr<'_>>],
+ node: &WithSpan<'_, T>,
+ ) -> Result {
+ 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(
&mut self,
ctx: &Context<'_>,
From ecd0e6e84e33be51cf4c670bf82af87a3422fcc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Kijewski?=
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 {
+ 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
+ --> :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
+ --> :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
+ --> :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
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
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
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" ] }