|  | #!/usr/bin/env python3 | 
|  | # Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file | 
|  | # for details. All rights reserved. Use of this source code is governed by a | 
|  | # BSD-style license that can be found in the LICENSE file. | 
|  | """Templating to help generate structured text.""" | 
|  |  | 
|  | import logging | 
|  | import re | 
|  |  | 
|  | _logger = logging.getLogger('emitter') | 
|  |  | 
|  |  | 
|  | def Format(template, **parameters): | 
|  | """Create a string using the same template syntax as Emitter.Emit.""" | 
|  | e = Emitter() | 
|  | e._Emit(template, parameters) | 
|  | return ''.join(e.Fragments()) | 
|  |  | 
|  |  | 
|  | class Emitter(object): | 
|  | """An Emitter collects string fragments to be assembled into a single string. | 
|  | """ | 
|  |  | 
|  | def __init__(self, bindings=None): | 
|  | self._items = []  # A new list | 
|  | self._bindings = bindings or Emitter.Frame({}, None) | 
|  |  | 
|  | def EmitRaw(self, item): | 
|  | """Emits literal string with no substitution.""" | 
|  | self._items.append(item) | 
|  |  | 
|  | def Emit(self, template_source, **parameters): | 
|  | """Emits a template, substituting named parameters and returning emitters to | 
|  | fill the named holes. | 
|  |  | 
|  | Ordinary substitution occurs at $NAME or $(NAME).  If there is no parameter | 
|  | called NAME, the text is left as-is. So long as you don't bind FOO as a | 
|  | parameter, $FOO in the template will pass through to the generated text. | 
|  |  | 
|  | Substitution of $?NAME and $(?NAME) yields an empty string if NAME is not a | 
|  | parameter. | 
|  |  | 
|  | Values passed as named parameters should be strings or simple integral | 
|  | values (int or long). | 
|  |  | 
|  | Named holes are created at $!NAME or $(!NAME).  A hole marks a position in | 
|  | the template that may be filled in later.  An Emitter is returned for each | 
|  | named hole in the template.  The holes are filled by emitting to the | 
|  | corresponding emitter. | 
|  |  | 
|  | Subtemplates can be created by using $#NAME(...), where text can be placed | 
|  | inside of the parentheses and will conditionally expand depending on if | 
|  | NAME is set to True or False. The text inside the parentheses may use | 
|  | further $#NAME and $NAME substitutions, but is not permitted to create | 
|  | holes. | 
|  |  | 
|  | Emit returns either a single Emitter if the template contains one hole or a | 
|  | tuple of emitters for several holes, in the order that the holes occur in | 
|  | the template. | 
|  |  | 
|  | The emitters for the holes remember the parameters passed to the initial | 
|  | call to Emit.  Holes can be used to provide a binding context. | 
|  | """ | 
|  | return self._Emit(template_source, parameters) | 
|  |  | 
|  | def _Emit(self, template_source, parameters): | 
|  | """Implementation of Emit, with map in place of named parameters.""" | 
|  | template = self._ParseTemplate(template_source) | 
|  | parameter_bindings = self._bindings.Extend(parameters) | 
|  |  | 
|  | hole_names = template._holes | 
|  |  | 
|  | if hole_names: | 
|  | hole_map = {} | 
|  | replacements = {} | 
|  | for name in hole_names: | 
|  | emitter = Emitter(parameter_bindings) | 
|  | replacements[name] = emitter._items | 
|  | hole_map[name] = emitter | 
|  | full_bindings = parameter_bindings.Extend(replacements) | 
|  | else: | 
|  | full_bindings = parameter_bindings | 
|  |  | 
|  | self._ApplyTemplate(template, full_bindings, self._items) | 
|  |  | 
|  | # Return None, a singleton or tuple of the hole names. | 
|  | if not hole_names: | 
|  | return None | 
|  | if len(hole_names) == 1: | 
|  | return hole_map[hole_names[0]] | 
|  | else: | 
|  | return tuple(hole_map[name] for name in hole_names) | 
|  |  | 
|  | def Fragments(self): | 
|  | """Returns a list of all the string fragments emitted.""" | 
|  |  | 
|  | def _FlattenTo(item, output): | 
|  | if isinstance(item, list): | 
|  | for subitem in item: | 
|  | _FlattenTo(subitem, output) | 
|  | elif isinstance(item, Emitter.DeferredLookup): | 
|  | value = item._environment.Lookup(item._lookup._name, | 
|  | item._lookup._value_if_missing) | 
|  | if item._lookup._subtemplate: | 
|  | _FlattenSubtemplate(item, value, output) | 
|  | else: | 
|  | _FlattenTo(value, output) | 
|  | else: | 
|  | output.append(str(item)) | 
|  |  | 
|  | def _FlattenSubtemplate(item, value, output): | 
|  | """Handles subtemplates created by $#NAME(...)""" | 
|  | if value is True: | 
|  | # Expand items in subtemplate | 
|  | _FlattenTo(item._lookup._subitems, output) | 
|  | elif value is not False: | 
|  | if value != item._lookup._value_if_missing: | 
|  | raise RuntimeError( | 
|  | 'Value for NAME in $#NAME(...) syntax must be a boolean' | 
|  | ) | 
|  | # Expand it into the string literal composed of $#NAME(, | 
|  | # the values inside the parentheses, and ). | 
|  | _FlattenTo(value, output) | 
|  | _FlattenTo(item._lookup._subitems, output) | 
|  | _FlattenTo(')', output) | 
|  |  | 
|  | output = [] | 
|  | _FlattenTo(self._items, output) | 
|  | return output | 
|  |  | 
|  | def Bind(self, var, template_source, **parameters): | 
|  | """Adds a binding for var to this emitter.""" | 
|  | template = self._ParseTemplate(template_source) | 
|  | if template._holes: | 
|  | raise RuntimeError('Cannot have holes in Emitter.Bind') | 
|  | bindings = self._bindings.Extend(parameters) | 
|  | value = Emitter(bindings) | 
|  | value._ApplyTemplate(template, bindings, self._items) | 
|  | self._bindings = self._bindings.Extend({var: value._items}) | 
|  | return value | 
|  |  | 
|  | def _ParseTemplate(self, source): | 
|  | """Converts the template string into a Template object.""" | 
|  | # TODO(sra): Cache the parsing. | 
|  | items = [] | 
|  | holes = [] | 
|  |  | 
|  | # Break source into a sequence of text fragments and substitution lookups. | 
|  | pos = 0 | 
|  | while True: | 
|  | match = Emitter._SUBST_RE.search(source, pos) | 
|  | if not match: | 
|  | items.append(source[pos:]) | 
|  | break | 
|  | text_fragment = source[pos:match.start()] | 
|  | if text_fragment: | 
|  | items.append(text_fragment) | 
|  | pos = match.end() | 
|  | term = match.group() | 
|  | name = match.group(1) or match.group(2)  # $NAME and $(NAME) | 
|  | if name: | 
|  | item = Emitter.Lookup(name, term, term) | 
|  | items.append(item) | 
|  | continue | 
|  | name = match.group(3) or match.group(4)  # $!NAME and $(!NAME) | 
|  | if name: | 
|  | item = Emitter.Lookup(name, term, term) | 
|  | items.append(item) | 
|  | holes.append(name) | 
|  | continue | 
|  | name = match.group(5) or match.group(6)  # $?NAME and $(?NAME) | 
|  | if name: | 
|  | item = Emitter.Lookup(name, term, '') | 
|  | items.append(item) | 
|  | holes.append(name) | 
|  | continue | 
|  | name = match.group(7)                    # $#NAME(...) | 
|  | if name: | 
|  | # Since it's possible for this to nest, find the matching right | 
|  | # paren for this left paren. | 
|  | paren_count = 1 | 
|  | curr_pos = pos | 
|  | while curr_pos < len(source): | 
|  | if source[curr_pos] == ')': | 
|  | paren_count -= 1 | 
|  | if paren_count == 0: | 
|  | break | 
|  | elif source[curr_pos] == '(': | 
|  | # Account for nested parentheses | 
|  | paren_count += 1 | 
|  | curr_pos += 1 | 
|  | if curr_pos == len(source): | 
|  | # No matching right paren, so not a lookup. Ignore and | 
|  | # continue. | 
|  | items.append(term) | 
|  | continue | 
|  | matched_template = self._ParseTemplate(source[pos:curr_pos]) | 
|  | if len(matched_template._holes) > 0: | 
|  | raise RuntimeError( | 
|  | '$#NAME syntax cannot contains holes in its arguments') | 
|  | item = Emitter.Lookup(name, term, term, matched_template) | 
|  | items.append(item) | 
|  | # Continue after the right paren | 
|  | pos = curr_pos + 1 | 
|  | continue | 
|  | raise RuntimeError('Unexpected group') | 
|  |  | 
|  | if len(holes) != len(set(holes)): | 
|  | raise RuntimeError('Cannot have repeated holes %s' % holes) | 
|  | return Emitter.Template(items, holes) | 
|  |  | 
|  | _SUBST_RE = re.compile( | 
|  | #  $FOO    $(FOO)      $!FOO    $(!FOO)      $?FOO     $(?FOO)       $#FOO( | 
|  | r'\$(\w+)|\$\((\w+)\)|\$!(\w+)|\$\(!(\w+)\)|\$\?(\w+)|\$\(\?(\w+)\)|\$#(\w+)\(' | 
|  | ) | 
|  |  | 
|  | def _ApplyTemplate(self, template, bindings, items_list): | 
|  | """Emits the items from the parsed template.""" | 
|  | result = [] | 
|  | for item in template._items: | 
|  | if isinstance(item, str): | 
|  | if item: | 
|  | result.append(item) | 
|  | elif isinstance(item, Emitter.Lookup): | 
|  | # Bind lookup to the current environment (bindings) | 
|  | # TODO(sra): More space efficient to do direct lookup. | 
|  | result.append(Emitter.DeferredLookup(item, bindings)) | 
|  | # If the item has a subtemplate, apply the subtemplate and save | 
|  | # the result in the item's subitems | 
|  | if item._subtemplate: | 
|  | self._ApplyTemplate(item._subtemplate, bindings, | 
|  | item._subitems) | 
|  | else: | 
|  | raise RuntimeError('Unexpected template element') | 
|  | # Collected fragments are in a sublist, so self._items contains one element | 
|  | # (sublist) per template application. | 
|  | items_list.append(result) | 
|  |  | 
|  | class Lookup(object): | 
|  | """An element of a parsed template.""" | 
|  |  | 
|  | def __init__(self, name, original, default, subtemplate=None): | 
|  | self._name = name | 
|  | self._original = original | 
|  | self._value_if_missing = default | 
|  | self._subtemplate = subtemplate | 
|  | self._subitems = [] | 
|  |  | 
|  | class DeferredLookup(object): | 
|  | """A lookup operation that is deferred until final string generation.""" | 
|  |  | 
|  | # TODO(sra): A deferred lookup will be useful when we add expansions that | 
|  | # have behaviour conditional on the contents, e.g. adding separators between | 
|  | # a list of items. | 
|  | def __init__(self, lookup, environment): | 
|  | self._lookup = lookup | 
|  | self._environment = environment | 
|  |  | 
|  | class Template(object): | 
|  | """A parsed template.""" | 
|  |  | 
|  | def __init__(self, items, holes): | 
|  | self._items = items  # strings and lookups | 
|  | self._holes = holes | 
|  |  | 
|  | class Frame(object): | 
|  | """A Frame is a set of bindings derived from a parent.""" | 
|  |  | 
|  | def __init__(self, map, parent): | 
|  | self._map = map | 
|  | self._parent = parent | 
|  |  | 
|  | def Lookup(self, name, default): | 
|  | if name in self._map: | 
|  | return self._map[name] | 
|  | if self._parent: | 
|  | return self._parent.Lookup(name, default) | 
|  | return default | 
|  |  | 
|  | def Extend(self, map): | 
|  | return Emitter.Frame(map, self) |