| #!/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) |