1187 lines
43 KiB
Python
1187 lines
43 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
jinja.parser
|
|
~~~~~~~~~~~~
|
|
|
|
Implements the template parser.
|
|
|
|
The Jinja template parser is not a real parser but a combination of the
|
|
python compiler package and some postprocessing. The tokens yielded by
|
|
the lexer are used to separate template data and expressions. The
|
|
expression tokens are then converted into strings again and processed
|
|
by the python parser.
|
|
|
|
:copyright: 2007 by Armin Ronacher.
|
|
:license: BSD, see LICENSE for more details.
|
|
"""
|
|
from jinja import nodes
|
|
from jinja.datastructure import StateTest
|
|
from jinja.exceptions import TemplateSyntaxError
|
|
from jinja.utils import set
|
|
|
|
|
|
__all__ = ['Parser']
|
|
|
|
|
|
# general callback functions for the parser
|
|
end_of_block = StateTest.expect_token('block_end',
|
|
msg='expected end of block tag')
|
|
end_of_variable = StateTest.expect_token('variable_end',
|
|
msg='expected end of variable')
|
|
end_of_comment = StateTest.expect_token('comment_end',
|
|
msg='expected end of comment')
|
|
|
|
# internal tag callbacks
|
|
switch_for = StateTest.expect_token('else', 'endfor')
|
|
end_of_for = StateTest.expect_token('endfor')
|
|
switch_if = StateTest.expect_token('else', 'elif', 'endif')
|
|
end_of_if = StateTest.expect_token('endif')
|
|
end_of_filter = StateTest.expect_token('endfilter')
|
|
end_of_macro = StateTest.expect_token('endmacro')
|
|
end_of_call = StateTest.expect_token('endcall')
|
|
end_of_block_tag = StateTest.expect_token('endblock')
|
|
end_of_trans = StateTest.expect_token('endtrans')
|
|
|
|
# this ends a tuple
|
|
tuple_edge_tokens = set(['rparen', 'block_end', 'variable_end', 'in',
|
|
'recursive'])
|
|
|
|
|
|
class Parser(object):
|
|
"""
|
|
The template parser class.
|
|
|
|
Transforms sourcecode into an abstract syntax tree.
|
|
"""
|
|
|
|
def __init__(self, environment, source, filename=None):
|
|
self.environment = environment
|
|
if isinstance(source, str):
|
|
source = source.decode(environment.template_charset, 'ignore')
|
|
if isinstance(filename, unicode):
|
|
filename = filename.encode('utf-8')
|
|
self.source = source
|
|
self.filename = filename
|
|
self.closed = False
|
|
|
|
#: set for blocks in order to keep them unique
|
|
self.blocks = set()
|
|
|
|
#: mapping of directives that require special treatment
|
|
self.directives = {
|
|
# "fake" directives that just trigger errors
|
|
'raw': self.parse_raw_directive,
|
|
'extends': self.parse_extends_directive,
|
|
|
|
# real directives
|
|
'for': self.parse_for_loop,
|
|
'if': self.parse_if_condition,
|
|
'cycle': self.parse_cycle_directive,
|
|
'call': self.parse_call_directive,
|
|
'set': self.parse_set_directive,
|
|
'filter': self.parse_filter_directive,
|
|
'print': self.parse_print_directive,
|
|
'macro': self.parse_macro_directive,
|
|
'block': self.parse_block_directive,
|
|
'include': self.parse_include_directive,
|
|
'trans': self.parse_trans_directive
|
|
}
|
|
|
|
#: set of directives that are only available in a certain
|
|
#: context.
|
|
self.context_directives = set([
|
|
'elif', 'else', 'endblock', 'endfilter', 'endfor', 'endif',
|
|
'endmacro', 'endraw', 'endtrans', 'pluralize'
|
|
])
|
|
|
|
#: get the `no_variable_block` flag
|
|
self.no_variable_block = self.environment.lexer.no_variable_block
|
|
|
|
self.stream = environment.lexer.tokenize(source, filename)
|
|
|
|
def parse_raw_directive(self):
|
|
"""
|
|
Handle fake raw directive. (real raw directives are handled by
|
|
the lexer. But if there are arguments to raw or the end tag
|
|
is missing the parser tries to resolve this directive. In that
|
|
case present the user a useful error message.
|
|
"""
|
|
if self.stream:
|
|
raise TemplateSyntaxError('raw directive does not support '
|
|
'any arguments.', self.stream.lineno,
|
|
self.filename)
|
|
raise TemplateSyntaxError('missing end tag for raw directive.',
|
|
self.stream.lineno, self.filename)
|
|
|
|
def parse_extends_directive(self):
|
|
"""
|
|
Handle the extends directive used for inheritance.
|
|
"""
|
|
raise TemplateSyntaxError('mispositioned extends tag. extends must '
|
|
'be the first tag of a template.',
|
|
self.stream.lineno, self.filename)
|
|
|
|
def parse_for_loop(self):
|
|
"""
|
|
Handle a for directive and return a ForLoop node
|
|
"""
|
|
token = self.stream.expect('for')
|
|
item = self.parse_tuple_expression(simplified=True)
|
|
if not item.allows_assignments():
|
|
raise TemplateSyntaxError('cannot assign to expression',
|
|
token.lineno, self.filename)
|
|
|
|
self.stream.expect('in')
|
|
seq = self.parse_tuple_expression()
|
|
if self.stream.current.type == 'recursive':
|
|
self.stream.next()
|
|
recursive = True
|
|
else:
|
|
recursive = False
|
|
self.stream.expect('block_end')
|
|
|
|
body = self.subparse(switch_for)
|
|
|
|
# do we have an else section?
|
|
if self.stream.current.type == 'else':
|
|
self.stream.next()
|
|
self.stream.expect('block_end')
|
|
else_ = self.subparse(end_of_for, True)
|
|
else:
|
|
self.stream.next()
|
|
else_ = None
|
|
self.stream.expect('block_end')
|
|
|
|
return nodes.ForLoop(item, seq, body, else_, recursive,
|
|
token.lineno, self.filename)
|
|
|
|
def parse_if_condition(self):
|
|
"""
|
|
Handle if/else blocks.
|
|
"""
|
|
token = self.stream.expect('if')
|
|
expr = self.parse_expression()
|
|
self.stream.expect('block_end')
|
|
tests = [(expr, self.subparse(switch_if))]
|
|
else_ = None
|
|
|
|
# do we have an else section?
|
|
while True:
|
|
if self.stream.current.type == 'else':
|
|
self.stream.next()
|
|
self.stream.expect('block_end')
|
|
else_ = self.subparse(end_of_if, True)
|
|
elif self.stream.current.type == 'elif':
|
|
self.stream.next()
|
|
expr = self.parse_expression()
|
|
self.stream.expect('block_end')
|
|
tests.append((expr, self.subparse(switch_if)))
|
|
continue
|
|
else:
|
|
self.stream.next()
|
|
break
|
|
self.stream.expect('block_end')
|
|
|
|
return nodes.IfCondition(tests, else_, token.lineno, self.filename)
|
|
|
|
def parse_cycle_directive(self):
|
|
"""
|
|
Handle {% cycle foo, bar, baz %}.
|
|
"""
|
|
token = self.stream.expect('cycle')
|
|
expr = self.parse_tuple_expression()
|
|
self.stream.expect('block_end')
|
|
return nodes.Cycle(expr, token.lineno, self.filename)
|
|
|
|
def parse_set_directive(self):
|
|
"""
|
|
Handle {% set foo = 'value of foo' %}.
|
|
"""
|
|
token = self.stream.expect('set')
|
|
name = self.stream.expect('name')
|
|
self.test_name(name.value)
|
|
self.stream.expect('assign')
|
|
value = self.parse_expression()
|
|
if self.stream.current.type == 'bang':
|
|
self.stream.next()
|
|
scope_local = False
|
|
else:
|
|
scope_local = True
|
|
self.stream.expect('block_end')
|
|
return nodes.Set(name.value, value, scope_local,
|
|
token.lineno, self.filename)
|
|
|
|
def parse_filter_directive(self):
|
|
"""
|
|
Handle {% filter foo|bar %} directives.
|
|
"""
|
|
token = self.stream.expect('filter')
|
|
filters = []
|
|
while self.stream.current.type != 'block_end':
|
|
if filters:
|
|
self.stream.expect('pipe')
|
|
token = self.stream.expect('name')
|
|
args = []
|
|
if self.stream.current.type == 'lparen':
|
|
self.stream.next()
|
|
while self.stream.current.type != 'rparen':
|
|
if args:
|
|
self.stream.expect('comma')
|
|
args.append(self.parse_expression())
|
|
self.stream.expect('rparen')
|
|
filters.append((token.value, args))
|
|
self.stream.expect('block_end')
|
|
body = self.subparse(end_of_filter, True)
|
|
self.stream.expect('block_end')
|
|
return nodes.Filter(body, filters, token.lineno, self.filename)
|
|
|
|
def parse_print_directive(self):
|
|
"""
|
|
Handle {% print foo %}.
|
|
"""
|
|
token = self.stream.expect('print')
|
|
expr = self.parse_tuple_expression()
|
|
node = nodes.Print(expr, token.lineno, self.filename)
|
|
self.stream.expect('block_end')
|
|
return node
|
|
|
|
def parse_macro_directive(self):
|
|
"""
|
|
Handle {% macro foo bar, baz %} as well as
|
|
{% macro foo(bar, baz) %}.
|
|
"""
|
|
token = self.stream.expect('macro')
|
|
macro_name = self.stream.expect('name')
|
|
self.test_name(macro_name.value)
|
|
if self.stream.current.type == 'lparen':
|
|
self.stream.next()
|
|
needle_token = 'rparen'
|
|
else:
|
|
needle_token = 'block_end'
|
|
|
|
args = []
|
|
while self.stream.current.type != needle_token:
|
|
if args:
|
|
self.stream.expect('comma')
|
|
name = self.stream.expect('name').value
|
|
self.test_name(name)
|
|
if self.stream.current.type == 'assign':
|
|
self.stream.next()
|
|
default = self.parse_expression()
|
|
else:
|
|
default = None
|
|
args.append((name, default))
|
|
|
|
self.stream.next()
|
|
if needle_token == 'rparen':
|
|
self.stream.expect('block_end')
|
|
|
|
body = self.subparse(end_of_macro, True)
|
|
self.stream.expect('block_end')
|
|
|
|
return nodes.Macro(macro_name.value, args, body, token.lineno,
|
|
self.filename)
|
|
|
|
def parse_call_directive(self):
|
|
"""
|
|
Handle {% call foo() %}...{% endcall %}
|
|
"""
|
|
token = self.stream.expect('call')
|
|
expr = self.parse_call_expression()
|
|
self.stream.expect('block_end')
|
|
body = self.subparse(end_of_call, True)
|
|
self.stream.expect('block_end')
|
|
return nodes.Call(expr, body, token.lineno, self.filename)
|
|
|
|
def parse_block_directive(self):
|
|
"""
|
|
Handle block directives used for inheritance.
|
|
"""
|
|
token = self.stream.expect('block')
|
|
name = self.stream.expect('name').value
|
|
|
|
# check if this block does not exist by now.
|
|
if name in self.blocks:
|
|
raise TemplateSyntaxError('block %r defined twice' %
|
|
name, token.lineno,
|
|
self.filename)
|
|
self.blocks.add(name)
|
|
|
|
if self.stream.current.type != 'block_end':
|
|
lineno = self.stream.lineno
|
|
expr = self.parse_tuple_expression()
|
|
node = nodes.Print(expr, lineno, self.filename)
|
|
body = nodes.NodeList([node], lineno, self.filename)
|
|
self.stream.expect('block_end')
|
|
else:
|
|
# otherwise parse the body and attach it to the block
|
|
self.stream.expect('block_end')
|
|
body = self.subparse(end_of_block_tag, True)
|
|
self.stream.expect('block_end')
|
|
return nodes.Block(name, body, token.lineno, self.filename)
|
|
|
|
def parse_include_directive(self):
|
|
"""
|
|
Handle the include directive used for template inclusion.
|
|
"""
|
|
token = self.stream.expect('include')
|
|
template = self.stream.expect('string').value
|
|
self.stream.expect('block_end')
|
|
return nodes.Include(template, token.lineno, self.filename)
|
|
|
|
def parse_trans_directive(self):
|
|
"""
|
|
Handle translatable sections.
|
|
"""
|
|
trans_token = self.stream.expect('trans')
|
|
|
|
# string based translations {% trans "foo" %}
|
|
if self.stream.current.type == 'string':
|
|
text = self.stream.expect('string')
|
|
self.stream.expect('block_end')
|
|
return nodes.Trans(text.value, None, None, None,
|
|
trans_token.lineno, self.filename)
|
|
|
|
# block based translations
|
|
replacements = {}
|
|
plural_var = None
|
|
|
|
while self.stream.current.type != 'block_end':
|
|
if replacements:
|
|
self.stream.expect('comma')
|
|
name = self.stream.expect('name')
|
|
if self.stream.current.type == 'assign':
|
|
self.stream.next()
|
|
value = self.parse_expression()
|
|
else:
|
|
value = nodes.NameExpression(name.value, name.lineno,
|
|
self.filename)
|
|
if name.value in replacements:
|
|
raise TemplateSyntaxError('translation variable %r '
|
|
'is defined twice' % name.value,
|
|
name.lineno, self.filename)
|
|
replacements[name.value] = value
|
|
if plural_var is None:
|
|
plural_var = name.value
|
|
self.stream.expect('block_end')
|
|
|
|
def process_variable():
|
|
var_name = self.stream.expect('name')
|
|
if var_name.value not in replacements:
|
|
raise TemplateSyntaxError('unregistered translation variable'
|
|
" '%s'." % var_name.value,
|
|
var_name.lineno, self.filename)
|
|
buf.append('%%(%s)s' % var_name.value)
|
|
|
|
buf = singular = []
|
|
plural = None
|
|
|
|
while True:
|
|
token = self.stream.current
|
|
if token.type == 'data':
|
|
buf.append(token.value.replace('%', '%%'))
|
|
self.stream.next()
|
|
elif token.type == 'variable_begin':
|
|
self.stream.next()
|
|
process_variable()
|
|
self.stream.expect('variable_end')
|
|
elif token.type == 'block_begin':
|
|
self.stream.next()
|
|
if plural is None and self.stream.current.type == 'pluralize':
|
|
self.stream.next()
|
|
if self.stream.current.type == 'name':
|
|
plural_var = self.stream.expect('name').value
|
|
plural = buf = []
|
|
elif self.stream.current.type == 'endtrans':
|
|
self.stream.next()
|
|
self.stream.expect('block_end')
|
|
break
|
|
else:
|
|
if self.no_variable_block:
|
|
process_variable()
|
|
else:
|
|
raise TemplateSyntaxError('blocks are not allowed '
|
|
'in trans tags',
|
|
self.stream.lineno,
|
|
self.filename)
|
|
self.stream.expect('block_end')
|
|
else:
|
|
assert False, 'something very strange happened'
|
|
|
|
singular = u''.join(singular)
|
|
if plural is not None:
|
|
plural = u''.join(plural)
|
|
return nodes.Trans(singular, plural, plural_var, replacements,
|
|
trans_token.lineno, self.filename)
|
|
|
|
def parse_expression(self):
|
|
"""
|
|
Parse one expression from the stream.
|
|
"""
|
|
return self.parse_conditional_expression()
|
|
|
|
def parse_subscribed_expression(self):
|
|
"""
|
|
Like parse_expression but parses slices too. Because this
|
|
parsing function requires a border the two tokens rbracket
|
|
and comma mark the end of the expression in some situations.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
|
|
if self.stream.current.type == 'colon':
|
|
self.stream.next()
|
|
args = [None]
|
|
else:
|
|
node = self.parse_expression()
|
|
if self.stream.current.type != 'colon':
|
|
return node
|
|
self.stream.next()
|
|
args = [node]
|
|
|
|
if self.stream.current.type == 'colon':
|
|
args.append(None)
|
|
elif self.stream.current.type not in ('rbracket', 'comma'):
|
|
args.append(self.parse_expression())
|
|
else:
|
|
args.append(None)
|
|
|
|
if self.stream.current.type == 'colon':
|
|
self.stream.next()
|
|
if self.stream.current.type not in ('rbracket', 'comma'):
|
|
args.append(self.parse_expression())
|
|
else:
|
|
args.append(None)
|
|
else:
|
|
args.append(None)
|
|
|
|
return nodes.SliceExpression(*(args + [lineno, self.filename]))
|
|
|
|
def parse_conditional_expression(self):
|
|
"""
|
|
Parse a conditional expression (foo if bar else baz)
|
|
"""
|
|
lineno = self.stream.lineno
|
|
expr1 = self.parse_or_expression()
|
|
while self.stream.current.type == 'if':
|
|
self.stream.next()
|
|
expr2 = self.parse_or_expression()
|
|
self.stream.expect('else')
|
|
expr3 = self.parse_conditional_expression()
|
|
expr1 = nodes.ConditionalExpression(expr2, expr1, expr3,
|
|
lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return expr1
|
|
|
|
def parse_or_expression(self):
|
|
"""
|
|
Parse something like {{ foo or bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_and_expression()
|
|
while self.stream.current.type == 'or':
|
|
self.stream.next()
|
|
right = self.parse_and_expression()
|
|
left = nodes.OrExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_and_expression(self):
|
|
"""
|
|
Parse something like {{ foo and bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_compare_expression()
|
|
while self.stream.current.type == 'and':
|
|
self.stream.next()
|
|
right = self.parse_compare_expression()
|
|
left = nodes.AndExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_compare_expression(self):
|
|
"""
|
|
Parse something like {{ foo == bar }}.
|
|
"""
|
|
known_operators = set(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq', 'in'])
|
|
lineno = self.stream.lineno
|
|
expr = self.parse_add_expression()
|
|
ops = []
|
|
while True:
|
|
if self.stream.current.type in known_operators:
|
|
op = self.stream.current.type
|
|
self.stream.next()
|
|
ops.append([op, self.parse_add_expression()])
|
|
elif self.stream.current.type == 'not' and \
|
|
self.stream.look().type == 'in':
|
|
self.stream.skip(2)
|
|
ops.append(['not in', self.parse_add_expression()])
|
|
else:
|
|
break
|
|
if not ops:
|
|
return expr
|
|
return nodes.CompareExpression(expr, ops, lineno, self.filename)
|
|
|
|
def parse_add_expression(self):
|
|
"""
|
|
Parse something like {{ foo + bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_sub_expression()
|
|
while self.stream.current.type == 'add':
|
|
self.stream.next()
|
|
right = self.parse_sub_expression()
|
|
left = nodes.AddExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_sub_expression(self):
|
|
"""
|
|
Parse something like {{ foo - bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_concat_expression()
|
|
while self.stream.current.type == 'sub':
|
|
self.stream.next()
|
|
right = self.parse_concat_expression()
|
|
left = nodes.SubExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_concat_expression(self):
|
|
"""
|
|
Parse something like {{ foo ~ bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
args = [self.parse_mul_expression()]
|
|
while self.stream.current.type == 'tilde':
|
|
self.stream.next()
|
|
args.append(self.parse_mul_expression())
|
|
if len(args) == 1:
|
|
return args[0]
|
|
return nodes.ConcatExpression(args, lineno, self.filename)
|
|
|
|
def parse_mul_expression(self):
|
|
"""
|
|
Parse something like {{ foo * bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_div_expression()
|
|
while self.stream.current.type == 'mul':
|
|
self.stream.next()
|
|
right = self.parse_div_expression()
|
|
left = nodes.MulExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_div_expression(self):
|
|
"""
|
|
Parse something like {{ foo / bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_floor_div_expression()
|
|
while self.stream.current.type == 'div':
|
|
self.stream.next()
|
|
right = self.parse_floor_div_expression()
|
|
left = nodes.DivExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_floor_div_expression(self):
|
|
"""
|
|
Parse something like {{ foo // bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_mod_expression()
|
|
while self.stream.current.type == 'floordiv':
|
|
self.stream.next()
|
|
right = self.parse_mod_expression()
|
|
left = nodes.FloorDivExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_mod_expression(self):
|
|
"""
|
|
Parse something like {{ foo % bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_pow_expression()
|
|
while self.stream.current.type == 'mod':
|
|
self.stream.next()
|
|
right = self.parse_pow_expression()
|
|
left = nodes.ModExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_pow_expression(self):
|
|
"""
|
|
Parse something like {{ foo ** bar }}.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
left = self.parse_unary_expression()
|
|
while self.stream.current.type == 'pow':
|
|
self.stream.next()
|
|
right = self.parse_unary_expression()
|
|
left = nodes.PowExpression(left, right, lineno, self.filename)
|
|
lineno = self.stream.lineno
|
|
return left
|
|
|
|
def parse_unary_expression(self):
|
|
"""
|
|
Parse all kinds of unary expressions.
|
|
"""
|
|
if self.stream.current.type == 'not':
|
|
return self.parse_not_expression()
|
|
elif self.stream.current.type == 'sub':
|
|
return self.parse_neg_expression()
|
|
elif self.stream.current.type == 'add':
|
|
return self.parse_pos_expression()
|
|
return self.parse_primary_expression()
|
|
|
|
def parse_not_expression(self):
|
|
"""
|
|
Parse something like {{ not foo }}.
|
|
"""
|
|
token = self.stream.expect('not')
|
|
node = self.parse_unary_expression()
|
|
return nodes.NotExpression(node, token.lineno, self.filename)
|
|
|
|
def parse_neg_expression(self):
|
|
"""
|
|
Parse something like {{ -foo }}.
|
|
"""
|
|
token = self.stream.expect('sub')
|
|
node = self.parse_unary_expression()
|
|
return nodes.NegExpression(node, token.lineno, self.filename)
|
|
|
|
def parse_pos_expression(self):
|
|
"""
|
|
Parse something like {{ +foo }}.
|
|
"""
|
|
token = self.stream.expect('add')
|
|
node = self.parse_unary_expression()
|
|
return nodes.PosExpression(node, token.lineno, self.filename)
|
|
|
|
def parse_primary_expression(self, parse_postfix=True):
|
|
"""
|
|
Parse a primary expression such as a name or literal.
|
|
"""
|
|
current = self.stream.current
|
|
if current.type == 'name':
|
|
if current.value in ('true', 'false'):
|
|
node = self.parse_bool_expression()
|
|
elif current.value == 'none':
|
|
node = self.parse_none_expression()
|
|
elif current.value == 'undefined':
|
|
node = self.parse_undefined_expression()
|
|
elif current.value == '_':
|
|
node = self.parse_gettext_call()
|
|
else:
|
|
node = self.parse_name_expression()
|
|
elif current.type in ('integer', 'float'):
|
|
node = self.parse_number_expression()
|
|
elif current.type == 'string':
|
|
node = self.parse_string_expression()
|
|
elif current.type == 'regex':
|
|
node = self.parse_regex_expression()
|
|
elif current.type == 'lparen':
|
|
node = self.parse_paren_expression()
|
|
elif current.type == 'lbracket':
|
|
node = self.parse_list_expression()
|
|
elif current.type == 'lbrace':
|
|
node = self.parse_dict_expression()
|
|
elif current.type == 'at':
|
|
node = self.parse_set_expression()
|
|
else:
|
|
raise TemplateSyntaxError("unexpected token '%s'" %
|
|
self.stream.current,
|
|
self.stream.current.lineno,
|
|
self.filename)
|
|
if parse_postfix:
|
|
node = self.parse_postfix_expression(node)
|
|
return node
|
|
|
|
def parse_tuple_expression(self, enforce=False, simplified=False):
|
|
"""
|
|
Parse multiple expressions into a tuple. This can also return
|
|
just one expression which is not a tuple. If you want to enforce
|
|
a tuple, pass it enforce=True.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
if simplified:
|
|
parse = self.parse_primary_expression
|
|
else:
|
|
parse = self.parse_expression
|
|
args = []
|
|
is_tuple = False
|
|
while True:
|
|
if args:
|
|
self.stream.expect('comma')
|
|
if self.stream.current.type in tuple_edge_tokens:
|
|
break
|
|
args.append(parse())
|
|
if self.stream.current.type == 'comma':
|
|
is_tuple = True
|
|
else:
|
|
break
|
|
if not is_tuple and args:
|
|
if enforce:
|
|
raise TemplateSyntaxError('tuple expected', lineno,
|
|
self.filename)
|
|
return args[0]
|
|
return nodes.TupleExpression(args, lineno, self.filename)
|
|
|
|
def parse_bool_expression(self):
|
|
"""
|
|
Parse a boolean literal.
|
|
"""
|
|
token = self.stream.expect('name')
|
|
if token.value == 'true':
|
|
value = True
|
|
elif token.value == 'false':
|
|
value = False
|
|
else:
|
|
raise TemplateSyntaxError("expected boolean literal",
|
|
token.lineno, self.filename)
|
|
return nodes.ConstantExpression(value, token.lineno, self.filename)
|
|
|
|
def parse_none_expression(self):
|
|
"""
|
|
Parse a none literal.
|
|
"""
|
|
token = self.stream.expect('name', 'none')
|
|
return nodes.ConstantExpression(None, token.lineno, self.filename)
|
|
|
|
def parse_undefined_expression(self):
|
|
"""
|
|
Parse an undefined literal.
|
|
"""
|
|
token = self.stream.expect('name', 'undefined')
|
|
return nodes.UndefinedExpression(token.lineno, self.filename)
|
|
|
|
def parse_gettext_call(self):
|
|
"""
|
|
parse {{ _('foo') }}.
|
|
"""
|
|
# XXX: check if only one argument was passed and if
|
|
# it is a string literal. Maybe that should become a special
|
|
# expression anyway.
|
|
token = self.stream.expect('name', '_')
|
|
node = nodes.NameExpression(token.value, token.lineno, self.filename)
|
|
return self.parse_call_expression(node)
|
|
|
|
def parse_name_expression(self):
|
|
"""
|
|
Parse any name.
|
|
"""
|
|
token = self.stream.expect('name')
|
|
self.test_name(token.value)
|
|
return nodes.NameExpression(token.value, token.lineno, self.filename)
|
|
|
|
def parse_number_expression(self):
|
|
"""
|
|
Parse a number literal.
|
|
"""
|
|
token = self.stream.current
|
|
if token.type not in ('integer', 'float'):
|
|
raise TemplateSyntaxError('integer or float literal expected',
|
|
token.lineno, self.filename)
|
|
self.stream.next()
|
|
return nodes.ConstantExpression(token.value, token.lineno, self.filename)
|
|
|
|
def parse_string_expression(self):
|
|
"""
|
|
Parse a string literal.
|
|
"""
|
|
token = self.stream.expect('string')
|
|
return nodes.ConstantExpression(token.value, token.lineno, self.filename)
|
|
|
|
def parse_regex_expression(self):
|
|
"""
|
|
Parse a regex literal.
|
|
"""
|
|
token = self.stream.expect('regex')
|
|
return nodes.RegexExpression(token.value, token.lineno, self.filename)
|
|
|
|
def parse_paren_expression(self):
|
|
"""
|
|
Parse a parenthized expression.
|
|
"""
|
|
self.stream.expect('lparen')
|
|
try:
|
|
return self.parse_tuple_expression()
|
|
finally:
|
|
self.stream.expect('rparen')
|
|
|
|
def parse_list_expression(self):
|
|
"""
|
|
Parse something like {{ [1, 2, "three"] }}
|
|
"""
|
|
token = self.stream.expect('lbracket')
|
|
items = []
|
|
while self.stream.current.type != 'rbracket':
|
|
if items:
|
|
self.stream.expect('comma')
|
|
if self.stream.current.type == 'rbracket':
|
|
break
|
|
items.append(self.parse_expression())
|
|
self.stream.expect('rbracket')
|
|
|
|
return nodes.ListExpression(items, token.lineno, self.filename)
|
|
|
|
def parse_dict_expression(self):
|
|
"""
|
|
Parse something like {{ {1: 2, 3: 4} }}
|
|
"""
|
|
token = self.stream.expect('lbrace')
|
|
items = []
|
|
while self.stream.current.type != 'rbrace':
|
|
if items:
|
|
self.stream.expect('comma')
|
|
if self.stream.current.type == 'rbrace':
|
|
break
|
|
key = self.parse_expression()
|
|
self.stream.expect('colon')
|
|
value = self.parse_expression()
|
|
items.append((key, value))
|
|
self.stream.expect('rbrace')
|
|
|
|
return nodes.DictExpression(items, token.lineno, self.filename)
|
|
|
|
def parse_set_expression(self):
|
|
"""
|
|
Parse something like {{ @(1, 2, 3) }}.
|
|
"""
|
|
token = self.stream.expect('at')
|
|
self.stream.expect('lparen')
|
|
items = []
|
|
while self.stream.current.type != 'rparen':
|
|
if items:
|
|
self.stream.expect('comma')
|
|
if self.stream.current.type == 'rparen':
|
|
break
|
|
items.append(self.parse_expression())
|
|
self.stream.expect('rparen')
|
|
|
|
return nodes.SetExpression(items, token.lineno, self.filename)
|
|
|
|
def parse_postfix_expression(self, node):
|
|
"""
|
|
Parse a postfix expression such as a filter statement or a
|
|
function call.
|
|
"""
|
|
while True:
|
|
current = self.stream.current.type
|
|
if current == 'dot' or current == 'lbracket':
|
|
node = self.parse_subscript_expression(node)
|
|
elif current == 'lparen':
|
|
node = self.parse_call_expression(node)
|
|
elif current == 'pipe':
|
|
node = self.parse_filter_expression(node)
|
|
elif current == 'is':
|
|
node = self.parse_test_expression(node)
|
|
else:
|
|
break
|
|
return node
|
|
|
|
def parse_subscript_expression(self, node):
|
|
"""
|
|
Parse a subscript statement. Gets attributes and items from an
|
|
object.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
if self.stream.current.type == 'dot':
|
|
self.stream.next()
|
|
token = self.stream.current
|
|
if token.type in ('name', 'integer'):
|
|
arg = nodes.ConstantExpression(token.value, token.lineno,
|
|
self.filename)
|
|
else:
|
|
raise TemplateSyntaxError('expected name or number',
|
|
token.lineno, self.filename)
|
|
self.stream.next()
|
|
elif self.stream.current.type == 'lbracket':
|
|
self.stream.next()
|
|
args = []
|
|
while self.stream.current.type != 'rbracket':
|
|
if args:
|
|
self.stream.expect('comma')
|
|
args.append(self.parse_subscribed_expression())
|
|
self.stream.expect('rbracket')
|
|
if len(args) == 1:
|
|
arg = args[0]
|
|
else:
|
|
arg = nodes.TupleExpression(args, lineno, self.filename)
|
|
else:
|
|
raise TemplateSyntaxError('expected subscript expression',
|
|
self.lineno, self.filename)
|
|
return nodes.SubscriptExpression(node, arg, lineno, self.filename)
|
|
|
|
def parse_call_expression(self, node=None):
|
|
"""
|
|
Parse a call.
|
|
"""
|
|
if node is None:
|
|
node = self.parse_primary_expression(parse_postfix=False)
|
|
token = self.stream.expect('lparen')
|
|
args = []
|
|
kwargs = []
|
|
dyn_args = None
|
|
dyn_kwargs = None
|
|
require_comma = False
|
|
|
|
def ensure(expr):
|
|
if not expr:
|
|
raise TemplateSyntaxError('invalid syntax for function '
|
|
'call expression', token.lineno,
|
|
self.filename)
|
|
|
|
while self.stream.current.type != 'rparen':
|
|
if require_comma:
|
|
self.stream.expect('comma')
|
|
# support for trailing comma
|
|
if self.stream.current.type == 'rparen':
|
|
break
|
|
if self.stream.current.type == 'mul':
|
|
ensure(dyn_args is None and dyn_kwargs is None)
|
|
self.stream.next()
|
|
dyn_args = self.parse_expression()
|
|
elif self.stream.current.type == 'pow':
|
|
ensure(dyn_kwargs is None)
|
|
self.stream.next()
|
|
dyn_kwargs = self.parse_expression()
|
|
else:
|
|
ensure(dyn_args is None and dyn_kwargs is None)
|
|
if self.stream.current.type == 'name' and \
|
|
self.stream.look().type == 'assign':
|
|
key = self.stream.current.value
|
|
self.stream.skip(2)
|
|
kwargs.append((key, self.parse_expression()))
|
|
else:
|
|
ensure(not kwargs)
|
|
args.append(self.parse_expression())
|
|
|
|
require_comma = True
|
|
self.stream.expect('rparen')
|
|
|
|
return nodes.CallExpression(node, args, kwargs, dyn_args,
|
|
dyn_kwargs, token.lineno,
|
|
self.filename)
|
|
|
|
def parse_filter_expression(self, node):
|
|
"""
|
|
Parse filter calls.
|
|
"""
|
|
lineno = self.stream.lineno
|
|
filters = []
|
|
while self.stream.current.type == 'pipe':
|
|
self.stream.next()
|
|
token = self.stream.expect('name')
|
|
args = []
|
|
if self.stream.current.type == 'lparen':
|
|
self.stream.next()
|
|
while self.stream.current.type != 'rparen':
|
|
if args:
|
|
self.stream.expect('comma')
|
|
args.append(self.parse_expression())
|
|
self.stream.expect('rparen')
|
|
filters.append((token.value, args))
|
|
return nodes.FilterExpression(node, filters, lineno, self.filename)
|
|
|
|
def parse_test_expression(self, node):
|
|
"""
|
|
Parse test calls.
|
|
"""
|
|
token = self.stream.expect('is')
|
|
if self.stream.current.type == 'not':
|
|
self.stream.next()
|
|
negated = True
|
|
else:
|
|
negated = False
|
|
name = self.stream.expect('name').value
|
|
args = []
|
|
if self.stream.current.type == 'lparen':
|
|
self.stream.next()
|
|
while self.stream.current.type != 'rparen':
|
|
if args:
|
|
self.stream.expect('comma')
|
|
args.append(self.parse_expression())
|
|
self.stream.expect('rparen')
|
|
elif self.stream.current.type in ('name', 'string', 'integer',
|
|
'float', 'lparen', 'lbracket',
|
|
'lbrace', 'regex'):
|
|
args.append(self.parse_expression())
|
|
node = nodes.TestExpression(node, name, args, token.lineno,
|
|
self.filename)
|
|
if negated:
|
|
node = nodes.NotExpression(node, token.lineno, self.filename)
|
|
return node
|
|
|
|
def test_name(self, name):
|
|
"""
|
|
Test if a name is not a special constant
|
|
"""
|
|
if name in ('true', 'false', 'none', 'undefined', '_'):
|
|
raise TemplateSyntaxError('expected name not special constant',
|
|
self.stream.lineno, self.filename)
|
|
|
|
def subparse(self, test, drop_needle=False):
|
|
"""
|
|
Helper function used to parse the sourcecode until the test
|
|
function which is passed a tuple in the form (lineno, token, data)
|
|
returns True. In that case the current token is pushed back to
|
|
the stream and the generator ends.
|
|
|
|
The test function is only called for the first token after a
|
|
block tag. Variable tags are *not* aliases for {% print %} in
|
|
that case.
|
|
|
|
If drop_needle is True the needle_token is removed from the
|
|
stream.
|
|
"""
|
|
if self.closed:
|
|
raise RuntimeError('parser is closed')
|
|
result = []
|
|
buffer = []
|
|
next = self.stream.next
|
|
lineno = self.stream.lineno
|
|
|
|
def assemble_list():
|
|
push_buffer()
|
|
return nodes.NodeList(result, lineno, self.filename)
|
|
|
|
def push_variable():
|
|
buffer.append((True, self.parse_tuple_expression()))
|
|
|
|
def push_data():
|
|
buffer.append((False, self.stream.expect('data')))
|
|
|
|
def push_buffer():
|
|
if not buffer:
|
|
return
|
|
template = []
|
|
variables = []
|
|
for is_var, data in buffer:
|
|
if is_var:
|
|
template.append('%s')
|
|
variables.append(data)
|
|
else:
|
|
template.append(data.value.replace('%', '%%'))
|
|
result.append(nodes.Text(u''.join(template), variables,
|
|
buffer[0][1].lineno, self.filename))
|
|
del buffer[:]
|
|
|
|
def push_node(node):
|
|
push_buffer()
|
|
result.append(node)
|
|
|
|
while self.stream:
|
|
token_type = self.stream.current.type
|
|
if token_type == 'variable_begin':
|
|
next()
|
|
push_variable()
|
|
self.stream.expect('variable_end')
|
|
elif token_type == 'raw_begin':
|
|
next()
|
|
push_data()
|
|
self.stream.expect('raw_end')
|
|
elif token_type == 'block_begin':
|
|
next()
|
|
if test is not None and test(self.stream.current):
|
|
if drop_needle:
|
|
next()
|
|
return assemble_list()
|
|
handler = self.directives.get(self.stream.current.type)
|
|
if handler is None:
|
|
if self.no_variable_block:
|
|
push_variable()
|
|
self.stream.expect('block_end')
|
|
elif self.stream.current.type in self.context_directives:
|
|
raise TemplateSyntaxError('unexpected directive %r.' %
|
|
self.stream.current.type,
|
|
lineno, self.filename)
|
|
else:
|
|
name = self.stream.current.value
|
|
raise TemplateSyntaxError('unknown directive %r.' %
|
|
name, lineno, self.filename)
|
|
else:
|
|
node = handler()
|
|
if node is not None:
|
|
push_node(node)
|
|
elif token_type == 'data':
|
|
push_data()
|
|
|
|
# this should be unreachable code
|
|
else:
|
|
assert False, "unexpected token %r" % self.stream.current
|
|
|
|
if test is not None:
|
|
msg = isinstance(test, StateTest) and ': ' + test.msg or ''
|
|
raise TemplateSyntaxError('unexpected end of stream' + msg,
|
|
self.stream.lineno, self.filename)
|
|
|
|
return assemble_list()
|
|
|
|
def sanitize_tree(self, body, extends):
|
|
self._sanitize_tree([body], [body], extends, body)
|
|
return body
|
|
|
|
def _sanitize_tree(self, nodelist, stack, extends, body):
|
|
"""
|
|
This is not a closure because python leaks memory if it is. It's used
|
|
by `parse()` to make sure blocks do not trigger unexpected behavior.
|
|
"""
|
|
for node in nodelist:
|
|
if extends is not None and \
|
|
node.__class__ is nodes.Block and \
|
|
stack[-1] is not body:
|
|
for n in stack:
|
|
if n.__class__ is nodes.Block:
|
|
break
|
|
else:
|
|
raise TemplateSyntaxError('misplaced block %r, '
|
|
'blocks in child '
|
|
'templates must be '
|
|
'either top level or '
|
|
'located in a block '
|
|
'tag.' % node.name,
|
|
node.lineno,
|
|
self.filename)
|
|
stack.append(node)
|
|
self._sanitize_tree(node.get_child_nodes(), stack, extends, body)
|
|
stack.pop()
|
|
|
|
def parse(self):
|
|
"""
|
|
Parse the template and return a Template node. This also does some
|
|
post processing sanitizing and parses for an extends tag.
|
|
"""
|
|
if self.closed:
|
|
raise RuntimeError('parser is closed')
|
|
|
|
try:
|
|
# get the leading whitespace, if we are not in a child
|
|
# template we push that back to the stream later.
|
|
leading_whitespace = self.stream.read_whitespace()
|
|
|
|
# parse an optional extends which *must* be the first node
|
|
# of a template.
|
|
if self.stream.current.type == 'block_begin' and \
|
|
self.stream.look().type == 'extends':
|
|
self.stream.skip(2)
|
|
extends = self.stream.expect('string').value
|
|
self.stream.expect('block_end')
|
|
else:
|
|
extends = None
|
|
if leading_whitespace:
|
|
self.stream.shift(leading_whitespace)
|
|
|
|
body = self.sanitize_tree(self.subparse(None), extends)
|
|
return nodes.Template(extends, body, 1, self.filename)
|
|
finally:
|
|
self.close()
|
|
|
|
def close(self):
|
|
"""Clean up soon."""
|
|
self.closed = True
|
|
self.stream = self.directives = self.stream = self.blocks = \
|
|
self.environment = None
|