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