Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | """ |
| 3 | jinja2.datastructure |
| 4 | ~~~~~~~~~~~~~~~~~~~~ |
| 5 | |
| 6 | Module that helds several data types used in the template engine. |
| 7 | |
| 8 | :copyright: 2007 by Armin Ronacher. |
| 9 | :license: BSD, see LICENSE for more details. |
| 10 | """ |
| 11 | from operator import itemgetter |
Armin Ronacher | 82b3f3d | 2008-03-31 20:01:08 +0200 | [diff] [blame] | 12 | from collections import deque |
| 13 | from jinja2.exceptions import TemplateSyntaxError, TemplateRuntimeError |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 14 | |
| 15 | |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 16 | class Token(tuple): |
| 17 | """ |
| 18 | Token class. |
| 19 | """ |
| 20 | __slots__ = () |
Armin Ronacher | 82b3f3d | 2008-03-31 20:01:08 +0200 | [diff] [blame] | 21 | lineno, type, value = (property(itemgetter(x)) for x in range(3)) |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 22 | |
| 23 | def __new__(cls, lineno, type, value): |
Armin Ronacher | 82b3f3d | 2008-03-31 20:01:08 +0200 | [diff] [blame] | 24 | return tuple.__new__(cls, (lineno, intern(str(type)), value)) |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 25 | |
| 26 | def __str__(self): |
| 27 | from jinja.lexer import keywords, reverse_operators |
| 28 | if self.type in keywords: |
| 29 | return self.type |
| 30 | elif self.type in reverse_operators: |
| 31 | return reverse_operators[self.type] |
Armin Ronacher | aaf010d | 2008-05-01 13:14:30 +0200 | [diff] [blame^] | 32 | elif self.type is 'name': |
| 33 | return self.value |
| 34 | return self.type |
Armin Ronacher | f59bac2 | 2008-04-20 13:11:43 +0200 | [diff] [blame] | 35 | |
| 36 | def test(self, expr): |
| 37 | """Test a token against a token expression. This can either be a |
| 38 | token type or 'token_type:token_value'. This can only test against |
| 39 | string values! |
| 40 | """ |
| 41 | # here we do a regular string equality check as test_many is usually |
| 42 | # passed an iterable of not interned strings. |
| 43 | if self.type == expr: |
| 44 | return True |
| 45 | elif ':' in expr: |
| 46 | return expr.split(':', 1) == [self.type, self.value] |
| 47 | return False |
| 48 | |
| 49 | def test_many(self, iterable): |
| 50 | """Test against multiple token expressions.""" |
| 51 | for expr in iterable: |
| 52 | if self.test(expr): |
| 53 | return True |
| 54 | return False |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 55 | |
| 56 | def __repr__(self): |
| 57 | return 'Token(%r, %r, %r)' % ( |
| 58 | self.lineno, |
| 59 | self.type, |
| 60 | self.value |
| 61 | ) |
| 62 | |
| 63 | |
| 64 | class TokenStreamIterator(object): |
| 65 | """ |
| 66 | The iterator for tokenstreams. Iterate over the stream |
| 67 | until the eof token is reached. |
| 68 | """ |
| 69 | |
| 70 | def __init__(self, stream): |
| 71 | self._stream = stream |
| 72 | |
| 73 | def __iter__(self): |
| 74 | return self |
| 75 | |
| 76 | def next(self): |
| 77 | token = self._stream.current |
| 78 | if token.type == 'eof': |
| 79 | self._stream.close() |
| 80 | raise StopIteration() |
Armin Ronacher | e791c2a | 2008-04-07 18:39:54 +0200 | [diff] [blame] | 81 | self._stream.next(False) |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 82 | return token |
| 83 | |
| 84 | |
| 85 | class TokenStream(object): |
| 86 | """ |
| 87 | A token stream wraps a generator and supports pushing tokens back. |
| 88 | It also provides some functions to expect tokens and similar stuff. |
| 89 | |
| 90 | Important note: Do never push more than one token back to the |
| 91 | stream. Although the stream object won't stop you |
| 92 | from doing so, the behavior is undefined. Multiple |
| 93 | pushed tokens are only used internally! |
| 94 | """ |
| 95 | |
| 96 | def __init__(self, generator, filename): |
| 97 | self._next = generator.next |
Armin Ronacher | 82b3f3d | 2008-03-31 20:01:08 +0200 | [diff] [blame] | 98 | self._pushed = deque() |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 99 | self.current = Token(1, 'initial', '') |
| 100 | self.filename = filename |
| 101 | self.next() |
| 102 | |
| 103 | def __iter__(self): |
| 104 | return TokenStreamIterator(self) |
| 105 | |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 106 | def __nonzero__(self): |
| 107 | """Are we at the end of the tokenstream?""" |
| 108 | return bool(self._pushed) or self.current.type != 'eof' |
| 109 | |
| 110 | eos = property(lambda x: not x.__nonzero__(), doc=__nonzero__.__doc__) |
| 111 | |
| 112 | def push(self, token): |
| 113 | """Push a token back to the stream.""" |
| 114 | self._pushed.append(token) |
| 115 | |
Armin Ronacher | 82b3f3d | 2008-03-31 20:01:08 +0200 | [diff] [blame] | 116 | def look(self): |
| 117 | """Look at the next token.""" |
Armin Ronacher | 8efc522 | 2008-04-08 14:47:40 +0200 | [diff] [blame] | 118 | old_token = self.next() |
| 119 | result = self.current |
| 120 | self.push(result) |
| 121 | self.current = old_token |
| 122 | return result |
Armin Ronacher | 82b3f3d | 2008-03-31 20:01:08 +0200 | [diff] [blame] | 123 | |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 124 | def skip(self, n): |
| 125 | """Got n tokens ahead.""" |
| 126 | for x in xrange(n): |
| 127 | self.next() |
| 128 | |
Armin Ronacher | e791c2a | 2008-04-07 18:39:54 +0200 | [diff] [blame] | 129 | def next(self, skip_eol=True): |
| 130 | """Go one token ahead and return the old one""" |
| 131 | rv = self.current |
| 132 | while 1: |
| 133 | if self._pushed: |
| 134 | self.current = self._pushed.popleft() |
| 135 | elif self.current.type is not 'eof': |
| 136 | try: |
| 137 | self.current = self._next() |
| 138 | except StopIteration: |
| 139 | self.close() |
| 140 | if not skip_eol or self.current.type is not 'eol': |
| 141 | break |
| 142 | return rv |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 143 | |
| 144 | def close(self): |
| 145 | """Close the stream.""" |
| 146 | self.current = Token(self.current.lineno, 'eof', '') |
| 147 | self._next = None |
| 148 | |
Armin Ronacher | f59bac2 | 2008-04-20 13:11:43 +0200 | [diff] [blame] | 149 | def expect(self, expr): |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 150 | """Expect a given token type and return it""" |
Armin Ronacher | f59bac2 | 2008-04-20 13:11:43 +0200 | [diff] [blame] | 151 | if not self.current.test(expr): |
Armin Ronacher | aaf010d | 2008-05-01 13:14:30 +0200 | [diff] [blame^] | 152 | if ':' in expr: |
| 153 | expr = expr.split(':')[1] |
| 154 | if self.current.type is 'eof': |
| 155 | raise TemplateSyntaxError('unexpected end of template, ' |
| 156 | 'expected %r.' % expr, |
| 157 | self.current.lineno, |
| 158 | self.filename) |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 159 | raise TemplateSyntaxError("expected token %r, got %r" % |
Armin Ronacher | aaf010d | 2008-05-01 13:14:30 +0200 | [diff] [blame^] | 160 | (expr, str(self.current)), |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 161 | self.current.lineno, |
| 162 | self.filename) |
| 163 | try: |
| 164 | return self.current |
| 165 | finally: |
| 166 | self.next() |