From 462c04c0a07da057878877777f7fb9505f5af66d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sun, 28 Apr 2024 13:48:09 +0200 Subject: [PATCH 1/4] Make JSON prettifying optional This PR adds an optional argument to the `|tojson` filter, which controls if the serialized JSON data gets prettified or not. The arguments works the same as flask's [`|tojson`][flask] filter, which passes the argument to python's [`json.dumps()`][python]: * Omitting the argument, providing a negative integer, or `None`, then compact JSON data is generated. * Providing a non-negative integer, then this amount of ASCII spaces is used to indent the data. (Capped to 16 characters.) * Providing a string, then this string is used as prefix. I attempts are made to ensure that the prefix actually consists of whitespaces, because chances are, that if you provide e.g. `&nsbp;`, then you are doing it intentionally. This is a breaking change, because it changes the default behavior to not prettify the data. This is done intentionally, because this is how it works in flask. [flask]: https://jinja.palletsprojects.com/en/3.1.x/templates/#jinja-filters.tojson [python]: https://docs.python.org/3/library/json.html#json.dump --- book/src/filters.md | 12 ++ rinja/benches/to-json.rs | 4 +- rinja/src/filters/json.rs | 151 ++++++++++++++++++++--- rinja/src/filters/mod.rs | 2 +- rinja_derive/src/generator.rs | 8 +- testing/templates/allow-whitespaces.html | 10 +- testing/templates/json.html | 4 - testing/tests/filters.rs | 72 ++++++++++- 8 files changed, 233 insertions(+), 30 deletions(-) delete mode 100644 testing/templates/json.html diff --git a/book/src/filters.md b/book/src/filters.md index 95b92f07..e52f97e6 100644 --- a/book/src/filters.md +++ b/book/src/filters.md @@ -468,6 +468,18 @@ Ugly: Ugly: ``` +By default, a compact representation of the data is generated, i.e. no whitespaces are generated +between individual values. To generate a readable representation, you can either pass an integer +how many spaces to use as indentation, or you can pass a string that gets used as prefix: + +```jinja2 +Prefix with four spaces: + + +Prefix with two   characters: +

{{data|tojson("\u{a0}\u{a0}")}}

+``` + ## Custom Filters [#custom-filters]: #custom-filters diff --git a/rinja/benches/to-json.rs b/rinja/benches/to-json.rs index da6444f3..cb6ea877 100644 --- a/rinja/benches/to-json.rs +++ b/rinja/benches/to-json.rs @@ -13,7 +13,7 @@ fn functions(c: &mut Criterion) { fn escape_json(b: &mut criterion::Bencher<'_>) { b.iter(|| { for &s in STRINGS { - format!("{}", json(s).unwrap()); + format!("{}", json(s, ()).unwrap()); } }); } @@ -21,7 +21,7 @@ fn escape_json(b: &mut criterion::Bencher<'_>) { fn escape_json_for_html(b: &mut criterion::Bencher<'_>) { b.iter(|| { for &s in STRINGS { - format!("{}", MarkupDisplay::new_unsafe(json(s).unwrap(), Html)); + format!("{}", MarkupDisplay::new_unsafe(json(s, ()).unwrap(), Html)); } }); } diff --git a/rinja/src/filters/json.rs b/rinja/src/filters/json.rs index 93091824..a108ccfc 100644 --- a/rinja/src/filters/json.rs +++ b/rinja/src/filters/json.rs @@ -2,7 +2,7 @@ use std::convert::Infallible; use std::{fmt, io, str}; use serde::Serialize; -use serde_json::to_writer_pretty; +use serde_json::ser::{to_writer, PrettyFormatter, Serializer}; /// Serialize to JSON (requires `json` feature) /// @@ -19,17 +19,105 @@ use serde_json::to_writer_pretty; /// or in apostrophes with the (optional) safe filter `'{{data|json|safe}}'`. /// In HTML texts the output of e.g. `
{{data|json|safe}}
` is safe, too. #[inline] -pub fn json(s: S) -> Result { - Ok(ToJson(s)) +pub fn json(value: impl Serialize, indent: impl AsIndent) -> Result { + Ok(ToJson { value, indent }) +} + +pub trait AsIndent { + fn as_indent(&self) -> Option<&str>; +} + +impl AsIndent for str { + #[inline] + fn as_indent(&self) -> Option<&str> { + Some(self) + } +} + +impl AsIndent for String { + #[inline] + fn as_indent(&self) -> Option<&str> { + Some(self) + } +} + +impl AsIndent for isize { + fn as_indent(&self) -> Option<&str> { + const SPACES: &str = " "; + match *self < 0 { + true => None, + false => Some(&SPACES[..(*self as usize).min(SPACES.len())]), + } + } +} + +impl AsIndent for () { + #[inline] + fn as_indent(&self) -> Option<&str> { + None + } +} + +impl AsIndent for &T { + #[inline] + fn as_indent(&self) -> Option<&str> { + T::as_indent(self) + } +} + +impl AsIndent for Option { + #[inline] + fn as_indent(&self) -> Option<&str> { + self.as_ref().and_then(T::as_indent) + } +} + +impl AsIndent for Box { + #[inline] + fn as_indent(&self) -> Option<&str> { + T::as_indent(self.as_ref()) + } +} + +impl AsIndent for std::borrow::Cow<'_, T> { + #[inline] + fn as_indent(&self) -> Option<&str> { + T::as_indent(self.as_ref()) + } +} + +impl AsIndent for std::rc::Rc { + #[inline] + fn as_indent(&self) -> Option<&str> { + T::as_indent(self.as_ref()) + } +} + +impl AsIndent for std::sync::Arc { + #[inline] + fn as_indent(&self) -> Option<&str> { + T::as_indent(self.as_ref()) + } } #[derive(Debug, Clone)] -struct ToJson(S); +struct ToJson { + value: S, + indent: I, +} -impl fmt::Display for ToJson { - #[inline] +impl fmt::Display for ToJson { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - to_writer_pretty(JsonWriter(f), &self.0).map_err(|_| fmt::Error) + let f = JsonWriter(f); + if let Some(indent) = self.indent.as_indent() { + let formatter = PrettyFormatter::with_indent(indent.as_bytes()); + let mut serializer = Serializer::with_formatter(f, formatter); + self.value + .serialize(&mut serializer) + .map_err(|_| fmt::Error) + } else { + to_writer(f, &self.value).map_err(|_| fmt::Error) + } } } @@ -77,20 +165,55 @@ mod tests { use super::*; #[test] - fn test_json() { - assert_eq!(json(true).unwrap().to_string(), "true"); - assert_eq!(json("foo").unwrap().to_string(), r#""foo""#); - assert_eq!(json(true).unwrap().to_string(), "true"); - assert_eq!(json("foo").unwrap().to_string(), r#""foo""#); + fn test_ugly() { + assert_eq!(json(true, ()).unwrap().to_string(), "true"); + assert_eq!(json("foo", ()).unwrap().to_string(), r#""foo""#); + assert_eq!(json(true, ()).unwrap().to_string(), "true"); + assert_eq!(json("foo", ()).unwrap().to_string(), r#""foo""#); assert_eq!( - json("