Code

Ticket #7295: 7295.2.diff

File 7295.2.diff, 11.2 KB (added by akaihola, 6 years ago)

Patch now handles un-escaping correctly and optimizes numeric literals.

Line 
1Index: django/template/__init__.py
2===================================================================
3--- django/template/__init__.py (revision 7547)
4+++ django/template/__init__.py (working copy)
5@@ -54,7 +54,7 @@
6 from django.template.context import Context, RequestContext, ContextPopException
7 from django.utils.itercompat import is_iterable
8 from django.utils.functional import curry, Promise
9-from django.utils.text import smart_split
10+from django.utils.text import smart_split, unescape_string_literal
11 from django.utils.encoding import smart_unicode, force_unicode
12 from django.utils.translation import ugettext as _
13 from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
14@@ -431,33 +431,44 @@
15             self.pointer = i
16             return s
17 
18+constant_string = r"""
19+(?:%(i18n_open)s%(strdq)s%(i18n_close)s|
20+%(i18n_open)s%(strsq)s%(i18n_close)s|
21+%(strdq)s|
22+%(strsq)s)|
23+%(num)s
24+""" % {
25+    'strdq': r'''"[^"\\]*(?:\\.[^"\\]*)*"''', # double-quoted string
26+    'strsq': r"""'[^'\\]*(?:\\.[^'\\]*)*'""", # single-quoted string
27+    'num': r'[-+\.]?\d[\d\.e]*', # numeric constant
28+    'i18n_open' : re.escape("_("),
29+    'i18n_close' : re.escape(")"),
30+  }
31+constant_string = constant_string.replace("\n", "")
32+
33 filter_raw_string = r"""
34-^%(i18n_open)s"(?P<i18n_constant>%(str)s)"%(i18n_close)s|
35-^"(?P<constant>%(str)s)"|
36+^(?P<constant>%(constant)s)|
37 ^(?P<var>[%(var_chars)s]+)|
38  (?:%(filter_sep)s
39      (?P<filter_name>\w+)
40          (?:%(arg_sep)s
41              (?:
42-              %(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s|
43-              "(?P<constant_arg>%(str)s)"|
44+              (?P<constant_arg>%(constant)s)|
45               (?P<var_arg>[%(var_chars)s]+)
46              )
47          )?
48  )""" % {
49-    'str': r"""[^"\\]*(?:\\.[^"\\]*)*""",
50+    'constant': constant_string,
51     'var_chars': "\w\." ,
52     'filter_sep': re.escape(FILTER_SEPARATOR),
53     'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR),
54-    'i18n_open' : re.escape("_("),
55-    'i18n_close' : re.escape(")"),
56   }
57 
58 filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "")
59 filter_re = re.compile(filter_raw_string, re.UNICODE)
60 
61 class FilterExpression(object):
62-    """
63+    r"""
64     Parses a variable token and its optional filters (all as a single string),
65     and return a list of tuples of the filter name and arguments.
66     Sample:
67@@ -469,8 +480,47 @@
68         >>> fe.var
69         'variable'
70 
71+        >>> c = {'article': {'section': u'News'}}
72+        >>> def fe_test(s): return FilterExpression(s, p).resolve(c)
73+        >>> fe_test('article.section')
74+        u'News'
75+        >>> fe_test('article.section|upper')
76+        u'NEWS'
77+        >>> fe_test(u'"News"')
78+        u'News'
79+        >>> fe_test(u"'News'")
80+        u'News'
81+        >>> fe_test(ur'"Some \"Good\" News"')
82+        u'Some "Good" News'
83+        >>> fe_test(ur"'Some \'Bad\' News'")
84+        u"Some 'Bad' News"
85+
86+        >>> fe = FilterExpression(ur'"Some \"Good\" News"', p)
87+        >>> fe.filters
88+        []
89+        >>> fe.var
90+        u'Some "Good" News'
91+
92     This class should never be instantiated outside of the
93     get_filters_from_token helper function.
94+
95+    The filter_re regular expression is responsible for tokenizing the filter
96+    expression::
97+
98+        >>> def fre_test(s):
99+        ...     print '|'.join(','.join("%s=%s"%(key, val) for key, val in sorted(match.groupdict().items()) if val is not None) for match in filter_re.finditer(s))
100+        >>> fre_test('myvar')
101+        var=myvar
102+        >>> fre_test('myvar|myfilt:myarg')
103+        var=myvar|filter_name=myfilt,var_arg=myarg
104+        >>> fre_test(r'"Some \"Good\" News"|myfilt:"Some \"Bad\" News"')
105+        constant="Some \"Good\" News"|constant_arg="Some \"Bad\" News",filter_name=myfilt
106+        >>> fre_test(r"'More \'Good\' News'|myfilt:'More \'Bad\' News'")
107+        constant='More \'Good\' News'|constant_arg='More \'Bad\' News',filter_name=myfilt
108+
109+
110+        >>> p.compile_filter(r'"Some \"Good\" News"').resolve({})
111+        'Some "Good" News'
112     """
113     def __init__(self, token, parser):
114         self.token = token
115@@ -484,24 +534,25 @@
116                 raise TemplateSyntaxError("Could not parse some characters: %s|%s|%s"  % \
117                                            (token[:upto], token[upto:start], token[start:]))
118             if var == None:
119-                var, constant, i18n_constant = match.group("var", "constant", "i18n_constant")
120-                if i18n_constant:
121-                    var = '"%s"' %  _(i18n_constant.replace(r'\"', '"'))
122-                elif constant:
123-                    var = '"%s"' % constant.replace(r'\"', '"')
124-                upto = match.end()
125-                if var == None:
126+                var, constant = match.group("var", "constant")
127+                if constant:
128+                    try:
129+                        self.var = Variable(constant).resolve({})
130+                    except VariableDoesNotExist:
131+                        self.var = None
132+                elif var == None:
133                     raise TemplateSyntaxError("Could not find variable at start of %s" % token)
134                 elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
135                     raise TemplateSyntaxError("Variables and attributes may not begin with underscores: '%s'" % var)
136+                else:
137+                    self.var = Variable(var)
138+                upto = match.end()
139             else:
140                 filter_name = match.group("filter_name")
141                 args = []
142-                constant_arg, i18n_arg, var_arg = match.group("constant_arg", "i18n_arg", "var_arg")
143-                if i18n_arg:
144-                    args.append((False, _(i18n_arg.replace(r'\"', '"'))))
145-                elif constant_arg is not None:
146-                    args.append((False, constant_arg.replace(r'\"', '"')))
147+                constant_arg, var_arg = match.group("constant_arg", "var_arg")
148+                if constant_arg:
149+                    args.append((False, Variable(constant_arg).resolve({})))
150                 elif var_arg:
151                     args.append((True, Variable(var_arg)))
152                 filter_func = parser.find_filter(filter_name)
153@@ -511,24 +562,26 @@
154         if upto != len(token):
155             raise TemplateSyntaxError("Could not parse the remainder: '%s' from '%s'" % (token[upto:], token))
156         self.filters = filters
157-        self.var = Variable(var)
158 
159     def resolve(self, context, ignore_failures=False):
160-        try:
161-            obj = self.var.resolve(context)
162-        except VariableDoesNotExist:
163-            if ignore_failures:
164-                obj = None
165-            else:
166-                if settings.TEMPLATE_STRING_IF_INVALID:
167-                    global invalid_var_format_string
168-                    if invalid_var_format_string is None:
169-                        invalid_var_format_string = '%s' in settings.TEMPLATE_STRING_IF_INVALID
170-                    if invalid_var_format_string:
171-                        return settings.TEMPLATE_STRING_IF_INVALID % self.var
172-                    return settings.TEMPLATE_STRING_IF_INVALID
173+        if isinstance(self.var, Variable):
174+            try:
175+                obj = self.var.resolve(context)
176+            except VariableDoesNotExist:
177+                if ignore_failures:
178+                    obj = None
179                 else:
180-                    obj = settings.TEMPLATE_STRING_IF_INVALID
181+                    if settings.TEMPLATE_STRING_IF_INVALID:
182+                        global invalid_var_format_string
183+                        if invalid_var_format_string is None:
184+                            invalid_var_format_string = '%s' in settings.TEMPLATE_STRING_IF_INVALID
185+                        if invalid_var_format_string:
186+                            return settings.TEMPLATE_STRING_IF_INVALID % self.var
187+                        return settings.TEMPLATE_STRING_IF_INVALID
188+                    else:
189+                        obj = settings.TEMPLATE_STRING_IF_INVALID
190+        else:
191+            obj = self.var
192         for func, args in self.filters:
193             arg_vals = []
194             for lookup, arg in args:
195@@ -593,7 +646,7 @@
196     return Variable(path).resolve(context)
197 
198 class Variable(object):
199-    """
200+    r"""
201     A template variable, resolvable against a given context. The variable may be
202     a hard-coded string (if it begins and ends with single or double quote
203     marks)::
204@@ -609,8 +662,28 @@
205         >>> c.article.section = 'News'
206         >>> Variable('article.section').resolve(c)
207         u'News'
208+        >>> Variable(u'"News"').resolve(c)
209+        u'News'
210+        >>> Variable(u"'News'").resolve(c)
211+        u'News'
212 
213     (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
214+
215+    Translated strings are also handled correctly::
216+
217+        >>> Variable('_(article.section)').resolve(c)
218+        u'News'
219+        >>> Variable('_("Good News")').resolve(c)
220+        u'Good News'
221+        >>> Variable("_('Better News')").resolve(c)
222+        u'Better News'
223+
224+    Escaped quotes work correctly as well::
225+
226+        >>> Variable(ur'"Some \"Good\" News"').resolve(c)
227+        u'Some "Good" News'
228+        >>> Variable(ur"'Some \'Better\' News'").resolve(c)
229+        u"Some 'Better' News"
230     """
231 
232     def __init__(self, var):
233@@ -645,9 +718,9 @@
234                 var = var[2:-1]
235             # If it's wrapped with quotes (single or double), then
236             # we're also dealing with a literal.
237-            if var[0] in "\"'" and var[0] == var[-1]:
238-                self.literal = mark_safe(var[1:-1])
239-            else:
240+            try:
241+                self.literal = mark_safe(unescape_string_literal(var))
242+            except ValueError:
243                 # Otherwise we'll set self.lookups so that resolve() knows we're
244                 # dealing with a bonafide variable
245                 self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR))
246Index: django/utils/text.py
247===================================================================
248--- django/utils/text.py        (revision 7547)
249+++ django/utils/text.py        (working copy)
250@@ -209,12 +209,27 @@
251     """
252     text = force_unicode(text)
253     for bit in smart_split_re.finditer(text):
254-        bit = bit.group(0)
255-        if bit[0] == '"' and bit[-1] == '"':
256-            yield '"' + bit[1:-1].replace('\\"', '"').replace('\\\\', '\\') + '"'
257-        elif bit[0] == "'" and bit[-1] == "'":
258-            yield "'" + bit[1:-1].replace("\\'", "'").replace("\\\\", "\\") + "'"
259-        else:
260-            yield bit
261+        yield bit.group(0)
262 smart_split = allow_lazy(smart_split, unicode)
263 
264+def unescape_string_literal(s):
265+    r"""
266+    Convert quoted string literals to unquoted strings with escaped quotes and
267+    backslashes unquoted::
268+
269+        >>> unescape_string_literal('"abc"')
270+        'abc'
271+        >>> unescape_string_literal("'abc'")
272+        'abc'
273+        >>> unescape_string_literal('"a \"bc\""')
274+        'a "bc"'
275+        >>> unescape_string_literal("'\'ab\' c'")
276+        "'ab' c"
277+
278+    Would this function be more appropriate in the django.utils.text module?
279+    """
280+    if s[0] not in "\"'" or s[-1] != s[0]:
281+        raise ValueError("Not a string literal: %r" % s)
282+    quote = s[0]
283+    return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
284+unescape_string_literal = allow_lazy(unescape_string_literal)