Repository: askama-rs/askama (Rust)

Issue:
Allow whitespaces around `|` (the filter operator)
Given that #16 was closed, namely disambiguating between `42|safe` (filter) and `42 | 43` (bitwise or), and thus the whitespace surrounding `|` (i.e. `<space>|<space>`) isn't syntactically meaningful anymore, it would be nice to allow users to include spaces between the filter chain.

This would improve readability, especially with long filters, and ones where arguments are used, like for example:
* `fn(42, 43, 44)|filter_1|filter_2` would be more readable as
* `fn(42, 43, 44) | filter_1 | filter_2`

----

As a counterpoint to this proposal, projects that want migrate from Askama to `rinja`:
* if one currently uses `42 | 43` in Askama, when migrating they'll get the "use bitor operator" error;
* meanwhile, if this feature is implemented (i.e. allowing spaces between `|` for filters), then the user would get a strange error regarding `43` not being a valid filter;

Although, given that one is aware of the advantages of switching to `rinja`, and the required changes (i.e. that the bitwise operators are spelled differently), this risk is minimized.



Discussion:
We don't allow whitespace in order to follow jinja's spec to make it compatible (as much as possible) with the official jinja implementation.

Normally, `rinja` provides a good error message to help you understand what's wrong.

Personally, I'd be more inclined to keep things as is.
> We don't allow whitespace in order to follow jinja's spec to make it compatible (as much as possible) with the official jinja implementation.

I've tried to locate the Jinja specification (I searched on Google a little, but found only the Jinja Python documentation, thus the "original" implementation), and the following document is full of examples like these:
=> <https://jinja.palletsprojects.com/en/stable/templates/>
~~~~
{% for key, value in my_dict | dictsort %}
{% set reply | wordwrap %}
{{ data | selectattr('name', '==', 'Jinja') | list | last }}
{{ value | markdown }}
~~~~

Thus, the Python Jinja documentation makes me believe that spaces are allowed between the `|` segments.

----

> Normally, `rinja` provides a good error message to help you understand what's wrong.

Indeed, however, the choice of whitespaces is more a matter of taste, some people (myself included) likes to put whitespaces between things.  :)

