1 # Copyright 2012 Benjamin Kalman
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
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
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.
27 '''Handlebar templates are data binding templates more-than-loosely inspired by
30 from handlebar import Handlebar
32 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world')
40 print(template.render(input).text)
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.
46 class CustomContext(object):
49 print(Handlebar('hello {{world}}').render(CustomContext()).text)
51 will print 'hello 10'.
54 class ParseException(Exception):
55 '''The exception thrown while parsing a template.
57 def __init__(self
, error
):
58 Exception.__init
__(self
, error
)
60 class RenderResult(object):
61 '''The result of a render operation.
63 def __init__(self
, text
, errors
):
68 return '%s(text=%s, errors=%s)' % (
69 self
.__class
__.__name
__, self
.text
, self
.errors
)
74 class _StringBuilder(object):
75 '''Efficiently builds strings.
82 return len(self
._buf
[0])
84 def Append(self
, string
):
85 if not isinstance(string
, basestring
):
87 self
._buf
.append(string
)
94 self
._buf
= [u
''.join(self
._buf
)]
97 return self
.ToString()
102 class _Contexts(object):
103 '''Tracks a stack of context objects, providing efficient key/value retrieval.
106 '''A node within the stack. Wraps a real context and maintains the key/value
109 def __init__(self
, value
):
111 self
._value
_has
_get
= hasattr(value
, 'get')
115 '''Returns the list of keys that |_value| contains.
117 return self
._found
.keys()
120 '''Returns the value for |key|, or None if not found (including if
121 |_value| doesn't support key retrieval).
123 if not self
._value
_has
_get
:
125 value
= self
._found
.get(key
)
126 if value
is not None:
128 value
= self
._value
.get(key
)
129 if value
is not None:
130 self
._found
[key
] = value
134 return 'Node(value=%s, found=%s)' % (self
._value
, self
._found
)
139 def __init__(self
, globals_
):
140 '''Initializes with the initial global contexts, listed in order from most
143 self
._nodes
= map(_Contexts
._Node
, globals_
)
144 self
._first
_local
= len(self
._nodes
)
145 self
._value
_info
= {}
147 def CreateFromGlobals(self
):
149 new
._nodes
= self
._nodes
[:self
._first
_local
]
150 new
._first
_local
= self
._first
_local
153 def Push(self
, context
):
154 self
._nodes
.append(_Contexts
._Node
(context
))
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()
163 def GetTopLocal(self
):
164 if len(self
._nodes
) == self
._first
_local
:
166 return self
._nodes
[-1]._value
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)
174 found
= self
._nodes
[-1]._value
176 found
= self
._FindNodeValue
(key
)
181 for part
in tail
.split('.'):
182 if not hasattr(found
, 'get'):
184 found
= found
.get(part
)
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
)
193 self
._value
_info
[key
] = info
194 found_node_list
, checked_node_set
= info
196 # Check all the nodes not yet checked for |key|.
198 for node
in reversed(self
._nodes
):
199 if node
in checked_node_set
:
201 value
= node
.Get(key
)
202 if value
is not None:
203 newly_found
.append(node
)
204 checked_node_set
.add(node
)
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
:
212 return found_node_list
[-1]._value
.get(key
)
214 class _Stack(object):
216 def __init__(self
, name
, id_
):
220 def __init__(self
, entries
=[]):
221 self
.entries
= entries
223 def Descend(self
, name
, id_
):
224 descended
= list(self
.entries
)
225 descended
.append(_Stack
.Entry(name
, id_
))
226 return _Stack(entries
=descended
)
228 class _RenderState(object):
229 '''The state of a render call.
231 def __init__(self
, name
, contexts
, _stack
=_Stack()):
232 self
.text
= _StringBuilder()
233 self
.contexts
= contexts
238 def AddResolutionError(self
, id_
):
240 id_
.CreateResolutionErrorMessage(self
._name
, stack
=self
._stack
))
244 self
._name
, self
.contexts
, _stack
=self
._stack
)
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_
))
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
)
260 return RenderResult(self
.text
.ToString(), self
._errors
);
262 class _Identifier(object):
263 ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'.
265 def __init__(self
, name
, line
, column
):
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())
275 def GetDescription(self
):
276 return '\'%s\' at line %s column %s' % (self
.name
, self
.line
, self
.column
)
278 def CreateResolutionErrorMessage(self
, name
, stack
=None):
279 message
= _StringBuilder()
280 message
.Append('Failed to resolve %s in %s\n' % (self
.GetDescription(),
282 if stack
is not None:
283 for entry
in stack
.entries
:
284 message
.Append(' included as %s in %s\n' % (entry
.id_
.GetDescription(),
286 return message
.ToString()
295 def __init__(self
, number
):
299 return str(self
.number
)
304 class _LeafNode(object):
305 def __init__(self
, start_line
, end_line
):
306 self
._start
_line
= start_line
307 self
._end
_line
= end_line
309 def StartsWithNewLine(self
):
312 def TrimStartingNewLine(self
):
315 def TrimEndingSpaces(self
):
318 def TrimEndingNewLine(self
):
321 def EndsWithEmptyLine(self
):
324 def GetStartLine(self
):
325 return self
._start
_line
327 def GetEndLine(self
):
328 return self
._end
_line
330 class _DecoratorNode(object):
331 def __init__(self
, content
):
332 self
._content
= content
334 def StartsWithNewLine(self
):
335 return self
._content
.StartsWithNewLine()
337 def TrimStartingNewLine(self
):
338 self
._content
.TrimStartingNewLine()
340 def TrimEndingSpaces(self
):
341 return self
._content
.TrimEndingSpaces()
343 def TrimEndingNewLine(self
):
344 self
._content
.TrimEndingNewLine()
346 def EndsWithEmptyLine(self
):
347 return self
._content
.EndsWithEmptyLine()
349 def GetStartLine(self
):
350 return self
._content
.GetStartLine()
352 def GetEndLine(self
):
353 return self
._content
.GetEndLine()
356 return str(self
._content
)
361 class _InlineNode(_DecoratorNode
):
362 def __init__(self
, content
):
363 _DecoratorNode
.__init
__(self
, content
)
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', ''))
371 class _IndentedNode(_DecoratorNode
):
372 def __init__(self
, content
, indentation
):
373 _DecoratorNode
.__init
__(self
, content
)
374 self
._indent
_str
= ' ' * indentation
376 def Render(self
, render_state
):
377 if isinstance(self
._content
, _CommentNode
):
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
))
386 return buf
.ToString()
387 render_state
.Merge(content_render_state
, text_transform
=AddIndentation
)
389 class _BlockNode(_DecoratorNode
):
390 def __init__(self
, content
):
391 _DecoratorNode
.__init
__(self
, content
)
392 content
.TrimStartingNewLine()
393 content
.TrimEndingSpaces()
395 def Render(self
, render_state
):
396 self
._content
.Render(render_state
)
398 class _NodeCollection(object):
399 def __init__(self
, nodes
):
403 def Render(self
, render_state
):
404 for node
in self
._nodes
:
405 node
.Render(render_state
)
407 def StartsWithNewLine(self
):
408 return self
._nodes
[0].StartsWithNewLine()
410 def TrimStartingNewLine(self
):
411 self
._nodes
[0].TrimStartingNewLine()
413 def TrimEndingSpaces(self
):
414 return self
._nodes
[-1].TrimEndingSpaces()
416 def TrimEndingNewLine(self
):
417 self
._nodes
[-1].TrimEndingNewLine()
419 def EndsWithEmptyLine(self
):
420 return self
._nodes
[-1].EndsWithEmptyLine()
422 def GetStartLine(self
):
423 return self
._nodes
[0].GetStartLine()
425 def GetEndLine(self
):
426 return self
._nodes
[-1].GetEndLine()
429 return ''.join(str(node
) for node
in self
._nodes
)
434 class _StringNode(object):
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
442 def Render(self
, render_state
):
443 render_state
.text
.Append(self
._string
)
445 def StartsWithNewLine(self
):
446 return self
._string
.startswith('\n')
448 def TrimStartingNewLine(self
):
449 if self
.StartsWithNewLine():
450 self
._string
= self
._string
[1:]
452 def TrimEndingSpaces(self
):
453 original_length
= len(self
._string
)
454 self
._string
= self
._string
[:self
._LastIndexOfSpaces
()]
455 return original_length
- len(self
._string
)
457 def TrimEndingNewLine(self
):
458 if self
._string
.endswith('\n'):
459 self
._string
= self
._string
[:len(self
._string
) - 1]
461 def EndsWithEmptyLine(self
):
462 index
= self
._LastIndexOfSpaces
()
463 return index
== 0 or self
._string
[index
- 1] == '\n'
465 def _LastIndexOfSpaces(self
):
466 index
= len(self
._string
)
467 while index
> 0 and self
._string
[index
- 1] == ' ':
471 def GetStartLine(self
):
472 return self
._start
_line
474 def GetEndLine(self
):
475 return self
._end
_line
483 class _EscapedVariableNode(_LeafNode
):
486 def __init__(self
, id_
):
487 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
490 def Render(self
, render_state
):
491 value
= render_state
.contexts
.Resolve(self
._id
.name
)
493 render_state
.AddResolutionError(self
._id
)
495 string
= value
if isinstance(value
, basestring
) else str(value
)
496 render_state
.text
.Append(string
.replace('&', '&')
497 .replace('<', '<')
498 .replace('>', '>'))
501 return '{{%s}}' % self
._id
506 class _UnescapedVariableNode(_LeafNode
):
509 def __init__(self
, id_
):
510 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
513 def Render(self
, render_state
):
514 value
= render_state
.contexts
.Resolve(self
._id
.name
)
516 render_state
.AddResolutionError(self
._id
)
518 string
= value
if isinstance(value
, basestring
) else str(value
)
519 render_state
.text
.Append(string
)
522 return '{{{%s}}}' % self
._id
527 class _CommentNode(_LeafNode
):
528 '''{{- This is a comment -}}
529 An empty placeholder node for correct indented rendering behaviour.
531 def __init__(self
, start_line
, end_line
):
532 _LeafNode
.__init
__(self
, start_line
, end_line
)
534 def Render(self
, render_state
):
543 class _SectionNode(_DecoratorNode
):
544 ''' {{#foo}} ... {{/}}
546 def __init__(self
, id_
, content
):
547 _DecoratorNode
.__init
__(self
, content
)
550 def Render(self
, render_state
):
551 value
= render_state
.contexts
.Resolve(self
._id
.name
)
552 if isinstance(value
, list):
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()
564 render_state
.AddResolutionError(self
._id
)
567 return '{{#%s}}%s{{/%s}}' % (
568 self
._id
, _DecoratorNode
.__repr
__(self
), self
._id
)
573 class _VertedSectionNode(_DecoratorNode
):
574 ''' {{?foo}} ... {{/}}
576 def __init__(self
, id_
, content
):
577 _DecoratorNode
.__init
__(self
, content
)
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()
588 return '{{?%s}}%s{{/%s}}' % (
589 self
._id
, _DecoratorNode
.__repr
__(self
), self
._id
)
595 def ShouldRender(value
):
598 if isinstance(value
, bool):
600 if isinstance(value
, list):
601 return len(value
) > 0
604 class _InvertedSectionNode(_DecoratorNode
):
605 ''' {{^foo}} ... {{/}}
607 def __init__(self
, id_
, content
):
608 _DecoratorNode
.__init
__(self
, content
)
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
)
617 return '{{^%s}}%s{{/%s}}' % (
618 self
._id
, _DecoratorNode
.__repr
__(self
), self
._id
)
623 class _JsonNode(_LeafNode
):
626 def __init__(self
, id_
):
627 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
630 def Render(self
, render_state
):
631 value
= render_state
.contexts
.Resolve(self
._id
.name
)
633 render_state
.AddResolutionError(self
._id
)
635 render_state
.text
.Append(json
.dumps(value
, separators
=(',',':')))
638 return '{{*%s}}' % self
._id
643 class _PartialNode(_LeafNode
):
646 def __init__(self
, id_
):
647 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
650 self
._local
_context
_id
= None
652 def Render(self
, render_state
):
653 value
= render_state
.contexts
.Resolve(self
._id
.name
)
655 render_state
.AddResolutionError(self
._id
)
657 if not isinstance(value
, Handlebar
):
658 render_state
.AddResolutionError(self
._id
)
661 partial_render_state
= render_state
.ForkPartial(value
._name
, self
._id
)
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
)
668 if self
._args
is not None:
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
)
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
)
681 value
._top
_node
.Render(partial_render_state
)
684 partial_render_state
,
685 text_transform
=lambda text
: text
[:-1] if text
.endswith('\n') else text
)
687 def AddArgument(self
, key
, id_
):
688 if self
._args
is None:
690 self
._args
[key
] = id_
692 def SetLocalContext(self
, id_
):
693 self
._local
_context
_id
= id_
696 return '{{+%s}}' % self
._id
703 class _Token(object):
704 ''' The tokens that can appear in a template.
707 def __init__(self
, name
, text
, clazz
):
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
)
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)
736 class _TokenStream(object):
737 ''' Tokeniser for template parsing.
739 def __init__(self
, string
):
740 self
.next_token
= None
741 self
.next_line
= _Line(1)
743 self
._string
= string
748 return self
.next_token
is not None
751 if self
._cursor
> 0 and self
._string
[self
._cursor
- 1] == '\n':
752 self
.next_line
= _Line(self
.next_line
.number
+ 1)
754 elif self
.next_token
is not None:
755 self
.next_column
+= len(self
.next_token
.text
)
757 self
.next_token
= None
759 if self
._cursor
== len(self
._string
):
761 assert self
._cursor
< len(self
._string
)
763 if (self
._cursor
+ 1 < len(self
._string
) and
764 self
._string
[self
._cursor
+ 1] in '{}'):
766 _TOKENS
.get(self
._string
[self
._cursor
:self
._cursor
+3]) or
767 _TOKENS
.get(self
._string
[self
._cursor
:self
._cursor
+2]))
769 if self
.next_token
is None:
770 self
.next_token
= _Token
.CHARACTER
772 self
._cursor
+= len(self
.next_token
.text
)
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
,
781 return self
.Advance()
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
):
789 end
= self
._cursor
- (len(self
.next_token
.text
) if self
.next_token
else 0)
790 return self
._string
[start
:end
]
792 def AdvanceToNextWhitespace(self
):
793 return self
.AdvanceOverNextString(excluded
=' \n\r\t')
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'):
801 class Handlebar(object):
802 ''' A handlebar template.
804 def __init__(self
, template
, name
=None):
805 self
.source
= template
807 tokens
= _TokenStream(template
)
808 self
._top
_node
= self
._ParseSection
(tokens
)
809 if not self
._top
_node
:
810 raise ParseException('Template is empty')
812 raise ParseException('There are still tokens remaining at %s, '
813 'was there an end-section without a start-section?'
816 def _ParseSection(self
, tokens
):
818 while tokens
.HasNext():
819 if tokens
.next_token
in (_Token
.OPEN_END_SECTION
,
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.
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
,
830 nodes
+= self
._ParseNextOpenToken
(tokens
)
832 for i
, node
in enumerate(nodes
):
833 if isinstance(node
, _StringNode
):
836 previous_node
= nodes
[i
- 1] if i
> 0 else None
837 next_node
= nodes
[i
+ 1] if i
< len(nodes
) - 1 else None
840 if node
.GetStartLine() != node
.GetEndLine():
841 rendered_node
= _BlockNode(node
)
843 previous_node
.TrimEndingSpaces()
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())):
851 indentation
= previous_node
.TrimEndingSpaces()
853 next_node
.TrimStartingNewLine()
854 rendered_node
= _IndentedNode(node
, indentation
)
856 rendered_node
= _InlineNode(node
)
858 nodes
[i
] = rendered_node
864 return _NodeCollection(nodes
)
866 def _ParseNextOpenToken(self
, tokens
):
867 next_token
= tokens
.next_token
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
:
883 column_start
= tokens
.next_column
+ 1
884 id_
= _Identifier(tokens
.AdvanceToNextWhitespace(),
887 partial_node
= _PartialNode(id_
)
888 while tokens
.next_token
is _Token
.CHARACTER
:
889 tokens
.SkipWhitespace()
890 key
= tokens
.AdvanceOverNextString(excluded
=':')
892 column_start
= tokens
.next_column
+ 1
893 id_
= _Identifier(tokens
.AdvanceToNextWhitespace(),
897 partial_node
.SetLocalContext(id_
)
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
)
905 if inline_node
is None:
906 section
= self
._ParseSection
(tokens
)
907 self
._CloseSection
(tokens
, id_
)
909 if section
is not None:
910 nodes
.append(_SectionNode(id_
, section
))
912 nodes
.append(_SectionNode(id_
, inline_node
))
914 elif next_token
in (_Token
.OPEN_START_VERTED_SECTION
,
915 _Token
.OPEN_START_INVERTED_SECTION
):
916 id_
, inline_node
= self
._OpenSectionOrTag
(tokens
)
918 if inline_node
is None:
919 section
= self
._ParseSection
(tokens
)
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_
)
926 nodes
.append(next_token
.clazz(id_
, section
))
928 nodes
.append(next_token
.ElseNodeClass()(id_
, else_section
))
930 nodes
.append(next_token
.clazz(id_
, inline_node
))
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
)]
937 def _AdvanceOverComment(self
, tokens
):
938 tokens
.AdvanceOver(_Token
.OPEN_COMMENT
)
940 while tokens
.HasNext() and depth
> 0:
941 if tokens
.next_token
is _Token
.OPEN_COMMENT
:
943 elif tokens
.next_token
is _Token
.CLOSE_COMMENT
:
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
)
959 id_
= _Identifier(*NextIdentifierArgs())
960 if tokens
.next_token
is close_token
:
961 tokens
.AdvanceOver(close_token
)
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('*'):
970 elif name
.startswith('+'):
972 if clazz
is not _UnescapedVariableNode
:
975 inline_node
= clazz(_Identifier(name
, line
, column
))
976 return (id_
, inline_node
)
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
)
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
)
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}}).
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()
1003 def render(self
, *contexts
):
1004 return self
.Render(*contexts
)
1007 return str('%s(%s)' % (self
.__class
__.__name
__, self
._top
_node
))