| 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 |
|
| 17 | JSON Template is a minimal and powerful templating language for transforming a
|
| 18 | JSON dictionary to arbitrary text.
|
| 19 |
|
| 20 | To use this module, you will typically use the Template constructor, and catch
|
| 21 | various exceptions thrown. You may also want to use the FromFile/FromString
|
| 22 | methods, which allow Template constructor options to be embedded in the template
|
| 23 | string itself.
|
| 24 |
|
| 25 | Other functions are exposed for tools which may want to process templates.
|
| 26 |
|
| 27 | Unicode
|
| 28 | -------
|
| 29 |
|
| 30 | JSON Template can work on unicode strings or byte strings. The template parser
|
| 31 | and expansion loop don't care which type they get.
|
| 32 |
|
| 33 | However, it's up to the caller to ensure that they get *compatible* types.
|
| 34 | Python auto-conversion can make this a bit confusing.
|
| 35 |
|
| 36 | If you have a byte string template and a dictionary with byte strings, expansion
|
| 37 | will work:
|
| 38 |
|
| 39 | 'Hello {name}' + {name: '\xb5'} -> 'Hello \xb5'
|
| 40 |
|
| 41 | If you have a unicode template and unicode data in the dictionary, it will work:
|
| 42 |
|
| 43 | u'Hello {name}' + {name: u'\u00B5'} -> u'Hello \u00B5'
|
| 44 |
|
| 45 | If you have a unicode template and byte string data, Python will try to decode
|
| 46 | the byte strings using the utf-8 encoding. This may not be possible, in which
|
| 47 | case you'll get a UnicodeDecodeError.
|
| 48 |
|
| 49 | u'Hello {name}' + {name: 'there'} -> 'Hello there'
|
| 50 | u'Hello {name}' + {name: '\xb5'} -> ERROR: \xb5 is not decodable as ASCII
|
| 51 |
|
| 52 | Mixing 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 |
|
| 68 | import StringIO
|
| 69 | import pprint
|
| 70 | import re
|
| 71 | import sys
|
| 72 |
|
| 73 | # For formatters
|
| 74 | import cgi # cgi.escape
|
| 75 | import time # for strftime
|
| 76 | import urllib # for urllib.encode
|
| 77 | import urlparse # for urljoin
|
| 78 |
|
| 79 |
|
| 80 | class 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 |
|
| 99 | class UsageError(Error):
|
| 100 | """Errors in using the API, not a result of compilation or evaluation."""
|
| 101 |
|
| 102 |
|
| 103 | class CompilationError(Error):
|
| 104 | """Base class for errors that happen during the compilation stage."""
|
| 105 |
|
| 106 |
|
| 107 | class 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 |
|
| 118 | class BadFormatter(CompilationError):
|
| 119 | """A bad formatter was specified, e.g. {variable|BAD}"""
|
| 120 |
|
| 121 | class BadPredicate(CompilationError):
|
| 122 | """A bad predicate was specified, e.g. {.BAD?}"""
|
| 123 |
|
| 124 | class MissingFormatter(CompilationError):
|
| 125 | """
|
| 126 | Raised when formatters are required, and a variable is missing a formatter.
|
| 127 | """
|
| 128 |
|
| 129 | class ConfigurationError(CompilationError):
|
| 130 | """
|
| 131 | Raised when the Template options are invalid and it can't even be compiled.
|
| 132 | """
|
| 133 |
|
| 134 | class TemplateSyntaxError(CompilationError):
|
| 135 | """Syntax error in the template text."""
|
| 136 |
|
| 137 | class 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.
|
| 147 | SIMPLE_FUNC, ENHANCED_FUNC = 0, 1
|
| 148 |
|
| 149 | # Templates are a third kind of function, but only for formatters currently
|
| 150 | TEMPLATE_FORMATTER = 2
|
| 151 |
|
| 152 |
|
| 153 | class 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 |
|
| 182 | def _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 |
|
| 193 | class 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 |
|
| 203 | class 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 |
|
| 213 | class 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 |
|
| 246 | class _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 |
|
| 267 | class _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 |
|
| 300 | class 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 |
|
| 316 | class _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 |
|
| 474 | class _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 |
|
| 492 | class _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 |
|
| 524 | class _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 |
|
| 532 | class _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 |
|
| 547 | class _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 |
|
| 559 | class _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 |
|
| 700 | def _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.
|
| 714 | def _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 |
|
| 721 | def _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 |
|
| 728 | def _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 |
|
| 742 | def _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 |
|
| 750 | def _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 |
|
| 826 | def _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 |
|
| 845 | def _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 |
|
| 851 | def _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 |
|
| 861 | def _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 |
|
| 870 | def _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 |
|
| 879 | def _IsDebugMode(unused_value, context, unused_args):
|
| 880 | return _TestAttribute(unused_value, context, ('debug',))
|
| 881 |
|
| 882 |
|
| 883 | def _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 |
|
| 895 | def _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 |
|
| 918 | def 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 |
|
| 934 | def 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 |
|
| 979 | COMMENT_BEGIN = '##BEGIN'
|
| 980 | COMMENT_END = '##END'
|
| 981 |
|
| 982 | OPTION_STRIP_LINE = '.OPTION strip-line'
|
| 983 | OPTION_END = '.END'
|
| 984 |
|
| 985 |
|
| 986 | def _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 |
|
| 1028 | def _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 |
|
| 1123 | def _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 |
|
| 1275 | def FromString(s, **kwargs):
|
| 1276 | """Like FromFile, but takes a string."""
|
| 1277 |
|
| 1278 | f = StringIO.StringIO(s)
|
| 1279 | return FromFile(f, **kwargs)
|
| 1280 |
|
| 1281 |
|
| 1282 | def 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 |
|
| 1349 | class 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 |
|
| 1528 | class 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 |
|
| 1552 | def _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 |
|
| 1572 | def 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 |
|
| 1595 | def 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 |
|
| 1615 | def _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 |
|
| 1648 | def _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 |
|
| 1661 | def _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 |
|
| 1681 | def _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 |
|
| 1688 | def _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 |
|
| 1749 | def _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 |
|
| 1779 | def 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 | ###
|
| 1792 | def _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 | ####
|
| 1806 | def 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 |
|
| 1820 | def 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)
|