From: Ben Darnell Date: Sun, 5 Jul 2015 21:03:45 +0000 (-0400) Subject: Add Loader parameter to control whitespace mode. X-Git-Tag: v4.3.0b1~78 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=72251a29d25e90bd1b452b7a104153e7dc8419f2;p=thirdparty%2Ftornado.git Add Loader parameter to control whitespace mode. Also add Application setting. Closes #178. --- diff --git a/docs/template.rst b/docs/template.rst index caf345d25..3fc3242ee 100644 --- a/docs/template.rst +++ b/docs/template.rst @@ -6,7 +6,7 @@ Class reference --------------- - .. autoclass:: Template(template_string, name="", loader=None, compress_whitespace=None, autoescape="xhtml_escape") + .. autoclass:: Template(template_string, name="", loader=None, compress_whitespace=None, autoescape="xhtml_escape", whitespace=None) :members: .. autoclass:: BaseLoader @@ -19,3 +19,5 @@ :members: .. autoexception:: ParseError + + .. autofunction:: filter_whitespace diff --git a/docs/web.rst b/docs/web.rst index c96972478..e9bd6e1e0 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -225,6 +225,9 @@ If this setting is used the ``template_path`` and ``autoescape`` settings are ignored. Can be further customized by overriding `RequestHandler.create_template_loader`. + * ``template_whitespace``: Controls handling of whitespace in + templates; see `tornado.template.filter_whitespace` for allowed + values. New in Tornado 4.3. Static file settings: diff --git a/tornado/template.py b/tornado/template.py index 31d8f67ef..d0d35b3e8 100644 --- a/tornado/template.py +++ b/tornado/template.py @@ -210,6 +210,27 @@ _DEFAULT_AUTOESCAPE = "xhtml_escape" _UNSET = object() +def filter_whitespace(mode, text): + """Transform whitespace in ``text`` according to ``mode``. + + Available modes are: + + * ``all``: Return all whitespace unmodified. + * ``single``: Collapse consecutive whitespace with a single whitespace + character, preserving newlines. + + .. versionadded:: 4.3 + """ + if mode == 'all': + return text + elif mode == 'single': + text = re.sub(r"([\t ]+)", " ", text) + text = re.sub(r"(\s*\n\s*)", "\n", text) + return text + else: + raise Exception("invalid whitespace mode %s" % mode) + + class Template(object): """A compiled template. @@ -220,20 +241,56 @@ class Template(object): # autodoc because _UNSET looks like garbage. When changing # this signature update website/sphinx/template.rst too. def __init__(self, template_string, name="", loader=None, - compress_whitespace=None, autoescape=_UNSET): + compress_whitespace=_UNSET, autoescape=_UNSET, + whitespace=None): + """Construct a Template. + + :arg str template_string: the contents of the template file. + :arg str name: the filename from which the template was loaded + (used for error message). + :arg tornado.template.BaseLoader loader: the `~tornado.template.BaseLoader` responsible for this template, + used to resolve ``{% include %}`` and ``{% extend %}`` + directives. + :arg bool compress_whitespace: Deprecated since Tornado 4.3. + Equivalent to ``whitespace="single"`` if true and + ``whitespace="all"`` if false. + :arg str autoescape: The name of a function in the template + namespace, or ``None`` to disable escaping by default. + :arg str whitespace: A string specifying treatment of whitespace; + see `filter_whitespace` for options. + + .. versionchanged:: 4.3 + Added ``whitespace`` parameter; deprecated ``compress_whitespace``. + """ self.name = name - if compress_whitespace is None: - compress_whitespace = name.endswith(".html") or \ - name.endswith(".js") + + if compress_whitespace is not _UNSET: + # Convert deprecated compress_whitespace (bool) to whitespace (str). + if whitespace is not None: + raise Exception("cannot set both whitespace and compress_whitespace") + whitespace = "single" if compress_whitespace else "all" + if whitespace is None: + if loader and loader.whitespace: + whitespace = loader.whitespace + else: + # Whitespace defaults by filename. + if name.endswith(".html") or name.endswith(".js"): + whitespace = "single" + else: + whitespace = "all" + # Validate the whitespace setting. + filter_whitespace(whitespace, '') + if autoescape is not _UNSET: self.autoescape = autoescape elif loader: self.autoescape = loader.autoescape else: self.autoescape = _DEFAULT_AUTOESCAPE + self.namespace = loader.namespace if loader else {} reader = _TemplateReader(name, escape.native_str(template_string), - compress_whitespace) + whitespace) self.file = _File(self, _parse(reader, self)) self.code = self._generate_python(loader) self.loader = loader @@ -313,12 +370,26 @@ class BaseLoader(object): ``{% extends %}`` and ``{% include %}``. The loader caches all templates after they are loaded the first time. """ - def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None): - """``autoescape`` must be either None or a string naming a function - in the template namespace, such as "xhtml_escape". + def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None, + whitespace=None): + """Construct a template loader. + + :arg str autoescape: The name of a function in the template + namespace, such as "xhtml_escape", or ``None`` to disable + autoescaping by default. + :arg dict namespace: A dictionary to be added to the default template + namespace, or ``None``. + :arg str whitespace: A string specifying default behavior for + whitespace in templates; see `filter_whitespace` for options. + Default is "single" for files ending in ".html" and ".js" and + "all" for other files. + + .. versionchanged:: 4.3 + Added ``whitespace`` parameter. """ self.autoescape = autoescape self.namespace = namespace or {} + self.whitespace = whitespace self.templates = {} # self.lock protects self.templates. It's a reentrant lock # because templates may load other templates via `include` or @@ -559,20 +630,18 @@ class _Module(_Expression): class _Text(_Node): - def __init__(self, value, line, compress_whitespace): + def __init__(self, value, line, whitespace): self.value = value self.line = line - self.compress_whitespace = compress_whitespace + self.whitespace = whitespace def generate(self, writer): value = self.value - # Compress lots of white space to a single character. If the whitespace - # breaks a line, have it continue to break a line, but just with a - # single \n character - if self.compress_whitespace and "
" not in value:
-            value = re.sub(r"([\t ]+)", " ", value)
-            value = re.sub(r"(\s*\n\s*)", "\n", value)
+        # Compress whitespace if requested, with a crude heuristic to avoid
+        # altering preformatted whitespace.
+        if "
" not in value:
+            value = filter_whitespace(self.whitespace, value)
 
         if value:
             writer.write_line('_tt_append(%r)' % escape.utf8(value), self.line)
