Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
emitter.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5"""Templating to help generate structured text."""
6
7import logging
8import re
9
10_logger = logging.getLogger('emitter')
11
12
13def Format(template, **parameters):
14 """Create a string using the same template syntax as Emitter.Emit."""
15 e = Emitter()
16 e._Emit(template, parameters)
17 return ''.join(e.Fragments())
18
19
20class Emitter(object):
21 """An Emitter collects string fragments to be assembled into a single string.
22 """
23
24 def __init__(self, bindings=None):
25 self._items = [] # A new list
26 self._bindings = bindings or Emitter.Frame({}, None)
27
28 def EmitRaw(self, item):
29 """Emits literal string with no substitution."""
30 self._items.append(item)
31
32 def Emit(self, template_source, **parameters):
33 """Emits a template, substituting named parameters and returning emitters to
34 fill the named holes.
35
36 Ordinary substitution occurs at $NAME or $(NAME). If there is no parameter
37 called NAME, the text is left as-is. So long as you don't bind FOO as a
38 parameter, $FOO in the template will pass through to the generated text.
39
40 Substitution of $?NAME and $(?NAME) yields an empty string if NAME is not a
41 parameter.
42
43 Values passed as named parameters should be strings or simple integral
44 values (int or long).
45
46 Named holes are created at $!NAME or $(!NAME). A hole marks a position in
47 the template that may be filled in later. An Emitter is returned for each
48 named hole in the template. The holes are filled by emitting to the
49 corresponding emitter.
50
51 Subtemplates can be created by using $#NAME(...), where text can be placed
52 inside of the parentheses and will conditionally expand depending on if
53 NAME is set to True or False. The text inside the parentheses may use
54 further $#NAME and $NAME substitutions, but is not permitted to create
55 holes.
56
57 Emit returns either a single Emitter if the template contains one hole or a
58 tuple of emitters for several holes, in the order that the holes occur in
59 the template.
60
61 The emitters for the holes remember the parameters passed to the initial
62 call to Emit. Holes can be used to provide a binding context.
63 """
64 return self._Emit(template_source, parameters)
65
66 def _Emit(self, template_source, parameters):
67 """Implementation of Emit, with map in place of named parameters."""
68 template = self._ParseTemplate(template_source)
69 parameter_bindings = self._bindings.Extend(parameters)
70
71 hole_names = template._holes
72
73 if hole_names:
74 hole_map = {}
75 replacements = {}
76 for name in hole_names:
77 emitter = Emitter(parameter_bindings)
78 replacements[name] = emitter._items
79 hole_map[name] = emitter
80 full_bindings = parameter_bindings.Extend(replacements)
81 else:
82 full_bindings = parameter_bindings
83
84 self._ApplyTemplate(template, full_bindings, self._items)
85
86 # Return None, a singleton or tuple of the hole names.
87 if not hole_names:
88 return None
89 if len(hole_names) == 1:
90 return hole_map[hole_names[0]]
91 else:
92 return tuple(hole_map[name] for name in hole_names)
93
94 def Fragments(self):
95 """Returns a list of all the string fragments emitted."""
96
97 def _FlattenTo(item, output):
98 if isinstance(item, list):
99 for subitem in item:
100 _FlattenTo(subitem, output)
101 elif isinstance(item, Emitter.DeferredLookup):
102 value = item._environment.Lookup(item._lookup._name,
103 item._lookup._value_if_missing)
104 if item._lookup._subtemplate:
105 _FlattenSubtemplate(item, value, output)
106 else:
107 _FlattenTo(value, output)
108 else:
109 output.append(str(item))
110
111 def _FlattenSubtemplate(item, value, output):
112 """Handles subtemplates created by $#NAME(...)"""
113 if value is True:
114 # Expand items in subtemplate
115 _FlattenTo(item._lookup._subitems, output)
116 elif value is not False:
117 if value != item._lookup._value_if_missing:
118 raise RuntimeError(
119 'Value for NAME in $#NAME(...) syntax must be a boolean'
120 )
121 # Expand it into the string literal composed of $#NAME(,
122 # the values inside the parentheses, and ).
123 _FlattenTo(value, output)
124 _FlattenTo(item._lookup._subitems, output)
125 _FlattenTo(')', output)
126
127 output = []
128 _FlattenTo(self._items, output)
129 return output
130
131 def Bind(self, var, template_source, **parameters):
132 """Adds a binding for var to this emitter."""
133 template = self._ParseTemplate(template_source)
134 if template._holes:
135 raise RuntimeError('Cannot have holes in Emitter.Bind')
136 bindings = self._bindings.Extend(parameters)
137 value = Emitter(bindings)
138 value._ApplyTemplate(template, bindings, self._items)
139 self._bindings = self._bindings.Extend({var: value._items})
140 return value
141
142 def _ParseTemplate(self, source):
143 """Converts the template string into a Template object."""
144 # TODO(sra): Cache the parsing.
145 items = []
146 holes = []
147
148 # Break source into a sequence of text fragments and substitution lookups.
149 pos = 0
150 while True:
151 match = Emitter._SUBST_RE.search(source, pos)
152 if not match:
153 items.append(source[pos:])
154 break
155 text_fragment = source[pos:match.start()]
156 if text_fragment:
157 items.append(text_fragment)
158 pos = match.end()
159 term = match.group()
160 name = match.group(1) or match.group(2) # $NAME and $(NAME)
161 if name:
162 item = Emitter.Lookup(name, term, term)
163 items.append(item)
164 continue
165 name = match.group(3) or match.group(4) # $!NAME and $(!NAME)
166 if name:
167 item = Emitter.Lookup(name, term, term)
168 items.append(item)
169 holes.append(name)
170 continue
171 name = match.group(5) or match.group(6) # $?NAME and $(?NAME)
172 if name:
173 item = Emitter.Lookup(name, term, '')
174 items.append(item)
175 holes.append(name)
176 continue
177 name = match.group(7) # $#NAME(...)
178 if name:
179 # Since it's possible for this to nest, find the matching right
180 # paren for this left paren.
181 paren_count = 1
182 curr_pos = pos
183 while curr_pos < len(source):
184 if source[curr_pos] == ')':
185 paren_count -= 1
186 if paren_count == 0:
187 break
188 elif source[curr_pos] == '(':
189 # Account for nested parentheses
190 paren_count += 1
191 curr_pos += 1
192 if curr_pos == len(source):
193 # No matching right paren, so not a lookup. Ignore and
194 # continue.
195 items.append(term)
196 continue
197 matched_template = self._ParseTemplate(source[pos:curr_pos])
198 if len(matched_template._holes) > 0:
199 raise RuntimeError(
200 '$#NAME syntax cannot contains holes in its arguments')
201 item = Emitter.Lookup(name, term, term, matched_template)
202 items.append(item)
203 # Continue after the right paren
204 pos = curr_pos + 1
205 continue
206 raise RuntimeError('Unexpected group')
207
208 if len(holes) != len(set(holes)):
209 raise RuntimeError('Cannot have repeated holes %s' % holes)
210 return Emitter.Template(items, holes)
211
212 _SUBST_RE = re.compile(
213 # $FOO $(FOO) $!FOO $(!FOO) $?FOO $(?FOO) $#FOO(
214 r'\$(\w+)|\$\‍((\w+)\‍)|\$!(\w+)|\$\‍(!(\w+)\‍)|\$\?(\w+)|\$\‍(\?(\w+)\‍)|\$#(\w+)\‍('
215 )
216
217 def _ApplyTemplate(self, template, bindings, items_list):
218 """Emits the items from the parsed template."""
219 result = []
220 for item in template._items:
221 if isinstance(item, str):
222 if item:
223 result.append(item)
224 elif isinstance(item, Emitter.Lookup):
225 # Bind lookup to the current environment (bindings)
226 # TODO(sra): More space efficient to do direct lookup.
227 result.append(Emitter.DeferredLookup(item, bindings))
228 # If the item has a subtemplate, apply the subtemplate and save
229 # the result in the item's subitems
230 if item._subtemplate:
231 self._ApplyTemplate(item._subtemplate, bindings,
232 item._subitems)
233 else:
234 raise RuntimeError('Unexpected template element')
235 # Collected fragments are in a sublist, so self._items contains one element
236 # (sublist) per template application.
237 items_list.append(result)
238
239 class Lookup(object):
240 """An element of a parsed template."""
241
242 def __init__(self, name, original, default, subtemplate=None):
243 self._name = name
244 self._original = original
245 self._value_if_missing = default
246 self._subtemplate = subtemplate
247 self._subitems = []
248
249 class DeferredLookup(object):
250 """A lookup operation that is deferred until final string generation."""
251
252 # TODO(sra): A deferred lookup will be useful when we add expansions that
253 # have behaviour conditional on the contents, e.g. adding separators between
254 # a list of items.
255 def __init__(self, lookup, environment):
256 self._lookup = lookup
257 self._environment = environment
258
259 class Template(object):
260 """A parsed template."""
261
262 def __init__(self, items, holes):
263 self._items = items # strings and lookups
264 self._holes = holes
265
266 class Frame(object):
267 """A Frame is a set of bindings derived from a parent."""
268
269 def __init__(self, map, parent):
270 self._map = map
271 self._parent = parent
272
273 def Lookup(self, name, default):
274 if name in self._map:
275 return self._map[name]
276 if self._parent:
277 return self._parent.Lookup(name, default)
278 return default
279
280 def Extend(self, map):
281 return Emitter.Frame(map, self)
_Emit(self, template_source, parameters)
Definition emitter.py:66
_ParseTemplate(self, source)
Definition emitter.py:142
Bind(self, var, template_source, **parameters)
Definition emitter.py:131
Emit(self, template_source, **parameters)
Definition emitter.py:32
__init__(self, bindings=None)
Definition emitter.py:24
EmitRaw(self, item)
Definition emitter.py:28
static void append(char **dst, size_t *count, const char *src, size_t n)
Definition editor.cpp:211
Format(template, **parameters)
Definition emitter.py:13