Added consistent support for double- and single-quote delimiters in templates.
Some template filters and tags understood single-quoted arguments, others didn't. This makes everything consistent. Based on a patch from akaihola. Fixed #7295. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10118 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
f5c07f89e3
commit
a6f429e37e
|
@ -50,12 +50,13 @@ u'<html></html>'
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from inspect import getargspec
|
from inspect import getargspec
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.context import Context, RequestContext, ContextPopException
|
from django.template.context import Context, RequestContext, ContextPopException
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
from django.utils.itercompat import is_iterable
|
from django.utils.itercompat import is_iterable
|
||||||
from django.utils.functional import curry, Promise
|
from django.utils.functional import curry, Promise
|
||||||
from django.utils.text import smart_split
|
from django.utils.text import smart_split, unescape_string_literal
|
||||||
from django.utils.encoding import smart_unicode, force_unicode, smart_str
|
from django.utils.encoding import smart_unicode, force_unicode, smart_str
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
|
from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
|
||||||
|
@ -444,33 +445,44 @@ class TokenParser(object):
|
||||||
self.pointer = i
|
self.pointer = i
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
constant_string = r"""
|
||||||
|
(?:%(i18n_open)s%(strdq)s%(i18n_close)s|
|
||||||
|
%(i18n_open)s%(strsq)s%(i18n_close)s|
|
||||||
|
%(strdq)s|
|
||||||
|
%(strsq)s)|
|
||||||
|
%(num)s
|
||||||
|
""" % {
|
||||||
|
'strdq': r'"[^"\\]*(?:\\.[^"\\]*)*"', # double-quoted string
|
||||||
|
'strsq': r"'[^'\\]*(?:\\.[^'\\]*)*'", # single-quoted string
|
||||||
|
'num': r'[-+\.]?\d[\d\.e]*', # numeric constant
|
||||||
|
'i18n_open' : re.escape("_("),
|
||||||
|
'i18n_close' : re.escape(")"),
|
||||||
|
}
|
||||||
|
constant_string = constant_string.replace("\n", "")
|
||||||
|
|
||||||
filter_raw_string = r"""
|
filter_raw_string = r"""
|
||||||
^%(i18n_open)s"(?P<i18n_constant>%(str)s)"%(i18n_close)s|
|
^(?P<constant>%(constant)s)|
|
||||||
^"(?P<constant>%(str)s)"|
|
|
||||||
^(?P<var>[%(var_chars)s]+)|
|
^(?P<var>[%(var_chars)s]+)|
|
||||||
(?:%(filter_sep)s
|
(?:%(filter_sep)s
|
||||||
(?P<filter_name>\w+)
|
(?P<filter_name>\w+)
|
||||||
(?:%(arg_sep)s
|
(?:%(arg_sep)s
|
||||||
(?:
|
(?:
|
||||||
%(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s|
|
(?P<constant_arg>%(constant)s)|
|
||||||
"(?P<constant_arg>%(str)s)"|
|
|
||||||
(?P<var_arg>[%(var_chars)s]+)
|
(?P<var_arg>[%(var_chars)s]+)
|
||||||
)
|
)
|
||||||
)?
|
)?
|
||||||
)""" % {
|
)""" % {
|
||||||
'str': r"""[^"\\]*(?:\\.[^"\\]*)*""",
|
'constant': constant_string,
|
||||||
'var_chars': "\w\." ,
|
'var_chars': "\w\." ,
|
||||||
'filter_sep': re.escape(FILTER_SEPARATOR),
|
'filter_sep': re.escape(FILTER_SEPARATOR),
|
||||||
'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR),
|
'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR),
|
||||||
'i18n_open' : re.escape("_("),
|
|
||||||
'i18n_close' : re.escape(")"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "")
|
filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "")
|
||||||
filter_re = re.compile(filter_raw_string, re.UNICODE)
|
filter_re = re.compile(filter_raw_string, re.UNICODE)
|
||||||
|
|
||||||
class FilterExpression(object):
|
class FilterExpression(object):
|
||||||
"""
|
r"""
|
||||||
Parses a variable token and its optional filters (all as a single string),
|
Parses a variable token and its optional filters (all as a single string),
|
||||||
and return a list of tuples of the filter name and arguments.
|
and return a list of tuples of the filter name and arguments.
|
||||||
Sample:
|
Sample:
|
||||||
|
@ -488,7 +500,7 @@ class FilterExpression(object):
|
||||||
def __init__(self, token, parser):
|
def __init__(self, token, parser):
|
||||||
self.token = token
|
self.token = token
|
||||||
matches = filter_re.finditer(token)
|
matches = filter_re.finditer(token)
|
||||||
var = None
|
var_obj = None
|
||||||
filters = []
|
filters = []
|
||||||
upto = 0
|
upto = 0
|
||||||
for match in matches:
|
for match in matches:
|
||||||
|
@ -496,30 +508,25 @@ class FilterExpression(object):
|
||||||
if upto != start:
|
if upto != start:
|
||||||
raise TemplateSyntaxError("Could not parse some characters: %s|%s|%s" % \
|
raise TemplateSyntaxError("Could not parse some characters: %s|%s|%s" % \
|
||||||
(token[:upto], token[upto:start], token[start:]))
|
(token[:upto], token[upto:start], token[start:]))
|
||||||
if var == None:
|
if var_obj is None:
|
||||||
var, constant, i18n_constant = match.group("var", "constant", "i18n_constant")
|
var, constant = match.group("var", "constant")
|
||||||
if i18n_constant is not None:
|
if constant:
|
||||||
# Don't pass the empty string to gettext, because the empty
|
try:
|
||||||
# string translates to meta information.
|
var_obj = Variable(constant).resolve({})
|
||||||
if i18n_constant == "":
|
except VariableDoesNotExist:
|
||||||
var = '""'
|
var_obj = None
|
||||||
else:
|
elif var is None:
|
||||||
var = '"%s"' % _(i18n_constant.replace(r'\"', '"'))
|
raise TemplateSyntaxError("Could not find variable at start of %s." % token)
|
||||||
elif constant is not None:
|
|
||||||
var = '"%s"' % constant.replace(r'\"', '"')
|
|
||||||
upto = match.end()
|
|
||||||
if var == None:
|
|
||||||
raise TemplateSyntaxError("Could not find variable at start of %s" % token)
|
|
||||||
elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
|
elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
|
||||||
raise TemplateSyntaxError("Variables and attributes may not begin with underscores: '%s'" % var)
|
raise TemplateSyntaxError("Variables and attributes may not begin with underscores: '%s'" % var)
|
||||||
|
else:
|
||||||
|
var_obj = Variable(var)
|
||||||
else:
|
else:
|
||||||
filter_name = match.group("filter_name")
|
filter_name = match.group("filter_name")
|
||||||
args = []
|
args = []
|
||||||
constant_arg, i18n_arg, var_arg = match.group("constant_arg", "i18n_arg", "var_arg")
|
constant_arg, var_arg = match.group("constant_arg", "var_arg")
|
||||||
if i18n_arg:
|
if constant_arg:
|
||||||
args.append((False, _(i18n_arg.replace(r'\"', '"'))))
|
args.append((False, Variable(constant_arg).resolve({})))
|
||||||
elif constant_arg is not None:
|
|
||||||
args.append((False, constant_arg.replace(r'\"', '"')))
|
|
||||||
elif var_arg:
|
elif var_arg:
|
||||||
args.append((True, Variable(var_arg)))
|
args.append((True, Variable(var_arg)))
|
||||||
filter_func = parser.find_filter(filter_name)
|
filter_func = parser.find_filter(filter_name)
|
||||||
|
@ -528,10 +535,12 @@ class FilterExpression(object):
|
||||||
upto = match.end()
|
upto = match.end()
|
||||||
if upto != len(token):
|
if upto != len(token):
|
||||||
raise TemplateSyntaxError("Could not parse the remainder: '%s' from '%s'" % (token[upto:], token))
|
raise TemplateSyntaxError("Could not parse the remainder: '%s' from '%s'" % (token[upto:], token))
|
||||||
|
|
||||||
self.filters = filters
|
self.filters = filters
|
||||||
self.var = Variable(var)
|
self.var = var_obj
|
||||||
|
|
||||||
def resolve(self, context, ignore_failures=False):
|
def resolve(self, context, ignore_failures=False):
|
||||||
|
if isinstance(self.var, Variable):
|
||||||
try:
|
try:
|
||||||
obj = self.var.resolve(context)
|
obj = self.var.resolve(context)
|
||||||
except VariableDoesNotExist:
|
except VariableDoesNotExist:
|
||||||
|
@ -547,6 +556,8 @@ class FilterExpression(object):
|
||||||
return settings.TEMPLATE_STRING_IF_INVALID
|
return settings.TEMPLATE_STRING_IF_INVALID
|
||||||
else:
|
else:
|
||||||
obj = settings.TEMPLATE_STRING_IF_INVALID
|
obj = settings.TEMPLATE_STRING_IF_INVALID
|
||||||
|
else:
|
||||||
|
obj = self.var
|
||||||
for func, args in self.filters:
|
for func, args in self.filters:
|
||||||
arg_vals = []
|
arg_vals = []
|
||||||
for lookup, arg in args:
|
for lookup, arg in args:
|
||||||
|
@ -611,7 +622,7 @@ def resolve_variable(path, context):
|
||||||
return Variable(path).resolve(context)
|
return Variable(path).resolve(context)
|
||||||
|
|
||||||
class Variable(object):
|
class Variable(object):
|
||||||
"""
|
r"""
|
||||||
A template variable, resolvable against a given context. The variable may be
|
A template variable, resolvable against a given context. The variable may be
|
||||||
a hard-coded string (if it begins and ends with single or double quote
|
a hard-coded string (if it begins and ends with single or double quote
|
||||||
marks)::
|
marks)::
|
||||||
|
@ -625,8 +636,6 @@ class Variable(object):
|
||||||
>>> c = AClass()
|
>>> c = AClass()
|
||||||
>>> c.article = AClass()
|
>>> c.article = AClass()
|
||||||
>>> c.article.section = u'News'
|
>>> c.article.section = u'News'
|
||||||
>>> Variable('article.section').resolve(c)
|
|
||||||
u'News'
|
|
||||||
|
|
||||||
(The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
|
(The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
|
||||||
"""
|
"""
|
||||||
|
@ -663,9 +672,9 @@ class Variable(object):
|
||||||
var = var[2:-1]
|
var = var[2:-1]
|
||||||
# If it's wrapped with quotes (single or double), then
|
# If it's wrapped with quotes (single or double), then
|
||||||
# we're also dealing with a literal.
|
# we're also dealing with a literal.
|
||||||
if var[0] in "\"'" and var[0] == var[-1]:
|
try:
|
||||||
self.literal = mark_safe(var[1:-1])
|
self.literal = mark_safe(unescape_string_literal(var))
|
||||||
else:
|
except ValueError:
|
||||||
# Otherwise we'll set self.lookups so that resolve() knows we're
|
# Otherwise we'll set self.lookups so that resolve() knows we're
|
||||||
# dealing with a bonafide variable
|
# dealing with a bonafide variable
|
||||||
self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR))
|
self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR))
|
||||||
|
|
|
@ -203,24 +203,19 @@ def smart_split(text):
|
||||||
Generator that splits a string by spaces, leaving quoted phrases together.
|
Generator that splits a string by spaces, leaving quoted phrases together.
|
||||||
Supports both single and double quotes, and supports escaping quotes with
|
Supports both single and double quotes, and supports escaping quotes with
|
||||||
backslashes. In the output, strings will keep their initial and trailing
|
backslashes. In the output, strings will keep their initial and trailing
|
||||||
quote marks.
|
quote marks and escaped quotes will remain escaped (the results can then
|
||||||
|
be further processed with unescape_string_literal()).
|
||||||
|
|
||||||
>>> list(smart_split(r'This is "a person\'s" test.'))
|
>>> list(smart_split(r'This is "a person\'s" test.'))
|
||||||
[u'This', u'is', u'"a person\\\'s"', u'test.']
|
[u'This', u'is', u'"a person\\\'s"', u'test.']
|
||||||
>>> list(smart_split(r"Another 'person\'s' test."))
|
>>> list(smart_split(r"Another 'person\'s' test."))
|
||||||
[u'Another', u"'person's'", u'test.']
|
[u'Another', u"'person\\'s'", u'test.']
|
||||||
>>> list(smart_split(r'A "\"funky\" style" test.'))
|
>>> list(smart_split(r'A "\"funky\" style" test.'))
|
||||||
[u'A', u'""funky" style"', u'test.']
|
[u'A', u'"\\"funky\\" style"', u'test.']
|
||||||
"""
|
"""
|
||||||
text = force_unicode(text)
|
text = force_unicode(text)
|
||||||
for bit in smart_split_re.finditer(text):
|
for bit in smart_split_re.finditer(text):
|
||||||
bit = bit.group(0)
|
yield bit.group(0)
|
||||||
if bit[0] == '"' and bit[-1] == '"':
|
|
||||||
yield '"' + bit[1:-1].replace('\\"', '"').replace('\\\\', '\\') + '"'
|
|
||||||
elif bit[0] == "'" and bit[-1] == "'":
|
|
||||||
yield "'" + bit[1:-1].replace("\\'", "'").replace("\\\\", "\\") + "'"
|
|
||||||
else:
|
|
||||||
yield bit
|
|
||||||
smart_split = allow_lazy(smart_split, unicode)
|
smart_split = allow_lazy(smart_split, unicode)
|
||||||
|
|
||||||
def _replace_entity(match):
|
def _replace_entity(match):
|
||||||
|
@ -246,3 +241,24 @@ _entity_re = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
|
||||||
def unescape_entities(text):
|
def unescape_entities(text):
|
||||||
return _entity_re.sub(_replace_entity, text)
|
return _entity_re.sub(_replace_entity, text)
|
||||||
unescape_entities = allow_lazy(unescape_entities, unicode)
|
unescape_entities = allow_lazy(unescape_entities, unicode)
|
||||||
|
|
||||||
|
def unescape_string_literal(s):
|
||||||
|
r"""
|
||||||
|
Convert quoted string literals to unquoted strings with escaped quotes and
|
||||||
|
backslashes unquoted::
|
||||||
|
|
||||||
|
>>> unescape_string_literal('"abc"')
|
||||||
|
'abc'
|
||||||
|
>>> unescape_string_literal("'abc'")
|
||||||
|
'abc'
|
||||||
|
>>> unescape_string_literal('"a \"bc\""')
|
||||||
|
'a "bc"'
|
||||||
|
>>> unescape_string_literal("'\'ab\' c'")
|
||||||
|
"'ab' c"
|
||||||
|
"""
|
||||||
|
if s[0] not in "\"'" or s[-1] != s[0]:
|
||||||
|
raise ValueError("Not a string literal: %r" % s)
|
||||||
|
quote = s[0]
|
||||||
|
return s[1:-1].replace(r'\%s' % quote, quote).replace(r'\\', '\\')
|
||||||
|
unescape_string_literal = allow_lazy(unescape_string_literal)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""
|
||||||
|
Testing some internals of the template processing. These are *not* examples to be copied in user code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filter_parsing = r"""
|
||||||
|
>>> from django.template import FilterExpression, Parser
|
||||||
|
|
||||||
|
>>> c = {'article': {'section': u'News'}}
|
||||||
|
>>> p = Parser("")
|
||||||
|
>>> def fe_test(s): return FilterExpression(s, p).resolve(c)
|
||||||
|
|
||||||
|
>>> fe_test('article.section')
|
||||||
|
u'News'
|
||||||
|
>>> fe_test('article.section|upper')
|
||||||
|
u'NEWS'
|
||||||
|
>>> fe_test(u'"News"')
|
||||||
|
u'News'
|
||||||
|
>>> fe_test(u"'News'")
|
||||||
|
u'News'
|
||||||
|
>>> fe_test(ur'"Some \"Good\" News"')
|
||||||
|
u'Some "Good" News'
|
||||||
|
>>> fe_test(ur"'Some \'Bad\' News'")
|
||||||
|
u"Some 'Bad' News"
|
||||||
|
|
||||||
|
>>> fe = FilterExpression(ur'"Some \"Good\" News"', p)
|
||||||
|
>>> fe.filters
|
||||||
|
[]
|
||||||
|
>>> fe.var
|
||||||
|
u'Some "Good" News'
|
||||||
|
"""
|
||||||
|
|
||||||
|
variable_parsing = r"""
|
||||||
|
>>> from django.template import Variable
|
||||||
|
|
||||||
|
>>> c = {'article': {'section': u'News'}}
|
||||||
|
>>> Variable('article.section').resolve(c)
|
||||||
|
u'News'
|
||||||
|
>>> Variable(u'"News"').resolve(c)
|
||||||
|
u'News'
|
||||||
|
>>> Variable(u"'News'").resolve(c)
|
||||||
|
u'News'
|
||||||
|
|
||||||
|
Translated strings are handled correctly.
|
||||||
|
|
||||||
|
>>> Variable('_(article.section)').resolve(c)
|
||||||
|
u'News'
|
||||||
|
>>> Variable('_("Good News")').resolve(c)
|
||||||
|
u'Good News'
|
||||||
|
>>> Variable("_('Better News')").resolve(c)
|
||||||
|
u'Better News'
|
||||||
|
|
||||||
|
Escaped quotes work correctly as well.
|
||||||
|
|
||||||
|
>>> Variable(ur'"Some \"Good\" News"').resolve(c)
|
||||||
|
u'Some "Good" News'
|
||||||
|
>>> Variable(ur"'Some \'Better\' News'").resolve(c)
|
||||||
|
u"Some 'Better' News"
|
||||||
|
|
||||||
|
"""
|
|
@ -20,6 +20,7 @@ from django.utils.tzinfo import LocalTimezone
|
||||||
|
|
||||||
from unicode import unicode_tests
|
from unicode import unicode_tests
|
||||||
from context import context_tests
|
from context import context_tests
|
||||||
|
from parser import filter_parsing, variable_parsing
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from loaders import *
|
from loaders import *
|
||||||
|
@ -31,7 +32,8 @@ import filters
|
||||||
# Some other tests we would like to run
|
# Some other tests we would like to run
|
||||||
__test__ = {
|
__test__ = {
|
||||||
'unicode': unicode_tests,
|
'unicode': unicode_tests,
|
||||||
'context': context_tests
|
'context': context_tests,
|
||||||
|
'filter_parsing': filter_parsing,
|
||||||
}
|
}
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
|
|
|
@ -10,7 +10,7 @@ r"""
|
||||||
>>> print list(smart_split(r'''This is "a person's" test.'''))[2]
|
>>> print list(smart_split(r'''This is "a person's" test.'''))[2]
|
||||||
"a person's"
|
"a person's"
|
||||||
>>> print list(smart_split(r'''This is "a person\"s" test.'''))[2]
|
>>> print list(smart_split(r'''This is "a person\"s" test.'''))[2]
|
||||||
"a person"s"
|
"a person\"s"
|
||||||
>>> list(smart_split('''"a 'one'''))
|
>>> list(smart_split('''"a 'one'''))
|
||||||
[u'"a', u"'one"]
|
[u'"a', u"'one"]
|
||||||
>>> print list(smart_split(r'''all friends' tests'''))[1]
|
>>> print list(smart_split(r'''all friends' tests'''))[1]
|
||||||
|
|
Loading…
Reference in New Issue