mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-30 06:21:13 +00:00
Merge pull request #32 from Kijewski/pr-uglier-json
Make JSON prettifying optional
This commit is contained in:
commit
214c4450b4
@ -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 characters:
|
||||
<p>{{data|tojson("\u{a0}\u{a0}")}}</p>
|
||||
```
|
||||
|
||||
## Custom Filters
|
||||
[#custom-filters]: #custom-filters
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
]"#
|
||||
);
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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 }}
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"foo": "{{ foo }}",
|
||||
"bar": {{ bar|json|safe }}
|
||||
}
|
@ -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 {
|
||||
|
10
testing/tests/ui/json-too-many-args.rs
Normal file
10
testing/tests/ui/json-too-many-args.rs
Normal 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() {
|
||||
}
|
9
testing/tests/ui/json-too-many-args.stderr
Normal file
9
testing/tests/ui/json-too-many-args.stderr
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user