1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# copied from https://github.com/avelino/quik
5#
6# copied license statement:
7# -------------------------------------------------------------------------
8# The MIT License (MIT)
9#
10# Copyright (c) 2013 Thiago Avelino
11#
12# Permission is hereby granted, free of charge, to any person obtaining a copy of
13# this software and associated documentation files (the "Software"), to deal in
14# the Software without restriction, including without limitation the rights to
15# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
16# the Software, and to permit persons to whom the Software is furnished to do so,
17# subject to the following conditions:
18#
19# The above copyright notice and this permission notice shall be included in all
20# copies or substantial portions of the Software.
21#
22# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
24# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
25# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
26# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
27# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28# -------------------------------------------------------------------------
29#
30# Changed:
31#  - replaced '#' character by '%' to make it work better with C++
32#
33
34import re, operator, os
35try:
36    from StringIO import StringIO
37except ImportError:
38    from io import StringIO
39    xrange = range
40
41
42VERSION = (0, 2, 3)
43VERSION_TAG = 'dev'
44
45__version__ = ".".join(map(str, VERSION))
46if VERSION_TAG:
47    __version__ = "{0}-{1}".format(
48        __version__, VERSION_TAG)
49
50
51class Template:
52    def __init__(self, content):
53        self.content = content
54        self.root_element = None
55
56    def render(self, namespace, loader=None):
57        output = StoppableStream()
58        self.merge_to(namespace, output, loader)
59        return output.getvalue()
60
61    def ensure_compiled(self):
62        if not self.root_element:
63            self.root_element = TemplateBody(self.content)
64
65    def merge_to(self, namespace, fileobj, loader=None):
66        if loader is None:
67            loader = NullLoader()
68        self.ensure_compiled()
69        self.root_element.evaluate(fileobj, namespace, loader)
70
71
72class TemplateError(Exception):
73    pass
74
75
76class TemplateSyntaxError(TemplateError):
77    def __init__(self, element, expected):
78        self.element = element
79        self.text_understood = element.full_text()[:element.end]
80        self.line = 1 + self.text_understood.count('\n')
81        self.column = len(self.text_understood) - self.text_understood.rfind('\n')
82        got = element.next_text()
83        if len(got) > 40:
84            got = got[:36] + ' ...'
85        Exception.__init__(self, "line %d, column %d: expected %s in %s, got: %s ..." % (self.line, self.column, expected, self.element_name(), got))
86
87    def get_position_strings(self):
88        error_line_start = 1 + self.text_understood.rfind('\n')
89        if '\n' in self.element.next_text():
90            error_line_end = self.element.next_text().find('\n') + self.element.end
91        else:
92            error_line_end = len(self.element.full_text())
93        error_line = self.element.full_text()[error_line_start:error_line_end]
94        caret_pos = self.column
95        return [error_line, ' ' * (caret_pos - 1) + '^']
96
97    def element_name(self):
98        return re.sub('([A-Z])', lambda m: ' ' + m.group(1).lower(), self.element.__class__.__name__).strip()
99
100
101class NullLoader:
102    def load_text(self, name):
103        raise TemplateError("no loader available for '%s'" % name)
104
105    def load_template(self, name):
106        raise self.load_text(name)
107
108
109class FileLoader:
110    def __init__(self, basedir, debugging=False):
111        self.basedir = basedir
112        self.known_templates = {}
113        self.debugging = debugging
114        if debugging:
115            print("creating caching file loader with basedir: {0}".format(basedir))
116
117    def filename_of(self, name):
118        return os.path.join(self.basedir, name)
119
120    def load_text(self, name):
121        if self.debugging:
122            print("Loading text from {0} {1}".format(self.basedir, name))
123        f = open(self.filename_of(name))
124        try:
125            return f.read()
126        finally:
127            f.close()
128
129    def load_template(self, name):
130        if self.debugging:
131            print("Loading template... {0}".format(name))
132
133        mtime = os.path.getmtime(self.filename_of(name))
134        if self.known_templates.get(name, None):
135            template, prev_mtime = self.known_templates[name]
136            if mtime <= prev_mtime:
137                if self.debugging:
138                    print("loading parsed template from cache")
139                return template
140        if self.debugging:
141            print("loading text from disk")
142        template = Template(self.load_text(name))
143        template.ensure_compiled()
144        self.known_templates[name] = (template, mtime)
145        return template
146
147
148class StoppableStream(StringIO):
149    def __init__(self, buf=''):
150        self.stop = False
151        StringIO.__init__(self, buf)
152
153    def write(self, s):
154        if not self.stop:
155            StringIO.write(self, s)
156
157
158WHITESPACE_TO_END_OF_LINE = re.compile(r'[ \t\r]*\n(.*)', re.S)
159
160class NoMatch(Exception): pass
161
162
163class LocalNamespace(dict):
164    def __init__(self, parent):
165        dict.__init__(self)
166        self.parent = parent
167
168    def __getitem__(self, key):
169        try: return dict.__getitem__(self, key)
170        except KeyError:
171            parent_value = self.parent[key]
172            self[key] = parent_value
173            return parent_value
174
175    def top(self):
176        if hasattr(self.parent, "top"):
177            return self.parent.top()
178        return self.parent
179
180    def __repr__(self):
181        return dict.__repr__(self) + '->' + repr(self.parent)
182
183
184class _Element:
185    def __init__(self, text, start=0):
186        self._full_text = text
187        self.start = self.end = start
188        self.parse()
189
190    def next_text(self):
191        return self._full_text[self.end:]
192
193    def my_text(self):
194        return self._full_text[self.start:self.end]
195
196    def full_text(self):
197        return self._full_text
198
199    def syntax_error(self, expected):
200        return TemplateSyntaxError(self, expected)
201
202    def identity_match(self, pattern):
203        m = pattern.match(self._full_text, self.end)
204        if not m: raise NoMatch()
205        self.end = m.start(pattern.groups)
206        return m.groups()[:-1]
207
208    def next_match(self, pattern):
209        m = pattern.match(self._full_text, self.end)
210        if not m: return False
211        self.end = m.start(pattern.groups)
212        return m.groups()[:-1]
213
214    def optional_match(self, pattern):
215        m = pattern.match(self._full_text, self.end)
216        if not m: return False
217        self.end = m.start(pattern.groups)
218        return True
219
220    def require_match(self, pattern, expected):
221        m = pattern.match(self._full_text, self.end)
222        if not m: raise self.syntax_error(expected)
223        self.end = m.start(pattern.groups)
224        return m.groups()[:-1]
225
226    def next_element(self, element_spec):
227        if callable(element_spec):
228            element = element_spec(self._full_text, self.end)
229            self.end = element.end
230            return element
231        else:
232            for element_class in element_spec:
233                try: element = element_class(self._full_text, self.end)
234                except NoMatch: pass
235                else:
236                    self.end = element.end
237                    return element
238            raise NoMatch()
239
240    def require_next_element(self, element_spec, expected):
241        if callable(element_spec):
242            try: element = element_spec(self._full_text, self.end)
243            except NoMatch: raise self.syntax_error(expected)
244            else:
245                self.end = element.end
246                return element
247        else:
248            for element_class in element_spec:
249                try: element = element_class(self._full_text, self.end)
250                except NoMatch: pass
251                else:
252                    self.end = element.end
253                    return element
254            expected = ', '.join([cls.__name__ for cls in element_spec])
255            raise self.syntax_error('one of: ' + expected)
256
257
258class Text(_Element):
259    PLAIN = re.compile(r'((?:[^\\\@%]+|\\[\@%])+|\@[^!\{a-z0-9_]|\@@|%@|%[^\{\}a-zA-Z0-9%\*]+|\\.)(.*)$', re.S + re.I)
260    ESCAPED_CHAR = re.compile(r'\\([\\\@%])')
261
262    def parse(self):
263        text, = self.identity_match(self.PLAIN)
264        def unescape(match):
265            return match.group(1)
266        self.text = self.ESCAPED_CHAR.sub(unescape, text)
267
268    def evaluate(self, stream, namespace, loader):
269        stream.write(self.text)
270
271
272class FallthroughHashText(_Element):
273    """ Plain tex, starting a hash, but which wouldn't be matched
274        by a directive or a macro earlier.
275        The canonical example is an HTML color spec.
276        Another good example, is in-document hypertext links
277        (or the dummy versions thereof often used a href targets
278        when javascript is used.
279        Note that it MUST NOT match block-ending directives. """
280    # because of earlier elements, this will always start with a hash
281    PLAIN = re.compile(r'(\%+\{?[\d\w]*\}?)(.*)$', re.S)
282
283    def parse(self):
284        self.text, = self.identity_match(self.PLAIN)
285        if self.text.startswith('%end') or self.text.startswith('%{end}') or self.text.startswith('%else') or self.text.startswith('%{else}') or self.text.startswith('%elseif') or self.text.startswith('%{elseif}'):
286            raise NoMatch
287
288    def evaluate(self, stream, namespace, loader):
289        stream.write(self.text)
290
291
292class IntegerLiteral(_Element):
293    INTEGER = re.compile(r'(-?\d+)(.*)', re.S)
294
295    def parse(self):
296        self.value, = self.identity_match(self.INTEGER)
297        self.value = int(self.value)
298
299    def calculate(self, namespace, loader):
300        return self.value
301
302
303class FloatingPointLiteral(_Element):
304    FLOAT = re.compile(r'(-?\d+\.\d+)(.*)', re.S)
305
306    def parse(self):
307        self.value, = self.identity_match(self.FLOAT)
308        self.value = float(self.value)
309
310    def calculate(self, namespace, loader):
311        return self.value
312
313
314class BooleanLiteral(_Element):
315    BOOLEAN = re.compile(r'((?:true)|(?:false))(.*)', re.S | re.I)
316
317    def parse(self):
318        self.value, = self.identity_match(self.BOOLEAN)
319        self.value = self.value.lower() == 'true'
320
321    def calculate(self, namespace, loader):
322        return self.value
323
324
325class StringLiteral(_Element):
326    STRING = re.compile(r"'((?:\\['nrbt\\\\\\@]|[^'\\])*)'(.*)", re.S)
327    ESCAPED_CHAR = re.compile(r"\\([nrbt'\\])")
328
329    def parse(self):
330        value, = self.identity_match(self.STRING)
331        def unescape(match):
332            return {'n': '\n', 'r': '\r', 'b': '\b', 't': '\t', '"': '"', '\\': '\\', "'": "'"}.get(match.group(1), '\\' + match.group(1))
333        self.value = self.ESCAPED_CHAR.sub(unescape, value)
334
335    def calculate(self, namespace, loader):
336        return self.value
337
338class InterpolatedStringLiteral(StringLiteral):
339    STRING = re.compile(r'"((?:\\["nrbt\\\\\\@]|[^"\\])*)"(.*)', re.S)
340    ESCAPED_CHAR = re.compile(r'\\([nrbt"\\])')
341
342    def parse(self):
343        StringLiteral.parse(self)
344        self.block = Block(self.value, 0)
345
346    def calculate(self, namespace, loader):
347        output = StoppableStream()
348        self.block.evaluate(output, namespace, loader)
349        return output.getvalue()
350
351
352class Range(_Element):
353    MIDDLE = re.compile(r'([ \t]*\.\.[ \t]*)(.*)$', re.S)
354
355    def parse(self):
356        self.value1 = self.next_element((FormalReference, IntegerLiteral))
357        self.identity_match(self.MIDDLE)
358        self.value2 = self.next_element((FormalReference, IntegerLiteral))
359
360    def calculate(self, namespace, loader):
361        value1 = self.value1.calculate(namespace, loader)
362        value2 = self.value2.calculate(namespace, loader)
363        if value2 < value1:
364            return xrange(value1, value2 - 1, -1)
365        return xrange(value1, value2 + 1)
366
367
368class ValueList(_Element):
369    COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
370
371    def parse(self):
372        self.values = []
373        try: value = self.next_element(Value)
374        except NoMatch:
375            pass
376        else:
377            self.values.append(value)
378            while self.optional_match(self.COMMA):
379                value = self.require_next_element(Value, 'value')
380                self.values.append(value)
381
382    def calculate(self, namespace, loader):
383        return [value.calculate(namespace, loader) for value in self.values]
384
385
386class _EmptyValues:
387    def calculate(self, namespace, loader):
388        return []
389
390
391class ArrayLiteral(_Element):
392    START = re.compile(r'\[[ \t]*(.*)$', re.S)
393    END =   re.compile(r'[ \t]*\](.*)$', re.S)
394    values = _EmptyValues()
395
396    def parse(self):
397        self.identity_match(self.START)
398        try:
399            self.values = self.next_element((Range, ValueList))
400        except NoMatch:
401            pass
402        self.require_match(self.END, ']')
403        self.calculate = self.values.calculate
404
405class DictionaryLiteral(_Element):
406    START = re.compile(r'{[ \t]*(.*)$', re.S)
407    END =   re.compile(r'[ \t]*}(.*)$', re.S)
408    KEYVALSEP = re.compile(r'[ \t]*:[[ \t]*(.*)$', re.S)
409    PAIRSEP = re.compile(r'[ \t]*,[ \t]*(.*)$', re.S)
410
411    def parse(self):
412        self.identity_match(self.START)
413        self.local_data = {}
414        if self.optional_match(self.END):
415            # it's an empty dictionary
416            return
417        while(True):
418            key = self.next_element(Value)
419            self.require_match(self.KEYVALSEP, ':')
420            value = self.next_element(Value)
421            self.local_data[key] = value
422            if not self.optional_match(self.PAIRSEP): break
423        self.require_match(self.END, '}')
424
425    # Note that this delays calculation of values until it's used.
426    # TODO confirm that that's correct.
427    def calculate(self, namespace, loader):
428        tmp = {}
429        for (key,val) in self.local_data.items():
430            tmp[key.calculate(namespace, loader)] = val.calculate(namespace, loader)
431        return tmp
432
433
434class Value(_Element):
435    def parse(self):
436        self.expression = self.next_element((FormalReference, FloatingPointLiteral, IntegerLiteral,
437                                             StringLiteral, InterpolatedStringLiteral, ArrayLiteral,
438                                             DictionaryLiteral, ParenthesizedExpression, UnaryOperatorValue,
439                                             BooleanLiteral))
440
441    def calculate(self, namespace, loader):
442        return self.expression.calculate(namespace, loader)
443
444
445class NameOrCall(_Element):
446    NAME = re.compile(r'([a-zA-Z0-9_]+)(.*)$', re.S)
447    parameters = None
448
449    def parse(self):
450        self.name, = self.identity_match(self.NAME)
451        try: self.parameters = self.next_element(ParameterList)
452        except NoMatch: pass
453
454    def calculate(self, current_object, loader, top_namespace):
455        look_in_dict = True
456        if not isinstance(current_object, LocalNamespace):
457            try:
458                result = getattr(current_object, self.name)
459                look_in_dict = False
460            except AttributeError:
461                pass
462        if look_in_dict:
463            try: result = current_object[self.name]
464            except KeyError: result = None
465            except TypeError: result = None
466            except AttributeError: result = None
467        if result is None:
468            return None ## TODO: an explicit 'not found' exception?
469        if self.parameters is not None:
470            result = result(*self.parameters.calculate(top_namespace, loader))
471        return result
472
473
474class SubExpression(_Element):
475    DOT = re.compile('\.(.*)', re.S)
476
477    def parse(self):
478        self.identity_match(self.DOT)
479        self.expression = self.next_element(VariableExpression)
480
481    def calculate(self, current_object, loader, global_namespace):
482        return self.expression.calculate(current_object, loader, global_namespace)
483
484
485class VariableExpression(_Element):
486    subexpression = None
487
488    def parse(self):
489        self.part = self.next_element(NameOrCall)
490        try: self.subexpression = self.next_element(SubExpression)
491        except NoMatch: pass
492
493    def calculate(self, namespace, loader, global_namespace=None):
494        if global_namespace is None:
495            global_namespace = namespace
496        value = self.part.calculate(namespace, loader, global_namespace)
497        if self.subexpression:
498            value = self.subexpression.calculate(value, loader, global_namespace)
499        return value
500
501
502class ParameterList(_Element):
503    START = re.compile(r'\(\s*(.*)$', re.S)
504    COMMA = re.compile(r'\s*,\s*(.*)$', re.S)
505    END = re.compile(r'\s*\)(.*)$', re.S)
506    values = _EmptyValues()
507
508    def parse(self):
509        self.identity_match(self.START)
510        try: self.values = self.next_element(ValueList)
511        except NoMatch: pass
512        self.require_match(self.END, ')')
513
514    def calculate(self, namespace, loader):
515        return self.values.calculate(namespace, loader)
516
517
518class FormalReference(_Element):
519    START = re.compile(r'\@(!?)(\{?)(.*)$', re.S)
520    CLOSING_BRACE = re.compile(r'\}(.*)$', re.S)
521
522    def parse(self):
523        self.silent, braces = self.identity_match(self.START)
524        self.expression = self.require_next_element(VariableExpression, 'expression')
525        if braces: self.require_match(self.CLOSING_BRACE, '}')
526        self.calculate = self.expression.calculate
527
528    def evaluate(self, stream, namespace, loader):
529        value = self.expression.calculate(namespace, loader)
530        if value is None:
531            if self.silent: value = ''
532            else: value = self.my_text()
533
534        try:
535            basestring
536            def is_string(s):
537                return isinstance(s, basestring)
538        except NameError:
539            def is_string(s):
540                return type(s) == type('')
541
542        if is_string(value):
543            stream.write(value)
544        else:
545            stream.write(str(value))
546
547
548class Null:
549    def evaluate(self, stream, namespace, loader): pass
550
551
552class Comment(_Element, Null):
553    COMMENT = re.compile('%(?:%.*?(?:\n|$)|\*.*?\*%(?:[ \t]*\n)?)(.*)$', re.M + re.S)
554
555    def parse(self):
556        self.identity_match(self.COMMENT)
557
558
559def boolean_value(variable_value):
560    if variable_value == False:
561        return False
562    return not (variable_value is None)
563
564
565class BinaryOperator(_Element):
566
567    BINARY_OP = re.compile(r'\s*(>=|<=|<|==|!=|>|%|\|\||&&|or|and|\+|\-|\*|\/|\%)\s*(.*)$', re.S)
568    try:
569        operator.__gt__
570    except AttributeError:
571        operator.__gt__ = lambda a, b: a > b
572        operator.__lt__ = lambda a, b: a < b
573        operator.__ge__ = lambda a, b: a >= b
574        operator.__le__ = lambda a, b: a <= b
575        operator.__eq__ = lambda a, b: a == b
576        operator.__ne__ = lambda a, b: a != b
577        operator.mod = lambda a, b: a % b
578
579    OPERATORS = {
580        '>': operator.gt,
581        '>=': operator.ge,
582        '<': operator.lt,
583        '<=': operator.le,
584        '==': operator.eq,
585        '!=': operator.ne,
586        '%': operator.mod,
587        '||': lambda a,b : boolean_value(a) or boolean_value(b),
588        '&&': lambda a,b : boolean_value(a) and boolean_value(b),
589        'or': lambda a,b : boolean_value(a) or boolean_value(b),
590        'and': lambda a,b : boolean_value(a) and boolean_value(b),
591        '+' : operator.add,
592        '-' : operator.sub,
593        '/' : lambda a, b: a / b,
594        '*' : operator.mul}
595    PRECEDENCE = {
596        '>': 2,
597        '<': 2,
598        '==': 2,
599        '>=': 2,
600        '<=': 2,
601        '!=': 2,
602        '||': 1,
603        '&&': 1,
604        'or': 1,
605        'and': 1,
606        '+': 3,
607        '-': 3,
608        '*': 3,
609        '/': 3,
610        '%': 3}
611
612    def parse(self):
613        op_string, = self.identity_match(self.BINARY_OP)
614        self.apply_to = self.OPERATORS[op_string]
615        self.precedence = self.PRECEDENCE[op_string]
616
617    def greater_precedence_than(self, other):
618        return self.precedence > other.precedence
619
620
621class UnaryOperatorValue(_Element):
622    UNARY_OP = re.compile(r'\s*(!)\s*(.*)$', re.S)
623    OPERATORS = {'!': operator.__not__}
624    def parse(self):
625        op_string, = self.identity_match(self.UNARY_OP)
626        self.value = self.next_element(Value)
627        self.op = self.OPERATORS[op_string]
628
629    def calculate(self, namespace, loader):
630        return self.op(self.value.calculate(namespace, loader))
631
632
633class Expression(_Element):
634
635    def parse(self):
636        self.expression = [self.next_element(Value)]
637        while(True):
638            try:
639                binary_operator = self.next_element(BinaryOperator)
640                value = self.require_next_element(Value, 'value')
641                self.expression.append(binary_operator)
642                self.expression.append(value)
643            except NoMatch:
644                break
645
646    def calculate(self, namespace, loader):
647        if not self.expression or len(self.expression) == 0:
648            return False
649        #TODO: how does velocity deal with an empty condition expression?
650
651        opstack = []
652        valuestack = [self.expression[0]]
653        terms = self.expression[1:]
654
655        # use top of opstack on top 2 values of valuestack
656        def stack_calculate(ops, values, namespace, loader):
657            value2 = values.pop()
658            if isinstance(value2, Value):
659                value2 = value2.calculate(namespace, loader)
660            value1 = values.pop()
661            if isinstance(value1, Value):
662                value1 = value1.calculate(namespace, loader)
663            result = ops.pop().apply_to(value1, value2)
664            # TODO this doesn't short circuit -- does velocity?
665            # also note they're eval'd out of order
666            values.append(result)
667
668        while terms:
669            # next is a binary operator
670            if not opstack or terms[0].greater_precedence_than(opstack[-1]):
671                opstack.append(terms[0])
672                valuestack.append(terms[1])
673                terms = terms[2:]
674            else:
675                stack_calculate(opstack, valuestack, namespace, loader)
676
677        # now clean out the stacks
678        while opstack:
679            stack_calculate(opstack, valuestack, namespace, loader)
680
681        if len(valuestack) != 1:
682            print("evaluation of expression in Condition.calculate is messed up: final length of stack is not one")
683            #TODO handle this officially
684
685        result = valuestack[0]
686        if isinstance(result, Value):
687            result = result.calculate(namespace, loader)
688        return result
689
690
691class ParenthesizedExpression(_Element):
692    START = re.compile(r'\(\s*(.*)$', re.S)
693    END = re.compile(r'\s*\)(.*)$', re.S)
694
695    def parse(self):
696        self.identity_match(self.START)
697        expression = self.next_element(Expression)
698        self.require_match(self.END, ')')
699        self.calculate = expression.calculate
700
701
702class Condition(_Element):
703    def parse(self):
704        expression = self.next_element(ParenthesizedExpression)
705        self.optional_match(WHITESPACE_TO_END_OF_LINE)
706        self.calculate = expression.calculate
707        # TODO do I need to do anything else here?
708
709
710class End(_Element):
711    END = re.compile(r'%(?:end|{end})(.*)', re.I + re.S)
712
713    def parse(self):
714        self.identity_match(self.END)
715        self.optional_match(WHITESPACE_TO_END_OF_LINE)
716
717
718class ElseBlock(_Element):
719    START = re.compile(r'%(?:else|{else})(.*)$', re.S + re.I)
720
721    def parse(self):
722        self.identity_match(self.START)
723        self.block = self.require_next_element(Block, 'block')
724        self.evaluate = self.block.evaluate
725
726
727class ElseifBlock(_Element):
728    START = re.compile(r'%elseif\b\s*(.*)$', re.S + re.I)
729
730    def parse(self):
731        self.identity_match(self.START)
732        self.condition = self.require_next_element(Condition, 'condition')
733        self.block = self.require_next_element(Block, 'block')
734        self.calculate = self.condition.calculate
735        self.evaluate = self.block.evaluate
736
737
738class IfDirective(_Element):
739    START = re.compile(r'%if\b\s*(.*)$', re.S + re.I)
740    else_block = Null()
741
742    def parse(self):
743        self.identity_match(self.START)
744        self.condition = self.next_element(Condition)
745        self.block = self.require_next_element(Block, "block")
746        self.elseifs = []
747        while True:
748            try: self.elseifs.append(self.next_element(ElseifBlock))
749            except NoMatch: break
750        try: self.else_block = self.next_element(ElseBlock)
751        except NoMatch: pass
752        self.require_next_element(End, '%else, %elseif or %end')
753
754    def evaluate(self, stream, namespace, loader):
755        if self.condition.calculate(namespace, loader):
756            self.block.evaluate(stream, namespace, loader)
757        else:
758            for elseif in self.elseifs:
759                if elseif.calculate(namespace, loader):
760                    elseif.evaluate(stream, namespace, loader)
761                    return
762            self.else_block.evaluate(stream, namespace, loader)
763
764
765class Assignment(_Element):
766    START = re.compile(r'\s*\@([a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*)\s*=\s*(.*)$', re.S + re.I)
767    END = re.compile(r'\s*\:(?:[ \t]*\r?\n)?(.*)$', re.S + re.M)
768
769    def parse(self):
770        var_name, = self.identity_match(self.START)
771        self.terms = var_name.split('.')
772        self.value = self.require_next_element(Expression, "expression")
773        self.require_match(self.END, ')')
774
775    def evaluate(self, stream, namespace, loader):
776        thingy = namespace
777        for term in self.terms[0:-1]:
778            if thingy == None: return
779            look_in_dict = True
780            if not isinstance(thingy, LocalNamespace):
781                try:
782                    thingy = getattr(thingy, term)
783                    look_in_dict = False
784                except AttributeError:
785                    pass
786            if look_in_dict:
787                try:
788                    thingy = thingy[term]
789                except KeyError: thingy = None
790                except TypeError: thingy = None
791                except AttributeError: thingy = None
792        if thingy is not None:
793            thingy[self.terms[-1]] = self.value.calculate(namespace, loader)
794
795class MacroDefinition(_Element):
796    START = re.compile(r'%macro\b(.*)', re.S + re.I)
797    OPEN_PAREN = re.compile(r'[ \t]\s*(.*)$', re.S)
798    NAME = re.compile(r'\s*([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
799    CLOSE_PAREN = re.compile(r'[ \t]*\:(.*)$', re.S)
800    ARG_NAME = re.compile(r'[, \t]+\@([a-z][a-z_0-9]*)(.*)$', re.S + re.I)
801    RESERVED_NAMES = ('if', 'else', 'elseif', 'set', 'macro',
802                      'for', 'parse', 'include', 'stop', 'end')
803    def parse(self):
804        self.identity_match(self.START)
805        self.require_match(self.OPEN_PAREN, '(')
806        self.macro_name, = self.require_match(self.NAME, 'macro name')
807        if self.macro_name.lower() in self.RESERVED_NAMES:
808            raise self.syntax_error('non-reserved name')
809        self.arg_names = []
810        while True:
811            m = self.next_match(self.ARG_NAME)
812            if not m: break
813            self.arg_names.append(m[0])
814        self.require_match(self.CLOSE_PAREN, ') or arg name')
815        self.optional_match(WHITESPACE_TO_END_OF_LINE)
816        self.block = self.require_next_element(Block, 'block')
817        self.require_next_element(End, 'block')
818
819    def evaluate(self, stream, namespace, loader):
820        global_ns = namespace.top()
821        macro_key = '%' + self.macro_name.lower()
822        if global_ns.get(macro_key, None):
823            raise Exception("cannot redefine macro")
824        global_ns[macro_key] = self
825
826    def execute_macro(self, stream, namespace, arg_value_elements, loader):
827        if len(arg_value_elements) != len(self.arg_names):
828            raise Exception("expected %d arguments, got %d" % (len(self.arg_names), len(arg_value_elements)))
829        macro_namespace = LocalNamespace(namespace)
830        for arg_name, arg_value in zip(self.arg_names, arg_value_elements):
831            macro_namespace[arg_name] = arg_value.calculate(namespace, loader)
832        self.block.evaluate(stream, macro_namespace, loader)
833
834
835class MacroCall(_Element):
836    START = re.compile(r'%([a-z][a-z_0-9]*)\b(.*)', re.S + re.I)
837    OPEN_PAREN = re.compile(r'[ \t]\s*(.*)$', re.S)
838    CLOSE_PAREN = re.compile(r'[ \t]*\:(.*)$', re.S)
839    SPACE_OR_COMMA = re.compile(r'[ \t]*(?:,|[ \t])[ \t]*(.*)$', re.S)
840
841    def parse(self):
842        macro_name, = self.identity_match(self.START)
843        self.macro_name = macro_name.lower()
844        self.args = []
845        if self.macro_name in MacroDefinition.RESERVED_NAMES or self.macro_name.startswith('end'):
846            raise NoMatch()
847        if not self.optional_match(self.OPEN_PAREN):
848            # It's not really a macro call,
849            # it's just a spare pound sign with text after it,
850            # the typical example being a color spec: "#ffffff"
851            # call it not-a-match and then let another thing catch it
852            raise NoMatch()
853        while True:
854            try: self.args.append(self.next_element(Value))
855            except NoMatch: break
856            if not self.optional_match(self.SPACE_OR_COMMA): break
857        self.require_match(self.CLOSE_PAREN, 'argument value or )')
858
859    def evaluate(self, stream, namespace, loader):
860        try: macro = namespace['%' + self.macro_name]
861        except KeyError: raise Exception('no such macro: ' + self.macro_name)
862        macro.execute_macro(stream, namespace, self.args, loader)
863
864
865class IncludeDirective(_Element):
866    START = re.compile(r'%include\b(.*)', re.S + re.I)
867    OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
868    CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
869
870    def parse(self):
871        self.identity_match(self.START)
872        self.require_match(self.OPEN_PAREN, '(')
873        self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, FormalReference), 'template name')
874        self.require_match(self.CLOSE_PAREN, ')')
875
876    def evaluate(self, stream, namespace, loader):
877        stream.write(loader.load_text(self.name.calculate(namespace, loader)))
878
879
880class ParseDirective(_Element):
881    START = re.compile(r'%parse\b(.*)', re.S + re.I)
882    OPEN_PAREN = re.compile(r'[ \t]*\(\s*(.*)$', re.S)
883    CLOSE_PAREN = re.compile(r'[ \t]*\)(.*)$', re.S)
884
885    def parse(self):
886        self.identity_match(self.START)
887        self.require_match(self.OPEN_PAREN, '(')
888        self.name = self.require_next_element((StringLiteral, InterpolatedStringLiteral, FormalReference), 'template name')
889        self.require_match(self.CLOSE_PAREN, ')')
890
891    def evaluate(self, stream, namespace, loader):
892        template = loader.load_template(self.name.calculate(namespace, loader))
893        ## TODO: local namespace?
894        template.merge_to(namespace, stream, loader=loader)
895
896
897class StopDirective(_Element):
898    STOP = re.compile(r'%stop\b(.*)', re.S + re.I)
899
900    def parse(self):
901        self.identity_match(self.STOP)
902
903    def evaluate(self, stream, namespace, loader):
904        if hasattr(stream, 'stop'):
905            stream.stop = True
906
907
908# Represents a SINGLE user-defined directive
909class UserDefinedDirective(_Element):
910    DIRECTIVES = []
911
912    def parse(self):
913        self.directive = self.next_element(self.DIRECTIVES)
914
915    def evaluate(self, stream, namespace, loader):
916        self.directive.evaluate(stream, namespace, loader)
917
918
919class SetDirective(_Element):
920    START = re.compile(r'%set\b(.*)', re.S + re.I)
921
922    def parse(self):
923        self.identity_match(self.START)
924        self.assignment = self.require_next_element(Assignment, 'assignment')
925
926    def evaluate(self, stream, namespace, loader):
927        self.assignment.evaluate(stream, namespace, loader)
928
929
930class ForDirective(_Element):
931    START = re.compile(r'%for\b(.*)$', re.S + re.I)
932    OPEN_PAREN = re.compile(r'[ \t]\s*(.*)$', re.S)
933    IN = re.compile(r'[ \t]+in[ \t]+(.*)$', re.S)
934    LOOP_VAR_NAME = re.compile(r'\@([a-z_][a-z0-9_]*)(.*)$', re.S + re.I)
935    CLOSE_PAREN = re.compile(r'[ \t]*\:(.*)$', re.S)
936
937    def parse(self):
938        ## Could be cleaner b/c syntax error if no '('
939        self.identity_match(self.START)
940        self.require_match(self.OPEN_PAREN, '(')
941        self.loop_var_name, = self.require_match(self.LOOP_VAR_NAME, 'loop var name')
942        self.require_match(self.IN, 'in')
943        self.value = self.next_element(Value)
944        self.require_match(self.CLOSE_PAREN, ')')
945        self.block = self.next_element(Block)
946        self.require_next_element(End, '%end')
947
948    def evaluate(self, stream, namespace, loader):
949        iterable = self.value.calculate(namespace, loader)
950        counter = 1
951        try:
952            if iterable is None:
953                return
954            if hasattr(iterable, 'keys'): iterable = iterable.keys()
955            try:
956                length = len(iterable)
957            except TypeError:
958                raise ValueError("value for @%s is not iterable in %%for: %s" % (self.loop_var_name, iterable))
959            for item in iterable:
960                namespace = LocalNamespace(namespace)
961                namespace['velocityCount'] = counter
962                namespace['velocityHasNext'] = counter < length
963                namespace[self.loop_var_name] = item
964                self.block.evaluate(stream, namespace, loader)
965                counter += 1
966        except TypeError:
967            raise
968
969
970class TemplateBody(_Element):
971    def parse(self):
972        self.block = self.next_element(Block)
973        if self.next_text():
974            raise self.syntax_error('block element')
975
976    def evaluate(self, stream, namespace, loader):
977        namespace = LocalNamespace(namespace)
978        self.block.evaluate(stream, namespace, loader)
979
980
981class Block(_Element):
982    def parse(self):
983        self.children = []
984        while True:
985            try: self.children.append(self.next_element((Text, FormalReference, Comment, IfDirective, SetDirective,
986                                                         ForDirective, IncludeDirective, ParseDirective,
987                                                         MacroDefinition, StopDirective, UserDefinedDirective,
988                                                         MacroCall, FallthroughHashText)))
989            except NoMatch: break
990
991    def evaluate(self, stream, namespace, loader):
992        for child in self.children:
993            child.evaluate(stream, namespace, loader)
994