blob: c4a4728579d60455c531f4dec0587279854f235b [file] [log] [blame]
# -*- coding: utf-8 -*-
"""
jinja.parser
~~~~~~~~~~~~
Implements the template parser.
:copyright: 2006 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
import re
from compiler import ast, parse
from compiler.misc import set_filename
from jinja import nodes
from jinja.datastructure import TokenStream
from jinja.exceptions import TemplateSyntaxError
# callback functions for the subparse method
end_of_block = lambda p, t, d: t == 'block_end'
end_of_variable = lambda p, t, d: t == 'variable_end'
switch_for = lambda p, t, d: t == 'name' and d in ('else', 'endfor')
end_of_for = lambda p, t, d: t == 'name' and d == 'endfor'
switch_if = lambda p, t, d: t == 'name' and d in ('else', 'elif', 'endif')
end_of_if = lambda p, t, d: t == 'name' and d == 'endif'
end_of_macro = lambda p, t, d: t == 'name' and d == 'endmacro'
end_of_block_tag = lambda p, t, d: t == 'name' and d == 'endblock'
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')
self.source = source
self.filename = filename
self.tokenstream = environment.lexer.tokenize(source)
self.extends = None
self.blocks = {}
self.directives = {
'for': self.handle_for_directive,
'if': self.handle_if_directive,
'cycle': self.handle_cycle_directive,
'print': self.handle_print_directive,
'macro': self.handle_macro_directive,
'block': self.handle_block_directive,
'extends': self.handle_extends_directive
}
def handle_for_directive(self, lineno, gen):
"""
Handle a for directive and return a ForLoop node
"""
#XXX: maybe we could make the "recurse" part optional by using
# a static analysis later.
recursive = []
def wrapgen():
"""Wrap the generator to check if we have a recursive for loop."""
for token in gen:
if token[1:] == ('name', 'recursive'):
try:
item = gen.next()
except StopIteration:
recursive.append(True)
return
yield token
yield item
else:
yield token
ast = self.parse_python(lineno, wrapgen(), 'for %s:pass')
body = self.subparse(switch_for)
# do we have an else section?
if self.tokenstream.next()[2] == 'else':
self.close_remaining_block()
else_ = self.subparse(end_of_for, True)
else:
else_ = None
self.close_remaining_block()
return nodes.ForLoop(lineno, ast.assign, ast.list, body, else_, bool(recursive))
def handle_if_directive(self, lineno, gen):
"""
Handle if/else blocks.
"""
ast = self.parse_python(lineno, gen, 'if %s:pass')
tests = [(ast.tests[0][0], self.subparse(switch_if))]
# do we have an else section?
while True:
lineno, token, needle = self.tokenstream.next()
if needle == 'else':
self.close_remaining_block()
else_ = self.subparse(end_of_if, True)
break
elif needle == 'elif':
gen = self.tokenstream.fetch_until(end_of_block, True)
ast = self.parse_python(lineno, gen, 'if %s:pass')
tests.append((ast.tests[0][0], self.subparse(switch_if)))
else:
else_ = None
break
self.close_remaining_block()
return nodes.IfCondition(lineno, tests, else_)
def handle_cycle_directive(self, lineno, gen):
"""
Handle {% cycle foo, bar, baz %}.
"""
ast = self.parse_python(lineno, gen, '_cycle((%s))')
# ast is something like Discard(CallFunc(Name('_cycle'), ...))
# skip that.
return nodes.Cycle(lineno, ast.expr.args[0])
def handle_print_directive(self, lineno, gen):
"""
Handle {{ foo }} and {% print foo %}.
"""
ast = self.parse_python(lineno, gen, 'print_(%s)')
# ast is something like Discard(CallFunc(Name('print_'), ...))
# so just use the args
arguments = ast.expr.args
# we only accept one argument
if len(arguments) != 1:
raise TemplateSyntaxError('invalid argument count for print; '
'print requires exactly one argument, '
'got %d.' % len(arguments), lineno)
return nodes.Print(lineno, arguments[0])
def handle_macro_directive(self, lineno, gen):
"""
Handle {% macro foo(bar, baz) %}.
"""
try:
macro_name = gen.next()
except StopIteration:
raise TemplateSyntaxError('macro requires a name', lineno)
if macro_name[1] != 'name':
raise TemplateSyntaxError('expected \'name\', got %r' %
macro_name[1], lineno)
ast = self.parse_python(lineno, gen, 'def %s(%%s):pass' % str(macro_name[2]))
body = self.subparse(end_of_macro, True)
self.close_remaining_block()
if ast.varargs or ast.kwargs:
raise TemplateSyntaxError('variable length macro signature '
'not allowed.', lineno)
defaults = [None] * (len(ast.argnames) - len(ast.defaults)) + ast.defaults
return nodes.Macro(lineno, ast.name, zip(ast.argnames, defaults), body)
def handle_block_directive(self, lineno, gen):
"""
Handle block directives used for inheritance.
"""
tokens = list(gen)
if not tokens:
raise TemplateSyntaxError('block requires a name', lineno)
block_name = tokens.pop(0)
if block_name[1] != 'name':
raise TemplateSyntaxError('expected \'name\', got %r' %
block_name[1], lineno)
if tokens:
print tokens
raise TemplateSyntaxError('block got too many arguments, '
'requires one.', lineno)
if block_name[2] in self.blocks:
raise TemplateSyntaxError('block %r defined twice' %
block_name[2], lineno)
body = self.subparse(end_of_block_tag, True)
self.close_remaining_block()
rv = nodes.Block(lineno, block_name[2], body)
self.blocks[block_name[2]] = rv
return rv
def handle_extends_directive(self, lineno, gen):
"""
Handle extends directive used for inheritance.
"""
tokens = list(gen)
if len(tokens) != 1 or tokens[0][1] != 'string':
raise TemplateSyntaxError('extends requires a string', lineno)
if self.extends is not None:
raise TemplateSyntaxError('extends called twice', lineno)
self.extends = nodes.Extends(lineno, tokens[0][2][1:-1])
def parse_python(self, lineno, gen, template='%s'):
"""
Convert the passed generator into a flat string representing
python sourcecode and return an ast node or raise a
TemplateSyntaxError.
"""
tokens = []
for t_lineno, t_token, t_data in gen:
if t_token == 'string':
tokens.append('u' + t_data)
else:
tokens.append(t_data)
source = '\xef\xbb\xbf' + (template % (u' '.join(tokens)).encode('utf-8'))
try:
ast = parse(source, 'exec')
except SyntaxError, e:
raise TemplateSyntaxError('invalid syntax', lineno + e.lineno)
assert len(ast.node.nodes) == 1, 'get %d nodes, 1 expected' % len(ast.node.nodes)
result = ast.node.nodes[0]
nodes.inc_lineno(lineno, result)
return result
def parse(self):
"""
Parse the template and return a Template node.
"""
body = self.subparse(None)
return nodes.Template(self.filename, body, self.blocks, self.extends)
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 tokenstream 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 tokenstream.
"""
def finish():
"""Helper function to remove unused nodelists."""
if len(result) == 1:
return result[0]
return result
lineno = self.tokenstream.last[0]
result = nodes.NodeList(lineno)
for lineno, token, data in self.tokenstream:
# this token marks the begin or a variable section.
# parse everything till the end of it.
if token == 'variable_begin':
gen = self.tokenstream.fetch_until(end_of_variable, True)
result.append(self.directives['print'](lineno, gen))
# this token marks the start of a block. like for variables
# just parse everything until the end of the block
elif token == 'block_begin':
gen = self.tokenstream.fetch_until(end_of_block, True)
try:
lineno, token, data = gen.next()
except StopIteration:
raise TemplateSyntaxError('unexpected end of block', lineno)
# first token *must* be a name token
if token != 'name':
raise TemplateSyntaxError('unexpected %r token (%r)' % (
token, data), lineno)
# if a test function is passed to subparse we check if we
# reached the end of such a requested block.
if test is not None and test(lineno, token, data):
if not drop_needle:
self.tokenstream.push(lineno, token, data)
return finish()
# the first token tells us which directive we want to call.
# if if doesn't match any existing directive it's like a
# template syntax error.
if data in self.directives:
node = self.directives[data](lineno, gen)
else:
raise TemplateSyntaxError('unknown directive %r' % data, lineno)
# some tags like the extends tag do not output nodes.
# so just skip that.
if node is not None:
result.append(node)
# here the only token we should get is "data". all other
# tokens just exist in block or variable sections. (if the
# tokenizer is not brocken)
elif token == 'data':
result.append(nodes.Text(lineno, data))
# so this should be unreachable code
else:
raise AssertionError('unexpected token %r (%r)' % (token, data))
# still here and a test function is provided? raise and error
if test is not None:
raise TemplateSyntaxError('unexpected end of template', lineno)
return finish()
def close_remaining_block(self):
"""
If we opened a block tag because one of our tags requires an end
tag we can use this method to drop the rest of the block from
the stream. If the next token isn't the block end we throw an
error.
"""
lineno = self.tokenstream.last[0]
try:
lineno, token, data = self.tokenstream.next()
except StopIteration:
raise TemplateSyntaxError('missing closing tag', lineno)
if token != 'block_end':
raise TemplateSyntaxError('expected close tag, found %r' % token, lineno)