OLD | NEW |
| (Empty) |
1 #!/usr/bin/python | |
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 | |
6 """Templating to help generate structured text.""" | |
7 | |
8 import logging | |
9 import re | |
10 | |
11 _logger = logging.getLogger('emitter') | |
12 | |
13 | |
14 def Format(template, **parameters): | |
15 """Create a string using the same template syntax as Emitter.Emit.""" | |
16 e = Emitter() | |
17 e._Emit(template, parameters) | |
18 return ''.join(e.Fragments()) | |
19 | |
20 | |
21 class Emitter(object): | |
22 """An Emitter collects string fragments to be assembled into a single string. | |
23 """ | |
24 | |
25 def __init__(self, bindings=None): | |
26 self._items = [] # A new list | |
27 self._bindings = bindings or Emitter.Frame({}, None) | |
28 | |
29 def EmitRaw(self, item): | |
30 """Emits literal string with no substitition.""" | |
31 self._items.append(item) | |
32 | |
33 def Emit(self, template_source, **parameters): | |
34 """Emits a template, substituting named parameters and returning emitters to | |
35 fill the named holes. | |
36 | |
37 Ordinary substitution occurs at $NAME or $(NAME). If there is no parameter | |
38 called NAME, the text is left as-is. So long as you don't bind FOO as a | |
39 parameter, $FOO in the template will pass through to the generated text. | |
40 | |
41 Substitution of $?NAME and $(?NAME) yields an empty string if NAME is not a | |
42 parameter. | |
43 | |
44 Values passed as named parameters should be strings or simple integral | |
45 values (int or long). | |
46 | |
47 Named holes are created at $!NAME or $(!NAME). A hole marks a position in | |
48 the template that may be filled in later. An Emitter is returned for each | |
49 named hole in the template. The holes are filled by emitting to the | |
50 corresponding emitter. | |
51 | |
52 Emit returns either a single Emitter if the template contains one hole or a | |
53 tuple of emitters for several holes, in the order that the holes occur in | |
54 the template. | |
55 | |
56 The emitters for the holes remember the parameters passed to the initial | |
57 call to Emit. Holes can be used to provide a binding context. | |
58 """ | |
59 return self._Emit(template_source, parameters) | |
60 | |
61 def _Emit(self, template_source, parameters): | |
62 """Implementation of Emit, with map in place of named parameters.""" | |
63 template = self._ParseTemplate(template_source) | |
64 | |
65 parameter_bindings = self._bindings.Extend(parameters) | |
66 | |
67 hole_names = template._holes | |
68 | |
69 if hole_names: | |
70 hole_map = {} | |
71 replacements = {} | |
72 for name in hole_names: | |
73 emitter = Emitter(parameter_bindings) | |
74 replacements[name] = emitter._items | |
75 hole_map[name] = emitter | |
76 full_bindings = parameter_bindings.Extend(replacements) | |
77 else: | |
78 full_bindings = parameter_bindings | |
79 | |
80 self._ApplyTemplate(template, full_bindings) | |
81 | |
82 # Return None, a singleton or tuple of the hole names. | |
83 if not hole_names: | |
84 return None | |
85 if len(hole_names) == 1: | |
86 return hole_map[hole_names[0]] | |
87 else: | |
88 return tuple(hole_map[name] for name in hole_names) | |
89 | |
90 def Fragments(self): | |
91 """Returns a list of all the string fragments emitted.""" | |
92 def _FlattenTo(item, output): | |
93 if isinstance(item, list): | |
94 for subitem in item: | |
95 _FlattenTo(subitem, output) | |
96 elif isinstance(item, Emitter.DeferredLookup): | |
97 value = item._environment.Lookup(item._lookup._name, | |
98 item._lookup._value_if_missing) | |
99 _FlattenTo(value, output) | |
100 else: | |
101 output.append(str(item)) | |
102 | |
103 output = [] | |
104 _FlattenTo(self._items, output) | |
105 return output | |
106 | |
107 def Bind(self, var, template_source, **parameters): | |
108 """Adds a binding for var to this emitter.""" | |
109 template = self._ParseTemplate(template_source) | |
110 if template._holes: | |
111 raise RuntimeError('Cannot have holes in Emitter.Bind') | |
112 bindings = self._bindings.Extend(parameters) | |
113 value = Emitter(bindings) | |
114 value._ApplyTemplate(template, bindings) | |
115 self._bindings = self._bindings.Extend({var: value._items}) | |
116 return value | |
117 | |
118 def _ParseTemplate(self, source): | |
119 """Converts the template string into a Template object.""" | |
120 # TODO(sra): Cache the parsing. | |
121 items = [] | |
122 holes = [] | |
123 | |
124 # Break source into a sequence of text fragments and substitution lookups. | |
125 pos = 0 | |
126 while True: | |
127 match = Emitter._SUBST_RE.search(source, pos) | |
128 if not match: | |
129 items.append(source[pos:]) | |
130 break | |
131 text_fragment = source[pos : match.start()] | |
132 if text_fragment: | |
133 items.append(text_fragment) | |
134 pos = match.end() | |
135 term = match.group() | |
136 name = match.group(1) or match.group(2) # $NAME and $(NAME) | |
137 if name: | |
138 item = Emitter.Lookup(name, term, term) | |
139 items.append(item) | |
140 continue | |
141 name = match.group(3) or match.group(4) # $!NAME and $(!NAME) | |
142 if name: | |
143 item = Emitter.Lookup(name, term, term) | |
144 items.append(item) | |
145 holes.append(name) | |
146 continue | |
147 name = match.group(5) or match.group(6) # $?NAME and $(?NAME) | |
148 if name: | |
149 item = Emitter.Lookup(name, term, '') | |
150 items.append(item) | |
151 holes.append(name) | |
152 continue | |
153 raise RuntimeError('Unexpected group') | |
154 | |
155 if len(holes) != len(set(holes)): | |
156 raise RuntimeError('Cannot have repeated holes %s' % holes) | |
157 return Emitter.Template(items, holes) | |
158 | |
159 _SUBST_RE = re.compile( | |
160 # $FOO $(FOO) $!FOO $(!FOO) $?FOO $(?FOO) | |
161 r'\$(\w+)|\$\((\w+)\)|\$!(\w+)|\$\(!(\w+)\)|\$\?(\w+)|\$\(\?(\w+)\)') | |
162 | |
163 def _ApplyTemplate(self, template, bindings): | |
164 """Emits the items from the parsed template.""" | |
165 result = [] | |
166 for item in template._items: | |
167 if isinstance(item, str): | |
168 if item: | |
169 result.append(item) | |
170 elif isinstance(item, Emitter.Lookup): | |
171 # Bind lookup to the current environment (bindings) | |
172 # TODO(sra): More space efficient to do direct lookup. | |
173 result.append(Emitter.DeferredLookup(item, bindings)) | |
174 else: | |
175 raise RuntimeError('Unexpected template element') | |
176 # Collected fragments are in a sublist, so self._items contains one element | |
177 # (sublist) per template application. | |
178 self._items.append(result) | |
179 | |
180 class Lookup(object): | |
181 """An element of a parsed template.""" | |
182 def __init__(self, name, original, default): | |
183 self._name = name | |
184 self._original = original | |
185 self._value_if_missing = default | |
186 | |
187 class DeferredLookup(object): | |
188 """A lookup operation that is deferred until final string generation.""" | |
189 # TODO(sra): A deferred lookup will be useful when we add expansions that | |
190 # have behaviour condtional on the contents, e.g. adding separators between | |
191 # a list of items. | |
192 def __init__(self, lookup, environment): | |
193 self._lookup = lookup | |
194 self._environment = environment | |
195 | |
196 class Template(object): | |
197 """A parsed template.""" | |
198 def __init__(self, items, holes): | |
199 self._items = items # strings and lookups | |
200 self._holes = holes | |
201 | |
202 | |
203 class Frame(object): | |
204 """A Frame is a set of bindings derived from a parent.""" | |
205 def __init__(self, map, parent): | |
206 self._map = map | |
207 self._parent = parent | |
208 | |
209 def Lookup(self, name, default): | |
210 if name in self._map: | |
211 return self._map[name] | |
212 if self._parent: | |
213 return self._parent.Lookup(name, default) | |
214 return default | |
215 | |
216 def Extend(self, map): | |
217 return Emitter.Frame(map, self) | |
OLD | NEW |