askama/filters/
json.rs

1use std::convert::Infallible;
2use std::ops::Deref;
3use std::pin::Pin;
4use std::{fmt, io, str};
5
6use serde::Serialize;
7use serde_json::ser::{CompactFormatter, PrettyFormatter, Serializer};
8
9use super::FastWritable;
10use crate::ascii_str::{AsciiChar, AsciiStr};
11
12/// Serialize to JSON (requires `json` feature)
13///
14/// The generated string does not contain ampersands `&`, chevrons `< >`, or apostrophes `'`.
15/// To use it in a `<script>` you can combine it with the safe filter:
16///
17/// ``` html
18/// <script>
19/// var data = {{data|json|safe}};
20/// </script>
21/// ```
22///
23/// To use it in HTML attributes, you can either use it in quotation marks `"{{data|json}}"` as is,
24/// or in apostrophes with the (optional) safe filter `'{{data|json|safe}}'`.
25/// In HTML texts the output of e.g. `<pre>{{data|json|safe}}</pre>` is safe, too.
26///
27/// ```
28/// # #[cfg(feature = "code-in-doc")] {
29/// # use askama::Template;
30/// /// ```jinja
31/// /// <div><li data-extra='{{data|json|safe}}'>Example</li></div>
32/// /// ```
33///
34/// #[derive(Template)]
35/// #[template(ext = "html", in_doc = true)]
36/// struct Example<'a> {
37///     data: Vec<&'a str>,
38/// }
39///
40/// assert_eq!(
41///     Example { data: vec!["foo", "bar"] }.to_string(),
42///     "<div><li data-extra='[\"foo\",\"bar\"]'>Example</li></div>"
43/// );
44/// # }
45/// ```
46#[inline]
47pub fn json(value: impl Serialize) -> Result<impl fmt::Display, Infallible> {
48    Ok(ToJson { value })
49}
50
51/// Serialize to formatted/prettified JSON (requires `json` feature)
52///
53/// This filter works the same as [`json()`], but it formats the data for human readability.
54/// It has an additional "indent" argument, which can either be an integer how many spaces to use
55/// for indentation (capped to 16 characters), or a string (e.g. `"\u{A0}\u{A0}"` for two
56/// non-breaking spaces).
57///
58/// ### Note
59///
60/// In askama's template language, this filter is called `|json`, too. The right function is
61/// automatically selected depending on whether an `indent` argument was provided or not.
62///
63/// ```
64/// # #[cfg(feature = "code-in-doc")] {
65/// # use askama::Template;
66/// /// ```jinja
67/// /// <div>{{data|json(4)|safe}}</div>
68/// /// ```
69///
70/// #[derive(Template)]
71/// #[template(ext = "html", in_doc = true)]
72/// struct Example<'a> {
73///     data: Vec<&'a str>,
74/// }
75///
76/// assert_eq!(
77///     Example { data: vec!["foo", "bar"] }.to_string(),
78///     "<div>[
79///     \"foo\",
80///     \"bar\"
81/// ]</div>"
82/// );
83/// # }
84/// ```
85#[inline]
86pub fn json_pretty(
87    value: impl Serialize,
88    indent: impl AsIndent,
89) -> Result<impl fmt::Display, Infallible> {
90    Ok(ToJsonPretty { value, indent })
91}
92
93#[derive(Debug, Clone)]
94struct ToJson<S> {
95    value: S,
96}
97
98#[derive(Debug, Clone)]
99struct ToJsonPretty<S, I> {
100    value: S,
101    indent: I,
102}
103
104/// A prefix usable for indenting [prettified JSON data](json_pretty)
105///
106/// ```
107/// # use askama::filters::AsIndent;
108/// assert_eq!(4.as_indent(), "    ");
109/// assert_eq!(" -> ".as_indent(), " -> ");
110/// ```
111pub trait AsIndent {
112    /// Borrow `self` as prefix to use.
113    fn as_indent(&self) -> &str;
114}
115
116impl AsIndent for str {
117    #[inline]
118    fn as_indent(&self) -> &str {
119        self
120    }
121}
122
123#[cfg(feature = "alloc")]
124impl AsIndent for alloc::string::String {
125    #[inline]
126    fn as_indent(&self) -> &str {
127        self
128    }
129}
130
131impl AsIndent for usize {
132    #[inline]
133    fn as_indent(&self) -> &str {
134        spaces(*self)
135    }
136}
137
138impl AsIndent for std::num::Wrapping<usize> {
139    #[inline]
140    fn as_indent(&self) -> &str {
141        spaces(self.0)
142    }
143}
144
145impl AsIndent for std::num::NonZeroUsize {
146    #[inline]
147    fn as_indent(&self) -> &str {
148        spaces(self.get())
149    }
150}
151
152fn spaces(width: usize) -> &'static str {
153    const MAX_SPACES: usize = 16;
154    const SPACES: &str = match str::from_utf8(&[b' '; MAX_SPACES]) {
155        Ok(spaces) => spaces,
156        Err(_) => panic!(),
157    };
158
159    &SPACES[..width.min(SPACES.len())]
160}
161
162#[cfg(feature = "alloc")]
163impl<T: AsIndent + alloc::borrow::ToOwned + ?Sized> AsIndent for alloc::borrow::Cow<'_, T> {
164    #[inline]
165    fn as_indent(&self) -> &str {
166        T::as_indent(self)
167    }
168}
169
170crate::impl_for_ref! {
171    impl AsIndent for T {
172        #[inline]
173        fn as_indent(&self) -> &str {
174            <T>::as_indent(self)
175        }
176    }
177}
178
179impl<T> AsIndent for Pin<T>
180where
181    T: Deref,
182    <T as Deref>::Target: AsIndent,
183{
184    #[inline]
185    fn as_indent(&self) -> &str {
186        self.as_ref().get_ref().as_indent()
187    }
188}
189
190impl<S: Serialize> FastWritable for ToJson<S> {
191    fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> crate::Result<()> {
192        serialize(f, &self.value, CompactFormatter)
193    }
194}
195
196impl<S: Serialize> fmt::Display for ToJson<S> {
197    #[inline]
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        Ok(self.write_into(f)?)
200    }
201}
202
203impl<S: Serialize, I: AsIndent> FastWritable for ToJsonPretty<S, I> {
204    fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W) -> crate::Result<()> {
205        serialize(
206            f,
207            &self.value,
208            PrettyFormatter::with_indent(self.indent.as_indent().as_bytes()),
209        )
210    }
211}
212
213impl<S: Serialize, I: AsIndent> fmt::Display for ToJsonPretty<S, I> {
214    #[inline]
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        Ok(self.write_into(f)?)
217    }
218}
219
220#[inline]
221fn serialize<S, W, F>(dest: &mut W, value: &S, formatter: F) -> Result<(), crate::Error>
222where
223    S: Serialize + ?Sized,
224    W: fmt::Write + ?Sized,
225    F: serde_json::ser::Formatter,
226{
227    /// The struct must only ever be used with the output of `serde_json`.
228    /// `serde_json` only produces UTF-8 strings in its `io::Write::write()` calls,
229    /// and `<JsonWriter as io::Write>` depends on this invariant.
230    struct JsonWriter<'a, W: fmt::Write + ?Sized>(&'a mut W);
231
232    impl<W: fmt::Write + ?Sized> io::Write for JsonWriter<'_, W> {
233        /// Invariant: must be passed valid UTF-8 slices
234        #[inline]
235        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
236            self.write_all(bytes)?;
237            Ok(bytes.len())
238        }
239
240        /// Invariant: must be passed valid UTF-8 slices
241        fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
242            // SAFETY: `serde_json` only writes valid strings
243            let string = unsafe { std::str::from_utf8_unchecked(bytes) };
244            write_escaped_str(&mut *self.0, string)
245                .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
246        }
247
248        #[inline]
249        fn flush(&mut self) -> io::Result<()> {
250            Ok(())
251        }
252    }
253
254    /// Invariant: no character that needs escaping is multi-byte character when encoded in UTF-8;
255    /// that is true for characters in ASCII range.
256    #[inline]
257    fn write_escaped_str(dest: &mut (impl fmt::Write + ?Sized), src: &str) -> fmt::Result {
258        // This implementation reads one byte after another.
259        // It's not very fast, but should work well enough until portable SIMD gets stabilized.
260
261        let mut escaped_buf = ESCAPED_BUF_INIT;
262        let mut last = 0;
263
264        for (index, byte) in src.bytes().enumerate() {
265            if let Some(escaped) = get_escaped(byte) {
266                [escaped_buf[4], escaped_buf[5]] = escaped;
267                write_str_if_nonempty(dest, &src[last..index])?;
268                dest.write_str(AsciiStr::from_slice(&escaped_buf[..ESCAPED_BUF_LEN]))?;
269                last = index + 1;
270            }
271        }
272        write_str_if_nonempty(dest, &src[last..])
273    }
274
275    let mut serializer = Serializer::with_formatter(JsonWriter(dest), formatter);
276    Ok(value.serialize(&mut serializer)?)
277}
278
279/// Returns the decimal representation of the codepoint if the character needs HTML escaping.
280#[inline]
281fn get_escaped(byte: u8) -> Option<[AsciiChar; 2]> {
282    const _: () = assert!(CHAR_RANGE < 32);
283
284    if let MIN_CHAR..=MAX_CHAR = byte {
285        if (1u32 << (byte - MIN_CHAR)) & BITS != 0 {
286            return Some(TABLE.0[(byte - MIN_CHAR) as usize]);
287        }
288    }
289    None
290}
291
292#[inline(always)]
293fn write_str_if_nonempty(output: &mut (impl fmt::Write + ?Sized), input: &str) -> fmt::Result {
294    if !input.is_empty() {
295        output.write_str(input)
296    } else {
297        Ok(())
298    }
299}
300
301/// List of characters that need HTML escaping, not necessarily in ordinal order.
302const CHARS: &[u8] = br#"&'<>"#;
303
304/// The character with the lowest codepoint that needs HTML escaping.
305const MIN_CHAR: u8 = {
306    let mut v = u8::MAX;
307    let mut i = 0;
308    while i < CHARS.len() {
309        if v > CHARS[i] {
310            v = CHARS[i];
311        }
312        i += 1;
313    }
314    v
315};
316
317/// The character with the highest codepoint that needs HTML escaping.
318const MAX_CHAR: u8 = {
319    let mut v = u8::MIN;
320    let mut i = 0;
321    while i < CHARS.len() {
322        if v < CHARS[i] {
323            v = CHARS[i];
324        }
325        i += 1;
326    }
327    v
328};
329
330const BITS: u32 = {
331    let mut bits = 0;
332    let mut i = 0;
333    while i < CHARS.len() {
334        bits |= 1 << (CHARS[i] - MIN_CHAR);
335        i += 1;
336    }
337    bits
338};
339
340/// Number of codepoints between the lowest and highest character that needs escaping, incl.
341const CHAR_RANGE: usize = (MAX_CHAR - MIN_CHAR + 1) as usize;
342
343#[repr(align(64))]
344struct Table([[AsciiChar; 2]; CHAR_RANGE]);
345
346/// For characters that need HTML escaping, the codepoint is formatted as decimal digits,
347/// otherwise `b"\0\0"`. Starting at [`MIN_CHAR`].
348const TABLE: &Table = &{
349    let mut table = Table([UNESCAPED; CHAR_RANGE]);
350    let mut i = 0;
351    while i < CHARS.len() {
352        let c = CHARS[i];
353        table.0[c as u32 as usize - MIN_CHAR as usize] = AsciiChar::two_hex_digits(c as u32);
354        i += 1;
355    }
356    table
357};
358
359const UNESCAPED: [AsciiChar; 2] = AsciiStr::new_sized("");
360
361const ESCAPED_BUF_INIT_UNPADDED: &str = "\\u00__";
362// RATIONALE: llvm generates better code if the buffer is register sized
363const ESCAPED_BUF_INIT: [AsciiChar; 8] = AsciiStr::new_sized(ESCAPED_BUF_INIT_UNPADDED);
364const ESCAPED_BUF_LEN: usize = ESCAPED_BUF_INIT_UNPADDED.len();
365
366#[cfg(all(test, feature = "alloc"))]
367mod tests {
368    use alloc::string::ToString;
369    use alloc::vec;
370
371    use super::*;
372
373    #[test]
374    fn test_ugly() {
375        assert_eq!(json(true).unwrap().to_string(), "true");
376        assert_eq!(json("foo").unwrap().to_string(), r#""foo""#);
377        assert_eq!(json(true).unwrap().to_string(), "true");
378        assert_eq!(json("foo").unwrap().to_string(), r#""foo""#);
379        assert_eq!(
380            json("<script>").unwrap().to_string(),
381            r#""\u003cscript\u003e""#
382        );
383        assert_eq!(
384            json(vec!["foo", "bar"]).unwrap().to_string(),
385            r#"["foo","bar"]"#
386        );
387    }
388
389    #[test]
390    fn test_pretty() {
391        assert_eq!(json_pretty(true, "").unwrap().to_string(), "true");
392        assert_eq!(
393            json_pretty("<script>", "").unwrap().to_string(),
394            r#""\u003cscript\u003e""#
395        );
396        assert_eq!(
397            json_pretty(vec!["foo", "bar"], "").unwrap().to_string(),
398            r#"[
399"foo",
400"bar"
401]"#
402        );
403        assert_eq!(
404            json_pretty(vec!["foo", "bar"], 2).unwrap().to_string(),
405            r#"[
406  "foo",
407  "bar"
408]"#
409        );
410        assert_eq!(
411            json_pretty(vec!["foo", "bar"], "————").unwrap().to_string(),
412            r#"[
413————"foo",
414————"bar"
415]"#
416        );
417    }
418}