QUIC - enable persisting of QUICServerInfo (server config) to disk
[chromium-blink-merge.git] / third_party / handlebar / handlebar.py
blob7e06868e19380cfdcc4faa8c49f4620e4c0a5d1d
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. \{{, \{{-.
18 import json
19 import re
21 '''Handlebar templates are data binding templates more-than-loosely inspired by
22 ctemplate. Use like:
24 from handlebar import Handlebar
26 template = Handlebar('hello {{#foo bar/}} world')
27 input = {
28 'foo': [
29 { 'bar': 1 },
30 { 'bar': 2 },
31 { 'bar': 3 }
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):
41 def get(self, key):
42 return 10
43 print(Handlebar('hello {{world}}').render(CustomContext()).text)
45 will print 'hello 10'.
46 '''
48 class ParseException(Exception):
49 '''The exception thrown while parsing a template.
50 '''
51 def __init__(self, error):
52 Exception.__init__(self, error)
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
61 def __repr__(self):
62 return '%s(text=%s, errors=%s)' % (type(self).__name__,
63 self.text,
64 self.errors)
66 def __str__(self):
67 return repr(self)
69 class _StringBuilder(object):
70 '''Efficiently builds strings.
71 '''
72 def __init__(self):
73 self._buf = []
75 def __len__(self):
76 self._Collapse()
77 return len(self._buf[0])
79 def Append(self, string):
80 if not isinstance(string, basestring):
81 string = str(string)
82 self._buf.append(string)
84 def ToString(self):
85 self._Collapse()
86 return self._buf[0]
88 def _Collapse(self):
89 self._buf = [u''.join(self._buf)]
91 def __repr__(self):
92 return self.ToString()
94 def __str__(self):
95 return repr(self)
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.
104 def __init__(self, value):
105 self._value = value
106 self._value_has_get = hasattr(value, 'get')
107 self._found = {}
109 def GetKeys(self):
110 '''Returns the list of keys that |_value| contains.
112 return self._found.keys()
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).
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
128 def __repr__(self):
129 return 'Node(value=%s, found=%s)' % (self._value, self._found)
131 def __str__(self):
132 return repr(self)
134 def __init__(self, globals_):
135 '''Initializes with the initial global contexts, listed in order from most
136 to least important.
138 self._nodes = map(_Contexts._Node, globals_)
139 self._first_local = len(self._nodes)
140 self._value_info = {}
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
148 def Push(self, context):
149 self._nodes.append(_Contexts._Node(context))
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()
158 def FirstLocal(self):
159 if len(self._nodes) == self._first_local:
160 return None
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)
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
176 def Scope(self, context, fn, *args):
177 self.Push(context)
178 try:
179 return fn(*args)
180 finally:
181 self.Pop()
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
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)
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
208 return found_node_list[-1]._value.get(key)
210 class _Stack(object):
211 class Entry(object):
212 def __init__(self, name, id_):
213 self.name = name
214 self.id_ = 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):
225 def __init__(self):
226 self._render_state = None
228 def SetRenderState(self, render_state):
229 self._render_state = render_state
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
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
243 self._name = name
244 self._errors = []
245 self._stack = _stack
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)
253 def Copy(self):
254 return _RenderState(
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)
270 def GetResult(self):
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):
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())
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(),
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()
301 def __repr__(self):
302 return self.name
304 def __str__(self):
305 return repr(self)
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):
315 return False
317 def TrimStartingNewLine(self):
318 pass
320 def TrimEndingSpaces(self):
321 return 0
323 def TrimEndingNewLine(self):
324 pass
326 def EndsWithEmptyLine(self):
327 return False
329 def GetStartLine(self):
330 return self._start_line
332 def GetEndLine(self):
333 return self._end_line
335 def __str__(self):
336 return repr(self)
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()
363 def __repr__(self):
364 return str(self._content)
366 def __str__(self):
367 return repr(self)
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):
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)
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):
411 assert nodes
412 self._nodes = 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()
439 def __repr__(self):
440 return ''.join(str(node) for node in self._nodes)
442 class _StringNode(_Node):
443 '''Just a string.
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] == ' ':
476 index -= 1
477 return index
479 def GetStartLine(self):
480 return self._start_line
482 def GetEndLine(self):
483 return self._end_line
485 def __repr__(self):
486 return self._string
488 class _EscapedVariableNode(_LeafNode):
489 '''{{foo}}
491 def __init__(self, id_):
492 _LeafNode.__init__(self, id_.line, id_.line)
493 self._id = id_
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('<', '&lt;')
503 .replace('>', '&gt;'))
505 def __repr__(self):
506 return '{{%s}}' % self._id
508 class _UnescapedVariableNode(_LeafNode):
509 '''{{{foo}}}
511 def __init__(self, id_):
512 _LeafNode.__init__(self, id_.line, id_.line)
513 self._id = id_
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)
523 def __repr__(self):
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):
534 pass
536 def __repr__(self):
537 return '<comment>'
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
545 self._id = id_
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)
565 def __repr__(self):
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
575 self._id = id_
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)
586 def __repr__(self):
587 return '{{?%s}}%s{{/%s}}' % (
588 self._id, _DecoratorNode.__repr__(self), self._id)
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
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'
607 % (bind_to, id_))
608 self._id = id_
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)
615 def __repr__(self):
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)
624 self._id = id_
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)
631 def __repr__(self):
632 return '{{!%s %s}}' % (self._id, self._description)
634 class _JsonNode(_LeafNode):
635 '''{{*foo}}
637 def __init__(self, id_):
638 _LeafNode.__init__(self, id_.line, id_.line)
639 self._id = id_
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=(',',':')))
648 def __repr__(self):
649 return '{{*%s}}' % self._id
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
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
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
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
696 self._id = id_
697 self._content = content
698 self._args = None
699 self._pass_through_id = None
701 @classmethod
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)
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
714 if isinstance(value, Handlebar):
715 node, name = value._top_node, value._name
716 else:
717 node, name = value, None
719 partial_render_state = render_state.ForkPartial(name, self._id)
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)
749 node.Render(partial_render_state)
751 render_state.Merge(
752 partial_render_state,
753 text_transform=lambda text: text[:-1] if text.endswith('\n') else text)
755 def SetArguments(self, args):
756 self._args = args
758 def PassThroughArgument(self, id_):
759 self._pass_through_id = id_
761 def __repr__(self):
762 return '{{+%s}}' % self._id
764 _TOKENS = {}
766 class _Token(object):
767 '''The tokens that can appear in a template.
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
776 def ElseNodeClass(self):
777 if self.clazz == _VertedSectionNode:
778 return _InvertedSectionNode
779 if self.clazz == _InvertedSectionNode:
780 return _VertedSectionNode
781 return None
783 def __repr__(self):
784 return self.name
786 def __str__(self):
787 return repr(self)
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)
822 class _TokenStream(object):
823 '''Tokeniser for template parsing.
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()
833 def HasNext(self):
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]
839 return None
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)
848 self.next_token = None
850 if self._cursor == len(self._string):
851 return None
852 assert self._cursor < len(self._string)
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]))
860 if self.next_token is None:
861 self.next_token = _Token.CHARACTER
863 self._cursor += len(self.next_token.text)
864 return self
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()
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):
894 self.Advance()
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'):
905 self.Advance()
907 def __repr__(self):
908 return '%s(next_token=%s, remainder=%s)' % (type(self).__name__,
909 self.next_token,
910 self._string[self._cursor:])
912 def __str__(self):
913 return repr(self)
915 class Handlebar(object):
916 '''A handlebar template.
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)
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)
946 for i, node in enumerate(nodes):
947 if isinstance(node, _StringNode):
948 continue
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
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)
971 nodes[i] = rendered_node
973 if len(nodes) == 0:
974 return None
975 if len(nodes) == 1:
976 return nodes[0]
977 return _NodeCollection(nodes)
979 def _ParseNextOpenToken(self, tokens):
980 next_token = tokens.next_token
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)]
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()
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):
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
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_
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)
1135 contexts.append({
1136 '_': internal_context,
1137 'false': False,
1138 'true': True,
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)
1154 def __repr__(self):
1155 return str('%s(%s)' % (type(self).__name__, self._top_node))
1157 def __str__(self):
1158 return repr(self)