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: Some character other than {{{ }}} to print unescaped content? 16 # TODO: Only have @ while in a loop, and only defined in the top context of 17 # the loop. 18 # TODO: Consider trimming spaces around identifers like {{?t foo}}. 19 # TODO: Only transfer global contexts into partials, not the top local. 20 # TODO: Pragmas for asserting the presence of variables. 21 # TODO: Escaping control characters somehow. e.g. \{{, \{{-. 22 # TODO: Dump warnings-so-far into the output. 23 24 import json 25 import re 26 27 '''Handlebar templates are data binding templates more-than-loosely inspired by 28 ctemplate. Use like: 29 30 from handlebar import Handlebar 31 32 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') 33 input = { 34 'foo': [ 35 { 'bar': 1 }, 36 { 'bar': 2 }, 37 { 'bar': 3 } 38 ] 39 } 40 print(template.render(input).text) 41 42 Handlebar will use get() on contexts to return values, so to create custom 43 getters (for example, something that populates values lazily from keys), just 44 provide an object with a get() method. 45 46 class CustomContext(object): 47 def get(self, key): 48 return 10 49 print(Handlebar('hello {{world}}').render(CustomContext()).text) 50 51 will print 'hello 10'. 52 ''' 53 54 class ParseException(Exception): 55 '''The exception thrown while parsing a template. 56 ''' 57 def __init__(self, error): 58 Exception.__init__(self, error) 59 60 class RenderResult(object): 61 '''The result of a render operation. 62 ''' 63 def __init__(self, text, errors): 64 self.text = text; 65 self.errors = errors 66 67 def __repr__(self): 68 return '%s(text=%s, errors=%s)' % ( 69 self.__class__.__name__, self.text, self.errors) 70 71 def __str__(self): 72 return repr(self) 73 74 class _StringBuilder(object): 75 '''Efficiently builds strings. 76 ''' 77 def __init__(self): 78 self._buf = [] 79 80 def __len__(self): 81 self._Collapse() 82 return len(self._buf[0]) 83 84 def Append(self, string): 85 if not isinstance(string, basestring): 86 string = str(string) 87 self._buf.append(string) 88 89 def ToString(self): 90 self._Collapse() 91 return self._buf[0] 92 93 def _Collapse(self): 94 self._buf = [u''.join(self._buf)] 95 96 def __repr__(self): 97 return self.ToString() 98 99 def __str__(self): 100 return repr(self) 101 102 class _Contexts(object): 103 '''Tracks a stack of context objects, providing efficient key/value retrieval. 104 ''' 105 class _Node(object): 106 '''A node within the stack. Wraps a real context and maintains the key/value 107 pairs seen so far. 108 ''' 109 def __init__(self, value): 110 self._value = value 111 self._value_has_get = hasattr(value, 'get') 112 self._found = {} 113 114 def GetKeys(self): 115 '''Returns the list of keys that |_value| contains. 116 ''' 117 return self._found.keys() 118 119 def Get(self, key): 120 '''Returns the value for |key|, or None if not found (including if 121 |_value| doesn't support key retrieval). 122 ''' 123 if not self._value_has_get: 124 return None 125 value = self._found.get(key) 126 if value is not None: 127 return value 128 value = self._value.get(key) 129 if value is not None: 130 self._found[key] = value 131 return value 132 133 def __repr__(self): 134 return 'Node(value=%s, found=%s)' % (self._value, self._found) 135 136 def __str__(self): 137 return repr(self) 138 139 def __init__(self, globals_): 140 '''Initializes with the initial global contexts, listed in order from most 141 to least important. 142 ''' 143 self._nodes = map(_Contexts._Node, globals_) 144 self._first_local = len(self._nodes) 145 self._value_info = {} 146 147 def CreateFromGlobals(self): 148 new = _Contexts([]) 149 new._nodes = self._nodes[:self._first_local] 150 new._first_local = self._first_local 151 return new 152 153 def Push(self, context): 154 self._nodes.append(_Contexts._Node(context)) 155 156 def Pop(self): 157 node = self._nodes.pop() 158 assert len(self._nodes) >= self._first_local 159 for found_key in node.GetKeys(): 160 # [0] is the stack of nodes that |found_key| has been found in. 161 self._value_info[found_key][0].pop() 162 163 def GetTopLocal(self): 164 if len(self._nodes) == self._first_local: 165 return None 166 return self._nodes[-1]._value 167 168 def Resolve(self, path): 169 # This method is only efficient at finding |key|; if |tail| has a value (and 170 # |key| evaluates to an indexable value) we'll need to descend into that. 171 key, tail = path.split('.', 1) if '.' in path else (path, None) 172 173 if key == '@': 174 found = self._nodes[-1]._value 175 else: 176 found = self._FindNodeValue(key) 177 178 if tail is None: 179 return found 180 181 for part in tail.split('.'): 182 if not hasattr(found, 'get'): 183 return None 184 found = found.get(part) 185 return found 186 187 def _FindNodeValue(self, key): 188 # |found_node_list| will be all the nodes that |key| has been found in. 189 # |checked_node_set| are those that have been checked. 190 info = self._value_info.get(key) 191 if info is None: 192 info = ([], set()) 193 self._value_info[key] = info 194 found_node_list, checked_node_set = info 195 196 # Check all the nodes not yet checked for |key|. 197 newly_found = [] 198 for node in reversed(self._nodes): 199 if node in checked_node_set: 200 break 201 value = node.Get(key) 202 if value is not None: 203 newly_found.append(node) 204 checked_node_set.add(node) 205 206 # The nodes will have been found in reverse stack order. After extending 207 # the found nodes, the freshest value will be at the tip of the stack. 208 found_node_list.extend(reversed(newly_found)) 209 if not found_node_list: 210 return None 211 212 return found_node_list[-1]._value.get(key) 213 214 class _Stack(object): 215 class Entry(object): 216 def __init__(self, name, id_): 217 self.name = name 218 self.id_ = id_ 219 220 def __init__(self, entries=[]): 221 self.entries = entries 222 223 def Descend(self, name, id_): 224 descended = list(self.entries) 225 descended.append(_Stack.Entry(name, id_)) 226 return _Stack(entries=descended) 227 228 class _RenderState(object): 229 '''The state of a render call. 230 ''' 231 def __init__(self, name, contexts, _stack=_Stack()): 232 self.text = _StringBuilder() 233 self.contexts = contexts 234 self._name = name 235 self._errors = [] 236 self._stack = _stack 237 238 def AddResolutionError(self, id_): 239 self._errors.append( 240 id_.CreateResolutionErrorMessage(self._name, stack=self._stack)) 241 242 def Copy(self): 243 return _RenderState( 244 self._name, self.contexts, _stack=self._stack) 245 246 def ForkPartial(self, custom_name, id_): 247 name = custom_name or id_.name 248 return _RenderState(name, 249 self.contexts.CreateFromGlobals(), 250 _stack=self._stack.Descend(name, id_)) 251 252 def Merge(self, render_state, text_transform=None): 253 self._errors.extend(render_state._errors) 254 text = render_state.text.ToString() 255 if text_transform is not None: 256 text = text_transform(text) 257 self.text.Append(text) 258 259 def GetResult(self): 260 return RenderResult(self.text.ToString(), self._errors); 261 262 class _Identifier(object): 263 ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'. 264 ''' 265 def __init__(self, name, line, column): 266 self.name = name 267 self.line = line 268 self.column = column 269 if name == '': 270 raise ParseException('Empty identifier %s' % self.GetDescription()) 271 for part in name.split('.'): 272 if part != '@' and not re.match('^[a-zA-Z0-9_/-]+$', part): 273 raise ParseException('Invalid identifier %s' % self.GetDescription()) 274 275 def GetDescription(self): 276 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) 277 278 def CreateResolutionErrorMessage(self, name, stack=None): 279 message = _StringBuilder() 280 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), 281 name)) 282 if stack is not None: 283 for entry in stack.entries: 284 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), 285 entry.name)) 286 return message.ToString() 287 288 def __repr__(self): 289 return self.name 290 291 def __str__(self): 292 return repr(self) 293 294 class _Line(object): 295 def __init__(self, number): 296 self.number = number 297 298 def __repr__(self): 299 return str(self.number) 300 301 def __str__(self): 302 return repr(self) 303 304 class _LeafNode(object): 305 def __init__(self, start_line, end_line): 306 self._start_line = start_line 307 self._end_line = end_line 308 309 def StartsWithNewLine(self): 310 return False 311 312 def TrimStartingNewLine(self): 313 pass 314 315 def TrimEndingSpaces(self): 316 return 0 317 318 def TrimEndingNewLine(self): 319 pass 320 321 def EndsWithEmptyLine(self): 322 return False 323 324 def GetStartLine(self): 325 return self._start_line 326 327 def GetEndLine(self): 328 return self._end_line 329 330 class _DecoratorNode(object): 331 def __init__(self, content): 332 self._content = content 333 334 def StartsWithNewLine(self): 335 return self._content.StartsWithNewLine() 336 337 def TrimStartingNewLine(self): 338 self._content.TrimStartingNewLine() 339 340 def TrimEndingSpaces(self): 341 return self._content.TrimEndingSpaces() 342 343 def TrimEndingNewLine(self): 344 self._content.TrimEndingNewLine() 345 346 def EndsWithEmptyLine(self): 347 return self._content.EndsWithEmptyLine() 348 349 def GetStartLine(self): 350 return self._content.GetStartLine() 351 352 def GetEndLine(self): 353 return self._content.GetEndLine() 354 355 def __repr__(self): 356 return str(self._content) 357 358 def __str__(self): 359 return repr(self) 360 361 class _InlineNode(_DecoratorNode): 362 def __init__(self, content): 363 _DecoratorNode.__init__(self, content) 364 365 def Render(self, render_state): 366 content_render_state = render_state.Copy() 367 self._content.Render(content_render_state) 368 render_state.Merge(content_render_state, 369 text_transform=lambda text: text.replace('\n', '')) 370 371 class _IndentedNode(_DecoratorNode): 372 def __init__(self, content, indentation): 373 _DecoratorNode.__init__(self, content) 374 self._indent_str = ' ' * indentation 375 376 def Render(self, render_state): 377 if isinstance(self._content, _CommentNode): 378 return 379 content_render_state = render_state.Copy() 380 self._content.Render(content_render_state) 381 def AddIndentation(text): 382 buf = _StringBuilder() 383 buf.Append(self._indent_str) 384 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) 385 buf.Append('\n') 386 return buf.ToString() 387 render_state.Merge(content_render_state, text_transform=AddIndentation) 388 389 class _BlockNode(_DecoratorNode): 390 def __init__(self, content): 391 _DecoratorNode.__init__(self, content) 392 content.TrimStartingNewLine() 393 content.TrimEndingSpaces() 394 395 def Render(self, render_state): 396 self._content.Render(render_state) 397 398 class _NodeCollection(object): 399 def __init__(self, nodes): 400 assert nodes 401 self._nodes = nodes 402 403 def Render(self, render_state): 404 for node in self._nodes: 405 node.Render(render_state) 406 407 def StartsWithNewLine(self): 408 return self._nodes[0].StartsWithNewLine() 409 410 def TrimStartingNewLine(self): 411 self._nodes[0].TrimStartingNewLine() 412 413 def TrimEndingSpaces(self): 414 return self._nodes[-1].TrimEndingSpaces() 415 416 def TrimEndingNewLine(self): 417 self._nodes[-1].TrimEndingNewLine() 418 419 def EndsWithEmptyLine(self): 420 return self._nodes[-1].EndsWithEmptyLine() 421 422 def GetStartLine(self): 423 return self._nodes[0].GetStartLine() 424 425 def GetEndLine(self): 426 return self._nodes[-1].GetEndLine() 427 428 def __repr__(self): 429 return ''.join(str(node) for node in self._nodes) 430 431 def __str__(self): 432 return repr(self) 433 434 class _StringNode(object): 435 ''' Just a string. 436 ''' 437 def __init__(self, string, start_line, end_line): 438 self._string = string 439 self._start_line = start_line 440 self._end_line = end_line 441 442 def Render(self, render_state): 443 render_state.text.Append(self._string) 444 445 def StartsWithNewLine(self): 446 return self._string.startswith('\n') 447 448 def TrimStartingNewLine(self): 449 if self.StartsWithNewLine(): 450 self._string = self._string[1:] 451 452 def TrimEndingSpaces(self): 453 original_length = len(self._string) 454 self._string = self._string[:self._LastIndexOfSpaces()] 455 return original_length - len(self._string) 456 457 def TrimEndingNewLine(self): 458 if self._string.endswith('\n'): 459 self._string = self._string[:len(self._string) - 1] 460 461 def EndsWithEmptyLine(self): 462 index = self._LastIndexOfSpaces() 463 return index == 0 or self._string[index - 1] == '\n' 464 465 def _LastIndexOfSpaces(self): 466 index = len(self._string) 467 while index > 0 and self._string[index - 1] == ' ': 468 index -= 1 469 return index 470 471 def GetStartLine(self): 472 return self._start_line 473 474 def GetEndLine(self): 475 return self._end_line 476 477 def __repr__(self): 478 return self._string 479 480 def __str__(self): 481 return repr(self) 482 483 class _EscapedVariableNode(_LeafNode): 484 ''' {{foo}} 485 ''' 486 def __init__(self, id_): 487 _LeafNode.__init__(self, id_.line, id_.line) 488 self._id = id_ 489 490 def Render(self, render_state): 491 value = render_state.contexts.Resolve(self._id.name) 492 if value is None: 493 render_state.AddResolutionError(self._id) 494 return 495 string = value if isinstance(value, basestring) else str(value) 496 render_state.text.Append(string.replace('&', '&') 497 .replace('<', '<') 498 .replace('>', '>')) 499 500 def __repr__(self): 501 return '{{%s}}' % self._id 502 503 def __str__(self): 504 return repr(self) 505 506 class _UnescapedVariableNode(_LeafNode): 507 ''' {{{foo}}} 508 ''' 509 def __init__(self, id_): 510 _LeafNode.__init__(self, id_.line, id_.line) 511 self._id = id_ 512 513 def Render(self, render_state): 514 value = render_state.contexts.Resolve(self._id.name) 515 if value is None: 516 render_state.AddResolutionError(self._id) 517 return 518 string = value if isinstance(value, basestring) else str(value) 519 render_state.text.Append(string) 520 521 def __repr__(self): 522 return '{{{%s}}}' % self._id 523 524 def __str__(self): 525 return repr(self) 526 527 class _CommentNode(_LeafNode): 528 '''{{- This is a comment -}} 529 An empty placeholder node for correct indented rendering behaviour. 530 ''' 531 def __init__(self, start_line, end_line): 532 _LeafNode.__init__(self, start_line, end_line) 533 534 def Render(self, render_state): 535 pass 536 537 def __repr__(self): 538 return '<comment>' 539 540 def __str__(self): 541 return repr(self) 542 543 class _SectionNode(_DecoratorNode): 544 ''' {{#foo}} ... {{/}} 545 ''' 546 def __init__(self, id_, content): 547 _DecoratorNode.__init__(self, content) 548 self._id = id_ 549 550 def Render(self, render_state): 551 value = render_state.contexts.Resolve(self._id.name) 552 if isinstance(value, list): 553 for item in value: 554 # Always push context, even if it's not "valid", since we want to 555 # be able to refer to items in a list such as [1,2,3] via @. 556 render_state.contexts.Push(item) 557 self._content.Render(render_state) 558 render_state.contexts.Pop() 559 elif hasattr(value, 'get'): 560 render_state.contexts.Push(value) 561 self._content.Render(render_state) 562 render_state.contexts.Pop() 563 else: 564 render_state.AddResolutionError(self._id) 565 566 def __repr__(self): 567 return '{{#%s}}%s{{/%s}}' % ( 568 self._id, _DecoratorNode.__repr__(self), self._id) 569 570 def __str__(self): 571 return repr(self) 572 573 class _VertedSectionNode(_DecoratorNode): 574 ''' {{?foo}} ... {{/}} 575 ''' 576 def __init__(self, id_, content): 577 _DecoratorNode.__init__(self, content) 578 self._id = id_ 579 580 def Render(self, render_state): 581 value = render_state.contexts.Resolve(self._id.name) 582 if _VertedSectionNode.ShouldRender(value): 583 render_state.contexts.Push(value) 584 self._content.Render(render_state) 585 render_state.contexts.Pop() 586 587 def __repr__(self): 588 return '{{?%s}}%s{{/%s}}' % ( 589 self._id, _DecoratorNode.__repr__(self), self._id) 590 591 def __str__(self): 592 return repr(self) 593 594 @staticmethod 595 def ShouldRender(value): 596 if value is None: 597 return False 598 if isinstance(value, bool): 599 return value 600 if isinstance(value, list): 601 return len(value) > 0 602 return True 603 604 class _InvertedSectionNode(_DecoratorNode): 605 ''' {{^foo}} ... {{/}} 606 ''' 607 def __init__(self, id_, content): 608 _DecoratorNode.__init__(self, content) 609 self._id = id_ 610 611 def Render(self, render_state): 612 value = render_state.contexts.Resolve(self._id.name) 613 if not _VertedSectionNode.ShouldRender(value): 614 self._content.Render(render_state) 615 616 def __repr__(self): 617 return '{{^%s}}%s{{/%s}}' % ( 618 self._id, _DecoratorNode.__repr__(self), self._id) 619 620 def __str__(self): 621 return repr(self) 622 623 class _JsonNode(_LeafNode): 624 ''' {{*foo}} 625 ''' 626 def __init__(self, id_): 627 _LeafNode.__init__(self, id_.line, id_.line) 628 self._id = id_ 629 630 def Render(self, render_state): 631 value = render_state.contexts.Resolve(self._id.name) 632 if value is None: 633 render_state.AddResolutionError(self._id) 634 return 635 render_state.text.Append(json.dumps(value, separators=(',',':'))) 636 637 def __repr__(self): 638 return '{{*%s}}' % self._id 639 640 def __str__(self): 641 return repr(self) 642 643 class _PartialNode(_LeafNode): 644 ''' {{+foo}} 645 ''' 646 def __init__(self, id_): 647 _LeafNode.__init__(self, id_.line, id_.line) 648 self._id = id_ 649 self._args = None 650 self._local_context_id = None 651 652 def Render(self, render_state): 653 value = render_state.contexts.Resolve(self._id.name) 654 if value is None: 655 render_state.AddResolutionError(self._id) 656 return 657 if not isinstance(value, Handlebar): 658 render_state.AddResolutionError(self._id) 659 return 660 661 partial_render_state = render_state.ForkPartial(value._name, self._id) 662 663 # TODO: Don't do this. Force callers to do this by specifying an @ argument. 664 top_local = render_state.contexts.GetTopLocal() 665 if top_local is not None: 666 partial_render_state.contexts.Push(top_local) 667 668 if self._args is not None: 669 arg_context = {} 670 for key, value_id in self._args.items(): 671 context = render_state.contexts.Resolve(value_id.name) 672 if context is not None: 673 arg_context[key] = context 674 partial_render_state.contexts.Push(arg_context) 675 676 if self._local_context_id is not None: 677 local_context = render_state.contexts.Resolve(self._local_context_id.name) 678 if local_context is not None: 679 partial_render_state.contexts.Push(local_context) 680 681 value._top_node.Render(partial_render_state) 682 683 render_state.Merge( 684 partial_render_state, 685 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) 686 687 def AddArgument(self, key, id_): 688 if self._args is None: 689 self._args = {} 690 self._args[key] = id_ 691 692 def SetLocalContext(self, id_): 693 self._local_context_id = id_ 694 695 def __repr__(self): 696 return '{{+%s}}' % self._id 697 698 def __str__(self): 699 return repr(self) 700 701 _TOKENS = {} 702 703 class _Token(object): 704 ''' The tokens that can appear in a template. 705 ''' 706 class Data(object): 707 def __init__(self, name, text, clazz): 708 self.name = name 709 self.text = text 710 self.clazz = clazz 711 _TOKENS[text] = self 712 713 def ElseNodeClass(self): 714 if self.clazz == _VertedSectionNode: 715 return _InvertedSectionNode 716 if self.clazz == _InvertedSectionNode: 717 return _VertedSectionNode 718 raise ValueError('%s cannot have an else clause.' % self.clazz) 719 720 OPEN_START_SECTION = Data('OPEN_START_SECTION' , '{{#', _SectionNode) 721 OPEN_START_VERTED_SECTION = Data('OPEN_START_VERTED_SECTION' , '{{?', _VertedSectionNode) 722 OPEN_START_INVERTED_SECTION = Data('OPEN_START_INVERTED_SECTION', '{{^', _InvertedSectionNode) 723 OPEN_START_JSON = Data('OPEN_START_JSON' , '{{*', _JsonNode) 724 OPEN_START_PARTIAL = Data('OPEN_START_PARTIAL' , '{{+', _PartialNode) 725 OPEN_ELSE = Data('OPEN_ELSE' , '{{:', None) 726 OPEN_END_SECTION = Data('OPEN_END_SECTION' , '{{/', None) 727 INLINE_END_SECTION = Data('INLINE_END_SECTION' , '/}}', None) 728 OPEN_UNESCAPED_VARIABLE = Data('OPEN_UNESCAPED_VARIABLE' , '{{{', _UnescapedVariableNode) 729 CLOSE_MUSTACHE3 = Data('CLOSE_MUSTACHE3' , '}}}', None) 730 OPEN_COMMENT = Data('OPEN_COMMENT' , '{{-', _CommentNode) 731 CLOSE_COMMENT = Data('CLOSE_COMMENT' , '-}}', None) 732 OPEN_VARIABLE = Data('OPEN_VARIABLE' , '{{' , _EscapedVariableNode) 733 CLOSE_MUSTACHE = Data('CLOSE_MUSTACHE' , '}}' , None) 734 CHARACTER = Data('CHARACTER' , '.' , None) 735 736 class _TokenStream(object): 737 ''' Tokeniser for template parsing. 738 ''' 739 def __init__(self, string): 740 self.next_token = None 741 self.next_line = _Line(1) 742 self.next_column = 0 743 self._string = string 744 self._cursor = 0 745 self.Advance() 746 747 def HasNext(self): 748 return self.next_token is not None 749 750 def Advance(self): 751 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': 752 self.next_line = _Line(self.next_line.number + 1) 753 self.next_column = 0 754 elif self.next_token is not None: 755 self.next_column += len(self.next_token.text) 756 757 self.next_token = None 758 759 if self._cursor == len(self._string): 760 return None 761 assert self._cursor < len(self._string) 762 763 if (self._cursor + 1 < len(self._string) and 764 self._string[self._cursor + 1] in '{}'): 765 self.next_token = ( 766 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or 767 _TOKENS.get(self._string[self._cursor:self._cursor+2])) 768 769 if self.next_token is None: 770 self.next_token = _Token.CHARACTER 771 772 self._cursor += len(self.next_token.text) 773 return self 774 775 def AdvanceOver(self, token): 776 if self.next_token != token: 777 raise ParseException( 778 'Expecting token %s but got %s at line %s' % (token.name, 779 self.next_token.name, 780 self.next_line)) 781 return self.Advance() 782 783 def AdvanceOverNextString(self, excluded=''): 784 start = self._cursor - len(self.next_token.text) 785 while (self.next_token is _Token.CHARACTER and 786 # Can use -1 here because token length of CHARACTER is 1. 787 self._string[self._cursor - 1] not in excluded): 788 self.Advance() 789 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) 790 return self._string[start:end] 791 792 def AdvanceToNextWhitespace(self): 793 return self.AdvanceOverNextString(excluded=' \n\r\t') 794 795 def SkipWhitespace(self): 796 while (self.next_token is _Token.CHARACTER and 797 # Can use -1 here because token length of CHARACTER is 1. 798 self._string[self._cursor - 1] in ' \n\r\t'): 799 self.Advance() 800 801 class Handlebar(object): 802 ''' A handlebar template. 803 ''' 804 def __init__(self, template, name=None): 805 self.source = template 806 self._name = name 807 tokens = _TokenStream(template) 808 self._top_node = self._ParseSection(tokens) 809 if not self._top_node: 810 raise ParseException('Template is empty') 811 if tokens.HasNext(): 812 raise ParseException('There are still tokens remaining at %s, ' 813 'was there an end-section without a start-section?' 814 % tokens.next_line) 815 816 def _ParseSection(self, tokens): 817 nodes = [] 818 while tokens.HasNext(): 819 if tokens.next_token in (_Token.OPEN_END_SECTION, 820 _Token.OPEN_ELSE): 821 # Handled after running parseSection within the SECTION cases, so this 822 # is a terminating condition. If there *is* an orphaned 823 # OPEN_END_SECTION, it will be caught by noticing that there are 824 # leftover tokens after termination. 825 break 826 elif tokens.next_token in (_Token.CLOSE_MUSTACHE, 827 _Token.CLOSE_MUSTACHE3): 828 raise ParseException('Orphaned %s at line %s' % (tokens.next_token.name, 829 tokens.next_line)) 830 nodes += self._ParseNextOpenToken(tokens) 831 832 for i, node in enumerate(nodes): 833 if isinstance(node, _StringNode): 834 continue 835 836 previous_node = nodes[i - 1] if i > 0 else None 837 next_node = nodes[i + 1] if i < len(nodes) - 1 else None 838 rendered_node = None 839 840 if node.GetStartLine() != node.GetEndLine(): 841 rendered_node = _BlockNode(node) 842 if previous_node: 843 previous_node.TrimEndingSpaces() 844 if next_node: 845 next_node.TrimStartingNewLine() 846 elif (isinstance(node, _LeafNode) and 847 (not previous_node or previous_node.EndsWithEmptyLine()) and 848 (not next_node or next_node.StartsWithNewLine())): 849 indentation = 0 850 if previous_node: 851 indentation = previous_node.TrimEndingSpaces() 852 if next_node: 853 next_node.TrimStartingNewLine() 854 rendered_node = _IndentedNode(node, indentation) 855 else: 856 rendered_node = _InlineNode(node) 857 858 nodes[i] = rendered_node 859 860 if len(nodes) == 0: 861 return None 862 if len(nodes) == 1: 863 return nodes[0] 864 return _NodeCollection(nodes) 865 866 def _ParseNextOpenToken(self, tokens): 867 next_token = tokens.next_token 868 869 if next_token is _Token.CHARACTER: 870 start_line = tokens.next_line 871 string = tokens.AdvanceOverNextString() 872 return [_StringNode(string, start_line, tokens.next_line)] 873 elif next_token in (_Token.OPEN_VARIABLE, 874 _Token.OPEN_UNESCAPED_VARIABLE, 875 _Token.OPEN_START_JSON): 876 id_, inline_value_id = self._OpenSectionOrTag(tokens) 877 if inline_value_id is not None: 878 raise ParseException( 879 '%s cannot have an inline value' % id_.GetDescription()) 880 return [next_token.clazz(id_)] 881 elif next_token is _Token.OPEN_START_PARTIAL: 882 tokens.Advance() 883 column_start = tokens.next_column + 1 884 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), 885 tokens.next_line, 886 column_start) 887 partial_node = _PartialNode(id_) 888 while tokens.next_token is _Token.CHARACTER: 889 tokens.SkipWhitespace() 890 key = tokens.AdvanceOverNextString(excluded=':') 891 tokens.Advance() 892 column_start = tokens.next_column + 1 893 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), 894 tokens.next_line, 895 column_start) 896 if key == '@': 897 partial_node.SetLocalContext(id_) 898 else: 899 partial_node.AddArgument(key, id_) 900 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) 901 return [partial_node] 902 elif next_token is _Token.OPEN_START_SECTION: 903 id_, inline_node = self._OpenSectionOrTag(tokens) 904 nodes = [] 905 if inline_node is None: 906 section = self._ParseSection(tokens) 907 self._CloseSection(tokens, id_) 908 nodes = [] 909 if section is not None: 910 nodes.append(_SectionNode(id_, section)) 911 else: 912 nodes.append(_SectionNode(id_, inline_node)) 913 return nodes 914 elif next_token in (_Token.OPEN_START_VERTED_SECTION, 915 _Token.OPEN_START_INVERTED_SECTION): 916 id_, inline_node = self._OpenSectionOrTag(tokens) 917 nodes = [] 918 if inline_node is None: 919 section = self._ParseSection(tokens) 920 else_section = None 921 if tokens.next_token is _Token.OPEN_ELSE: 922 self._OpenElse(tokens, id_) 923 else_section = self._ParseSection(tokens) 924 self._CloseSection(tokens, id_) 925 if section: 926 nodes.append(next_token.clazz(id_, section)) 927 if else_section: 928 nodes.append(next_token.ElseNodeClass()(id_, else_section)) 929 else: 930 nodes.append(next_token.clazz(id_, inline_node)) 931 return nodes 932 elif next_token is _Token.OPEN_COMMENT: 933 start_line = tokens.next_line 934 self._AdvanceOverComment(tokens) 935 return [_CommentNode(start_line, tokens.next_line)] 936 937 def _AdvanceOverComment(self, tokens): 938 tokens.AdvanceOver(_Token.OPEN_COMMENT) 939 depth = 1 940 while tokens.HasNext() and depth > 0: 941 if tokens.next_token is _Token.OPEN_COMMENT: 942 depth += 1 943 elif tokens.next_token is _Token.CLOSE_COMMENT: 944 depth -= 1 945 tokens.Advance() 946 947 def _OpenSectionOrTag(self, tokens): 948 def NextIdentifierArgs(): 949 tokens.SkipWhitespace() 950 line = tokens.next_line 951 column = tokens.next_column + 1 952 name = tokens.AdvanceToNextWhitespace() 953 tokens.SkipWhitespace() 954 return (name, line, column) 955 close_token = (_Token.CLOSE_MUSTACHE3 956 if tokens.next_token is _Token.OPEN_UNESCAPED_VARIABLE else 957 _Token.CLOSE_MUSTACHE) 958 tokens.Advance() 959 id_ = _Identifier(*NextIdentifierArgs()) 960 if tokens.next_token is close_token: 961 tokens.AdvanceOver(close_token) 962 inline_node = None 963 else: 964 name, line, column = NextIdentifierArgs() 965 tokens.AdvanceOver(_Token.INLINE_END_SECTION) 966 # Support select other types of nodes, the most useful being partial. 967 clazz = _UnescapedVariableNode 968 if name.startswith('*'): 969 clazz = _JsonNode 970 elif name.startswith('+'): 971 clazz = _PartialNode 972 if clazz is not _UnescapedVariableNode: 973 name = name[1:] 974 column += 1 975 inline_node = clazz(_Identifier(name, line, column)) 976 return (id_, inline_node) 977 978 def _CloseSection(self, tokens, id_): 979 tokens.AdvanceOver(_Token.OPEN_END_SECTION) 980 next_string = tokens.AdvanceOverNextString() 981 if next_string != '' and next_string != id_.name: 982 raise ParseException( 983 'Start section %s doesn\'t match end %s' % (id_, next_string)) 984 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) 985 986 def _OpenElse(self, tokens, id_): 987 tokens.AdvanceOver(_Token.OPEN_ELSE) 988 next_string = tokens.AdvanceOverNextString() 989 if next_string != '' and next_string != id_.name: 990 raise ParseException( 991 'Start section %s doesn\'t match else %s' % (id_, next_string)) 992 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) 993 994 def Render(self, *contexts): 995 '''Renders this template given a variable number of contexts to read out 996 values from (such as those appearing in {{foo}}). 997 ''' 998 name = self._name or '<root>' 999 render_state = _RenderState(name, _Contexts(contexts)) 1000 self._top_node.Render(render_state) 1001 return render_state.GetResult() 1002 1003 def render(self, *contexts): 1004 return self.Render(*contexts) 1005 1006 def __repr__(self): 1007 return str('%s(%s)' % (self.__class__.__name__, self._top_node)) 1008 1009 def __str__(self): 1010 return repr(self) 1011