aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--askama_escape/Cargo.toml3
-rw-r--r--askama_escape/src/lib.rs56
-rw-r--r--askama_shared/Cargo.toml2
-rw-r--r--askama_shared/src/filters/json.rs39
-rw-r--r--askama_shared/src/generator.rs2
-rw-r--r--book/src/filters.md72
-rw-r--r--testing/templates/json.html2
-rw-r--r--testing/tests/filters.rs60
-rw-r--r--testing/tests/whitespace.rs4
9 files changed, 191 insertions, 49 deletions
diff --git a/askama_escape/Cargo.toml b/askama_escape/Cargo.toml
index ed7148c..643f81a 100644
--- a/askama_escape/Cargo.toml
+++ b/askama_escape/Cargo.toml
@@ -17,6 +17,9 @@ maintenance = { status = "actively-developed" }
[dev-dependencies]
criterion = "0.3"
+[features]
+json = []
+
[[bench]]
name = "all"
harness = false
diff --git a/askama_escape/src/lib.rs b/askama_escape/src/lib.rs
index ad08e55..1788843 100644
--- a/askama_escape/src/lib.rs
+++ b/askama_escape/src/lib.rs
@@ -1,10 +1,7 @@
-#![no_std]
+#![cfg_attr(not(any(feature = "json", test)), no_std)]
#![deny(elided_lifetimes_in_paths)]
#![deny(unreachable_pub)]
-#[cfg(test)]
-extern crate std;
-
use core::fmt::{self, Display, Formatter, Write};
use core::str;
@@ -175,6 +172,57 @@ pub trait Escaper {
const FLAG: u8 = b'>' - b'"';
+/// Escape chevrons, ampersand and apostrophes for use in JSON
+#[cfg(feature = "json")]
+#[derive(Debug, Clone, Default)]
+pub struct JsonEscapeBuffer(Vec<u8>);
+
+#[cfg(feature = "json")]
+impl JsonEscapeBuffer {
+ pub fn new() -> Self {
+ Self(Vec::new())
+ }
+
+ pub fn finish(self) -> String {
+ unsafe { String::from_utf8_unchecked(self.0) }
+ }
+}
+
+#[cfg(feature = "json")]
+impl std::io::Write for JsonEscapeBuffer {
+ fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
+ macro_rules! push_esc_sequence {
+ ($start:ident, $i:ident, $self:ident, $bytes:ident, $quote:expr) => {{
+ if $start < $i {
+ $self.0.extend_from_slice(&$bytes[$start..$i]);
+ }
+ $self.0.extend_from_slice($quote);
+ $start = $i + 1;
+ }};
+ }
+
+ self.0.reserve(bytes.len());
+ let mut start = 0;
+ for (i, b) in bytes.iter().enumerate() {
+ match *b {
+ b'&' => push_esc_sequence!(start, i, self, bytes, br#"\u0026"#),
+ b'\'' => push_esc_sequence!(start, i, self, bytes, br#"\u0027"#),
+ b'<' => push_esc_sequence!(start, i, self, bytes, br#"\u003c"#),
+ b'>' => push_esc_sequence!(start, i, self, bytes, br#"\u003e"#),
+ _ => (),
+ }
+ }
+ if start < bytes.len() {
+ self.0.extend_from_slice(&bytes[start..]);
+ }
+ Ok(bytes.len())
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ Ok(())
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/askama_shared/Cargo.toml b/askama_shared/Cargo.toml
index e19ad35..ea9e968 100644
--- a/askama_shared/Cargo.toml
+++ b/askama_shared/Cargo.toml
@@ -12,7 +12,7 @@ edition = "2018"
[features]
default = ["config", "humansize", "num-traits", "percent-encoding"]
config = ["serde", "toml"]
-json = ["serde", "serde_json"]
+json = ["serde", "serde_json", "askama_escape/json"]
markdown = ["comrak"]
yaml = ["serde", "serde_yaml"]
diff --git a/askama_shared/src/filters/json.rs b/askama_shared/src/filters/json.rs
index c0df707..e94e50c 100644
--- a/askama_shared/src/filters/json.rs
+++ b/askama_shared/src/filters/json.rs
@@ -1,33 +1,40 @@
use crate::error::{Error, Result};
-use askama_escape::{Escaper, MarkupDisplay};
+use askama_escape::JsonEscapeBuffer;
use serde::Serialize;
+use serde_json::to_writer_pretty;
-/// Serialize to JSON (requires `serde_json` feature)
+/// Serialize to JSON (requires `json` feature)
///
-/// ## Errors
+/// The generated string does not contain ampersands `&`, chevrons `< >`, or apostrophes `'`.
+/// To use it in a `<script>` you can combine it with the safe filter:
///
-/// This will panic if `S`'s implementation of `Serialize` decides to fail,
-/// or if `T` contains a map with non-string keys.
-pub fn json<E: Escaper, S: Serialize>(e: E, s: S) -> Result<MarkupDisplay<E, String>> {
- match serde_json::to_string_pretty(&s) {
- Ok(s) => Ok(MarkupDisplay::new_safe(s, e)),
- Err(e) => Err(Error::from(e)),
- }
+/// ``` html
+/// <script>
+/// var data = {{data|json|safe}};
+/// </script>
+/// ```
+///
+/// To use it 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.
+pub fn json<S: Serialize>(s: S) -> Result<String> {
+ let mut writer = JsonEscapeBuffer::new();
+ to_writer_pretty(&mut writer, &s).map_err(Error::from)?;
+ Ok(writer.finish())
}
#[cfg(test)]
mod tests {
use super::*;
- use askama_escape::Html;
#[test]
fn test_json() {
- assert_eq!(json(Html, true).unwrap().to_string(), "true");
- assert_eq!(json(Html, "foo").unwrap().to_string(), r#""foo""#);
- assert_eq!(json(Html, &true).unwrap().to_string(), "true");
- assert_eq!(json(Html, &"foo").unwrap().to_string(), r#""foo""#);
+ assert_eq!(json(true).unwrap(), "true");
+ assert_eq!(json("foo").unwrap(), r#""foo""#);
+ assert_eq!(json(&true).unwrap(), "true");
+ assert_eq!(json(&"foo").unwrap(), r#""foo""#);
assert_eq!(
- json(Html, &vec!["foo", "bar"]).unwrap().to_string(),
+ json(&vec!["foo", "bar"]).unwrap(),
r#"[
"foo",
"bar"
diff --git a/askama_shared/src/generator.rs b/askama_shared/src/generator.rs
index 6024436..ea22a83 100644
--- a/askama_shared/src/generator.rs
+++ b/askama_shared/src/generator.rs
@@ -1171,7 +1171,7 @@ impl<'a, S: std::hash::BuildHasher> Generator<'a, S> {
return Err("the `yaml` filter requires the `serde-yaml` feature to be enabled".into());
}
- const FILTERS: [&str; 3] = ["safe", "json", "yaml"];
+ const FILTERS: [&str; 2] = ["safe", "yaml"];
if FILTERS.contains(&name) {
buf.write(&format!(
"::askama::filters::{}({}, ",
diff --git a/book/src/filters.md b/book/src/filters.md
index d00d778..c24a94c 100644
--- a/book/src/filters.md
+++ b/book/src/filters.md
@@ -287,6 +287,53 @@ Output:
5
```
+## Optional / feature gated filters
+
+The following filters can be enabled by requesting the respective feature in the Cargo.toml
+[dependencies section](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html), e.g.
+
+```
+[dependencies]
+askama = { version = "0.11.0", features = "serde-json" }
+```
+
+### `json` | `tojson`
+
+Enabling the `serde-json` feature will enable the use of the `json` filter.
+This will output formatted JSON for any value that implements the required
+[`Serialize`](https://docs.rs/serde/1.*/serde/trait.Serialize.html) trait.
+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.
+
+```
+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>
+
+Ugly: <script>var data = "{{data|json}}";</script>
+Ugly: <script>var data = '{{data|json|safe}}';</script>
+```
+
+### `yaml`
+
+Enabling the `serde-yaml` feature will enable the use of the `yaml` filter.
+This will output formatted YAML for any value that implements the required
+[`Serialize`](https://docs.rs/serde/1.*/serde/trait.Serialize.html) trait.
+
+```jinja
+{{ foo|yaml }}
+```
+
+
## Custom Filters
To define your own filters, simply have a module named filters in scope of the context deriving a `Template` impl.
@@ -311,28 +358,3 @@ fn main() {
assert_eq!(t.render().unwrap(), "faa");
}
```
-
-## The `json` filter
-
-Enabling the `serde-json` feature will enable the use of the `json` filter.
-This will output formatted JSON for any value that implements the required
-`Serialize` trait.
-
-```jinja
-{
- "foo": "{{ foo }}",
- "bar": {{ bar|json }}
-}
-```
-
-For compatibility with Jinja, `tojson` can be used in place of `json`.
-
-## The `yaml` filter
-
-Enabling the `serde-yaml` feature will enable the use of the `yaml` filter.
-This will output formatted JSON for any value that implements the required
-`Serialize` trait.
-
-```
-{{ foo|yaml }}
-```
diff --git a/testing/templates/json.html b/testing/templates/json.html
index 250b7be..a1855ca 100644
--- a/testing/templates/json.html
+++ b/testing/templates/json.html
@@ -1,4 +1,4 @@
{
"foo": "{{ foo }}",
- "bar": {{ bar|json }}
+ "bar": {{ bar|json|safe }}
}
diff --git a/testing/tests/filters.rs b/testing/tests/filters.rs
index be3e0ab..7973f45 100644
--- a/testing/tests/filters.rs
+++ b/testing/tests/filters.rs
@@ -250,3 +250,63 @@ fn test_filter_truncate() {
};
assert_eq!(t.render().unwrap(), "alpha baralpha...");
}
+
+#[cfg(feature = "serde-json")]
+#[derive(Template)]
+#[template(source = r#"<li data-name="{{name|json}}"></li>"#, ext = "html")]
+struct JsonAttributeTemplate<'a> {
+ name: &'a str,
+}
+
+#[cfg(feature = "serde-json")]
+#[test]
+fn test_json_attribute() {
+ let t = JsonAttributeTemplate {
+ name: r#""><button>Hacked!</button>"#,
+ };
+ assert_eq!(
+ t.render().unwrap(),
+ r#"<li data-name="&quot;\&quot;\u003e\u003cbutton\u003eHacked!\u003c/button\u003e&quot;"></li>"#
+ );
+}
+
+#[cfg(feature = "serde-json")]
+#[derive(Template)]
+#[template(source = r#"<li data-name='{{name|json|safe}}'></li>"#, ext = "html")]
+struct JsonAttribute2Template<'a> {
+ name: &'a str,
+}
+
+#[cfg(feature = "serde-json")]
+#[test]
+fn test_json_attribute2() {
+ let t = JsonAttribute2Template {
+ name: r#"'><button>Hacked!</button>"#,
+ };
+ assert_eq!(
+ t.render().unwrap(),
+ r#"<li data-name='"\u0027\u003e\u003cbutton\u003eHacked!\u003c/button\u003e"'></li>"#
+ );
+}
+
+#[cfg(feature = "serde-json")]
+#[derive(Template)]
+#[template(
+ source = r#"<script>var user = {{name|json|safe}}</script>"#,
+ ext = "html"
+)]
+struct JsonScriptTemplate<'a> {
+ name: &'a str,
+}
+
+#[cfg(feature = "serde-json")]
+#[test]
+fn test_json_script() {
+ let t = JsonScriptTemplate {
+ name: r#"</script><button>Hacked!</button>"#,
+ };
+ assert_eq!(
+ t.render().unwrap(),
+ r#"<script>var user = "\u003c/script\u003e\u003cbutton\u003eHacked!\u003c/button\u003e"</script>"#
+ );
+}
diff --git a/testing/tests/whitespace.rs b/testing/tests/whitespace.rs
index ca72b23..cbcddd7 100644
--- a/testing/tests/whitespace.rs
+++ b/testing/tests/whitespace.rs
@@ -1,3 +1,5 @@
+#![cfg(feature = "serde-json")]
+
use askama::Template;
#[derive(askama::Template, Default)]
@@ -37,5 +39,5 @@ fn test_extra_whitespace() {
let mut template = AllowWhitespaces::default();
template.nested_1.nested_2.array = &["a0", "a1", "a2", "a3"];
template.nested_1.nested_2.hash.insert("key", "value");
- assert_eq!(template.render().unwrap(), "\n0\n0\n0\n0\n\n\n\n0\n0\n0\n0\n0\n\na0\na1\nvalue\n\n\n\n\n\n[\n \"a0\",\n \"a1\",\n \"a2\",\n \"a3\"\n]\n[\n \"a0\",\n \"a1\",\n \"a2\",\n \"a3\"\n][\n \"a0\",\n \"a1\",\n \"a2\",\n \"a3\"\n]\n[\n \"a1\"\n][\n \"a1\"\n]\n[\n \"a1\",\n \"a2\"\n][\n \"a1\",\n \"a2\"\n]\n[\n \"a1\"\n][\n \"a1\"\n]1-1-1\n3333 3\n2222 2\n0000 0\n3333 3\n\ntruefalse\nfalsefalsefalse\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
+ assert_eq!(template.render().unwrap(), "\n0\n0\n0\n0\n\n\n\n0\n0\n0\n0\n0\n\na0\na1\nvalue\n\n\n\n\n\n[\n &quot;a0&quot;,\n &quot;a1&quot;,\n &quot;a2&quot;,\n &quot;a3&quot;\n]\n[\n &quot;a0&quot;,\n &quot;a1&quot;,\n &quot;a2&quot;,\n &quot;a3&quot;\n][\n &quot;a0&quot;,\n &quot;a1&quot;,\n &quot;a2&quot;,\n &quot;a3&quot;\n]\n[\n &quot;a1&quot;\n][\n &quot;a1&quot;\n]\n[\n &quot;a1&quot;,\n &quot;a2&quot;\n][\n &quot;a1&quot;,\n &quot;a2&quot;\n]\n[\n &quot;a1&quot;\n][\n &quot;a1&quot;\n]1-1-1\n3333 3\n2222 2\n0000 0\n3333 3\n\ntruefalse\nfalsefalsefalse\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
}