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:
Malcolm Tredinnick 2009-03-23 09:40:25 +00:00
parent f5c07f89e3
commit a6f429e37e
5 changed files with 152 additions and 66 deletions

View File

@ -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))

View File

@ -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)

View File

@ -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"
"""

View File

@ -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,
} }
################################# #################################

View File

@ -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]