Moreover, from a programming language point of view, having whitespaces (that aren't tied to indentation) be syntactically meaningful is a "strange" choice.  :) :)

Damn, I was sure jinja (python) didn't allow whitespace. I'm gonna double check and if it allows them, then I see no reason to not allow it in `rinja`.
Just confirmed that whitespace characters are allowed around the `|` character. Lifting restriction then.
I confirm that the feature is now implemented in the latest `master` tip.

Thanks!

(Hopefully, soon we'll have a new release containing the latest fixes.)  :)

We have a few things we're working on but hopefully it should arrive soon enough. ;)



Patch:
diff --git a/book/src/filters.md b/book/src/filters.md
index 632efcbad..689c9616e 100644
--- a/book/src/filters.md
+++ b/book/src/filters.md
@@ -4,13 +4,11 @@ Values such as those obtained from variables can be post-processed
 using **filters**.
 Filters are applied to values using the pipe symbol (`|`) and may
 have optional extra arguments in parentheses.
-Note that the pipe symbol must not be surrounded by spaces;
-otherwise, it will be interpreted as the `BitOr` operator.
 Filters can be chained, in which case the output from one filter
 is passed to the next.
 
 ```
-{{ "HELLO"|lower }}
+{{ "HELLO" | lower }}
 ```
 
 Rinja has a collection of built-in filters, documented below, but can also include custom filters. 
@@ -26,7 +24,7 @@ Enable it with Cargo features (see below for more information).
 Capitalize a value. The first character will be uppercase, all others lowercase:
 
 ```jinja
-{{ "hello"|capitalize }}
+{{ "hello" | capitalize }}
 ```
 
 Output:
@@ -41,7 +39,7 @@ Hello
 Centers the value in a field of a given width:
 
 ```jinja
--{{ "a"|center(5) }}-
+-{{ "a" | center(5) }}-
 ```
 
 Output:
@@ -55,8 +53,8 @@ Output:
 Dereferences the given argument.
 
 ```jinja
-{% let s = String::from("a")|as_ref %}
-{% if s|deref == String::from("b") %}
+{% let s = String::from("a") | as_ref %}
+{% if s | deref == String::from("b") %}
 {% endif %}
 ```
 
@@ -73,7 +71,7 @@ if *s == String::from("b") {}
 Escapes HTML characters in strings:
 
 ```jinja
-{{ "Escape <>&"|e }}
+{{ "Escape <>&" | e }}
 ```
 
 Output:
@@ -88,11 +86,11 @@ However, somewhere escaping using the HTML escaper is desired.
 Then it is possible to override and use the HTML escaper like this:
 
 ```jinja
-{{ "Don't Escape <>&"|escape }}
-{{ "Don't Escape <>&"|e }}
+{{ "Don't Escape <>&" | escape }}
+{{ "Don't Escape <>&" | e }}
 
-{{ "Escape <>&"|escape("html") }}
-{{ "Escape <>&"|e("html") }}
+{{ "Escape <>&" | escape("html") }}
+{{ "Escape <>&" | e("html") }}
 ```
 
 Output:
@@ -113,7 +111,7 @@ Escape &lt;&gt;&amp;
 Returns adequate string representation (in KB, ..) of number of bytes:
 
 ```jinja
-{{ 1000|filesizeformat }}
+{{ 1000 | filesizeformat }}
 ```
 
 Output:
@@ -133,14 +131,14 @@ the Rinja code generator, but the order is swapped to support filter
 composition.
 
 ```jinja
-{{ value|fmt("{:?}") }}
+{{ value | fmt("{:?}") }}
 ```
 
 As an example, this allows filters to be composed like the following.
 Which is not possible using the `format` filter.
 
 ```jinja
-{{ value|capitalize|fmt("{:?}") }}
+{{ value | capitalize | fmt("{:?}") }}
 ```
 
 ### format
@@ -153,7 +151,7 @@ The first argument to this filter must be a string literal (as in normal Rust).
 All arguments are passed through to [`format!()`] by the Rinja code generator.
 
 ```jinja
-{{ "{:?}"|format(var) }}
+{{ "{:?}" | format(var) }}
 ```
 
 [`format!()`]: https://doc.rust-lang.org/stable/std/macro.format.html
@@ -164,7 +162,7 @@ All arguments are passed through to [`format!()`] by the Rinja code generator.
 Indent newlines with width spaces.
 
 ```jinja
-{{ "hello\nfoo\nbar"|indent(4) }}
+{{ "hello\nfoo\nbar" | indent(4) }}
 ```
 
 Output:
@@ -185,7 +183,7 @@ array = &["foo", "bar", "bazz"]
 ```
 
 ```jinja
-{{ array|join(", ") }}
+{{ array | join(", ") }}
 ```
 
 Output:
@@ -202,7 +200,7 @@ Replaces line breaks in plain text with appropriate HTML.
 A single newline becomes an HTML line break `<br>` and a new line followed by a blank line becomes a paragraph break `<p>`.
 
 ```jinja
-{{ "hello\nworld\n\nfrom\nrinja"|linebreaks }}
+{{ "hello\nworld\n\nfrom\nrinja" | linebreaks }}
 ```
 
 Output:
@@ -217,7 +215,7 @@ Output:
 Converts all newlines in a piece of plain text to HTML line breaks.
 
 ```jinja
-{{ "hello\nworld\n\nfrom\nrinja"|linebreaks }}
+{{ "hello\nworld\n\nfrom\nrinja" | linebreaks }}
 ```
 
 Output:
@@ -236,7 +234,7 @@ Consecutive double line breaks will be reduced down to a single paragraph break.
 This is useful in contexts where changing single line breaks to line break tags would interfere with other HTML elements, such as lists and nested `<div>` tags.
 
 ```jinja
-{{ "hello\nworld\n\nfrom\n\n\n\nrinja"|paragraphbreaks }}
+{{ "hello\nworld\n\nfrom\n\n\n\nrinja" | paragraphbreaks }}
 ```
 
 Output:
@@ -251,7 +249,7 @@ Output:
 Converts to lowercase.
 
 ```jinja
-{{ "HELLO"|lower }}
+{{ "HELLO" | lower }}
 ```
 
 Output:
@@ -268,20 +266,20 @@ 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 }}
+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") }}
+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") }}
+{{ count | pluralize("mouse", "mice") }}
 ```
 
 More complex languages that know multiple plurals might be impossible to implement with this filter,
@@ -293,8 +291,8 @@ though.
 Creates a reference to the given argument.
 
 ```jinja
-{{ "a"|ref }}
-{{ self.x|ref }}
+{{ "a" | ref }}
+{{ self.x | ref }}
 ```
 
 will become:
@@ -310,7 +308,7 @@ will become:
 Marks a string (or other Display type) as safe. By default all strings are escaped according to the format.
 
 ```
-{{ "<p>I'm Safe</p>"|safe }}
+{{ "<p>I'm Safe</p>" | safe }}
 ```
 
 Output:
@@ -326,7 +324,7 @@ Return a title cased version of the value. Words will start with uppercase lette
 remaining characters are lowercase.
 
 ```jinja
-{{ "hello WORLD"|title }}
+{{ "hello WORLD" | title }}
 ```
 
 Output:
@@ -341,7 +339,7 @@ Hello World
 Strip leading and trailing whitespace.
 
 ```jinja
-{{ " hello "|trim }}
+{{ " hello " | trim }}
 ```
 
 Output:
@@ -357,7 +355,7 @@ Limit string length, appends '...' if truncated.
 
 
 ```jinja
-{{ "hello"|truncate(2) }}
+{{ "hello" | truncate(2) }}
 ```
 
 Output:
@@ -372,7 +370,7 @@ he...
 Converts to uppercase.
 
 ```jinja
-{{ "hello"|upper }}
+{{ "hello" | upper }}
 ```
 
 Output:
@@ -402,7 +400,7 @@ hello%3Fworld
 Count the words in that string.
 
 ```jinja
-{{ "rinja is sort of cool"|wordcount }}
+{{ "rinja is sort of cool" | wordcount }}
 ```
 
 Output:
@@ -431,22 +429,22 @@ This will output formatted JSON for any value that implements the required
 The generated string does not contain ampersands `&`, chevrons `< >`, or apostrophes `'`.
 
 To use it in a `<script>` you can combine it with the safe filter.
-In HTML attributes, you can either use it in quotation marks `"{{data|json}}"` as is,
-or in apostrophes with the (optional) safe filter `'{{data|json|safe}}'`.
-In HTML texts the output of e.g. `<pre>{{data|json|safe}}</pre>` is safe, too.
+In HTML attributes, you can either use it in quotation marks `"{{data | json}}"` as is,
+or in apostrophes with the (optional) safe filter `'{{data | json | safe}}'`.
+In HTML texts the output of e.g. `<pre>{{data | json | safe}}</pre>` is safe, too.
 
 ```jinja
-Good: <li data-extra="{{data|json}}">…</li>
-Good: <li data-extra='{{data|json|safe}}'>…</li>
-Good: <pre>{{data|json|safe}}</pre>
-Good: <script>var data = {{data|json|safe}};</script>
+Good: <li data-extra="{{data | json}}">…</li>
+Good: <li data-extra='{{data | json | safe}}'>…</li>
+Good: <pre>{{data | json | safe}}</pre>
+Good: <script>var data = {{data | json | safe}};</script>
 
-Bad:  <li data-extra="{{data|json|safe}}">…</li>
-Bad:  <script>var data = {{data|json}};</script>
-Bad:  <script>var data = "{{data|json|safe}}";</script>
+Bad:  <li data-extra="{{data | json | safe}}">…</li>
+Bad:  <script>var data = {{data | json}};</script>
+Bad:  <script>var data = "{{data | json | safe}}";</script>
 
-Ugly: <script>var data = "{{data|json}}";</script>
-Ugly: <script>var data = '{{data|json|safe}}';</script>
+Ugly: <script>var data = "{{data | json}}";</script>
+Ugly: <script>var data = '{{data | json | safe}}';</script>
 ```
 
 By default, a compact representation of the data is generated, i.e. no whitespaces are generated
@@ -455,10 +453,10 @@ how many spaces to use as indentation, or you can pass a string that gets used a
 
 ```jinja2
 Prefix with four spaces:
-<textarea>{{data|tojson(4)}}</textarea>
+<textarea>{{data | tojson(4)}}</textarea>
 
 Prefix with two &nbsp; characters:
-<p>{{data|tojson("\u{a0}\u{a0}")}}</p>
+<p>{{data | tojson("\u{a0}\u{a0}")}}</p>
 ```
 
 ## Custom Filters
@@ -487,7 +485,7 @@ Implementing a filter that replaces all instances of `"oo"` for `"aa"`.
 use rinja::Template;
 
 #[derive(Template)]
-#[template(source = "{{ s|myfilter }}", ext = "txt")]
+#[template(source = "{{ s | myfilter }}", ext = "txt")]
 struct MyFilterTemplate<'a> {
     s: &'a str,
 }
@@ -512,7 +510,7 @@ Implementing a filter that replaces all instances of `"oo"` for `n` times `"a"`.
 use rinja::Template;
 
 #[derive(Template)]
-#[template(source = "{{ s|myfilter(4) }}", ext = "txt")]
+#[template(source = "{{ s | myfilter(4) }}", ext = "txt")]
 struct MyFilterTemplate<'a> {
     s: &'a str,
 }
