1 # Copyright 2012 Benjamin Kalman 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 # TODO: New name, not "handlebar". 16 # TODO: Escaping control characters somehow. e.g. \{{, \{{-. 17 18 import json 19 import re 20 21 '''Handlebar templates are data binding templates more-than-loosely inspired by 22 ctemplate. Use like: 23 24 from handlebar import Handlebar 25 26 template = Handlebar('hello {{#foo bar/}} world') 27 input = { 28 'foo': [ 29 { 'bar': 1 }, 30 { 'bar': 2 }, 31 { 'bar': 3 } 32 ] 33 } 34 print(template.render(input).text) 35 36 Handlebar will use get() on contexts to return values, so to create custom 37 getters (for example, something that populates values lazily from keys), just 38 provide an object with a get() method. 39 40 class CustomContext(object): 41 def get(self, key): 42 return 10 43 print(Handlebar('hello {{world}}').render(CustomContext()).text) 44 45 will print 'hello 10'. 46 ''' 47 48 class ParseException(Exception): 49 '''The exception thrown while parsing a template. 50 ''' 51 def __init__(self, error): 52 Exception.__init__(self, error) 53 54 class RenderResult(object): 55 '''The result of a render operation. 56 ''' 57 def __init__(self, text, errors): 58 self.text = text; 59 self.errors = errors 60 61 def __repr__(self): 62 return '%s(text=%s, errors=%s)' % (type(self).__name__, 63 self.text, 64 self.errors) 65 66 def __str__(self): 67 return repr(self) 68 69 class _StringBuilder(object): 70 '''Efficiently builds strings. 71 ''' 72 def __init__(self): 73 self._buf = [] 74 75 def __len__(self): 76 self._Collapse() 77 return len(self._buf[0]) 78 79 def Append(self, string): 80 if not isinstance(string, basestring): 81 string = str(string) 82 self._buf.append(string) 83 84 def ToString(self): 85 self._Collapse() 86 return self._buf[0] 87 88 def _Collapse(self): 89 self._buf = [u''.join(self._buf)] 90 91 def __repr__(self): 92 return self.ToString() 93 94 def __str__(self): 95 return repr(self) 96 97 class _Contexts(object): 98 '''Tracks a stack of context objects, providing efficient key/value retrieval. 99 ''' 100 class _Node(object): 101 '''A node within the stack. Wraps a real context and maintains the key/value 102 pairs seen so far. 103 ''' 104 def __init__(self, value): 105 self._value = value 106 self._value_has_get = hasattr(value, 'get') 107 self._found = {} 108 109 def GetKeys(self): 110 '''Returns the list of keys that |_value| contains. 111 ''' 112 return self._found.keys() 113 114 def Get(self, key): 115 '''Returns the value for |key|, or None if not found (including if 116 |_value| doesn't support key retrieval). 117 ''' 118 if not self._value_has_get: 119 return None 120 value = self._found.get(key) 121 if value is not None: 122 return value 123 value = self._value.get(key) 124 if value is not None: 125 self._found[key] = value 126 return value 127 128 def __repr__(self): 129 return 'Node(value=%s, found=%s)' % (self._value, self._found) 130 131 def __str__(self): 132 return repr(self) 133 134 def __init__(self, globals_): 135 '''Initializes with the initial global contexts, listed in order from most 136 to least important. 137 ''' 138 self._nodes = map(_Contexts._Node, globals_) 139 self._first_local = len(self._nodes) 140 self._value_info = {} 141 142 def CreateFromGlobals(self): 143 new = _Contexts([]) 144 new._nodes = self._nodes[:self._first_local] 145 new._first_local = self._first_local 146 return new 147 148 def Push(self, context): 149 self._nodes.append(_Contexts._Node(context)) 150 151 def Pop(self): 152 node = self._nodes.pop() 153 assert len(self._nodes) >= self._first_local 154 for found_key in node.GetKeys(): 155 # [0] is the stack of nodes that |found_key| has been found in. 156 self._value_info[found_key][0].pop() 157 158 def FirstLocal(self): 159 if len(self._nodes) == self._first_local: 160 return None 161 return self._nodes[-1]._value 162 163 def Resolve(self, path): 164 # This method is only efficient at finding |key|; if |tail| has a value (and 165 # |key| evaluates to an indexable value) we'll need to descend into that. 166 key, tail = path.split('.', 1) if '.' in path else (path, None) 167 found = self._FindNodeValue(key) 168 if tail is None: 169 return found 170 for part in tail.split('.'): 171 if not hasattr(found, 'get'): 172 return None 173 found = found.get(part) 174 return found 175 176 def Scope(self, context, fn, *args): 177 self.Push(context) 178 try: 179 return fn(*args) 180 finally: 181 self.Pop() 182 183 def _FindNodeValue(self, key): 184 # |found_node_list| will be all the nodes that |key| has been found in. 185 # |checked_node_set| are those that have been checked. 186 info = self._value_info.get(key) 187 if info is None: 188 info = ([], set()) 189 self._value_info[key] = info 190 found_node_list, checked_node_set = info 191 192 # Check all the nodes not yet checked for |key|. 193 newly_found = [] 194 for node in reversed(self._nodes): 195 if node in checked_node_set: 196 break 197 value = node.Get(key) 198 if value is not None: 199 newly_found.append(node) 200 checked_node_set.add(node) 201 202 # The nodes will have been found in reverse stack order. After extending 203 # the found nodes, the freshest value will be at the tip of the stack. 204 found_node_list.extend(reversed(newly_found)) 205 if not found_node_list: 206 return None 207 208 return found_node_list[-1]._value.get(key) 209 210 class _Stack(object): 211 class Entry(object): 212 def __init__(self, name, id_): 213 self.name = name 214 self.id_ = id_ 215 216 def __init__(self, entries=[]): 217 self.entries = entries 218 219 def Descend(self, name, id_): 220 descended = list(self.entries) 221 descended.append(_Stack.Entry(name, id_)) 222 return _Stack(entries=descended) 223 224 class _InternalContext(object): 225 def __init__(self): 226 self._render_state = None 227 228 def SetRenderState(self, render_state): 229 self._render_state = render_state 230 231 def get(self, key): 232 if key == 'errors': 233 errors = self._render_state._errors 234 return '\n'.join(errors) if errors else None 235 return None 236 237 class _RenderState(object): 238 '''The state of a render call. 239 ''' 240 def __init__(self, name, contexts, _stack=_Stack()): 241 self.text = _StringBuilder() 242 self.contexts = contexts 243 self._name = name 244 self._errors = [] 245 self._stack = _stack 246 247 def AddResolutionError(self, id_, description=None): 248 message = id_.CreateResolutionErrorMessage(self._name, stack=self._stack) 249 if description is not None: 250 message = '%s (%s)' % (message, description) 251 self._errors.append(message) 252 253 def Copy(self): 254 return _RenderState( 255 self._name, self.contexts, _stack=self._stack) 256 257 def ForkPartial(self, custom_name, id_): 258 name = custom_name or id_.name 259 return _RenderState(name, 260 self.contexts.CreateFromGlobals(), 261 _stack=self._stack.Descend(name, id_)) 262 263 def Merge(self, render_state, text_transform=None): 264 self._errors.extend(render_state._errors) 265 text = render_state.text.ToString() 266 if text_transform is not None: 267 text = text_transform(text) 268 self.text.Append(text) 269 270 def GetResult(self): 271 return RenderResult(self.text.ToString(), self._errors); 272 273 class _Identifier(object): 274 '''An identifier of the form 'foo', 'foo.bar.baz', 'foo-bar.baz', etc. 275 ''' 276 _VALID_ID_MATCHER = re.compile(r'^[a-zA-Z0-9@_/-]+$') 277 278 def __init__(self, name, line, column): 279 self.name = name 280 self.line = line 281 self.column = column 282 if name == '': 283 raise ParseException('Empty identifier %s' % self.GetDescription()) 284 for part in name.split('.'): 285 if not _Identifier._VALID_ID_MATCHER.match(part): 286 raise ParseException('Invalid identifier %s' % self.GetDescription()) 287 288 def GetDescription(self): 289 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) 290 291 def CreateResolutionErrorMessage(self, name, stack=None): 292 message = _StringBuilder() 293 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), 294 name)) 295 if stack is not None: 296 for entry in reversed(stack.entries): 297 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), 298 entry.name)) 299 return message.ToString().strip() 300 301 def __repr__(self): 302 return self.name 303 304 def __str__(self): 305 return repr(self) 306 307 class _Node(object): pass 308 309 class _LeafNode(_Node): 310 def __init__(self, start_line, end_line): 311 self._start_line = start_line 312 self._end_line = end_line 313 314 def StartsWithNewLine(self): 315 return False 316 317 def TrimStartingNewLine(self): 318 pass 319 320 def TrimEndingSpaces(self): 321 return 0 322 323 def TrimEndingNewLine(self): 324 pass 325 326 def EndsWithEmptyLine(self): 327 return False 328 329 def GetStartLine(self): 330 return self._start_line 331 332 def GetEndLine(self): 333 return self._end_line 334 335 def __str__(self): 336 return repr(self) 337 338 class _DecoratorNode(_Node): 339 def __init__(self, content): 340 self._content = content 341 342 def StartsWithNewLine(self): 343 return self._content.StartsWithNewLine() 344 345 def TrimStartingNewLine(self): 346 self._content.TrimStartingNewLine() 347 348 def TrimEndingSpaces(self): 349 return self._content.TrimEndingSpaces() 350 351 def TrimEndingNewLine(self): 352 self._content.TrimEndingNewLine() 353 354 def EndsWithEmptyLine(self): 355 return self._content.EndsWithEmptyLine() 356 357 def GetStartLine(self): 358 return self._content.GetStartLine() 359 360 def GetEndLine(self): 361 return self._content.GetEndLine() 362 363 def __repr__(self): 364 return str(self._content) 365 366 def __str__(self): 367 return repr(self) 368 369 class _InlineNode(_DecoratorNode): 370 def __init__(self, content): 371 _DecoratorNode.__init__(self, content) 372 373 def Render(self, render_state): 374 content_render_state = render_state.Copy() 375 self._content.Render(content_render_state) 376 render_state.Merge(content_render_state, 377 text_transform=lambda text: text.replace('\n', '')) 378 379 class _IndentedNode(_DecoratorNode): 380 def __init__(self, content, indentation): 381 _DecoratorNode.__init__(self, content) 382 self._indent_str = ' ' * indentation 383 384 def Render(self, render_state): 385 if isinstance(self._content, _CommentNode): 386 return 387 def inlinify(text): 388 if len(text) == 0: # avoid rendering a blank line 389 return '' 390 buf = _StringBuilder() 391 buf.Append(self._indent_str) 392 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) 393 if not text.endswith('\n'): # partials will often already end in a \n 394 buf.Append('\n') 395 return buf.ToString() 396 content_render_state = render_state.Copy() 397 self._content.Render(content_render_state) 398 render_state.Merge(content_render_state, text_transform=inlinify) 399 400 class _BlockNode(_DecoratorNode): 401 def __init__(self, content): 402 _DecoratorNode.__init__(self, content) 403 content.TrimStartingNewLine() 404 content.TrimEndingSpaces() 405 406 def Render(self, render_state): 407 self._content.Render(render_state) 408 409 class _NodeCollection(_Node): 410 def __init__(self, nodes): 411 assert nodes 412 self._nodes = nodes 413 414 def Render(self, render_state): 415 for node in self._nodes: 416 node.Render(render_state) 417 418 def StartsWithNewLine(self): 419 return self._nodes[0].StartsWithNewLine() 420 421 def TrimStartingNewLine(self): 422 self._nodes[0].TrimStartingNewLine() 423 424 def TrimEndingSpaces(self): 425 return self._nodes[-1].TrimEndingSpaces() 426 427 def TrimEndingNewLine(self): 428 self._nodes[-1].TrimEndingNewLine() 429 430 def EndsWithEmptyLine(self): 431 return self._nodes[-1].EndsWithEmptyLine() 432 433 def GetStartLine(self): 434 return self._nodes[0].GetStartLine() 435 436 def GetEndLine(self): 437 return self._nodes[-1].GetEndLine() 438 439 def __repr__(self): 440 return ''.join(str(node) for node in self._nodes) 441 442 class _StringNode(_Node): 443 '''Just a string. 444 ''' 445 def __init__(self, string, start_line, end_line): 446 self._string = string 447 self._start_line = start_line 448 self._end_line = end_line 449 450 def Render(self, render_state): 451 render_state.text.Append(self._string) 452 453 def StartsWithNewLine(self): 454 return self._string.startswith('\n') 455 456 def TrimStartingNewLine(self): 457 if self.StartsWithNewLine(): 458 self._string = self._string[1:] 459 460 def TrimEndingSpaces(self): 461 original_length = len(self._string) 462 self._string = self._string[:self._LastIndexOfSpaces()] 463 return original_length - len(self._string) 464 465 def TrimEndingNewLine(self): 466 if self._string.endswith('\n'): 467 self._string = self._string[:len(self._string) - 1] 468 469 def EndsWithEmptyLine(self): 470 index = self._LastIndexOfSpaces() 471 return index == 0 or self._string[index - 1] == '\n' 472 473 def _LastIndexOfSpaces(self): 474 index = len(self._string) 475 while index > 0 and self._string[index - 1] == ' ': 476 index -= 1 477 return index 478 479 def GetStartLine(self): 480 return self._start_line 481 482 def GetEndLine(self): 483 return self._end_line 484 485 def __repr__(self): 486 return self._string 487 488 class _EscapedVariableNode(_LeafNode): 489 '''{{foo}} 490 ''' 491 def __init__(self, id_): 492 _LeafNode.__init__(self, id_.line, id_.line) 493 self._id = id_ 494 495 def Render(self, render_state): 496 value = render_state.contexts.Resolve(self._id.name) 497 if value is None: 498 render_state.AddResolutionError(self._id) 499 return 500 string = value if isinstance(value, basestring) else str(value) 501 render_state.text.Append(string.replace('&', '&') 502 .replace('<', '<') 503 .replace('>', '>')) 504 505 def __repr__(self): 506 return '{{%s}}' % self._id 507 508 class _UnescapedVariableNode(_LeafNode): 509 '''{{{foo}}} 510 ''' 511 def __init__(self, id_): 512 _LeafNode.__init__(self, id_.line, id_.line) 513 self._id = id_ 514 515 def Render(self, render_state): 516 value = render_state.contexts.Resolve(self._id.name) 517 if value is None: 518 render_state.AddResolutionError(self._id) 519 return 520 string = value if isinstance(value, basestring) else str(value) 521 render_state.text.Append(string) 522 523 def __repr__(self): 524 return '{{{%s}}}' % self._id 525 526 class _CommentNode(_LeafNode): 527 '''{{- This is a comment -}} 528 An empty placeholder node for correct indented rendering behaviour. 529 ''' 530 def __init__(self, start_line, end_line): 531 _LeafNode.__init__(self, start_line, end_line) 532 533 def Render(self, render_state): 534 pass 535 536 def __repr__(self): 537 return '<comment>' 538 539 class _SectionNode(_DecoratorNode): 540 '''{{#var:foo}} ... {{/foo}} 541 ''' 542 def __init__(self, bind_to, id_, content): 543 _DecoratorNode.__init__(self, content) 544 self._bind_to = bind_to 545 self._id = id_ 546 547 def Render(self, render_state): 548 value = render_state.contexts.Resolve(self._id.name) 549 if isinstance(value, list): 550 for item in value: 551 if self._bind_to is not None: 552 render_state.contexts.Scope({self._bind_to.name: item}, 553 self._content.Render, render_state) 554 else: 555 self._content.Render(render_state) 556 elif hasattr(value, 'get'): 557 if self._bind_to is not None: 558 render_state.contexts.Scope({self._bind_to.name: value}, 559 self._content.Render, render_state) 560 else: 561 render_state.contexts.Scope(value, self._content.Render, render_state) 562 else: 563 render_state.AddResolutionError(self._id) 564 565 def __repr__(self): 566 return '{{#%s}}%s{{/%s}}' % ( 567 self._id, _DecoratorNode.__repr__(self), self._id) 568 569 class _VertedSectionNode(_DecoratorNode): 570 '''{{?var:foo}} ... {{/foo}} 571 ''' 572 def __init__(self, bind_to, id_, content): 573 _DecoratorNode.__init__(self, content) 574 self._bind_to = bind_to 575 self._id = id_ 576 577 def Render(self, render_state): 578 value = render_state.contexts.Resolve(self._id.name) 579 if _VertedSectionNode.ShouldRender(value): 580 if self._bind_to is not None: 581 render_state.contexts.Scope({self._bind_to.name: value}, 582 self._content.Render, render_state) 583 else: 584 self._content.Render(render_state) 585 586 def __repr__(self): 587 return '{{?%s}}%s{{/%s}}' % ( 588 self._id, _DecoratorNode.__repr__(self), self._id) 589 590 @staticmethod 591 def ShouldRender(value): 592 if value is None: 593 return False 594 if isinstance(value, bool): 595 return value 596 if isinstance(value, list): 597 return len(value) > 0 598 return True 599 600 class _InvertedSectionNode(_DecoratorNode): 601 '''{{^foo}} ... {{/foo}} 602 ''' 603 def __init__(self, bind_to, id_, content): 604 _DecoratorNode.__init__(self, content) 605 if bind_to is not None: 606 raise ParseException('{{^%s:%s}} does not support variable binding' 607 % (bind_to, id_)) 608 self._id = id_ 609 610 def Render(self, render_state): 611 value = render_state.contexts.Resolve(self._id.name) 612 if not _VertedSectionNode.ShouldRender(value): 613 self._content.Render(render_state) 614 615 def __repr__(self): 616 return '{{^%s}}%s{{/%s}}' % ( 617 self._id, _DecoratorNode.__repr__(self), self._id) 618 619 class _AssertionNode(_LeafNode): 620 '''{{!foo Some comment about foo}} 621 ''' 622 def __init__(self, id_, description): 623 _LeafNode.__init__(self, id_.line, id_.line) 624 self._id = id_ 625 self._description = description 626 627 def Render(self, render_state): 628 if render_state.contexts.Resolve(self._id.name) is None: 629 render_state.AddResolutionError(self._id, description=self._description) 630 631 def __repr__(self): 632 return '{{!%s %s}}' % (self._id, self._description) 633 634 class _JsonNode(_LeafNode): 635 '''{{*foo}} 636 ''' 637 def __init__(self, id_): 638 _LeafNode.__init__(self, id_.line, id_.line) 639 self._id = id_ 640 641 def Render(self, render_state): 642 value = render_state.contexts.Resolve(self._id.name) 643 if value is None: 644 render_state.AddResolutionError(self._id) 645 return 646 render_state.text.Append(json.dumps(value, separators=(',',':'))) 647 648 def __repr__(self): 649 return '{{*%s}}' % self._id 650 651 # TODO: Better common model of _PartialNodeWithArguments, _PartialNodeInContext, 652 # and _PartialNode. 653 class _PartialNodeWithArguments(_DecoratorNode): 654 def __init__(self, partial, args): 655 if isinstance(partial, Handlebar): 656 # Preserve any get() method that the caller has added. 657 if hasattr(partial, 'get'): 658 self.get = partial.get 659 partial = partial._top_node 660 _DecoratorNode.__init__(self, partial) 661 self._partial = partial 662 self._args = args 663 664 def Render(self, render_state): 665 render_state.contexts.Scope(self._args, self._partial.Render, render_state) 666 667 class _PartialNodeInContext(_DecoratorNode): 668 def __init__(self, partial, context): 669 if isinstance(partial, Handlebar): 670 # Preserve any get() method that the caller has added. 671 if hasattr(partial, 'get'): 672 self.get = partial.get 673 partial = partial._top_node 674 _DecoratorNode.__init__(self, partial) 675 self._partial = partial 676 self._context = context 677 678 def Render(self, render_state): 679 original_contexts = render_state.contexts 680 try: 681 render_state.contexts = self._context 682 render_state.contexts.Scope( 683 # The first local context of |original_contexts| will be the 684 # arguments that were passed to the partial, if any. 685 original_contexts.FirstLocal() or {}, 686 self._partial.Render, render_state) 687 finally: 688 render_state.contexts = original_contexts 689 690 class _PartialNode(_LeafNode): 691 '''{{+var:foo}} ... {{/foo}} 692 ''' 693 def __init__(self, bind_to, id_, content): 694 _LeafNode.__init__(self, id_.line, id_.line) 695 self._bind_to = bind_to 696 self._id = id_ 697 self._content = content 698 self._args = None 699 self._pass_through_id = None 700 701 @classmethod 702 def Inline(cls, id_): 703 return cls(None, id_, None) 704 705 def Render(self, render_state): 706 value = render_state.contexts.Resolve(self._id.name) 707 if value is None: 708 render_state.AddResolutionError(self._id) 709 return 710 if not isinstance(value, (Handlebar, _Node)): 711 render_state.AddResolutionError(self._id, description='not a partial') 712 return 713 714 if isinstance(value, Handlebar): 715 node, name = value._top_node, value._name 716 else: 717 node, name = value, None 718 719 partial_render_state = render_state.ForkPartial(name, self._id) 720 721 arg_context = {} 722 if self._pass_through_id is not None: 723 context = render_state.contexts.Resolve(self._pass_through_id.name) 724 if context is not None: 725 arg_context[self._pass_through_id.name] = context 726 if self._args is not None: 727 def resolve_args(args): 728 resolved = {} 729 for key, value in args.iteritems(): 730 if isinstance(value, dict): 731 assert len(value.keys()) == 1 732 id_of_partial, partial_args = value.items()[0] 733 partial = render_state.contexts.Resolve(id_of_partial.name) 734 if partial is not None: 735 resolved[key] = _PartialNodeWithArguments( 736 partial, resolve_args(partial_args)) 737 else: 738 context = render_state.contexts.Resolve(value.name) 739 if context is not None: 740 resolved[key] = context 741 return resolved 742 arg_context.update(resolve_args(self._args)) 743 if self._bind_to and self._content: 744 arg_context[self._bind_to.name] = _PartialNodeInContext( 745 self._content, render_state.contexts) 746 if arg_context: 747 partial_render_state.contexts.Push(arg_context) 748 749 node.Render(partial_render_state) 750 751 render_state.Merge( 752 partial_render_state, 753 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) 754 755 def SetArguments(self, args): 756 self._args = args 757 758 def PassThroughArgument(self, id_): 759 self._pass_through_id = id_ 760 761 def __repr__(self): 762 return '{{+%s}}' % self._id 763 764 _TOKENS = {} 765 766 class _Token(object): 767 '''The tokens that can appear in a template. 768 ''' 769 class Data(object): 770 def __init__(self, name, text, clazz): 771 self.name = name 772 self.text = text 773 self.clazz = clazz 774 _TOKENS[text] = self 775 776 def ElseNodeClass(self): 777 if self.clazz == _VertedSectionNode: 778 return _InvertedSectionNode 779 if self.clazz == _InvertedSectionNode: 780 return _VertedSectionNode 781 return None 782 783 def __repr__(self): 784 return self.name 785 786 def __str__(self): 787 return repr(self) 788 789 OPEN_START_SECTION = Data( 790 'OPEN_START_SECTION' , '{{#', _SectionNode) 791 OPEN_START_VERTED_SECTION = Data( 792 'OPEN_START_VERTED_SECTION' , '{{?', _VertedSectionNode) 793 OPEN_START_INVERTED_SECTION = Data( 794 'OPEN_START_INVERTED_SECTION', '{{^', _InvertedSectionNode) 795 OPEN_ASSERTION = Data( 796 'OPEN_ASSERTION' , '{{!', _AssertionNode) 797 OPEN_JSON = Data( 798 'OPEN_JSON' , '{{*', _JsonNode) 799 OPEN_PARTIAL = Data( 800 'OPEN_PARTIAL' , '{{+', _PartialNode) 801 OPEN_ELSE = Data( 802 'OPEN_ELSE' , '{{:', None) 803 OPEN_END_SECTION = Data( 804 'OPEN_END_SECTION' , '{{/', None) 805 INLINE_END_SECTION = Data( 806 'INLINE_END_SECTION' , '/}}', None) 807 OPEN_UNESCAPED_VARIABLE = Data( 808 'OPEN_UNESCAPED_VARIABLE' , '{{{', _UnescapedVariableNode) 809 CLOSE_MUSTACHE3 = Data( 810 'CLOSE_MUSTACHE3' , '}}}', None) 811 OPEN_COMMENT = Data( 812 'OPEN_COMMENT' , '{{-', _CommentNode) 813 CLOSE_COMMENT = Data( 814 'CLOSE_COMMENT' , '-}}', None) 815 OPEN_VARIABLE = Data( 816 'OPEN_VARIABLE' , '{{' , _EscapedVariableNode) 817 CLOSE_MUSTACHE = Data( 818 'CLOSE_MUSTACHE' , '}}' , None) 819 CHARACTER = Data( 820 'CHARACTER' , '.' , None) 821 822 class _TokenStream(object): 823 '''Tokeniser for template parsing. 824 ''' 825 def __init__(self, string): 826 self.next_token = None 827 self.next_line = 1 828 self.next_column = 0 829 self._string = string 830 self._cursor = 0 831 self.Advance() 832 833 def HasNext(self): 834 return self.next_token is not None 835 836 def NextCharacter(self): 837 if self.next_token is _Token.CHARACTER: 838 return self._string[self._cursor - 1] 839 return None 840 841 def Advance(self): 842 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': 843 self.next_line += 1 844 self.next_column = 0 845 elif self.next_token is not None: 846 self.next_column += len(self.next_token.text) 847 848 self.next_token = None 849 850 if self._cursor == len(self._string): 851 return None 852 assert self._cursor < len(self._string) 853 854 if (self._cursor + 1 < len(self._string) and 855 self._string[self._cursor + 1] in '{}'): 856 self.next_token = ( 857 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or 858 _TOKENS.get(self._string[self._cursor:self._cursor+2])) 859 860 if self.next_token is None: 861 self.next_token = _Token.CHARACTER 862 863 self._cursor += len(self.next_token.text) 864 return self 865 866 def AdvanceOver(self, token, description=None): 867 parse_error = None 868 if not self.next_token: 869 parse_error = 'Reached EOF but expected %s' % token.name 870 elif self.next_token is not token: 871 parse_error = 'Expecting token %s but got %s at line %s' % ( 872 token.name, self.next_token.name, self.next_line) 873 if parse_error: 874 parse_error += ' %s' % description or '' 875 raise ParseException(parse_error) 876 return self.Advance() 877 878 def AdvanceOverSeparator(self, char, description=None): 879 self.SkipWhitespace() 880 next_char = self.NextCharacter() 881 if next_char != char: 882 parse_error = 'Expected \'%s\'. got \'%s\'' % (char, next_char) 883 if description is not None: 884 parse_error += ' (%s)' % description 885 raise ParseException(parse_error) 886 self.AdvanceOver(_Token.CHARACTER) 887 self.SkipWhitespace() 888 889 def AdvanceOverNextString(self, excluded=''): 890 start = self._cursor - len(self.next_token.text) 891 while (self.next_token is _Token.CHARACTER and 892 # Can use -1 here because token length of CHARACTER is 1. 893 self._string[self._cursor - 1] not in excluded): 894 self.Advance() 895 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) 896 return self._string[start:end] 897 898 def AdvanceToNextWhitespace(self): 899 return self.AdvanceOverNextString(excluded=' \n\r\t') 900 901 def SkipWhitespace(self): 902 while (self.next_token is _Token.CHARACTER and 903 # Can use -1 here because token length of CHARACTER is 1. 904 self._string[self._cursor - 1] in ' \n\r\t'): 905 self.Advance() 906 907 def __repr__(self): 908 return '%s(next_token=%s, remainder=%s)' % (type(self).__name__, 909 self.next_token, 910 self._string[self._cursor:]) 911 912 def __str__(self): 913 return repr(self) 914 915 class Handlebar(object): 916 '''A handlebar template. 917 ''' 918 def __init__(self, template, name=None): 919 self.source = template 920 self._name = name 921 tokens = _TokenStream(template) 922 self._top_node = self._ParseSection(tokens) 923 if not self._top_node: 924 raise ParseException('Template is empty') 925 if tokens.HasNext(): 926 raise ParseException('There are still tokens remaining at %s, ' 927 'was there an end-section without a start-section?' % 928 tokens.next_line) 929 930 def _ParseSection(self, tokens): 931 nodes = [] 932 while tokens.HasNext(): 933 if tokens.next_token in (_Token.OPEN_END_SECTION, 934 _Token.OPEN_ELSE): 935 # Handled after running parseSection within the SECTION cases, so this 936 # is a terminating condition. If there *is* an orphaned 937 # OPEN_END_SECTION, it will be caught by noticing that there are 938 # leftover tokens after termination. 939 break 940 elif tokens.next_token in (_Token.CLOSE_MUSTACHE, 941 _Token.CLOSE_MUSTACHE3): 942 raise ParseException('Orphaned %s at line %s' % (tokens.next_token.name, 943 tokens.next_line)) 944 nodes += self._ParseNextOpenToken(tokens) 945 946 for i, node in enumerate(nodes): 947 if isinstance(node, _StringNode): 948 continue 949 950 previous_node = nodes[i - 1] if i > 0 else None 951 next_node = nodes[i + 1] if i < len(nodes) - 1 else None 952 rendered_node = None 953 954 if node.GetStartLine() != node.GetEndLine(): 955 rendered_node = _BlockNode(node) 956 if previous_node: 957 previous_node.TrimEndingSpaces() 958 if next_node: 959 next_node.TrimStartingNewLine() 960 elif ((not previous_node or previous_node.EndsWithEmptyLine()) and 961 (not next_node or next_node.StartsWithNewLine())): 962 indentation = 0 963 if previous_node: 964 indentation = previous_node.TrimEndingSpaces() 965 if next_node: 966 next_node.TrimStartingNewLine() 967 rendered_node = _IndentedNode(node, indentation) 968 else: 969 rendered_node = _InlineNode(node) 970 971 nodes[i] = rendered_node 972 973 if len(nodes) == 0: 974 return None 975 if len(nodes) == 1: 976 return nodes[0] 977 return _NodeCollection(nodes) 978 979 def _ParseNextOpenToken(self, tokens): 980 next_token = tokens.next_token 981 982 if next_token is _Token.CHARACTER: 983 # Plain strings. 984 start_line = tokens.next_line 985 string = tokens.AdvanceOverNextString() 986 return [_StringNode(string, start_line, tokens.next_line)] 987 elif next_token in (_Token.OPEN_VARIABLE, 988 _Token.OPEN_UNESCAPED_VARIABLE, 989 _Token.OPEN_JSON): 990 # Inline nodes that don't take arguments. 991 tokens.Advance() 992 close_token = (_Token.CLOSE_MUSTACHE3 993 if next_token is _Token.OPEN_UNESCAPED_VARIABLE else 994 _Token.CLOSE_MUSTACHE) 995 id_ = self._NextIdentifier(tokens) 996 tokens.AdvanceOver(close_token) 997 return [next_token.clazz(id_)] 998 elif next_token is _Token.OPEN_ASSERTION: 999 # Inline nodes that take arguments. 1000 tokens.Advance() 1001 id_ = self._NextIdentifier(tokens) 1002 node = next_token.clazz(id_, tokens.AdvanceOverNextString()) 1003 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) 1004 return [node] 1005 elif next_token in (_Token.OPEN_PARTIAL, 1006 _Token.OPEN_START_SECTION, 1007 _Token.OPEN_START_VERTED_SECTION, 1008 _Token.OPEN_START_INVERTED_SECTION): 1009 # Block nodes, though they may have inline syntax like {{#foo bar /}}. 1010 tokens.Advance() 1011 bind_to, id_ = None, self._NextIdentifier(tokens) 1012 if tokens.NextCharacter() == ':': 1013 # This section has the format {{#bound:id}} as opposed to just {{id}}. 1014 # That is, |id_| is actually the identifier to bind what the section 1015 # is producing, not the identifier of where to find that content. 1016 tokens.AdvanceOverSeparator(':') 1017 bind_to, id_ = id_, self._NextIdentifier(tokens) 1018 partial_args = None 1019 if next_token is _Token.OPEN_PARTIAL: 1020 partial_args = self._ParsePartialNodeArgs(tokens) 1021 if tokens.next_token is not _Token.CLOSE_MUSTACHE: 1022 # Inline syntax for partial types. 1023 if bind_to is not None: 1024 raise ParseException( 1025 'Cannot bind %s to a self-closing partial' % bind_to) 1026 tokens.AdvanceOver(_Token.INLINE_END_SECTION) 1027 partial_node = _PartialNode.Inline(id_) 1028 partial_node.SetArguments(partial_args) 1029 return [partial_node] 1030 elif tokens.next_token is not _Token.CLOSE_MUSTACHE: 1031 # Inline syntax for non-partial types. Support select node types: 1032 # variables, partials, JSON. 1033 line, column = tokens.next_line, (tokens.next_column + 1) 1034 name = tokens.AdvanceToNextWhitespace() 1035 clazz = _UnescapedVariableNode 1036 if name.startswith('*'): 1037 clazz = _JsonNode 1038 elif name.startswith('+'): 1039 clazz = _PartialNode.Inline 1040 if clazz is not _UnescapedVariableNode: 1041 name = name[1:] 1042 column += 1 1043 inline_node = clazz(_Identifier(name, line, column)) 1044 if isinstance(inline_node, _PartialNode): 1045 inline_node.SetArguments(self._ParsePartialNodeArgs(tokens)) 1046 if bind_to is not None: 1047 inline_node.PassThroughArgument(bind_to) 1048 tokens.SkipWhitespace() 1049 tokens.AdvanceOver(_Token.INLINE_END_SECTION) 1050 return [next_token.clazz(bind_to, id_, inline_node)] 1051 # Block syntax. 1052 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) 1053 section = self._ParseSection(tokens) 1054 else_node_class = next_token.ElseNodeClass() # may not have one 1055 else_section = None 1056 if (else_node_class is not None and 1057 tokens.next_token is _Token.OPEN_ELSE): 1058 self._OpenElse(tokens, id_) 1059 else_section = self._ParseSection(tokens) 1060 self._CloseSection(tokens, id_) 1061 nodes = [] 1062 if section is not None: 1063 node = next_token.clazz(bind_to, id_, section) 1064 if partial_args: 1065 node.SetArguments(partial_args) 1066 nodes.append(node) 1067 if else_section is not None: 1068 nodes.append(else_node_class(bind_to, id_, else_section)) 1069 return nodes 1070 elif next_token is _Token.OPEN_COMMENT: 1071 # Comments. 1072 start_line = tokens.next_line 1073 self._AdvanceOverComment(tokens) 1074 return [_CommentNode(start_line, tokens.next_line)] 1075 1076 def _AdvanceOverComment(self, tokens): 1077 tokens.AdvanceOver(_Token.OPEN_COMMENT) 1078 depth = 1 1079 while tokens.HasNext() and depth > 0: 1080 if tokens.next_token is _Token.OPEN_COMMENT: 1081 depth += 1 1082 elif tokens.next_token is _Token.CLOSE_COMMENT: 1083 depth -= 1 1084 tokens.Advance() 1085 1086 def _CloseSection(self, tokens, id_): 1087 tokens.AdvanceOver(_Token.OPEN_END_SECTION, 1088 description='to match %s' % id_.GetDescription()) 1089 next_string = tokens.AdvanceOverNextString() 1090 if next_string != '' and next_string != id_.name: 1091 raise ParseException( 1092 'Start section %s doesn\'t match end %s' % (id_, next_string)) 1093 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) 1094 1095 def _OpenElse(self, tokens, id_): 1096 tokens.AdvanceOver(_Token.OPEN_ELSE) 1097 next_string = tokens.AdvanceOverNextString() 1098 if next_string != '' and next_string != id_.name: 1099 raise ParseException( 1100 'Start section %s doesn\'t match else %s' % (id_, next_string)) 1101 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) 1102 1103 def _ParsePartialNodeArgs(self, tokens): 1104 args = {} 1105 tokens.SkipWhitespace() 1106 while (tokens.next_token is _Token.CHARACTER and 1107 tokens.NextCharacter() != ')'): 1108 key = tokens.AdvanceOverNextString(excluded=':') 1109 tokens.AdvanceOverSeparator(':') 1110 if tokens.NextCharacter() == '(': 1111 tokens.AdvanceOverSeparator('(') 1112 inner_id = self._NextIdentifier(tokens) 1113 inner_args = self._ParsePartialNodeArgs(tokens) 1114 tokens.AdvanceOverSeparator(')') 1115 args[key] = {inner_id: inner_args} 1116 else: 1117 args[key] = self._NextIdentifier(tokens) 1118 return args or None 1119 1120 def _NextIdentifier(self, tokens): 1121 tokens.SkipWhitespace() 1122 column_start = tokens.next_column + 1 1123 id_ = _Identifier(tokens.AdvanceOverNextString(excluded=' \n\r\t:()'), 1124 tokens.next_line, 1125 column_start) 1126 tokens.SkipWhitespace() 1127 return id_ 1128 1129 def Render(self, *user_contexts): 1130 '''Renders this template given a variable number of contexts to read out 1131 values from (such as those appearing in {{foo}}). 1132 ''' 1133 internal_context = _InternalContext() 1134 contexts = list(user_contexts) 1135 contexts.append({ 1136 '_': internal_context, 1137 'false': False, 1138 'true': True, 1139 }) 1140 render_state = _RenderState(self._name or '<root>', _Contexts(contexts)) 1141 internal_context.SetRenderState(render_state) 1142 self._top_node.Render(render_state) 1143 return render_state.GetResult() 1144 1145 def render(self, *contexts): 1146 return self.Render(*contexts) 1147 1148 def __eq__(self, other): 1149 return self.source == other.source and self._name == other._name 1150 1151 def __ne__(self, other): 1152 return not (self == other) 1153 1154 def __repr__(self): 1155 return str('%s(%s)' % (type(self).__name__, self._top_node)) 1156 1157 def __str__(self): 1158 return repr(self) 1159