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: New name, not "handlebar".
16 # TODO: Escaping control characters somehow. e.g. \{{, \{{-.
21 '''Handlebar templates are data binding templates more-than-loosely inspired by
24 from handlebar import Handlebar
26 template = Handlebar('hello {{#foo bar/}} world')
34 print(template.render(input).text)
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.
40 class CustomContext(object):
43 print(Handlebar('hello {{world}}').render(CustomContext()).text)
45 will print 'hello 10'.
48 class ParseException(Exception):
49 '''The exception thrown while parsing a template.
51 def __init__(self
, error
):
52 Exception.__init
__(self
, error
)
54 class RenderResult(object):
55 '''The result of a render operation.
57 def __init__(self
, text
, errors
):
62 return '%s(text=%s, errors=%s)' % (type(self
).__name
__,
69 class _StringBuilder(object):
70 '''Efficiently builds strings.
77 return len(self
._buf
[0])
79 def Append(self
, string
):
80 if not isinstance(string
, basestring
):
82 self
._buf
.append(string
)
89 self
._buf
= [u
''.join(self
._buf
)]
92 return self
.ToString()
97 class _Contexts(object):
98 '''Tracks a stack of context objects, providing efficient key/value retrieval.
101 '''A node within the stack. Wraps a real context and maintains the key/value
104 def __init__(self
, value
):
106 self
._value
_has
_get
= hasattr(value
, 'get')
110 '''Returns the list of keys that |_value| contains.
112 return self
._found
.keys()
115 '''Returns the value for |key|, or None if not found (including if
116 |_value| doesn't support key retrieval).
118 if not self
._value
_has
_get
:
120 value
= self
._found
.get(key
)
121 if value
is not None:
123 value
= self
._value
.get(key
)
124 if value
is not None:
125 self
._found
[key
] = value
129 return 'Node(value=%s, found=%s)' % (self
._value
, self
._found
)
134 def __init__(self
, globals_
):
135 '''Initializes with the initial global contexts, listed in order from most
138 self
._nodes
= map(_Contexts
._Node
, globals_
)
139 self
._first
_local
= len(self
._nodes
)
140 self
._value
_info
= {}
142 def CreateFromGlobals(self
):
144 new
._nodes
= self
._nodes
[:self
._first
_local
]
145 new
._first
_local
= self
._first
_local
148 def Push(self
, context
):
149 self
._nodes
.append(_Contexts
._Node
(context
))
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()
158 def FirstLocal(self
):
159 if len(self
._nodes
) == self
._first
_local
:
161 return self
._nodes
[-1]._value
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
)
170 for part
in tail
.split('.'):
171 if not hasattr(found
, 'get'):
173 found
= found
.get(part
)
176 def Scope(self
, context
, fn
, *args
):
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
)
189 self
._value
_info
[key
] = info
190 found_node_list
, checked_node_set
= info
192 # Check all the nodes not yet checked for |key|.
194 for node
in reversed(self
._nodes
):
195 if node
in checked_node_set
:
197 value
= node
.Get(key
)
198 if value
is not None:
199 newly_found
.append(node
)
200 checked_node_set
.add(node
)
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
:
208 return found_node_list
[-1]._value
.get(key
)
210 class _Stack(object):
212 def __init__(self
, name
, id_
):
216 def __init__(self
, entries
=[]):
217 self
.entries
= entries
219 def Descend(self
, name
, id_
):
220 descended
= list(self
.entries
)
221 descended
.append(_Stack
.Entry(name
, id_
))
222 return _Stack(entries
=descended
)
224 class _InternalContext(object):
226 self
._render
_state
= None
228 def SetRenderState(self
, render_state
):
229 self
._render
_state
= render_state
233 errors
= self
._render
_state
._errors
234 return '\n'.join(errors
) if errors
else None
237 class _RenderState(object):
238 '''The state of a render call.
240 def __init__(self
, name
, contexts
, _stack
=_Stack()):
241 self
.text
= _StringBuilder()
242 self
.contexts
= contexts
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
)
255 self
._name
, self
.contexts
, _stack
=self
._stack
)
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_
))
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
)
271 return RenderResult(self
.text
.ToString(), self
._errors
);
273 class _Identifier(object):
274 '''An identifier of the form 'foo', 'foo.bar.baz', 'foo-bar.baz', etc.
276 _VALID_ID_MATCHER
= re
.compile(r
'^[a-zA-Z0-9@_/-]+$')
278 def __init__(self
, name
, line
, column
):
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())
288 def GetDescription(self
):
289 return '\'%s\' at line %s column %s' % (self
.name
, self
.line
, self
.column
)
291 def CreateResolutionErrorMessage(self
, name
, stack
=None):
292 message
= _StringBuilder()
293 message
.Append('Failed to resolve %s in %s\n' % (self
.GetDescription(),
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(),
299 return message
.ToString().strip()
307 class _Node(object): pass
309 class _LeafNode(_Node
):
310 def __init__(self
, start_line
, end_line
):
311 self
._start
_line
= start_line
312 self
._end
_line
= end_line
314 def StartsWithNewLine(self
):
317 def TrimStartingNewLine(self
):
320 def TrimEndingSpaces(self
):
323 def TrimEndingNewLine(self
):
326 def EndsWithEmptyLine(self
):
329 def GetStartLine(self
):
330 return self
._start
_line
332 def GetEndLine(self
):
333 return self
._end
_line
338 class _DecoratorNode(_Node
):
339 def __init__(self
, content
):
340 self
._content
= content
342 def StartsWithNewLine(self
):
343 return self
._content
.StartsWithNewLine()
345 def TrimStartingNewLine(self
):
346 self
._content
.TrimStartingNewLine()
348 def TrimEndingSpaces(self
):
349 return self
._content
.TrimEndingSpaces()
351 def TrimEndingNewLine(self
):
352 self
._content
.TrimEndingNewLine()
354 def EndsWithEmptyLine(self
):
355 return self
._content
.EndsWithEmptyLine()
357 def GetStartLine(self
):
358 return self
._content
.GetStartLine()
360 def GetEndLine(self
):
361 return self
._content
.GetEndLine()
364 return str(self
._content
)
369 class _InlineNode(_DecoratorNode
):
370 def __init__(self
, content
):
371 _DecoratorNode
.__init
__(self
, content
)
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', ''))
379 class _IndentedNode(_DecoratorNode
):
380 def __init__(self
, content
, indentation
):
381 _DecoratorNode
.__init
__(self
, content
)
382 self
._indent
_str
= ' ' * indentation
384 def Render(self
, render_state
):
385 if isinstance(self
._content
, _CommentNode
):
388 if len(text
) == 0: # avoid rendering a blank line
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
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
)
400 class _BlockNode(_DecoratorNode
):
401 def __init__(self
, content
):
402 _DecoratorNode
.__init
__(self
, content
)
403 content
.TrimStartingNewLine()
404 content
.TrimEndingSpaces()
406 def Render(self
, render_state
):
407 self
._content
.Render(render_state
)
409 class _NodeCollection(_Node
):
410 def __init__(self
, nodes
):
414 def Render(self
, render_state
):
415 for node
in self
._nodes
:
416 node
.Render(render_state
)
418 def StartsWithNewLine(self
):
419 return self
._nodes
[0].StartsWithNewLine()
421 def TrimStartingNewLine(self
):
422 self
._nodes
[0].TrimStartingNewLine()
424 def TrimEndingSpaces(self
):
425 return self
._nodes
[-1].TrimEndingSpaces()
427 def TrimEndingNewLine(self
):
428 self
._nodes
[-1].TrimEndingNewLine()
430 def EndsWithEmptyLine(self
):
431 return self
._nodes
[-1].EndsWithEmptyLine()
433 def GetStartLine(self
):
434 return self
._nodes
[0].GetStartLine()
436 def GetEndLine(self
):
437 return self
._nodes
[-1].GetEndLine()
440 return ''.join(str(node
) for node
in self
._nodes
)
442 class _StringNode(_Node
):
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
450 def Render(self
, render_state
):
451 render_state
.text
.Append(self
._string
)
453 def StartsWithNewLine(self
):
454 return self
._string
.startswith('\n')
456 def TrimStartingNewLine(self
):
457 if self
.StartsWithNewLine():
458 self
._string
= self
._string
[1:]
460 def TrimEndingSpaces(self
):
461 original_length
= len(self
._string
)
462 self
._string
= self
._string
[:self
._LastIndexOfSpaces
()]
463 return original_length
- len(self
._string
)
465 def TrimEndingNewLine(self
):
466 if self
._string
.endswith('\n'):
467 self
._string
= self
._string
[:len(self
._string
) - 1]
469 def EndsWithEmptyLine(self
):
470 index
= self
._LastIndexOfSpaces
()
471 return index
== 0 or self
._string
[index
- 1] == '\n'
473 def _LastIndexOfSpaces(self
):
474 index
= len(self
._string
)
475 while index
> 0 and self
._string
[index
- 1] == ' ':
479 def GetStartLine(self
):
480 return self
._start
_line
482 def GetEndLine(self
):
483 return self
._end
_line
488 class _EscapedVariableNode(_LeafNode
):
491 def __init__(self
, id_
):
492 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
495 def Render(self
, render_state
):
496 value
= render_state
.contexts
.Resolve(self
._id
.name
)
498 render_state
.AddResolutionError(self
._id
)
500 string
= value
if isinstance(value
, basestring
) else str(value
)
501 render_state
.text
.Append(string
.replace('&', '&')
502 .replace('<', '<')
503 .replace('>', '>'))
506 return '{{%s}}' % self
._id
508 class _UnescapedVariableNode(_LeafNode
):
511 def __init__(self
, id_
):
512 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
515 def Render(self
, render_state
):
516 value
= render_state
.contexts
.Resolve(self
._id
.name
)
518 render_state
.AddResolutionError(self
._id
)
520 string
= value
if isinstance(value
, basestring
) else str(value
)
521 render_state
.text
.Append(string
)
524 return '{{{%s}}}' % self
._id
526 class _CommentNode(_LeafNode
):
527 '''{{- This is a comment -}}
528 An empty placeholder node for correct indented rendering behaviour.
530 def __init__(self
, start_line
, end_line
):
531 _LeafNode
.__init
__(self
, start_line
, end_line
)
533 def Render(self
, render_state
):
539 class _SectionNode(_DecoratorNode
):
540 '''{{#var:foo}} ... {{/foo}}
542 def __init__(self
, bind_to
, id_
, content
):
543 _DecoratorNode
.__init
__(self
, content
)
544 self
._bind
_to
= bind_to
547 def Render(self
, render_state
):
548 value
= render_state
.contexts
.Resolve(self
._id
.name
)
549 if isinstance(value
, list):
551 if self
._bind
_to
is not None:
552 render_state
.contexts
.Scope({self
._bind
_to
.name
: item
},
553 self
._content
.Render
, render_state
)
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
)
561 render_state
.contexts
.Scope(value
, self
._content
.Render
, render_state
)
563 render_state
.AddResolutionError(self
._id
)
566 return '{{#%s}}%s{{/%s}}' % (
567 self
._id
, _DecoratorNode
.__repr
__(self
), self
._id
)
569 class _VertedSectionNode(_DecoratorNode
):
570 '''{{?var:foo}} ... {{/foo}}
572 def __init__(self
, bind_to
, id_
, content
):
573 _DecoratorNode
.__init
__(self
, content
)
574 self
._bind
_to
= bind_to
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
)
584 self
._content
.Render(render_state
)
587 return '{{?%s}}%s{{/%s}}' % (
588 self
._id
, _DecoratorNode
.__repr
__(self
), self
._id
)
591 def ShouldRender(value
):
594 if isinstance(value
, bool):
596 if isinstance(value
, list):
597 return len(value
) > 0
600 class _InvertedSectionNode(_DecoratorNode
):
601 '''{{^foo}} ... {{/foo}}
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'
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
)
616 return '{{^%s}}%s{{/%s}}' % (
617 self
._id
, _DecoratorNode
.__repr
__(self
), self
._id
)
619 class _AssertionNode(_LeafNode
):
620 '''{{!foo Some comment about foo}}
622 def __init__(self
, id_
, description
):
623 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
625 self
._description
= description
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
)
632 return '{{!%s %s}}' % (self
._id
, self
._description
)
634 class _JsonNode(_LeafNode
):
637 def __init__(self
, id_
):
638 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
641 def Render(self
, render_state
):
642 value
= render_state
.contexts
.Resolve(self
._id
.name
)
644 render_state
.AddResolutionError(self
._id
)
646 render_state
.text
.Append(json
.dumps(value
, separators
=(',',':')))
649 return '{{*%s}}' % self
._id
651 # TODO: Better common model of _PartialNodeWithArguments, _PartialNodeInContext,
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
664 def Render(self
, render_state
):
665 render_state
.contexts
.Scope(self
._args
, self
._partial
.Render
, render_state
)
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
678 def Render(self
, render_state
):
679 original_contexts
= render_state
.contexts
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
)
688 render_state
.contexts
= original_contexts
690 class _PartialNode(_LeafNode
):
691 '''{{+var:foo}} ... {{/foo}}
693 def __init__(self
, bind_to
, id_
, content
):
694 _LeafNode
.__init
__(self
, id_
.line
, id_
.line
)
695 self
._bind
_to
= bind_to
697 self
._content
= content
699 self
._pass
_through
_id
= None
702 def Inline(cls
, id_
):
703 return cls(None, id_
, None)
705 def Render(self
, render_state
):
706 value
= render_state
.contexts
.Resolve(self
._id
.name
)
708 render_state
.AddResolutionError(self
._id
)
710 if not isinstance(value
, (Handlebar
, _Node
)):
711 render_state
.AddResolutionError(self
._id
, description
='not a partial')
714 if isinstance(value
, Handlebar
):
715 node
, name
= value
._top
_node
, value
._name
717 node
, name
= value
, None
719 partial_render_state
= render_state
.ForkPartial(name
, self
._id
)
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
):
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
))
738 context
= render_state
.contexts
.Resolve(value
.name
)
739 if context
is not None:
740 resolved
[key
] = context
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
)
747 partial_render_state
.contexts
.Push(arg_context
)
749 node
.Render(partial_render_state
)
752 partial_render_state
,
753 text_transform
=lambda text
: text
[:-1] if text
.endswith('\n') else text
)
755 def SetArguments(self
, args
):
758 def PassThroughArgument(self
, id_
):
759 self
._pass
_through
_id
= id_
762 return '{{+%s}}' % self
._id
766 class _Token(object):
767 '''The tokens that can appear in a template.
770 def __init__(self
, name
, text
, clazz
):
776 def ElseNodeClass(self
):
777 if self
.clazz
== _VertedSectionNode
:
778 return _InvertedSectionNode
779 if self
.clazz
== _InvertedSectionNode
:
780 return _VertedSectionNode
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
)
798 'OPEN_JSON' , '{{*', _JsonNode
)
800 'OPEN_PARTIAL' , '{{+', _PartialNode
)
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)
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)
820 'CHARACTER' , '.' , None)
822 class _TokenStream(object):
823 '''Tokeniser for template parsing.
825 def __init__(self
, string
):
826 self
.next_token
= None
829 self
._string
= string
834 return self
.next_token
is not None
836 def NextCharacter(self
):
837 if self
.next_token
is _Token
.CHARACTER
:
838 return self
._string
[self
._cursor
- 1]
842 if self
._cursor
> 0 and self
._string
[self
._cursor
- 1] == '\n':
845 elif self
.next_token
is not None:
846 self
.next_column
+= len(self
.next_token
.text
)
848 self
.next_token
= None
850 if self
._cursor
== len(self
._string
):
852 assert self
._cursor
< len(self
._string
)
854 if (self
._cursor
+ 1 < len(self
._string
) and
855 self
._string
[self
._cursor
+ 1] in '{}'):
857 _TOKENS
.get(self
._string
[self
._cursor
:self
._cursor
+3]) or
858 _TOKENS
.get(self
._string
[self
._cursor
:self
._cursor
+2]))
860 if self
.next_token
is None:
861 self
.next_token
= _Token
.CHARACTER
863 self
._cursor
+= len(self
.next_token
.text
)
866 def AdvanceOver(self
, token
, description
=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
)
874 parse_error
+= ' %s' % description
or ''
875 raise ParseException(parse_error
)
876 return self
.Advance()
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()
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
):
895 end
= self
._cursor
- (len(self
.next_token
.text
) if self
.next_token
else 0)
896 return self
._string
[start
:end
]
898 def AdvanceToNextWhitespace(self
):
899 return self
.AdvanceOverNextString(excluded
=' \n\r\t')
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'):
908 return '%s(next_token=%s, remainder=%s)' % (type(self
).__name
__,
910 self
._string
[self
._cursor
:])
915 class Handlebar(object):
916 '''A handlebar template.
918 def __init__(self
, template
, name
=None):
919 self
.source
= template
921 tokens
= _TokenStream(template
)
922 self
._top
_node
= self
._ParseSection
(tokens
)
923 if not self
._top
_node
:
924 raise ParseException('Template is empty')
926 raise ParseException('There are still tokens remaining at %s, '
927 'was there an end-section without a start-section?' %
930 def _ParseSection(self
, tokens
):
932 while tokens
.HasNext():
933 if tokens
.next_token
in (_Token
.OPEN_END_SECTION
,
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.
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
,
944 nodes
+= self
._ParseNextOpenToken
(tokens
)
946 for i
, node
in enumerate(nodes
):
947 if isinstance(node
, _StringNode
):
950 previous_node
= nodes
[i
- 1] if i
> 0 else None
951 next_node
= nodes
[i
+ 1] if i
< len(nodes
) - 1 else None
954 if node
.GetStartLine() != node
.GetEndLine():
955 rendered_node
= _BlockNode(node
)
957 previous_node
.TrimEndingSpaces()
959 next_node
.TrimStartingNewLine()
960 elif ((not previous_node
or previous_node
.EndsWithEmptyLine()) and
961 (not next_node
or next_node
.StartsWithNewLine())):
964 indentation
= previous_node
.TrimEndingSpaces()
966 next_node
.TrimStartingNewLine()
967 rendered_node
= _IndentedNode(node
, indentation
)
969 rendered_node
= _InlineNode(node
)
971 nodes
[i
] = rendered_node
977 return _NodeCollection(nodes
)
979 def _ParseNextOpenToken(self
, tokens
):
980 next_token
= tokens
.next_token
982 if next_token
is _Token
.CHARACTER
:
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
,
990 # Inline nodes that don't take arguments.
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.
1001 id_
= self
._NextIdentifier
(tokens
)
1002 node
= next_token
.clazz(id_
, tokens
.AdvanceOverNextString())
1003 tokens
.AdvanceOver(_Token
.CLOSE_MUSTACHE
)
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 /}}.
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
)
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('*'):
1038 elif name
.startswith('+'):
1039 clazz
= _PartialNode
.Inline
1040 if clazz
is not _UnescapedVariableNode
:
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
)]
1052 tokens
.AdvanceOver(_Token
.CLOSE_MUSTACHE
)
1053 section
= self
._ParseSection
(tokens
)
1054 else_node_class
= next_token
.ElseNodeClass() # may not have one
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_
)
1062 if section
is not None:
1063 node
= next_token
.clazz(bind_to
, id_
, section
)
1065 node
.SetArguments(partial_args
)
1067 if else_section
is not None:
1068 nodes
.append(else_node_class(bind_to
, id_
, else_section
))
1070 elif next_token
is _Token
.OPEN_COMMENT
:
1072 start_line
= tokens
.next_line
1073 self
._AdvanceOverComment
(tokens
)
1074 return [_CommentNode(start_line
, tokens
.next_line
)]
1076 def _AdvanceOverComment(self
, tokens
):
1077 tokens
.AdvanceOver(_Token
.OPEN_COMMENT
)
1079 while tokens
.HasNext() and depth
> 0:
1080 if tokens
.next_token
is _Token
.OPEN_COMMENT
:
1082 elif tokens
.next_token
is _Token
.CLOSE_COMMENT
:
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
)
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
)
1103 def _ParsePartialNodeArgs(self
, tokens
):
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
}
1117 args
[key
] = self
._NextIdentifier
(tokens
)
1120 def _NextIdentifier(self
, tokens
):
1121 tokens
.SkipWhitespace()
1122 column_start
= tokens
.next_column
+ 1
1123 id_
= _Identifier(tokens
.AdvanceOverNextString(excluded
=' \n\r\t:()'),
1126 tokens
.SkipWhitespace()
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}}).
1133 internal_context
= _InternalContext()
1134 contexts
= list(user_contexts
)
1136 '_': internal_context
,
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()
1145 def render(self
, *contexts
):
1146 return self
.Render(*contexts
)
1148 def __eq__(self
, other
):
1149 return self
.source
== other
.source
and self
._name
== other
._name
1151 def __ne__(self
, other
):
1152 return not (self
== other
)
1155 return str('%s(%s)' % (type(self
).__name
__, self
._top
_node
))