@@ -648,10 +717,10 @@ class _CodeWriter(object):
 
 
 class _TemplateReader(object):
-    def __init__(self, name, text, compress_whitespace):
+    def __init__(self, name, text, whitespace):
         self.name = name
         self.text = text
-        self.compress_whitespace = compress_whitespace
+        self.whitespace = whitespace
         self.line = 1
         self.pos = 0
 
@@ -726,7 +795,7 @@ def _parse(reader, template, in_block=None, in_loop=None):
                     reader.raise_parse_error(
                         "Missing {%% end %%} block for %s" % in_block)
                 body.chunks.append(_Text(reader.consume(), reader.line,
-                                         reader.compress_whitespace))
+                                         reader.whitespace))
                 return body
             # If the first curly brace is not the start of a special token,
             # start searching from the character after it
@@ -746,7 +815,7 @@ def _parse(reader, template, in_block=None, in_loop=None):
         if curly > 0:
             cons = reader.consume(curly)
             body.chunks.append(_Text(cons, reader.line,
-                                     reader.compress_whitespace))
+                                     reader.whitespace))
 
         start_brace = reader.consume(2)
         line = reader.line
@@ -758,7 +827,7 @@ def _parse(reader, template, in_block=None, in_loop=None):
         if reader.remaining() and reader[0] == "!":
             reader.consume(1)
             body.chunks.append(_Text(start_brace, line,
-                                     reader.compress_whitespace))
+                                     reader.whitespace))
             continue
 
         # Comment
diff --git a/tornado/test/template_test.py b/tornado/test/template_test.py
index 078ddc7bd..3808bb336 100644
--- a/tornado/test/template_test.py
+++ b/tornado/test/template_test.py
@@ -440,6 +440,18 @@ raw: {% raw name %}""",
         self.assertEqual(loader.load("include.txt").generate(),
                          b"\t\t\nasdf     ")
 
+    def test_whitespace_by_loader(self):
+        templates = {
+            "foo.html": "\t\tfoo",
+            "bar.txt": "\t\tbar",
+            }
+        loader = DictLoader(templates, whitespace='all')
+        self.assertEqual(loader.load("foo.html").generate(), b"\t\tfoo")
+        self.assertEqual(loader.load("bar.txt").generate(), b"\t\tbar")
+        loader = DictLoader(templates, whitespace='single')
+        self.assertEqual(loader.load("foo.html").generate(), b" foo")
+        self.assertEqual(loader.load("bar.txt").generate(), b" bar")
+
 
 class TemplateLoaderTest(unittest.TestCase):
     def setUp(self):
diff --git a/tornado/web.py b/tornado/web.py
index 219bc2a1f..cda04d0f0 100644
--- a/tornado/web.py
+++ b/tornado/web.py
@@ -838,8 +838,9 @@ class RequestHandler(object):
 
         May be overridden by subclasses.  By default returns a
         directory-based loader on the given path, using the
-        ``autoescape`` application setting.  If a ``template_loader``
-        application setting is supplied, uses that instead.
+        ``autoescape`` and ``template_whitespace`` application
+        settings.  If a ``template_loader`` application setting is
+        supplied, uses that instead.
         """
         settings = self.application.settings
         if "template_loader" in settings:
@@ -849,6 +850,8 @@ class RequestHandler(object):
             # autoescape=None means "no escaping", so we have to be sure
             # to only pass this kwarg if the user asked for it.
             kwargs["autoescape"] = settings["autoescape"]
+        if "template_whitespace" in settings:
+            kwargs["whitespace"] = settings["template_whitespace"]
         return template.Loader(template_path, **kwargs)
 
     def flush(self, include_footers=False, callback=None):