@@ -558,7 +556,7 @@ This automatically marks references `&MyStruct` as HTML-safe, too.
 ### Safe output of custom filters
 [#safe-output-of-custom-filters]: #safe-output-of-custom-filters
 
-Say, you have a custom filter `|strip` that removes all HTML-unsafe characters:
+Say, you have a custom filter `| strip` that removes all HTML-unsafe characters:
 
 ```rust
 fn strip(s: impl ToString) -> Result<String, rinja::Error> {
diff --git a/rinja_parser/src/lib.rs b/rinja_parser/src/lib.rs
index ef8a7501c..4dc3155cd 100644
--- a/rinja_parser/src/lib.rs
+++ b/rinja_parser/src/lib.rs
@@ -899,20 +899,10 @@ fn filter<'a>(
     i: &'a str,
     level: &mut Level,
 ) -> ParseResult<'a, (&'a str, Option<Vec<WithSpan<'a, Expr<'a>>>>)> {
-    let (j, _) = skip_ws0.parse_next(i)?;
-    let had_spaces = i.len() != j.len();
-    let (j, _) = ('|', not('|')).parse_next(j)?;
+    let (j, _) = ws(('|', not('|'))).parse_next(i)?;
 
-    if !had_spaces {
-        *level = level.nest(i)?.1;
-        cut_err((ws(identifier), opt(|i| Expr::arguments(i, *level, false)))).parse_next(j)
-    } else {
-        Err(winnow::error::ErrMode::Cut(ErrorContext::new(
-            "the filter operator `|` must not be preceded by any whitespace characters\n\
-            the binary OR operator is called `bitor` in rinja",
-            i,
-        )))
-    }
+    *level = level.nest(i)?.1;
+    cut_err((ws(identifier), opt(|i| Expr::arguments(i, *level, false)))).parse_next(j)
 }
 
 /// Returns the common parts of two paths.


Test patch:
diff --git a/testing/tests/filters.rs b/testing/tests/filters.rs
index 14222bc17..b933e9b5a 100644
--- a/testing/tests/filters.rs
+++ b/testing/tests/filters.rs
@@ -436,3 +436,17 @@ fn test_filesizeformat() {
 
     assert_eq!(S { s: Some(12) }.render().unwrap(), "12 B");
 }
+
+#[test]
+fn test_whitespace_around_filter_operator() {
+    #[derive(Template)]
+    #[template(
+        source = r#"{{ 12 |safe }}
+{{ 8| safe }}
+{{ 4   |    safe }}"#,
+        ext = "html"
+    )]
+    struct S;
+
+    assert_eq!(S.render().unwrap(), "12\n8\n4");
+}
diff --git a/testing/tests/ui/iso646.rs b/testing/tests/ui/iso646.rs
index efe9d642b..58a3f56e5 100644
--- a/testing/tests/ui/iso646.rs
+++ b/testing/tests/ui/iso646.rs
@@ -14,13 +14,6 @@ struct BitAndIso646 {
     b: u32,
 }
 
-#[derive(Template)]
-#[template(ext = "txt", source = "{{ a | b }}")]
-struct BitOr {
-    a: u32,
-    b: u32,
-}
-
 #[derive(Template)]
 #[template(ext = "txt", source = "{{ a bitor b }}")]
 struct BitOrIso646 {
@@ -36,7 +29,7 @@ struct Xor {
 }
 
 #[derive(Template)]
-#[template(ext = "txt", source = "{{ a xor b }}")]
+#[template(ext = "txt", source = "{{ a xor b }} {{ c }}")]
 struct XorIso646 {
     a: u32,
     b: u32,
diff --git a/testing/tests/ui/iso646.stderr b/testing/tests/ui/iso646.stderr
index 57fd27970..e66650fb1 100644
--- a/testing/tests/ui/iso646.stderr
+++ b/testing/tests/ui/iso646.stderr
@@ -6,19 +6,18 @@ error: the binary AND operator is called `bitand` in rinja
 4 | #[template(ext = "txt", source = "{{ a & b }}")]
   |                                  ^^^^^^^^^^^^^
 
-error: the filter operator `|` must not be preceded by any whitespace characters
-       the binary OR operator is called `bitor` in rinja
-         --> <source attribute>:1:4
-       " | b }}"
-  --> tests/ui/iso646.rs:18:34
-   |
-18 | #[template(ext = "txt", source = "{{ a | b }}")]
-   |                                  ^^^^^^^^^^^^^
-
 error: the binary XOR operator is called `xor` in rinja
  --> <source attribute>:1:6
        " b }}"
-  --> tests/ui/iso646.rs:32:34
+  --> tests/ui/iso646.rs:25:34
    |
-32 | #[template(ext = "txt", source = "{{ a ^ b }}")]
+25 | #[template(ext = "txt", source = "{{ a ^ b }}")]
    |                                  ^^^^^^^^^^^^^
+
+error[E0609]: no field `c` on type `&XorIso646`
+  --> tests/ui/iso646.rs:31:10
+   |
+31 | #[derive(Template)]
+   |          ^^^^^^^^ unknown field
+   |
+   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/testing/tests/ui/space-pipe.rs b/testing/tests/ui/space-pipe.rs
deleted file mode 100644
index 299917e23..000000000
--- a/testing/tests/ui/space-pipe.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-use rinja::Template;
-
-#[derive(Template)]
-#[template(ext = "txt", source = "{{a|lower}}")]
-struct Lower1<'a> {
-    a: &'a str,
-}
-
-#[derive(Template)]
-#[template(ext = "txt", source = "{{a |lower}}")]
-struct Lower2<'a> {
-    a: &'a str,
-}
-
-#[derive(Template)]
-#[template(ext = "txt", source = "{{a| lower}}")]
-struct Lower3<'a> {
-    a: &'a str,
-}
-
-#[derive(Template)]
-#[template(ext = "txt", source = "{{a | lower}}")]
-struct Lower4<'a> {
-    a: &'a str,
-}
-
-fn main() {
-}
diff --git a/testing/tests/ui/space-pipe.stderr b/testing/tests/ui/space-pipe.stderr
deleted file mode 100644
index ca16646ad..000000000
--- a/testing/tests/ui/space-pipe.stderr
+++ /dev/null
@@ -1,17 +0,0 @@
-error: the filter operator `|` must not be preceded by any whitespace characters
-       the binary OR operator is called `bitor` in rinja
-         --> <source attribute>:1:3
-       " |lower}}"
-  --> tests/ui/space-pipe.rs:10:34
-   |
-10 | #[template(ext = "txt", source = "{{a |lower}}")]
-   |                                  ^^^^^^^^^^^^^^
-
-error: the filter operator `|` must not be preceded by any whitespace characters
-       the binary OR operator is called `bitor` in rinja
-         --> <source attribute>:1:3
-       " | lower}}"
-  --> tests/ui/space-pipe.rs:22:34
-   |
-22 | #[template(ext = "txt", source = "{{a | lower}}")]
-   |                                  ^^^^^^^^^^^^^^^