]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
Ported tojson filter. Fixes #458
authorArmin Ronacher <armin.ronacher@active-4.com>
Fri, 6 Jan 2017 20:33:51 +0000 (21:33 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Fri, 6 Jan 2017 20:33:51 +0000 (21:33 +0100)
CHANGES
docs/api.rst
jinja2/defaults.py
jinja2/filters.py
jinja2/utils.py
tests/test_filters.py

diff --git a/CHANGES b/CHANGES
index 62ca6b08108fce5358803e8f0f35f99c381bff76..5f69137d8951f26cbfcfc977f6dc7ed664e8b128 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -25,6 +25,8 @@ Version 2.9
   the string is barely truncated at all.
 - Change the logic for macro autoescaping to be based on the runtime
   autoescaping information at call time instead of macro define time.
+- Ported a modified version of the `tojson` filter from Flask to Jinja2
+  and hooked it up with the new policy framework.
 
 Version 2.8.2
 -------------
index 107acd6514418afbba59544ef61ab778c91c24aa..8bf0fdfe0986e1fe73814c2aa6e9540bc3f65ec9 100644 (file)
@@ -565,6 +565,18 @@ Example::
     The default target that is issued for links from the `urlize` filter
     if no other target is defined by the call explicitly.
 
+``json.dumps_function``:
+    If this is set to a value other than `None` then the `tojson` filter
+    will dump with this function instead of the default one.  Note that
+    this function should accept arbitrary extra arguments which might be
+    passed in the future from the filter.  Currently the only argument
+    that might be passed is `indent`.  The default dump function is
+    ``json.dumps``.
+
+``json.dumps_kwargs``:
+    Keyword arguments to be passed to the dump function.  The default is
+    ``{'sort_keys': True}``.
+
 
 Utilities
 ---------
index bdb538d59d136ed4319e6504c654d9761369817d..90ccb65f851429b18ba9a45a653117631b0d3be4 100644 (file)
@@ -41,8 +41,10 @@ DEFAULT_NAMESPACE = {
 
 # default policies
 DEFAULT_POLICIES = {
-    'urlize.rel':       'noopener',
-    'urlize.target':    None,
+    'urlize.rel':           'noopener',
+    'urlize.target':        None,
+    'json.dumps_function':  None,
+    'json.dumps_kwargs':    {'sort_keys': True},
 }
 
 
index 33e0ff14dd6464fcf7701a223211d15f9b5588de..05c2fc4596c105fb3cd1327c0db6b690aa8d26ee 100644 (file)
@@ -15,7 +15,7 @@ from random import choice
 from itertools import groupby
 from collections import namedtuple
 from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \
-     unicode_urlencode
+     unicode_urlencode, htmlsafe_json_dumps
 from jinja2.runtime import Undefined
 from jinja2.exceptions import FilterArgumentError
 from jinja2._compat import imap, string_types, text_type, iteritems
@@ -916,6 +916,39 @@ def do_rejectattr(*args, **kwargs):
     return select_or_reject(args, kwargs, lambda x: not x, True)
 
 
+@evalcontextfilter
+def do_tojson(eval_ctx, value, indent=None):
+    """Dumps a structure to JSON so that it's safe to use in ``<script>``
+    tags.  It accepts the same arguments and returns a JSON string.  Note that
+    this is available in templates through the ``|tojson`` filter which will
+    also mark the result as safe.  Due to how this function escapes certain
+    characters this is safe even if used outside of ``<script>`` tags.
+
+    The following characters are escaped in strings:
+
+    -   ``<``
+    -   ``>``
+    -   ``&``
+    -   ``'``
+
+    This makes it safe to embed such strings in any place in HTML with the
+    notable exception of double quoted attributes.  In that case single
+    quote your attributes or HTML escape it in addition.
+
+    The indent parameter can be used to enable pretty printing.  Set it to
+    the number of spaces that the structures should be indented with.
+
+    .. versionadded:: 2.9
+    """
+    policies = eval_ctx.environment.policies
+    dumper = policies['json.dumps_function']
+    options = policies['json.dumps_kwargs']
+    if indent is not None:
+        options = dict(options)
+        options['indent'] = indent
+    return htmlsafe_json_dumps(value, dumper=dumper, **options)
+
+
 def prepare_map(args, kwargs):
     context = args[0]
     seq = args[1]
@@ -1021,4 +1054,5 @@ FILTERS = {
     'wordcount':            do_wordcount,
     'wordwrap':             do_wordwrap,
     'xmlattr':              do_xmlattr,
+    'tojson':               do_tojson,
 }
index 96b135210df25b1a0765912e01e16a0793bb7bd2..38e5edb2e3d543e518111c1aac2ae8b858e0cf8c 100644 (file)
@@ -9,6 +9,7 @@
     :license: BSD, see LICENSE for more details.
 """
 import re
+import json
 import errno
 from collections import deque
 from threading import Lock
@@ -37,6 +38,8 @@ internal_code = set()
 
 concat = u''.join
 
+_slash_escape = '\\/' not in json.dumps('/')
+
 
 def contextfunction(f):
     """This decorator can be used to mark a function or method context callable.
@@ -485,6 +488,34 @@ except ImportError:
     pass
 
 
+def htmlsafe_json_dumps(obj, dumper=None, **kwargs):
+    """Works exactly like :func:`dumps` but is safe for use in ``<script>``
+    tags.  It accepts the same arguments and returns a JSON string.  Note that
+    this is available in templates through the ``|tojson`` filter which will
+    also mark the result as safe.  Due to how this function escapes certain
+    characters this is safe even if used outside of ``<script>`` tags.
+
+    The following characters are escaped in strings:
+
+    -   ``<``
+    -   ``>``
+    -   ``&``
+    -   ``'``
+
+    This makes it safe to embed such strings in any place in HTML with the
+    notable exception of double quoted attributes.  In that case single
+    quote your attributes or HTML escape it in addition.
+    """
+    if dumper is None:
+        dumper = json.dumps
+    rv = dumper(obj, **kwargs) \
+        .replace(u'<', u'\\u003c') \
+        .replace(u'>', u'\\u003e') \
+        .replace(u'&', u'\\u0026') \
+        .replace(u"'", u'\\u0027')
+    return rv
+
+
 @implements_iterator
 class Cycler(object):
     """A cycle helper for templates."""
index ba57136d3a81dc02173bfc4468e3d32bd979faf3..1a8a1640e43a73014c692476fa305c596af10286 100644 (file)
@@ -576,3 +576,16 @@ class TestFilter(object):
         tmpl = env.from_string('{{ users|rejectattr("id", "odd")|'
                                'map(attribute="name")|join("|") }}')
         assert tmpl.render(users=users) == 'jane'
+
+    def test_json_dump(self):
+        env = Environment(autoescape=True)
+        t = env.from_string('{{ x|tojson }}')
+        assert t.render(x={'foo': 'bar'}) == '{&#34;foo&#34;: &#34;bar&#34;}'
+        assert t.render(x='"bar\'') == '&#34;\&#34;bar\u0027&#34;'
+
+        def my_dumps(value, **options):
+            assert options == {'foo': 'bar'}
+            return '42'
+        env.policies['json.dumps_function'] = my_dumps
+        env.policies['json.dumps_kwargs'] = {'foo': 'bar'}
+        assert t.render(x=23) == '42'