OILS / vendor / jsontemplate.py View on Github | oilshell.org

1839 lines, 893 significant
1# Copyright (C) 2009 Andy Chu
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Python implementation of json-template.
16
17JSON Template is a minimal and powerful templating language for transforming a
18JSON dictionary to arbitrary text.
19
20To use this module, you will typically use the Template constructor, and catch
21various exceptions thrown. You may also want to use the FromFile/FromString
22methods, which allow Template constructor options to be embedded in the template
23string itself.
24
25Other functions are exposed for tools which may want to process templates.
26
27Unicode
28-------
29
30JSON Template can work on unicode strings or byte strings. The template parser
31and expansion loop don't care which type they get.
32
33However, it's up to the caller to ensure that they get *compatible* types.
34Python auto-conversion can make this a bit confusing.
35
36If you have a byte string template and a dictionary with byte strings, expansion
37will work:
38
39'Hello {name}' + {name: '\xb5'} -> 'Hello \xb5'
40
41If you have a unicode template and unicode data in the dictionary, it will work:
42
43u'Hello {name}' + {name: u'\u00B5'} -> u'Hello \u00B5'
44
45If you have a unicode template and byte string data, Python will try to decode
46the byte strings using the utf-8 encoding. This may not be possible, in which
47case you'll get a UnicodeDecodeError.
48
49u'Hello {name}' + {name: 'there'} -> 'Hello there'
50u'Hello {name}' + {name: '\xb5'} -> ERROR: \xb5 is not decodable as ASCII
51
52Mixing types may incur a performance penalty.
53"""
54
55__author__ = 'Andy Chu'
56
57__all__ = [
58 # Errors
59 'Error', 'CompilationError', 'EvaluationError', 'BadFormatter',
60 'BadPredicate', 'MissingFormatter', 'ConfigurationError',
61 'TemplateSyntaxError', 'UndefinedVariable',
62 # API
63 'FromString', 'FromFile', 'Template', 'expand', 'Trace', 'FunctionRegistry',
64 'MakeTemplateGroup',
65 # Function API
66 'SIMPLE_FUNC', 'ENHANCED_FUNC']
67
68import StringIO
69import pprint
70import re
71import sys
72
73# For formatters
74import cgi # cgi.escape
75import time # for strftime
76import urllib # for urllib.encode
77import urlparse # for urljoin
78
79
80class Error(Exception):
81 """Base class for all exceptions in this module.
82
83 Thus you can "except jsontemplate.Error: to catch all exceptions thrown by
84 this module.
85 """
86
87 def __str__(self):
88 """This helps people debug their templates.
89
90 If a variable isn't defined, then some context is shown in the traceback.
91 TODO: Attach context for other errors.
92 """
93 if hasattr(self, 'near'):
94 return '%s\n\nNear: %s' % (self.args[0], pprint.pformat(self.near))
95 else:
96 return self.args[0]
97
98
99class UsageError(Error):
100 """Errors in using the API, not a result of compilation or evaluation."""
101
102
103class CompilationError(Error):
104 """Base class for errors that happen during the compilation stage."""
105
106
107class EvaluationError(Error):
108 """Base class for errors that happen when expanding the template.
109
110 This class of errors generally involve the data dictionary or the execution of
111 the formatters.
112 """
113 def __init__(self, msg, original_exc_info=None):
114 Error.__init__(self, msg)
115 self.original_exc_info = original_exc_info
116
117
118class BadFormatter(CompilationError):
119 """A bad formatter was specified, e.g. {variable|BAD}"""
120
121class BadPredicate(CompilationError):
122 """A bad predicate was specified, e.g. {.BAD?}"""
123
124class MissingFormatter(CompilationError):
125 """
126 Raised when formatters are required, and a variable is missing a formatter.
127 """
128
129class ConfigurationError(CompilationError):
130 """
131 Raised when the Template options are invalid and it can't even be compiled.
132 """
133
134class TemplateSyntaxError(CompilationError):
135 """Syntax error in the template text."""
136
137class UndefinedVariable(EvaluationError):
138 """The template contains a variable name not defined by the data dictionary."""
139
140
141# The last group is of the form 'name | f1 | f2 ...'
142_SECTION_RE = re.compile(r'(repeated)?\s*section\s+(.+)')
143
144# Some formatters and predicates need to look up values in the whole context,
145# rather than just the current node. 'Node functions' start with a lowercase
146# letter; 'Context functions' start with any other character.
147SIMPLE_FUNC, ENHANCED_FUNC = 0, 1
148
149# Templates are a third kind of function, but only for formatters currently
150TEMPLATE_FORMATTER = 2
151
152
153class FunctionRegistry(object):
154 """Abstract class for looking up formatters or predicates at compile time.
155
156 Users should implement either Lookup or LookupWithType, and then the
157 implementation calls LookupWithType.
158 """
159
160 def Lookup(self, user_str):
161 """Lookup a function.
162
163 Args:
164 user_str: A raw string from the user, which may include uninterpreted
165 arguments. For example, 'pluralize person people' or 'test? admin'
166
167 Returns:
168 A 2-tuple of (function, args)
169 function: Callable that formats data as a string
170 args: Extra arguments to be passed to the function at expansion time
171 Should be None to pass NO arguments, since it can pass a 0-tuple too.
172 """
173 raise NotImplementedError
174
175 def LookupWithType(self, user_str):
176 func, args = self.Lookup(user_str)
177 # If users need the complexity of FunctionRegistry, then they get the
178 # 3-arguments formatter signature (value, context, args)
179 return func, args, ENHANCED_FUNC
180
181
182def _DecideFuncType(user_str):
183 """
184 By default, formatters/predicates which start with a non-lowercase letter take
185 contexts rather than just the cursor.
186 """
187 if user_str[0].islower():
188 return SIMPLE_FUNC
189 else:
190 return ENHANCED_FUNC
191
192
193class DictRegistry(FunctionRegistry):
194 """Look up functions in a simple dictionary."""
195
196 def __init__(self, func_dict):
197 self.func_dict = func_dict
198
199 def LookupWithType(self, user_str):
200 return self.func_dict.get(user_str), None, _DecideFuncType(user_str)
201
202
203class CallableRegistry(FunctionRegistry):
204 """Look up functions in a (higher-order) function."""
205
206 def __init__(self, func):
207 self.func = func
208
209 def LookupWithType(self, user_str):
210 return self.func(user_str), None, _DecideFuncType(user_str)
211
212
213class PrefixRegistry(FunctionRegistry):
214 """Lookup functions with arguments.
215
216 The function name is identified by a prefix. The character after the prefix,
217 usually a space, is considered the argument delimiter (similar to sed/perl's
218 s/foo/bar s|foo|bar syntax).
219 """
220
221 def __init__(self, functions):
222 """
223 Args:
224 functions: List of 2-tuples (prefix, function), e.g.
225 [('pluralize', _Pluralize), ('cycle', _Cycle)]
226 """
227 self.functions = functions
228
229 def Lookup(self, user_str):
230 for prefix, func in self.functions:
231 if user_str.startswith(prefix):
232 i = len(prefix)
233
234 # Usually a space, but could be something else
235 try:
236 splitchar = user_str[i]
237 except IndexError:
238 args = () # No arguments
239 else:
240 args = user_str.split(splitchar)[1:]
241
242 return func, args
243 return None, ()
244
245
246class _TemplateRef(object):
247 """A reference from one template to another.
248
249 The _TemplateRef sits statically in the program tree as one of the formatters.
250 At runtime, _DoSubstitute calls Resolve() with the group being used.
251 """
252 def __init__(self, name=None, template=None):
253 self.name = name
254 self.template = template # a template that's already been resolved
255
256 def Resolve(self, context):
257 if self.template:
258 return self.template
259 if context.group:
260 return context.group.get(self.name)
261 else:
262 raise EvaluationError(
263 "Couldn't find template with name %r (create a template group?)"
264 % self.name)
265
266
267class _TemplateRegistry(FunctionRegistry):
268 """Each template owns a _TemplateRegistry.
269
270 LookupWithType always returns a TemplateRef to the template compiler
271 (_ProgramBuilder). At runtime, which may be after MakeTemplateGroup is
272 called, the names can be resolved.
273 """
274 def __init__(self, owner):
275 """
276 Args:
277 owner: The Template instance that owns this formatter. (There should be
278 exactly one)
279 """
280 self.owner = owner
281
282 def LookupWithType(self, user_str):
283 """
284 Returns:
285 ref: Either a template instance (itself) or _TemplateRef
286 """
287 prefix = 'template '
288 ref = None # fail the lookup by default
289 if user_str.startswith(prefix):
290 name = user_str[len(prefix):]
291 if name == 'SELF':
292 # we can resolve this right away
293 ref = _TemplateRef(template=self.owner) # special value
294 else:
295 ref = _TemplateRef(name)
296
297 return ref, (), TEMPLATE_FORMATTER
298
299
300class ChainedRegistry(FunctionRegistry):
301 """Look up functions in chain of other FunctionRegistry instances."""
302
303 def __init__(self, registries):
304 self.registries = registries
305
306 def LookupWithType(self, user_str):
307 for registry in self.registries:
308 func, args, func_type = registry.LookupWithType(user_str)
309 if func:
310 return func, args, func_type
311
312 # Nothing found
313 return None, None, SIMPLE_FUNC
314
315
316class _ProgramBuilder(object):
317 """
318 Receives method calls from the parser, and constructs a tree of _Section()
319 instances.
320 """
321
322 def __init__(self, formatters, predicates, template_registry):
323 """
324 Args:
325 formatters: See docstring for _CompileTemplate
326 predicates: See docstring for _CompileTemplate
327 """
328 self.current_section = _Section(None)
329 self.stack = [self.current_section]
330
331 # Passing a dictionary or a function is often more convenient than making a
332 # FunctionRegistry
333 if isinstance(formatters, dict):
334 formatters = DictRegistry(formatters)
335 elif callable(formatters):
336 formatters = CallableRegistry(formatters)
337
338 # default formatters with arguments
339 default_formatters = PrefixRegistry([
340 ('pluralize', _Pluralize),
341 ('cycle', _Cycle),
342 # These have to go first because 'strftime' is a prefix of
343 # strftime-local/gm!
344 ('strftime-local', _StrftimeLocal), # local
345 ('strftime-gm', _StrftimeGm), # world
346 ('strftime', _StrftimeLocal), # local by default
347 ])
348
349 # First consult user formatters, then templates enabled by
350 # MakeTemplateGroup, then the default formatters
351 self.formatters = ChainedRegistry([
352 formatters,
353 template_registry, # returns _TemplateRef instances
354 DictRegistry(_DEFAULT_FORMATTERS),
355 default_formatters])
356
357 # Same for predicates
358 if isinstance(predicates, dict):
359 predicates = DictRegistry(predicates)
360 elif callable(predicates):
361 predicates = CallableRegistry(predicates)
362
363 # default predicates with arguments
364 default_predicates = PrefixRegistry([
365 ('test', _TestAttribute),
366 ('template', _TemplateExists),
367 ])
368
369 self.predicates = ChainedRegistry(
370 [predicates, DictRegistry(_DEFAULT_PREDICATES), default_predicates])
371
372 def Append(self, statement):
373 """
374 Args:
375 statement: Append a literal
376 """
377 self.current_section.Append(statement)
378
379 def _GetFormatter(self, format_str):
380 """
381 The user's formatters are consulted first, then the default formatters.
382 """
383 formatter, args, func_type = self.formatters.LookupWithType(format_str)
384 if formatter:
385 return formatter, args, func_type
386 else:
387 raise BadFormatter('%r is not a valid formatter' % format_str)
388
389 def _GetPredicate(self, pred_str, test_attr=False):
390 """
391 The user's predicates are consulted first, then the default predicates.
392 """
393 predicate, args, func_type = self.predicates.LookupWithType(pred_str)
394 if predicate:
395 pred = predicate, args, func_type
396 else:
397 # Nicer syntax, {.debug?} is shorthand for {.if test debug}.
398 # Currently there is not if/elif chain; just use
399 # {.if test debug} {.or test release} {.or} {.end}
400 if test_attr:
401 assert pred_str.endswith('?')
402 # func, args, func_type
403 pred = (_TestAttribute, (pred_str[:-1],), ENHANCED_FUNC)
404 else:
405 raise BadPredicate('%r is not a valid predicate' % pred_str)
406 return pred
407
408 def AppendSubstitution(self, name, formatters):
409 formatters = [self._GetFormatter(f) for f in formatters]
410 self.current_section.Append((_DoSubstitute, (name, formatters)))
411
412 def AppendTemplateSubstitution(self, name):
413 # {.template BODY} is semantically something like {$|template BODY}, where $
414 # is the root
415 formatters = [self._GetFormatter('template ' + name)]
416 # None as the name indicates we use the context.Root()
417 self.current_section.Append((_DoSubstitute, (None, formatters)))
418
419 def _NewSection(self, func, new_block):
420 self.current_section.Append((func, new_block))
421 self.stack.append(new_block)
422 self.current_section = new_block
423
424 def NewSection(self, token_type, section_name, pre_formatters):
425 """For sections or repeated sections."""
426 pre_formatters = [self._GetFormatter(f) for f in pre_formatters]
427
428 # TODO: Consider getting rid of this dispatching, and turn _Do* into methods
429 if token_type == REPEATED_SECTION_TOKEN:
430 new_block = _RepeatedSection(section_name, pre_formatters)
431 func = _DoRepeatedSection
432 elif token_type == SECTION_TOKEN:
433 new_block = _Section(section_name, pre_formatters)
434 func = _DoSection
435 elif token_type == DEF_TOKEN:
436 new_block = _Section(section_name, [])
437 func = _DoDef
438 else:
439 raise AssertionError('Invalid token type %s' % token_type)
440
441 self._NewSection(func, new_block)
442
443 def NewOrClause(self, pred_str):
444 """
445 {.or ...} Can appear inside predicate blocks or section blocks, with
446 slightly different meaning.
447 """
448 if pred_str:
449 pred = self._GetPredicate(pred_str, test_attr=False)
450 else:
451 pred = None
452 self.current_section.NewOrClause(pred)
453
454 def AlternatesWith(self):
455 self.current_section.AlternatesWith()
456
457 def NewPredicateSection(self, pred_str, test_attr=False):
458 """For chains of predicate clauses."""
459 pred = self._GetPredicate(pred_str, test_attr=test_attr)
460 block = _PredicateSection()
461 block.NewOrClause(pred)
462
463 self._NewSection(_DoPredicates, block)
464
465 def EndSection(self):
466 self.stack.pop()
467 self.current_section = self.stack[-1]
468
469 def Root(self):
470 # It's assumed that we call this at the end of the program
471 return self.current_section
472
473
474class _AbstractSection(object):
475
476 def __init__(self):
477 # Pairs of func, args, or a literal string
478 self.current_clause = []
479
480 def Append(self, statement):
481 """Append a statement to this block."""
482 self.current_clause.append(statement)
483
484 def AlternatesWith(self):
485 raise TemplateSyntaxError(
486 '{.alternates with} can only appear with in {.repeated section ...}')
487
488 def NewOrClause(self, pred_str):
489 raise NotImplementedError
490
491
492class _Section(_AbstractSection):
493 """Represents a (repeated) section."""
494
495 def __init__(self, section_name, pre_formatters=[]):
496 """
497 Args:
498 section_name: name given as an argument to the section, None for the root
499 section
500 pre_formatters: List of formatters to be applied to the data dictinoary
501 before the expansion
502 """
503 _AbstractSection.__init__(self)
504 self.section_name = section_name
505 self.pre_formatters = pre_formatters
506
507 # Clauses is just a string and a list of statements.
508 self.statements = {'default': self.current_clause}
509
510 def __repr__(self):
511 return '<Section %s>' % self.section_name
512
513 def Statements(self, clause='default'):
514 return self.statements.get(clause, [])
515
516 def NewOrClause(self, pred):
517 if pred:
518 raise TemplateSyntaxError(
519 '{.or} clause only takes a predicate inside predicate blocks')
520 self.current_clause = []
521 self.statements['or'] = self.current_clause
522
523
524class _RepeatedSection(_Section):
525 """Repeated section is like section, but it supports {.alternates with}"""
526
527 def AlternatesWith(self):
528 self.current_clause = []
529 self.statements['alternates with'] = self.current_clause
530
531
532class _PredicateSection(_AbstractSection):
533 """Represents a sequence of predicate clauses."""
534
535 def __init__(self):
536 _AbstractSection.__init__(self)
537 # List of func, statements
538 self.clauses = []
539
540 def NewOrClause(self, pred):
541 # {.or} always executes if reached
542 pred = pred or (lambda x: True, None, SIMPLE_FUNC) # 3-tuple
543 self.current_clause = []
544 self.clauses.append((pred, self.current_clause))
545
546
547class _Frame(object):
548 """A stack frame."""
549
550 def __init__(self, context, index=-1):
551 # Public attributes
552 self.context = context
553 self.index = index # An iteration index. -1 means we're NOT iterating.
554
555 def __str__(self):
556 return 'Frame %s (%s)' % (self.context, self.index)
557
558
559class _ScopedContext(object):
560 """Allows scoped lookup of variables.
561
562 If the variable isn't in the current context, then we search up the stack.
563 This object also stores the group.
564 """
565
566 def __init__(self, context, undefined_str, group=None):
567 """
568 Args:
569 context: The root context
570 undefined_str: See Template() constructor.
571 group: Used by the {.if template FOO} predicate, and _DoSubstitute
572 which is passed the context.
573 """
574 self.stack = [_Frame(context)]
575 self.undefined_str = undefined_str
576 self.group = group # used by _DoSubstitute?
577 self.root = context
578
579 def Root(self):
580 """For {.template FOO} substitution."""
581 return self.root
582
583 def HasTemplate(self, name):
584 if not self.group: # Could be None?
585 return False
586 return name in self.group
587
588 def PushSection(self, name, pre_formatters):
589 """Given a section name, push it on the top of the stack.
590
591 Returns:
592 The new section, or None if there is no such section.
593 """
594 if name == '@':
595 value = self.stack[-1].context
596 else:
597 top = self.stack[-1].context
598 try:
599 value = top.get(name)
600 except AttributeError: # no .get()
601 raise EvaluationError(
602 "Can't get name %r from top value %s" % (name, top))
603
604 # Apply pre-formatters
605 for i, (f, args, formatter_type) in enumerate(pre_formatters):
606 if formatter_type == ENHANCED_FUNC:
607 value = f(value, self, args)
608 elif formatter_type == SIMPLE_FUNC:
609 value = f(value)
610 else:
611 assert False, 'Invalid formatter type %r' % formatter_type
612
613 self.stack.append(_Frame(value))
614 return value
615
616 def Pop(self):
617 self.stack.pop()
618
619 def Next(self):
620 """Advance to the next item in a repeated section.
621
622 Raises:
623 StopIteration if there are no more elements
624 """
625 stacktop = self.stack[-1]
626
627 # Now we're iterating -- push a new mutable object onto the stack
628 if stacktop.index == -1:
629 stacktop = _Frame(None, index=0)
630 self.stack.append(stacktop)
631
632 context_array = self.stack[-2].context
633
634 if stacktop.index == len(context_array):
635 self.stack.pop()
636 raise StopIteration
637
638 stacktop.context = context_array[stacktop.index]
639 stacktop.index += 1
640
641 return True # OK, we mutated the stack
642
643 def _Undefined(self, name):
644 if self.undefined_str is None:
645 raise UndefinedVariable('%r is not defined' % name)
646 else:
647 return self.undefined_str
648
649 def _LookUpStack(self, name):
650 """Look up the stack for the given name."""
651 i = len(self.stack) - 1
652 while 1:
653 frame = self.stack[i]
654 if name == '@index':
655 if frame.index != -1: # -1 is undefined
656 return frame.index # @index is 1-based
657 else:
658 context = frame.context
659 if hasattr(context, 'get'): # Can't look up names in a list or atom
660 try:
661 return context[name]
662 except KeyError:
663 pass
664
665 i -= 1 # Next frame
666 if i <= -1: # Couldn't find it anywhere
667 return self._Undefined(name)
668
669 def Lookup(self, name):
670 """Get the value associated with a name in the current context.
671
672 The current context could be an dictionary in a list, or a dictionary
673 outside a list.
674
675 Args:
676 name: name to lookup, e.g. 'foo' or 'foo.bar.baz'
677
678 Returns:
679 The value, or self.undefined_str
680
681 Raises:
682 UndefinedVariable if self.undefined_str is not set
683 """
684 if name == '@':
685 return self.stack[-1].context
686
687 parts = name.split('.')
688 value = self._LookUpStack(parts[0])
689
690 # Now do simple lookups of the rest of the parts
691 for part in parts[1:]:
692 try:
693 value = value[part]
694 except (KeyError, TypeError): # TypeError for non-dictionaries
695 return self._Undefined(part)
696
697 return value
698
699
700def _ToString(x):
701 """The default default formatter!."""
702 # Some cross-language values for primitives. This is tested in
703 # jsontemplate_test.py.
704 if x is None:
705 return 'null'
706 if isinstance(x, basestring):
707 return x
708 return pprint.pformat(x)
709
710
711# NOTE: We could consider making formatters act on strings only avoid this
712# repetitiveness. But I wanted to leave open the possibility of doing stuff
713# like {number|increment-by 1}, where formatters take and return integers.
714def _Html(x):
715 # If it's not string or unicode, make it a string
716 if not isinstance(x, basestring):
717 x = str(x)
718 return cgi.escape(x)
719
720
721def _HtmlAttrValue(x):
722 # If it's not string or unicode, make it a string
723 if not isinstance(x, basestring):
724 x = str(x)
725 return cgi.escape(x, quote=True)
726
727
728def _AbsUrl(relative_url, context, unused_args):
729 """Returns an absolute URL, given the current node as a relative URL.
730
731 Assumes that the context has a value named 'base-url'. This is a little like
732 the HTML <base> tag, but implemented with HTML generation.
733
734 Raises:
735 UndefinedVariable if 'base-url' doesn't exist
736 """
737 # urljoin is flexible about trailing/leading slashes -- it will add or de-dupe
738 # them
739 return urlparse.urljoin(context.Lookup('base-url'), relative_url)
740
741
742def _Reverse(x):
743 """
744 We use this on lists as section pre-formatters; it probably works for
745 strings too.
746 """
747 return list(reversed(x))
748
749
750def _Pairs(data):
751 """dictionary -> list of pairs"""
752 keys = sorted(data)
753 return [{'@key': k, '@value': data[k]} for k in keys]
754
755
756# See http://google-ctemplate.googlecode.com/svn/trunk/doc/howto.html for more
757# escape types.
758#
759# Also, we might want to take a look at Django filters.
760#
761# This is a *public* constant, so that callers can use it construct their own
762# formatter lookup dictionaries, and pass them in to Template.
763_DEFAULT_FORMATTERS = {
764 # Because "html" is often the default formatter, we want to let
765 # numbers/boolean/etc. pass through, so we add 'str' first.
766 'html': _Html,
767
768 # The 'htmltag' name is deprecated. The html-attr-value name is preferred
769 # because it can be read with "as":
770 # {url|html-attr-value} means:
771 # "substitute 'url' as an HTML attribute value"
772 'html-attr-value': _HtmlAttrValue,
773 'htmltag': _HtmlAttrValue,
774
775 'raw': lambda x: x,
776 # Used for the length of a list. Can be used for the size of a dictionary
777 # too, though I haven't run into that use case.
778 'size': lambda value: str(len(value)),
779
780 # The argument is a dictionary, and we get a a=1&b=2 string back.
781 'url-params': lambda x: urllib.urlencode(x, doseq=True),
782
783 # The argument is an atom, and it takes 'Search query?' -> 'Search+query%3F'
784 'url-param-value': urllib.quote_plus, # param is an atom
785
786 # The default formatter, when no other default is specifier. For debugging,
787 # this could be lambda x: json.dumps(x, indent=2), but here we want to be
788 # compatible to Python 2.4.
789 'str': _ToString,
790 # Python-specific representation for safely debugging any value as an ASCII
791 # string (unicode can cause issues when using 'str')
792 'repr': repr,
793
794 'upper': lambda x: x.upper(),
795 'lower': lambda x: x.lower(),
796
797 # Just show a plain URL on an HTML page (without anchor text).
798 'plain-url': lambda x: '<a href="%s">%s</a>' % (
799 cgi.escape(x, quote=True), cgi.escape(x)),
800
801 # A context formatter
802 'AbsUrl': _AbsUrl,
803
804 # Placeholders for "standard names". We're not including them by default
805 # since they require additional dependencies. We can provide a part of the
806 # "lookup chain" in formatters.py for people people want the dependency.
807
808 # 'json' formats arbitrary data dictionary nodes as JSON strings. 'json'
809 # and 'js-string' are identical (since a JavaScript string *is* JSON). The
810 # latter is meant to be serve as extra documentation when you want a string
811 # argument only, which is a common case.
812 'json': None,
813 'js-string': None,
814
815 'reverse': _Reverse,
816
817 # Given a dictinonary, returns a *list* of key-value pairs. Used for
818 # section formatters. See jsontemplate_test.py for usage.
819 #
820 # The name "pairs" is meant to be language-independent, so it makes sense
821 # in JavaScript, etc.
822 'pairs': _Pairs,
823 }
824
825
826def _Pluralize(value, unused_context, args):
827 """Formatter to pluralize words."""
828
829 if len(args) == 0:
830 s, p = '', 's'
831 elif len(args) == 1:
832 s, p = '', args[0]
833 elif len(args) == 2:
834 s, p = args
835 else:
836 # Should have been checked at compile time
837 raise AssertionError
838
839 if value > 1:
840 return p
841 else:
842 return s
843
844
845def _Cycle(value, unused_context, args):
846 """Cycle between various values on consecutive integers."""
847 # @index starts from 1, so used 1-based indexing
848 return args[(value - 1) % len(args)]
849
850
851def _StrftimeHelper(args, time_tuple):
852 try:
853 format_str = args[0]
854 except IndexError:
855 # If no format string, use some reasonable text format
856 return time.asctime(time_tuple)
857 else:
858 return time.strftime(format_str, time_tuple)
859
860
861def _StrftimeGm(value, unused_context, args):
862 """Convert a timestamp in seconds to a string based on the format string.
863
864 Returns GM time.
865 """
866 time_tuple = time.gmtime(value)
867 return _StrftimeHelper(args, time_tuple)
868
869
870def _StrftimeLocal(value, unused_context, args):
871 """Convert a timestamp in seconds to a string based on the format string.
872
873 Returns local time.
874 """
875 time_tuple = time.localtime(value)
876 return _StrftimeHelper(args, time_tuple)
877
878
879def _IsDebugMode(unused_value, context, unused_args):
880 return _TestAttribute(unused_value, context, ('debug',))
881
882
883def _TestAttribute(unused_value, context, args):
884 """Cycle between various values on consecutive integers."""
885 try:
886 name = args[0] # can used dotted syntax too, e.g. 'foo.bar'
887 except IndexError:
888 raise EvaluationError('The "test" predicate requires an argument.')
889 try:
890 return bool(context.Lookup(name))
891 except UndefinedVariable:
892 return False
893
894
895def _TemplateExists(unused_value, context, args):
896 """Returns whether the given name is in the current Template's template group."""
897 try:
898 name = args[0]
899 except IndexError:
900 raise EvaluationError('The "template" predicate requires an argument.')
901 return context.HasTemplate(name)
902
903
904_SINGULAR = lambda x: x == 1
905_PLURAL = lambda x: x > 1
906
907_DEFAULT_PREDICATES = {
908 # OLD, for backward compatibility: these are discouraged
909 'singular?': _SINGULAR,
910 'plural?': _PLURAL,
911 'Debug?': _IsDebugMode, # Also OLD
912
913 'singular': _SINGULAR,
914 'plural': _PLURAL,
915 }
916
917
918def SplitMeta(meta):
919 """Split and validate metacharacters.
920
921 Example: '{}' -> ('{', '}')
922
923 This is public so the syntax highlighter and other tools can use it.
924 """
925 n = len(meta)
926 if n % 2 == 1:
927 raise ConfigurationError(
928 '%r has an odd number of metacharacters' % meta)
929 return meta[:n/2], meta[n/2:]
930
931
932_token_re_cache = {}
933
934def MakeTokenRegex(meta_left, meta_right):
935 """Return a (compiled) regular expression for tokenization.
936
937 Args:
938 meta_left, meta_right: e.g. '{' and '}'
939
940 - The regular expressions are memoized.
941 - This function is public so the syntax highlighter can use it.
942 """
943 key = meta_left, meta_right
944 if key not in _token_re_cache:
945 # - Need () grouping for re.split
946 # - The first character must be a non-space. This allows us to ignore
947 # literals like function() { return 1; } when
948 # - There must be at least one (non-space) character inside {}
949 _token_re_cache[key] = re.compile(
950 r'(' +
951 re.escape(meta_left) +
952 r'\S.*?' +
953 re.escape(meta_right) +
954 r')')
955 return _token_re_cache[key]
956
957
958# Examples:
959
960( LITERAL_TOKEN, # "Hi"
961 META_LITERAL_TOKEN, # {.space}, etc.
962
963 SUBST_TOKEN, # {var|html}
964 SECTION_TOKEN, # {.section name}
965 REPEATED_SECTION_TOKEN, # {.repeated section name}
966 PREDICATE_TOKEN, # {.predicate?}
967 IF_TOKEN, # {.if predicate}
968 ALTERNATES_TOKEN, # {.or}
969 OR_TOKEN, # {.or}
970 END_TOKEN, # {.end}
971
972 SUBST_TEMPLATE_TOKEN, # {.template TITLE}
973 DEF_TOKEN, # {.define TITLE}
974
975 COMMENT_BEGIN_TOKEN, # {##BEGIN}
976 COMMENT_END_TOKEN, # {##END}
977 ) = range(14)
978
979COMMENT_BEGIN = '##BEGIN'
980COMMENT_END = '##END'
981
982OPTION_STRIP_LINE = '.OPTION strip-line'
983OPTION_END = '.END'
984
985
986def _MatchDirective(token):
987 """Helper function for matching certain directives."""
988 # Tokens below must start with '.'
989 if token.startswith('.'):
990 token = token[1:]
991 else:
992 return None, None
993
994 if token == 'end':
995 return END_TOKEN, None
996
997 if token == 'alternates with':
998 return ALTERNATES_TOKEN, token
999
1000 if token.startswith('or'):
1001 if token.strip() == 'or':
1002 return OR_TOKEN, None
1003 else:
1004 pred_str = token[2:].strip()
1005 return OR_TOKEN, pred_str
1006
1007 match = _SECTION_RE.match(token)
1008 if match:
1009 repeated, section_name = match.groups()
1010 if repeated:
1011 return REPEATED_SECTION_TOKEN, section_name
1012 else:
1013 return SECTION_TOKEN, section_name
1014
1015 if token.startswith('template '):
1016 return SUBST_TEMPLATE_TOKEN, token[9:].strip()
1017 if token.startswith('define '):
1018 return DEF_TOKEN, token[7:].strip()
1019
1020 if token.startswith('if '):
1021 return IF_TOKEN, token[3:].strip()
1022 if token.endswith('?'):
1023 return PREDICATE_TOKEN, token
1024
1025 return None, None # no match
1026
1027
1028def _Tokenize(template_str, meta_left, meta_right, whitespace):
1029 """Yields tokens, which are 2-tuples (TOKEN_TYPE, token_string)."""
1030
1031 trimlen = len(meta_left)
1032 token_re = MakeTokenRegex(meta_left, meta_right)
1033 do_strip = (whitespace == 'strip-line') # Do this outside loop
1034 do_strip_part = False
1035
1036 for line in template_str.splitlines(True): # retain newlines
1037 if do_strip or do_strip_part:
1038 line = line.strip()
1039
1040 tokens = token_re.split(line)
1041
1042 # Check for a special case first. If a comment or "block" directive is on a
1043 # line by itself (with only space surrounding it), then the space is
1044 # omitted. For simplicity, we don't handle the case where we have 2
1045 # directives, say '{.end} # {#comment}' on a line.
1046 if len(tokens) == 3:
1047 # ''.isspace() == False, so work around that
1048 if (tokens[0].isspace() or not tokens[0]) and \
1049 (tokens[2].isspace() or not tokens[2]):
1050 token = tokens[1][trimlen : -trimlen]
1051
1052 # Check the ones that begin with ## before #
1053 if token == COMMENT_BEGIN:
1054 yield COMMENT_BEGIN_TOKEN, None
1055 continue
1056 if token == COMMENT_END:
1057 yield COMMENT_END_TOKEN, None
1058 continue
1059 if token == OPTION_STRIP_LINE:
1060 do_strip_part = True
1061 continue
1062 if token == OPTION_END:
1063 do_strip_part = False
1064 continue
1065
1066 if token.startswith('#'):
1067 continue # The whole line is omitted
1068
1069 token_type, token = _MatchDirective(token)
1070 if token_type is not None:
1071 yield token_type, token # Only yield the token, not space
1072 continue
1073
1074 # The line isn't special; process it normally.
1075 for i, token in enumerate(tokens):
1076 if i % 2 == 0:
1077 yield LITERAL_TOKEN, token
1078
1079 else: # It's a "directive" in metachracters
1080 assert token.startswith(meta_left), repr(token)
1081 assert token.endswith(meta_right), repr(token)
1082 token = token[trimlen : -trimlen]
1083
1084 # Check the ones that begin with ## before #
1085 if token == COMMENT_BEGIN:
1086 yield COMMENT_BEGIN_TOKEN, None
1087 continue
1088 if token == COMMENT_END:
1089 yield COMMENT_END_TOKEN, None
1090 continue
1091 if token == OPTION_STRIP_LINE:
1092 do_strip_part = True
1093 continue
1094 if token == OPTION_END:
1095 do_strip_part = False
1096 continue
1097
1098 # A single-line comment
1099 if token.startswith('#'):
1100 continue
1101
1102 if token.startswith('.'):
1103 literal = {
1104 '.meta-left': meta_left,
1105 '.meta-right': meta_right,
1106 '.space': ' ',
1107 '.tab': '\t',
1108 '.newline': '\n',
1109 }.get(token)
1110
1111 if literal is not None:
1112 yield META_LITERAL_TOKEN, literal
1113 continue
1114
1115 token_type, token = _MatchDirective(token)
1116 if token_type is not None:
1117 yield token_type, token
1118
1119 else: # Now we know the directive is a substitution.
1120 yield SUBST_TOKEN, token
1121
1122
1123def _CompileTemplate(
1124 template_str, builder, meta='{}', format_char='|', default_formatter='str',
1125 whitespace='smart'):
1126 """Compile the template string, calling methods on the 'program builder'.
1127
1128 Args:
1129 template_str: The template string. It should not have any compilation
1130 options in the header -- those are parsed by FromString/FromFile
1131
1132 builder: The interface of _ProgramBuilder isn't fixed. Use at your own
1133 risk.
1134
1135 meta: The metacharacters to use, e.g. '{}', '[]'.
1136
1137 default_formatter: The formatter to use for substitutions that are missing a
1138 formatter. The 'str' formatter the "default default" -- it just tries
1139 to convert the context value to a string in some unspecified manner.
1140
1141 whitespace: 'smart' or 'strip-line'. In smart mode, if a directive is alone
1142 on a line, with only whitespace on either side, then the whitespace is
1143 removed. In 'strip-line' mode, every line is stripped of its
1144 leading and trailing whitespace.
1145
1146 Returns:
1147 The compiled program (obtained from the builder)
1148
1149 Raises:
1150 The various subclasses of CompilationError. For example, if
1151 default_formatter=None, and a variable is missing a formatter, then
1152 MissingFormatter is raised.
1153
1154 This function is public so it can be used by other tools, e.g. a syntax
1155 checking tool run before submitting a template to source control.
1156 """
1157 meta_left, meta_right = SplitMeta(meta)
1158
1159 # : is meant to look like Python 3000 formatting {foo:.3f}. According to
1160 # PEP 3101, that's also what .NET uses.
1161 # | is more readable, but, more importantly, reminiscent of pipes, which is
1162 # useful for multiple formatters, e.g. {name|js-string|html}
1163 if format_char not in (':', '|'):
1164 raise ConfigurationError(
1165 'Only format characters : and | are accepted (got %r)' % format_char)
1166
1167 if whitespace not in ('smart', 'strip-line'):
1168 raise ConfigurationError('Invalid whitespace mode %r' % whitespace)
1169
1170 # If we go to -1, then we got too many {end}. If end at 1, then we're missing
1171 # an {end}.
1172 balance_counter = 0
1173 comment_counter = 0 # ditto for ##BEGIN/##END
1174
1175 has_defines = False
1176
1177 for token_type, token in _Tokenize(template_str, meta_left, meta_right,
1178 whitespace):
1179 if token_type == COMMENT_BEGIN_TOKEN:
1180 comment_counter += 1
1181 continue
1182 if token_type == COMMENT_END_TOKEN:
1183 comment_counter -= 1
1184 if comment_counter < 0:
1185 raise CompilationError('Got too many ##END markers')
1186 continue
1187 # Don't process any tokens
1188 if comment_counter > 0:
1189 continue
1190
1191 if token_type in (LITERAL_TOKEN, META_LITERAL_TOKEN):
1192 if token:
1193 builder.Append(token)
1194 continue
1195
1196 if token_type in (SECTION_TOKEN, REPEATED_SECTION_TOKEN, DEF_TOKEN):
1197 parts = [p.strip() for p in token.split(format_char)]
1198 if len(parts) == 1:
1199 name = parts[0]
1200 formatters = []
1201 else:
1202 name = parts[0]
1203 formatters = parts[1:]
1204 builder.NewSection(token_type, name, formatters)
1205 balance_counter += 1
1206 if token_type == DEF_TOKEN:
1207 has_defines = True
1208 continue
1209
1210 if token_type == PREDICATE_TOKEN:
1211 # {.attr?} lookups
1212 builder.NewPredicateSection(token, test_attr=True)
1213 balance_counter += 1
1214 continue
1215
1216 if token_type == IF_TOKEN:
1217 builder.NewPredicateSection(token, test_attr=False)
1218 balance_counter += 1
1219 continue
1220
1221 if token_type == OR_TOKEN:
1222 builder.NewOrClause(token)
1223 continue
1224
1225 if token_type == ALTERNATES_TOKEN:
1226 builder.AlternatesWith()
1227 continue
1228
1229 if token_type == END_TOKEN:
1230 balance_counter -= 1
1231 if balance_counter < 0:
1232 # TODO: Show some context for errors
1233 raise TemplateSyntaxError(
1234 'Got too many %send%s statements. You may have mistyped an '
1235 "earlier 'section' or 'repeated section' directive."
1236 % (meta_left, meta_right))
1237 builder.EndSection()
1238 continue
1239
1240 if token_type == SUBST_TOKEN:
1241 parts = [p.strip() for p in token.split(format_char)]
1242 if len(parts) == 1:
1243 if default_formatter is None:
1244 raise MissingFormatter('This template requires explicit formatters.')
1245 # If no formatter is specified, the default is the 'str' formatter,
1246 # which the user can define however they desire.
1247 name = token
1248 formatters = [default_formatter]
1249 else:
1250 name = parts[0]
1251 formatters = parts[1:]
1252
1253 builder.AppendSubstitution(name, formatters)
1254 continue
1255
1256 if token_type == SUBST_TEMPLATE_TOKEN:
1257 # no formatters
1258 builder.AppendTemplateSubstitution(token)
1259 continue
1260
1261 if balance_counter != 0:
1262 raise TemplateSyntaxError('Got too few %send%s statements' %
1263 (meta_left, meta_right))
1264 if comment_counter != 0:
1265 raise CompilationError('Got %d more {##BEGIN}s than {##END}s' % comment_counter)
1266
1267 return builder.Root(), has_defines
1268
1269
1270_OPTION_RE = re.compile(r'^([a-zA-Z\-]+):\s*(.*)')
1271_OPTION_NAMES = ['meta', 'format-char', 'default-formatter', 'undefined-str',
1272 'whitespace']
1273
1274
1275def FromString(s, **kwargs):
1276 """Like FromFile, but takes a string."""
1277
1278 f = StringIO.StringIO(s)
1279 return FromFile(f, **kwargs)
1280
1281
1282def FromFile(f, more_formatters=lambda x: None, more_predicates=lambda x: None,
1283 _constructor=None):
1284 """Parse a template from a file, using a simple file format.
1285
1286 This is useful when you want to include template options in a data file,
1287 rather than in the source code.
1288
1289 The format is similar to HTTP or E-mail headers. The first lines of the file
1290 can specify template options, such as the metacharacters to use. One blank
1291 line must separate the options from the template body.
1292
1293 Example:
1294
1295 default-formatter: none
1296 meta: {{}}
1297 format-char: :
1298 <blank line required>
1299 Template goes here: {{variable:html}}
1300
1301 Args:
1302 f: A file handle to read from. Caller is responsible for opening and
1303 closing it.
1304 """
1305 _constructor = _constructor or Template
1306
1307 options = {}
1308
1309 # Parse lines until the first one that doesn't look like an option
1310 while 1:
1311 line = f.readline()
1312 match = _OPTION_RE.match(line)
1313 if match:
1314 name, value = match.group(1), match.group(2)
1315
1316 # Accept something like 'Default-Formatter: raw'. This syntax is like
1317 # HTTP/E-mail headers.
1318 name = name.lower()
1319 # In Python 2.4, kwargs must be plain strings
1320 name = name.encode('utf-8')
1321
1322 if name in _OPTION_NAMES:
1323 name = name.replace('-', '_')
1324 value = value.strip()
1325 if name == 'default_formatter' and value.lower() == 'none':
1326 value = None
1327 options[name] = value
1328 else:
1329 break
1330 else:
1331 break
1332
1333 if options:
1334 if line.strip():
1335 raise CompilationError(
1336 'Must be one blank line between template options and body (got %r)'
1337 % line)
1338 body = f.read()
1339 else:
1340 # There were no options, so no blank line is necessary.
1341 body = line + f.read()
1342
1343 return _constructor(body,
1344 more_formatters=more_formatters,
1345 more_predicates=more_predicates,
1346 **options)
1347
1348
1349class Template(object):
1350 """Represents a compiled template.
1351
1352 Like many template systems, the template string is compiled into a program,
1353 and then it can be expanded any number of times. For example, in a web app,
1354 you can compile the templates once at server startup, and use the expand()
1355 method at request handling time. expand() uses the compiled representation.
1356
1357 There are various options for controlling parsing -- see _CompileTemplate.
1358 Don't go crazy with metacharacters. {}, [], {{}} or <> should cover nearly
1359 any circumstance, e.g. generating HTML, CSS XML, JavaScript, C programs, text
1360 files, etc.
1361 """
1362
1363 def __init__(self, template_str,
1364 more_formatters=lambda x: None,
1365 more_predicates=lambda x: None,
1366 undefined_str=None,
1367 **compile_options):
1368 """
1369 Args:
1370 template_str: The template string.
1371
1372 more_formatters:
1373 Something that can map format strings to formatter functions. One of:
1374 - A plain dictionary of names -> functions e.g. {'html': cgi.escape}
1375 - A higher-order function which takes format strings and returns
1376 formatter functions. Useful for when formatters have parsed
1377 arguments.
1378 - A FunctionRegistry instance, giving the most control. This allows
1379 formatters which takes contexts as well.
1380
1381 more_predicates:
1382 Like more_formatters, but for predicates.
1383
1384 undefined_str: A string to appear in the output when a variable to be
1385 substituted is missing. If None, UndefinedVariable is raised.
1386 (Note: This is not really a compilation option, because affects
1387 template expansion rather than compilation. Nonetheless we make it a
1388 constructor argument rather than an .expand() argument for
1389 simplicity.)
1390
1391 It also accepts all the compile options that _CompileTemplate does.
1392 """
1393 r = _TemplateRegistry(self)
1394 self.undefined_str = undefined_str
1395 self.group = {} # optionally updated by _UpdateTemplateGroup
1396 builder = _ProgramBuilder(more_formatters, more_predicates, r)
1397 # None used by _FromSection
1398 if template_str is not None:
1399 self._program, self.has_defines = _CompileTemplate(
1400 template_str, builder, **compile_options)
1401 self.group = _MakeGroupFromRootSection(self._program, self.undefined_str)
1402
1403 @staticmethod
1404 def _FromSection(section, group, undefined_str):
1405 t = Template(None, undefined_str=undefined_str)
1406 t._program = section
1407 t.has_defines = False
1408 # This "subtemplate" needs the group too for its own references
1409 t.group = group
1410 return t
1411
1412 def _Statements(self):
1413 # for execute_with_style
1414 return self._program.Statements()
1415
1416 def _UpdateTemplateGroup(self, group):
1417 """Allow this template to reference templates in the group.
1418
1419 Args:
1420 group: dictionary of template name -> compiled Template instance
1421 """
1422 # TODO: Re-enable when Poly is converted
1423 #if self.has_defines:
1424 # raise UsageError(
1425 # "Can't make a template group out of a template with {.define}.")
1426 bad = []
1427 for name in group:
1428 if name in self.group:
1429 bad.append(name)
1430 if bad:
1431 raise UsageError(
1432 "This template already has these named templates defined: %s" % bad)
1433 self.group.update(group)
1434
1435 def _CheckRefs(self):
1436 """Check that the template names referenced in this template exist."""
1437 # TODO: Implement this.
1438 # This is called by MakeTemplateGroup.
1439 # We would walk the program Statements() tree, look for name=None
1440 # substitutions, with a template formatter, and call Resolve().
1441
1442 #
1443 # Public API
1444 #
1445
1446 def execute(self, data_dict, callback, group=None, trace=None):
1447 """Low level method to expand the template piece by piece.
1448
1449 Args:
1450 data_dict: The JSON data dictionary.
1451 callback: A callback which should be called with each expanded token.
1452 group: Dictionary of name -> Template instance (for styles)
1453
1454 Example: You can pass 'f.write' as the callback to write directly to a file
1455 handle.
1456 """
1457 # First try the passed in version, then the one set by _UpdateTemplateGroup.
1458 # May be None. Only one of these should be set.
1459 group = group or self.group
1460 context = _ScopedContext(data_dict, self.undefined_str, group=group)
1461 _Execute(self._program.Statements(), context, callback, trace)
1462
1463 render = execute # Alias for backward compatibility
1464
1465 def expand(self, *args, **kwargs):
1466 """Expands the template with the given data dictionary, returning a string.
1467
1468 This is a small wrapper around execute(), and is the most convenient
1469 interface.
1470
1471 Args:
1472 data_dict: The JSON data dictionary. Like the builtin dict() constructor,
1473 it can take a single dictionary as a positional argument, or arbitrary
1474 keyword arguments.
1475 trace: Trace object for debugging
1476 style: Template instance to be treated as a style for this template (the
1477 "outside")
1478
1479 Returns:
1480 The return value could be a str() or unicode() instance, depending on the
1481 the type of the template string passed in, and what the types the strings
1482 in the dictionary are.
1483 """
1484 if args:
1485 if len(args) == 1:
1486 data_dict = args[0]
1487 trace = kwargs.get('trace')
1488 style = kwargs.get('style')
1489 group = kwargs.get('group')
1490 else:
1491 raise TypeError(
1492 'expand() only takes 1 positional argument (got %s)' % args)
1493 else:
1494 # NOTE: A problem with this style is that passing invalid kwargs is
1495 # silently ignored. It should be expand_with(foo=bar)
1496
1497 data_dict = kwargs
1498 trace = None # Can't use trace= with the kwargs style
1499 style = None
1500 group = None
1501
1502 # Try the argument first, then the thing set by MakeTemplateGroup
1503 g = group or self.group
1504
1505 tokens = []
1506 if style:
1507 style.execute(data_dict, tokens.append, group=g, trace=trace)
1508 else:
1509 # Needs a group to reference its OWN {.define}s
1510 self.execute(data_dict, tokens.append, group=g, trace=trace)
1511
1512 return JoinTokens(tokens)
1513
1514 def tokenstream(self, data_dict):
1515 """Yields a list of tokens resulting from expansion.
1516
1517 This may be useful for WSGI apps. NOTE: In the current implementation, the
1518 entire expanded template must be stored memory.
1519
1520 NOTE: This is a generator, but JavaScript doesn't have generators.
1521 """
1522 tokens = []
1523 self.execute(data_dict, tokens.append)
1524 for token in tokens:
1525 yield token
1526
1527
1528class Trace(object):
1529 """Trace of execution for JSON Template.
1530
1531 This object should be passed into the execute/expand() function.
1532
1533 Useful for debugging, especially for templates which reference other
1534 templates.
1535 """
1536 def __init__(self):
1537 # Public mutable attributes
1538 self.exec_depth = 0
1539 self.template_depth = 0
1540 self.stack = []
1541
1542 def Push(self, obj):
1543 self.stack.append(obj)
1544
1545 def Pop(self):
1546 self.stack.pop()
1547
1548 def __str__(self):
1549 return 'Trace %s %s' % (self.exec_depth, self.template_depth)
1550
1551
1552def _MakeGroupFromRootSection(root_section, undefined_str):
1553 """Construct a dictionary { template name -> Template() instance }
1554
1555 Args:
1556 root_section: _Section instance -- root of the original parse tree
1557 """
1558 group = {}
1559 for statement in root_section.Statements():
1560 if isinstance(statement, basestring):
1561 continue
1562 func, args = statement
1563 # here the function acts as ID for the block type
1564 if func is _DoDef and isinstance(args, _Section):
1565 section = args
1566 # Construct a Template instance from a this _Section subtree
1567 t = Template._FromSection(section, group, undefined_str)
1568 group[section.section_name] = t
1569 return group
1570
1571
1572def MakeTemplateGroup(group):
1573 """Wire templates together so that they can reference each other by name.
1574
1575 This is a public API.
1576
1577 The templates becomes formatters with the 'template' prefix. For example:
1578 {var|template NAME} formats the node 'var' with the template 'NAME'
1579
1580 Templates may be mutually recursive.
1581
1582 This function *mutates* all the templates, so you shouldn't call it multiple
1583 times on a single Template() instance. It's possible to put a single template
1584 in multiple groups by creating multiple Template() instances from it.
1585
1586 Args:
1587 group: dictionary of template name -> compiled Template instance
1588 """
1589 # mutate all of the templates so that they can reference each other
1590 for t in group.itervalues():
1591 t._UpdateTemplateGroup(group)
1592 #t._CheckRefs()
1593
1594
1595def JoinTokens(tokens):
1596 """Join tokens (which may be a mix of unicode and str values).
1597
1598 See notes on unicode at the top. This function allows mixing encoded utf-8
1599 byte string tokens with unicode tokens. (Python's default encoding is ASCII,
1600 and we don't want to change that.)
1601
1602 We also want to support pure byte strings, so we can't get rid of the
1603 try/except. Two tries necessary.
1604
1605 If someone really wanted to use another encoding, they could monkey patch
1606 jsontemplate.JoinTokens (this function).
1607 """
1608 try:
1609 return ''.join(tokens)
1610 except UnicodeDecodeError:
1611 # This can still raise UnicodeDecodeError if that data isn't utf-8.
1612 return ''.join(t.decode('utf-8') for t in tokens)
1613
1614
1615def _DoRepeatedSection(args, context, callback, trace):
1616 """{.repeated section foo}"""
1617
1618 block = args
1619
1620 items = context.PushSection(block.section_name, block.pre_formatters)
1621 if items:
1622 if not isinstance(items, list):
1623 raise EvaluationError('Expected a list; got %s' % type(items))
1624
1625 last_index = len(items) - 1
1626 statements = block.Statements()
1627 alt_statements = block.Statements('alternates with')
1628 try:
1629 i = 0
1630 while True:
1631 context.Next()
1632 # Execute the statements in the block for every item in the list.
1633 # Execute the alternate block on every iteration except the last. Each
1634 # item could be an atom (string, integer, etc.) or a dictionary.
1635 _Execute(statements, context, callback, trace)
1636 if i != last_index:
1637 _Execute(alt_statements, context, callback, trace)
1638 i += 1
1639 except StopIteration:
1640 pass
1641
1642 else:
1643 _Execute(block.Statements('or'), context, callback, trace)
1644
1645 context.Pop()
1646
1647
1648def _DoSection(args, context, callback, trace):
1649 """{.section foo}"""
1650 block = args
1651 # If a section present and "true", push the dictionary onto the stack as the
1652 # new context, and show it
1653 if context.PushSection(block.section_name, block.pre_formatters):
1654 _Execute(block.Statements(), context, callback, trace)
1655 context.Pop()
1656 else: # missing or "false" -- show the {.or} section
1657 context.Pop()
1658 _Execute(block.Statements('or'), context, callback, trace)
1659
1660
1661def _DoPredicates(args, context, callback, trace):
1662 """{.predicate?}
1663
1664 Here we execute the first clause that evaluates to true, and then stop.
1665 """
1666 block = args
1667 value = context.Lookup('@')
1668 for (predicate, args, func_type), statements in block.clauses:
1669 if func_type == ENHANCED_FUNC:
1670 do_clause = predicate(value, context, args)
1671 else:
1672 do_clause = predicate(value)
1673
1674 if do_clause:
1675 if trace: trace.Push(predicate)
1676 _Execute(statements, context, callback, trace)
1677 if trace: trace.Pop()
1678 break
1679
1680
1681def _DoDef(args, context, callback, trace):
1682 """{.define TITLE}"""
1683 # We do nothing here -- the block is parsed into the template tree, turned
1684 # into a Template() instance, and then the template is called as a formatter
1685 # in _DoSubstitute.
1686
1687
1688def _DoSubstitute(args, context, callback, trace):
1689 """Variable substitution, i.e. {foo}
1690
1691 We also implement template formatters here, i.e. {foo|template bar} as well
1692 as {.template FOO} for templates that operate on the root of the data dict
1693 rather than a subtree.
1694 """
1695 name, formatters = args
1696
1697 if name is None:
1698 value = context.Root() # don't use the cursor
1699 else:
1700 try:
1701 value = context.Lookup(name)
1702 except TypeError, e:
1703 raise EvaluationError(
1704 'Error evaluating %r in context %r: %r' % (name, context, e))
1705
1706 last_index = len(formatters) - 1
1707 for i, (f, args, formatter_type) in enumerate(formatters):
1708 try:
1709 if formatter_type == TEMPLATE_FORMATTER:
1710 template = f.Resolve(context)
1711 if i == last_index:
1712 # In order to keep less template output in memory, we can just let the
1713 # other template write to our callback directly, and then stop.
1714 template.execute(value, callback, trace=trace)
1715 return # EARLY RETURN
1716 else:
1717 # We have more formatters to apply, so explicitly construct 'value'
1718 tokens = []
1719 template.execute(value, tokens.append, trace=trace)
1720 value = JoinTokens(tokens)
1721
1722 elif formatter_type == ENHANCED_FUNC:
1723 value = f(value, context, args)
1724
1725 elif formatter_type == SIMPLE_FUNC:
1726 value = f(value)
1727
1728 else:
1729 assert False, 'Invalid formatter type %r' % formatter_type
1730
1731 except (KeyboardInterrupt, EvaluationError):
1732 # Don't "wrap" recursive EvaluationErrors
1733 raise
1734
1735 except Exception, e:
1736 if formatter_type == TEMPLATE_FORMATTER:
1737 raise # in this case we want to see the original exception
1738 raise EvaluationError(
1739 'Formatting name %r, value %r with formatter %s raised exception: %r '
1740 '-- see e.original_exc_info' % (name, value, f, e),
1741 original_exc_info=sys.exc_info())
1742
1743 # TODO: Require a string/unicode instance here?
1744 if value is None:
1745 raise EvaluationError('Evaluating %r gave None value' % name)
1746 callback(value)
1747
1748
1749def _Execute(statements, context, callback, trace):
1750 """Execute a bunch of template statements in a ScopedContext.
1751
1752 Args:
1753 callback: Strings are "written" to this callback function.
1754 trace: Trace object, or None
1755
1756 This is called in a mutually recursive fashion.
1757 """
1758 # Every time we call _Execute, increase this depth
1759 if trace:
1760 trace.exec_depth += 1
1761 for i, statement in enumerate(statements):
1762 if isinstance(statement, basestring):
1763 callback(statement)
1764 else:
1765 # In the case of a substitution, args is a pair (name, formatters).
1766 # In the case of a section, it's a _Section instance.
1767 try:
1768 func, args = statement
1769 func(args, context, callback, trace)
1770 except UndefinedVariable, e:
1771 # Show context for statements
1772 start = max(0, i-3)
1773 end = i+3
1774 e.near = statements[start:end]
1775 e.trace = trace # Attach caller's trace (could be None)
1776 raise
1777
1778
1779def expand(template_str, dictionary, **kwargs):
1780 """Free function to expands a template string with a data dictionary.
1781
1782 This is useful for cases where you don't care about saving the result of
1783 compilation (similar to re.match('.*', s) vs DOT_STAR.match(s))
1784 """
1785 t = Template(template_str, **kwargs)
1786 return t.expand(dictionary)
1787
1788
1789###
1790# TODO: DELETE
1791###
1792def _FlattenToCallback(tokens, callback):
1793 """Takes a nested list structure and flattens it.
1794
1795 ['a', ['b', 'c']] -> callback('a'); callback('b'); callback('c');
1796 """
1797 for t in tokens:
1798 if isinstance(t, basestring):
1799 callback(t)
1800 else:
1801 _FlattenToCallback(t, callback)
1802
1803###
1804# TODO: DELETE execute_with_style_LEGACY after old apps cleaned up
1805####
1806def execute_with_style_LEGACY(template, style, data, callback, body_subtree='body'):
1807 """OBSOLETE old API."""
1808 try:
1809 body_data = data[body_subtree]
1810 except KeyError:
1811 raise EvaluationError('Data dictionary has no subtree %r' % body_subtree)
1812 tokens_body = []
1813 template.execute(body_data, tokens_body.append)
1814 data[body_subtree] = tokens_body
1815 tokens = []
1816 style.execute(data, tokens.append)
1817 _FlattenToCallback(tokens, callback)
1818
1819
1820def expand_with_style(template, style, data, body_subtree='body'):
1821 """Expand a data dictionary with a template AND a style.
1822
1823 DEPRECATED -- Remove this entire function in favor of expand(d, style=style)
1824
1825 A style is a Template instance that factors out the common strings in several
1826 "body" templates.
1827
1828 Args:
1829 template: Template instance for the inner "page content"
1830 style: Template instance for the outer "page style"
1831 data: Data dictionary, with a 'body' key (or body_subtree
1832 """
1833 if template.has_defines:
1834 return template.expand(data, style=style)
1835 else:
1836 tokens = []
1837 execute_with_style_LEGACY(template, style, data, tokens.append,
1838 body_subtree=body_subtree)
1839 return JoinTokens(tokens)