Merge pull request #32 from Kijewski/pr-uglier-json

Make JSON prettifying optional
This commit is contained in:
René Kijewski 2024-06-27 14:29:39 +02:00 committed by GitHub
commit 214c4450b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 270 additions and 24 deletions

View File

@ -468,6 +468,18 @@ 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
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:
<textarea>{{data|tojson(4)}}</textarea>
Prefix with two &nbsp; characters:
<p>{{data|tojson("\u{a0}\u{a0}")}}</p>
```
## Custom Filters
[#custom-filters]: #custom-filters

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, Criterion};
use rinja::filters::json;
use rinja::filters::{json, json_pretty};
use rinja_escape::{Html, MarkupDisplay};
criterion_main!(benches);
@ -7,7 +7,10 @@ criterion_group!(benches, functions);
fn functions(c: &mut Criterion) {
c.bench_function("escape JSON", escape_json);
c.bench_function("escape JSON (pretty)", escape_json_pretty);
c.bench_function("escape JSON for HTML", escape_json_for_html);
c.bench_function("escape JSON for HTML (pretty)", escape_json_for_html);
c.bench_function("escape JSON for HTML (pretty)", escape_json_for_html_pretty);
}
fn escape_json(b: &mut criterion::Bencher<'_>) {
@ -18,6 +21,14 @@ fn escape_json(b: &mut criterion::Bencher<'_>) {
});
}
fn escape_json_pretty(b: &mut criterion::Bencher<'_>) {
b.iter(|| {
for &s in STRINGS {
format!("{}", json_pretty(s, 2).unwrap());
}
});
}
fn escape_json_for_html(b: &mut criterion::Bencher<'_>) {
b.iter(|| {
for &s in STRINGS {
@ -26,6 +37,17 @@ fn escape_json_for_html(b: &mut criterion::Bencher<'_>) {
});
}
fn escape_json_for_html_pretty(b: &mut criterion::Bencher<'_>) {
b.iter(|| {
for &s in STRINGS {
format!(
"{}",
MarkupDisplay::new_unsafe(json_pretty(s, 2).unwrap(), Html),
);
}
});
}
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

View File

@ -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,120 @@ 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. `<pre>{{data|json|safe}}</pre>` is safe, too.
#[inline]
pub fn json<S: Serialize>(s: S) -> Result<impl fmt::Display, Infallible> {
Ok(ToJson(s))
pub fn json(value: impl Serialize) -> Result<impl fmt::Display, Infallible> {
Ok(ToJson { value })
}
/// Serialize to formatted/prettified JSON (requires `json` feature)
///
/// This filter works the same as [`json()`], but it formats the data for human readability.
/// It has an additional "indent" argument, which can either be an integer how many spaces to use
/// for indentation (capped to 16 characters), or a string (e.g. `"\u{A0}\u{A0}"` for two
/// non-breaking spaces).
///
/// ### Note
///
/// In rinja's template language, this filter is called `|json`, too. The right function is
/// automatically selected depending on whether an `indent` argument was provided or not.
#[inline]
pub fn json_pretty(
value: impl Serialize,
indent: impl AsIndent,
) -> Result<impl fmt::Display, Infallible> {
Ok(ToJsonPretty { value, indent })
}
#[derive(Debug, Clone)]
struct ToJson<S: Serialize>(S);
struct ToJson<S> {
value: S,
}
#[derive(Debug, Clone)]
struct ToJsonPretty<S, I> {
value: S,
indent: I,
}
pub trait AsIndent {
fn as_indent(&self) -> &str;
}
impl AsIndent for str {
#[inline]
fn as_indent(&self) -> &str {
self
}
}
impl AsIndent for String {
#[inline]
fn as_indent(&self) -> &str {
self
}
}
impl AsIndent for usize {
#[inline]
fn as_indent(&self) -> &str {
const MAX_SPACES: usize = 16;
const SPACES: &str = match str::from_utf8(&[b' '; MAX_SPACES]) {
Ok(spaces) => spaces,
Err(_) => panic!(),
};
&SPACES[..(*self).min(SPACES.len())]
}
}
impl<T: AsIndent + ?Sized> AsIndent for &T {
#[inline]
fn as_indent(&self) -> &str {
T::as_indent(self)
}
}
impl<T: AsIndent + ?Sized> AsIndent for Box<T> {
#[inline]
fn as_indent(&self) -> &str {
T::as_indent(self.as_ref())
}
}
impl<T: AsIndent + ToOwned + ?Sized> AsIndent for std::borrow::Cow<'_, T> {
#[inline]
fn as_indent(&self) -> &str {
T::as_indent(self.as_ref())
}
}
impl<T: AsIndent + ?Sized> AsIndent for std::rc::Rc<T> {
#[inline]
fn as_indent(&self) -> &str {
T::as_indent(self.as_ref())
}
}
impl<T: AsIndent + ?Sized> AsIndent for std::sync::Arc<T> {
#[inline]
fn as_indent(&self) -> &str {
T::as_indent(self.as_ref())
}
}
impl<S: Serialize> fmt::Display for ToJson<S> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
to_writer_pretty(JsonWriter(f), &self.0).map_err(|_| fmt::Error)
to_writer(JsonWriter(f), &self.value).map_err(|_| fmt::Error)
}
}
impl<S: Serialize, I: AsIndent> fmt::Display for ToJsonPretty<S, I> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let indent = self.indent.as_indent();
let formatter = PrettyFormatter::with_indent(indent.as_bytes());
let mut serializer = Serializer::with_formatter(JsonWriter(f), formatter);
self.value
.serialize(&mut serializer)
.map_err(|_| fmt::Error)
}
}
@ -77,7 +180,7 @@ mod tests {
use super::*;
#[test]
fn test_json() {
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");
@ -88,9 +191,36 @@ mod tests {
);
assert_eq!(
json(vec!["foo", "bar"]).unwrap().to_string(),
r#"["foo","bar"]"#
);
}
#[test]
fn test_pretty() {
assert_eq!(json_pretty(true, "").unwrap().to_string(), "true");
assert_eq!(
json_pretty("<script>", "").unwrap().to_string(),
r#""\u003cscript\u003e""#
);
assert_eq!(
json_pretty(vec!["foo", "bar"], "").unwrap().to_string(),
r#"[
"foo",
"bar"
]"#
);
assert_eq!(
json_pretty(vec!["foo", "bar"], 2).unwrap().to_string(),
r#"[
"foo",
"bar"
]"#
);
assert_eq!(
json_pretty(vec!["foo", "bar"], "————").unwrap().to_string(),
r#"[
"foo",
"bar"
]"#
);
}

View File

@ -10,7 +10,7 @@ use std::fmt::{self, Write};
#[cfg(feature = "serde_json")]
mod json;
#[cfg(feature = "serde_json")]
pub use self::json::json;
pub use self::json::{json, json_pretty, AsIndent};
#[cfg(feature = "humansize")]
use humansize::{ISizeFormatter, ToF64, DECIMAL};

View File

@ -1364,11 +1364,13 @@ impl<'a> Generator<'a> {
));
}
if args.len() != 1 {
return Err(ctx.generate_error("unexpected argument(s) in `json` filter", node));
}
buf.write(CRATE);
buf.write("::filters::json(");
let filter = match args.len() {
1 => "json",
2 => "json_pretty",
_ => return Err(ctx.generate_error("unexpected argument(s) in `json` filter", node)),
};
buf.write(format_args!("{CRATE}::filters::{filter}("));
self._visit_args(ctx, buf, args)?;
buf.write(")?");
Ok(DisplayWrap::Unwrapped)

View File

@ -17,6 +17,7 @@ phf = { version = "0.11", features = ["macros" ]}
serde_json = { version = "1.0", optional = true }
[dev-dependencies]
rinja = { path = "../rinja", version = "0.13", features = ["serde_json"] }
criterion = "0.5"
trybuild = "1.0.76"

View File

@ -25,11 +25,11 @@
{% let hash = &nested_1.nested_2.hash %}
#}
{{ array| json }}
{{ array[..]| json }}{{ array [ .. ]| json }}
{{ array[1..2]| json }}{{ array [ 1 .. 2 ]| json }}
{{ array[1..=2]| json }}{{ array [ 1 ..= 2 ]| json }}
{{ array[(0+1)..(3-1)]| json }}{{ array [ ( 0 + 1 ) .. ( 3 - 1 ) ]| json }}
{{ array| json(2) }}
{{ array[..]| json(2) }}{{ array [ .. ]| json(2) }}
{{ array[1..2]| json(2) }}{{ array [ 1 .. 2 ]| json(2) }}
{{ array[1..=2]| json(2) }}{{ array [ 1 ..= 2 ]| json(2) }}
{{ array[(0+1)..(3-1)]| json(2) }}{{ array [ ( 0 + 1 ) .. ( 3 - 1 ) ]| json(2) }}
{{-1}}{{ -1 }}{{ - 1 }}
{{1+2}}{{ 1+2 }}{{ 1 +2 }}{{ 1+ 2 }} {{ 1 + 2 }}

View File

@ -1,4 +0,0 @@
{
"foo": "{{ foo }}",
"bar": {{ bar|json|safe }}
}

View File

@ -164,7 +164,13 @@ fn test_vec_join() {
#[cfg(feature = "serde_json")]
#[derive(Template)]
#[template(path = "json.html")]
#[template(
source = r#"{
"foo": "{{ foo }}",
"bar": {{ bar|json|safe }}
}"#,
ext = "txt"
)]
struct JsonTemplate<'a> {
foo: &'a str,
bar: &'a Value,
@ -178,6 +184,37 @@ fn test_json() {
foo: "a",
bar: &val,
};
assert_eq!(
t.render().unwrap(),
r#"{
"foo": "a",
"bar": {"arr":["one",2,true,null]}
}"#
);
}
#[cfg(feature = "serde_json")]
#[derive(Template)]
#[template(
source = r#"{
"foo": "{{ foo }}",
"bar": {{ bar|json(2)|safe }}
}"#,
ext = "txt"
)]
struct PrettyJsonTemplate<'a> {
foo: &'a str,
bar: &'a Value,
}
#[cfg(feature = "serde_json")]
#[test]
fn test_pretty_json() {
let val = json!({"arr": [ "one", 2, true, null ]});
let t = PrettyJsonTemplate {
foo: "a",
bar: &val,
};
// Note: the json filter lacks a way to specify initial indentation
assert_eq!(
t.render().unwrap(),
@ -195,6 +232,33 @@ fn test_json() {
);
}
#[cfg(feature = "serde_json")]
#[derive(Template)]
#[template(source = r#"{{ bar|json(indent)|safe }}"#, ext = "txt")]
struct DynamicJsonTemplate<'a> {
bar: &'a Value,
indent: &'a str,
}
#[cfg(feature = "serde_json")]
#[test]
fn test_dynamic_json() {
let val = json!({"arr": ["one", 2]});
let t = DynamicJsonTemplate {
bar: &val,
indent: "?",
};
assert_eq!(
t.render().unwrap(),
r#"{
?"arr": [
??"one",
??2
?]
}"#
);
}
#[derive(Template)]
#[template(source = "{{ x|mytrim|safe }}", ext = "html")]
struct NestedFilterTemplate {

View File

@ -0,0 +1,10 @@
#![cfg(feature = "serde_json")]
use rinja::Template;
#[derive(Template)]
#[template(ext = "txt", source = "{{ 1|json(2, 3) }}")]
struct OneTwoThree;
fn main() {
}

View File

@ -0,0 +1,9 @@
error: unexpected argument(s) in `json` filter
--> OneTwoThree.txt:1:3
"1|json(2, 3) }}"
--> tests/ui/json-too-many-args.rs:5:10
|
5 | #[derive(Template)]
| ^^^^^^^^
|
= note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)