mirror of
https://github.com/dart-lang/sdk
synced 2024-10-03 12:21:36 +00:00
151dc015c0
NNBD will require explicit casts in various locations in the dart:html libraries. To avoid taking on this overhead in the pre-nnbd dart:html library, syntax is introduced in the emitter to allow tokens with arguments. Test: emitter_test.py Change-Id: Ie6de714f491e0cda654f33ee389a91b765cacc9b Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/133333 Reviewed-by: Stephen Adams <sra@google.com>
282 lines
11 KiB
Python
Executable file
282 lines
11 KiB
Python
Executable file
#!/usr/bin/python
|
|
# 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 substitition."""
|
|
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 depdending 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 condtional 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)
|