| OLD | NEW |
| 1 # Copyright 2012 Benjamin Kalman | 1 # Copyright 2012 Benjamin Kalman |
| 2 # | 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. | 4 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at | 5 # You may obtain a copy of the License at |
| 6 # | 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # | 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. | 13 # limitations under the License. |
| 14 | 14 |
| 15 # TODO: Some character other than {{{ }}} to print unescaped content? |
| 16 # TODO: Only have @ while in a loop, and only defined in the top context of |
| 17 # the loop. |
| 18 # TODO: Consider trimming spaces around identifers like {{?t foo}}. |
| 19 # TODO: Only transfer global contexts into partials, not the top local. |
| 20 # TODO: Pragmas for asserting the presence of variables. |
| 21 # TODO: Escaping control characters somehow. e.g. \{{, \{{-. |
| 22 # TODO: Dump warnings-so-far into the output. |
| 23 |
| 15 import json | 24 import json |
| 16 import re | 25 import re |
| 17 | 26 |
| 18 """ Handlebar templates are mostly-logicless templates inspired by ctemplate | 27 '''Handlebar templates are data binding templates more-than-loosely inspired by |
| 19 (or more specifically mustache templates) then taken in their own direction | 28 ctemplate. Use like: |
| 20 because I found those to be inadequate. | |
| 21 | 29 |
| 22 from handlebar import Handlebar | 30 from handlebar import Handlebar |
| 23 | 31 |
| 24 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') | 32 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') |
| 25 input = { | 33 input = { |
| 26 'foo': [ | 34 'foo': [ |
| 27 { 'bar': 1 }, | 35 { 'bar': 1 }, |
| 28 { 'bar': 2 }, | 36 { 'bar': 2 }, |
| 29 { 'bar': 3 } | 37 { 'bar': 3 } |
| 30 ] | 38 ] |
| 31 } | 39 } |
| 32 print(template.render(input).text) | 40 print(template.render(input).text) |
| 33 | 41 |
| 34 Handlebar will use get() on contexts to return values, so to create custom | 42 Handlebar will use get() on contexts to return values, so to create custom |
| 35 getters (e.g. something that populates values lazily from keys) just add | 43 getters (for example, something that populates values lazily from keys), just |
| 36 a get() method. | 44 provide an object with a get() method. |
| 37 | 45 |
| 38 class CustomContext(object): | 46 class CustomContext(object): |
| 39 def get(self, key): | 47 def get(self, key): |
| 40 return 10 | 48 return 10 |
| 49 print(Handlebar('hello {{world}}').render(CustomContext()).text) |
| 41 | 50 |
| 42 # Any time {{ }} is used, will fill it with 10. | 51 will print 'hello 10'. |
| 43 print(Handlebar('hello {{world}}').render(CustomContext()).text) | 52 ''' |
| 44 """ | |
| 45 | 53 |
| 46 class ParseException(Exception): | 54 class ParseException(Exception): |
| 47 """ Exception thrown while parsing the template. | 55 '''The exception thrown while parsing a template. |
| 48 """ | 56 ''' |
| 49 def __init__(self, error, line): | 57 def __init__(self, error): |
| 50 Exception.__init__(self, "%s (line %s)" % (error, line.number)) | 58 Exception.__init__(self, error) |
| 51 | 59 |
| 52 class RenderResult(object): | 60 class RenderResult(object): |
| 53 """ Result of a render operation. | 61 '''The result of a render operation. |
| 54 """ | 62 ''' |
| 55 def __init__(self, text, errors): | 63 def __init__(self, text, errors): |
| 56 self.text = text; | 64 self.text = text; |
| 57 self.errors = errors | 65 self.errors = errors |
| 58 | 66 |
| 59 class StringBuilder(object): | 67 def __str__(self): |
| 60 """ Mimics Java's StringBuilder for easy porting from the Java version of | 68 return self.text |
| 61 this file to Python. | 69 |
| 62 """ | 70 def __repr__(self): |
| 71 return '%s(text=%s, errors=%s)' % ( |
| 72 self.__class__.__name__, self.text, self.errors) |
| 73 |
| 74 class _StringBuilder(object): |
| 75 '''Efficiently builds strings. |
| 76 ''' |
| 63 def __init__(self): | 77 def __init__(self): |
| 64 self._buf = [] | 78 self._buf = [] |
| 65 | 79 |
| 66 def __len__(self): | 80 def __len__(self): |
| 67 self._Collapse() | 81 self._Collapse() |
| 68 return len(self._buf[0]) | 82 return len(self._buf[0]) |
| 69 | 83 |
| 70 def append(self, string): | 84 def Append(self, string): |
| 85 if not isinstance(string, basestring): |
| 86 string = str(string) |
| 71 self._buf.append(string) | 87 self._buf.append(string) |
| 72 | 88 |
| 73 def toString(self): | 89 def ToString(self): |
| 74 self._Collapse() | 90 self._Collapse() |
| 75 return self._buf[0] | 91 return self._buf[0] |
| 76 | 92 |
| 77 def __str__(self): | 93 def __str__(self): |
| 78 return self.toString() | 94 return self.ToString() |
| 79 | 95 |
| 80 def _Collapse(self): | 96 def _Collapse(self): |
| 81 self._buf = [u''.join(self._buf)] | 97 self._buf = [u''.join(self._buf)] |
| 82 | 98 |
| 83 class RenderState(object): | 99 class _Contexts(object): |
| 84 """ The state of a render call. | 100 '''Tracks a stack of context objects, providing efficient key/value retrieval. |
| 85 """ | 101 ''' |
| 86 def __init__(self, globalContexts, localContexts): | 102 class _Node(object): |
| 87 self.globalContexts = globalContexts | 103 '''A node within the stack. Wraps a real context and maintains the key/value |
| 88 self.localContexts = localContexts | 104 pairs seen so far. |
| 89 self.text = StringBuilder() | 105 ''' |
| 90 self.errors = [] | 106 def __init__(self, value): |
| 91 self._errorsDisabled = False | 107 self._value = value |
| 92 | 108 self._value_has_get = hasattr(value, 'get') |
| 93 def inSameContext(self): | 109 self._found = {} |
| 94 return RenderState(self.globalContexts, self.localContexts) | 110 |
| 95 | 111 def GetKeys(self): |
| 96 def getFirstContext(self): | 112 '''Returns the list of keys that |_value| contains. |
| 97 if len(self.localContexts) > 0: | 113 ''' |
| 98 return self.localContexts[0] | 114 return self._found.keys() |
| 99 if len(self.globalContexts) > 0: | 115 |
| 100 return self.globalContexts[0] | 116 def Get(self, key): |
| 101 return None | 117 '''Returns the value for |key|, or None if not found (including if |
| 102 | 118 |_value| doesn't support key retrieval). |
| 103 def disableErrors(self): | 119 ''' |
| 104 self._errorsDisabled = True | 120 if not self._value_has_get: |
| 105 return self | |
| 106 | |
| 107 def addError(self, *messages): | |
| 108 if self._errorsDisabled: | |
| 109 return self | |
| 110 buf = StringBuilder() | |
| 111 for message in messages: | |
| 112 buf.append(str(message)) | |
| 113 self.errors.append(buf.toString()) | |
| 114 return self | |
| 115 | |
| 116 def getResult(self): | |
| 117 return RenderResult(self.text.toString(), self.errors); | |
| 118 | |
| 119 class Identifier(object): | |
| 120 """ An identifier of the form "@", "foo.bar.baz", or "@.foo.bar.baz". | |
| 121 """ | |
| 122 def __init__(self, name, line): | |
| 123 self._isThis = (name == '@') | |
| 124 if self._isThis: | |
| 125 self._startsWithThis = False | |
| 126 self._path = [] | |
| 127 return | |
| 128 | |
| 129 thisDot = '@.' | |
| 130 self._startsWithThis = name.startswith(thisDot) | |
| 131 if self._startsWithThis: | |
| 132 name = name[len(thisDot):] | |
| 133 | |
| 134 if not re.match('^[a-zA-Z0-9._\\-/]+$', name): | |
| 135 raise ParseException(name + " is not a valid identifier", line) | |
| 136 self._path = name.split('.') | |
| 137 | |
| 138 def resolve(self, renderState): | |
| 139 if self._isThis: | |
| 140 return renderState.getFirstContext() | |
| 141 | |
| 142 if self._startsWithThis: | |
| 143 return self._resolveFromContext(renderState.getFirstContext()) | |
| 144 | |
| 145 resolved = self._resolveFromContexts(renderState.localContexts) | |
| 146 if resolved is None: | |
| 147 resolved = self._resolveFromContexts(renderState.globalContexts) | |
| 148 if resolved is None: | |
| 149 renderState.addError("Couldn't resolve identifier ", self._path) | |
| 150 return resolved | |
| 151 | |
| 152 def _resolveFromContexts(self, contexts): | |
| 153 for context in contexts: | |
| 154 resolved = self._resolveFromContext(context) | |
| 155 if resolved is not None: | |
| 156 return resolved | |
| 157 return None | |
| 158 | |
| 159 def _resolveFromContext(self, context): | |
| 160 result = context | |
| 161 for next in self._path: | |
| 162 # Only require that contexts provide a get method, meaning that callers | |
| 163 # can provide dict-like contexts (for example, to populate values lazily). | |
| 164 if result is None or not getattr(result, "get", None): | |
| 165 return None | 121 return None |
| 166 result = result.get(next) | 122 value = self._found.get(key) |
| 167 return result | 123 if value is not None: |
| 124 return value |
| 125 value = self._value.get(key) |
| 126 if value is not None: |
| 127 self._found[key] = value |
| 128 return value |
| 129 |
| 130 def __init__(self, globals_): |
| 131 '''Initializes with the initial global contexts, listed in order from most |
| 132 to least important. |
| 133 ''' |
| 134 self._nodes = map(_Contexts._Node, globals_) |
| 135 self._first_local = len(self._nodes) |
| 136 self._value_info = {} |
| 137 |
| 138 def CreateFromGlobals(self): |
| 139 new = _Contexts([]) |
| 140 new._nodes = self._nodes[:self._first_local] |
| 141 new._first_local = self._first_local |
| 142 return new |
| 143 |
| 144 def Push(self, context): |
| 145 self._nodes.append(_Contexts._Node(context)) |
| 146 |
| 147 def Pop(self): |
| 148 node = self._nodes.pop() |
| 149 assert len(self._nodes) >= self._first_local |
| 150 for found_key in node.GetKeys(): |
| 151 # [0] is the stack of nodes that |found_key| has been found in. |
| 152 self._value_info[found_key][0].pop() |
| 153 |
| 154 def GetTopLocal(self): |
| 155 if len(self._nodes) == self._first_local: |
| 156 return None |
| 157 return self._nodes[-1]._value |
| 158 |
| 159 def Resolve(self, path): |
| 160 # This method is only efficient at finding |key|; if |tail| has a value (and |
| 161 # |key| evaluates to an indexable value) we'll need to descend into that. |
| 162 key, tail = path.split('.', 1) if '.' in path else (path, None) |
| 163 |
| 164 if key == '@': |
| 165 found = self._nodes[-1]._value |
| 166 else: |
| 167 found = self._FindNodeValue(key) |
| 168 |
| 169 if tail is None: |
| 170 return found |
| 171 |
| 172 for part in tail.split('.'): |
| 173 if not hasattr(found, 'get'): |
| 174 return None |
| 175 found = found.get(part) |
| 176 return found |
| 177 |
| 178 def _FindNodeValue(self, key): |
| 179 # |found_node_list| will be all the nodes that |key| has been found in. |
| 180 # |checked_node_set| are those that have been checked. |
| 181 info = self._value_info.get(key) |
| 182 if info is None: |
| 183 info = ([], set()) |
| 184 self._value_info[key] = info |
| 185 found_node_list, checked_node_set = info |
| 186 |
| 187 # Check all the nodes not yet checked for |key|. |
| 188 newly_found = [] |
| 189 for node in reversed(self._nodes): |
| 190 if node in checked_node_set: |
| 191 break |
| 192 value = node.Get(key) |
| 193 if value is not None: |
| 194 newly_found.append(node) |
| 195 checked_node_set.add(node) |
| 196 |
| 197 # The nodes will have been found in reverse stack order. After extending |
| 198 # the found nodes, the freshest value will be at the tip of the stack. |
| 199 found_node_list.extend(reversed(newly_found)) |
| 200 if not found_node_list: |
| 201 return None |
| 202 |
| 203 return found_node_list[-1]._value[key] |
| 204 |
| 205 class _Stack(object): |
| 206 class Entry(object): |
| 207 def __init__(self, name, id_): |
| 208 self.name = name |
| 209 self.id_ = id_ |
| 210 |
| 211 def __init__(self, entries=[]): |
| 212 self.entries = entries |
| 213 |
| 214 def Descend(self, name, id_): |
| 215 descended = list(self.entries) |
| 216 descended.append(_Stack.Entry(name, id_)) |
| 217 return _Stack(entries=descended) |
| 218 |
| 219 class _RenderState(object): |
| 220 '''The state of a render call. |
| 221 ''' |
| 222 def __init__(self, name, contexts, _stack=_Stack()): |
| 223 self.text = _StringBuilder() |
| 224 self.contexts = contexts |
| 225 self._name = name |
| 226 self._errors = [] |
| 227 self._stack = _stack |
| 228 |
| 229 def AddResolutionError(self, id_): |
| 230 self._errors.append( |
| 231 id_.CreateResolutionErrorMessage(self._name, stack=self._stack)) |
| 232 |
| 233 def Copy(self): |
| 234 return _RenderState( |
| 235 self._name, self.contexts, _stack=self._stack) |
| 236 |
| 237 def ForkPartial(self, custom_name, id_): |
| 238 name = custom_name or id_.name |
| 239 return _RenderState(name, |
| 240 self.contexts.CreateFromGlobals(), |
| 241 _stack=self._stack.Descend(name, id_)) |
| 242 |
| 243 def Merge(self, render_state, text_transform=None): |
| 244 self._errors.extend(render_state._errors) |
| 245 text = render_state.text.ToString() |
| 246 if text_transform is not None: |
| 247 text = text_transform(text) |
| 248 self.text.Append(text) |
| 249 |
| 250 def GetResult(self): |
| 251 return RenderResult(self.text.ToString(), self._errors); |
| 252 |
| 253 class _Identifier(object): |
| 254 ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'. |
| 255 ''' |
| 256 def __init__(self, name, line, column): |
| 257 self.name = name |
| 258 self.line = line |
| 259 self.column = column |
| 260 if name == '': |
| 261 raise ParseException('Empty identifier %s' % self.GetDescription()) |
| 262 for part in name.split('.'): |
| 263 if part != '@' and not re.match('^[a-zA-Z0-9_/-]+$', part): |
| 264 raise ParseException('Invalid identifier %s' % self.GetDescription()) |
| 265 |
| 266 def GetDescription(self): |
| 267 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) |
| 268 |
| 269 def CreateResolutionErrorMessage(self, name, stack=None): |
| 270 message = _StringBuilder() |
| 271 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), |
| 272 name)) |
| 273 if stack is not None: |
| 274 for entry in stack.entries: |
| 275 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), |
| 276 entry.name)) |
| 277 return message.ToString() |
| 168 | 278 |
| 169 def __str__(self): | 279 def __str__(self): |
| 170 if self._isThis: | 280 raise ValueError() |
| 171 return '@' | 281 |
| 172 name = '.'.join(self._path) | 282 class _Line(object): |
| 173 return ('@.' + name) if self._startsWithThis else name | |
| 174 | |
| 175 class Line(object): | |
| 176 def __init__(self, number): | 283 def __init__(self, number): |
| 177 self.number = number | 284 self.number = number |
| 178 | 285 |
| 179 class LeafNode(object): | 286 def __str__(self): |
| 180 def __init__(self, line): | 287 return str(self.number) |
| 181 self._line = line | 288 |
| 182 | 289 class _LeafNode(object): |
| 183 def startsWithNewLine(self): | 290 def __init__(self, start_line, end_line): |
| 291 self._start_line = start_line |
| 292 self._end_line = end_line |
| 293 |
| 294 def StartsWithNewLine(self): |
| 184 return False | 295 return False |
| 185 | 296 |
| 186 def trimStartingNewLine(self): | 297 def TrimStartingNewLine(self): |
| 187 pass | 298 pass |
| 188 | 299 |
| 189 def trimEndingSpaces(self): | 300 def TrimEndingSpaces(self): |
| 190 return 0 | 301 return 0 |
| 191 | 302 |
| 192 def trimEndingNewLine(self): | 303 def TrimEndingNewLine(self): |
| 193 pass | 304 pass |
| 194 | 305 |
| 195 def endsWithEmptyLine(self): | 306 def EndsWithEmptyLine(self): |
| 196 return False | 307 return False |
| 197 | 308 |
| 198 def getStartLine(self): | 309 def GetStartLine(self): |
| 199 return self._line | 310 return self._start_line |
| 200 | 311 |
| 201 def getEndLine(self): | 312 def GetEndLine(self): |
| 202 return self._line | 313 return self._end_line |
| 203 | 314 |
| 204 class DecoratorNode(object): | 315 class _DecoratorNode(object): |
| 205 def __init__(self, content): | 316 def __init__(self, content): |
| 206 self._content = content | 317 self._content = content |
| 207 | 318 |
| 208 def startsWithNewLine(self): | 319 def StartsWithNewLine(self): |
| 209 return self._content.startsWithNewLine() | 320 return self._content.StartsWithNewLine() |
| 210 | 321 |
| 211 def trimStartingNewLine(self): | 322 def TrimStartingNewLine(self): |
| 212 self._content.trimStartingNewLine() | 323 self._content.TrimStartingNewLine() |
| 213 | 324 |
| 214 def trimEndingSpaces(self): | 325 def TrimEndingSpaces(self): |
| 215 return self._content.trimEndingSpaces() | 326 return self._content.TrimEndingSpaces() |
| 216 | 327 |
| 217 def trimEndingNewLine(self): | 328 def TrimEndingNewLine(self): |
| 218 self._content.trimEndingNewLine() | 329 self._content.TrimEndingNewLine() |
| 219 | 330 |
| 220 def endsWithEmptyLine(self): | 331 def EndsWithEmptyLine(self): |
| 221 return self._content.endsWithEmptyLine() | 332 return self._content.EndsWithEmptyLine() |
| 222 | 333 |
| 223 def getStartLine(self): | 334 def GetStartLine(self): |
| 224 return self._content.getStartLine() | 335 return self._content.GetStartLine() |
| 225 | 336 |
| 226 def getEndLine(self): | 337 def GetEndLine(self): |
| 227 return self._content.getEndLine() | 338 return self._content.GetEndLine() |
| 228 | 339 |
| 229 class InlineNode(DecoratorNode): | 340 class _InlineNode(_DecoratorNode): |
| 230 def __init__(self, content): | 341 def __init__(self, content): |
| 231 DecoratorNode.__init__(self, content) | 342 _DecoratorNode.__init__(self, content) |
| 232 | 343 |
| 233 def render(self, renderState): | 344 def Render(self, render_state): |
| 234 contentRenderState = renderState.inSameContext() | 345 content_render_state = render_state.Copy() |
| 235 self._content.render(contentRenderState) | 346 self._content.Render(content_render_state) |
| 236 | 347 render_state.Merge(content_render_state, |
| 237 renderState.errors.extend(contentRenderState.errors) | 348 text_transform=lambda text: text.replace('\n', '')) |
| 238 renderState.text.append( | 349 |
| 239 contentRenderState.text.toString().replace('\n', '')) | 350 class _IndentedNode(_DecoratorNode): |
| 240 | |
| 241 class IndentedNode(DecoratorNode): | |
| 242 def __init__(self, content, indentation): | 351 def __init__(self, content, indentation): |
| 243 DecoratorNode.__init__(self, content) | 352 _DecoratorNode.__init__(self, content) |
| 244 self._indent_str = ' ' * indentation | 353 self._indent_str = ' ' * indentation |
| 245 | 354 |
| 246 def render(self, renderState): | 355 def Render(self, render_state): |
| 247 contentRenderState = renderState.inSameContext() | 356 if isinstance(self._content, _CommentNode): |
| 248 self._content.render(contentRenderState) | 357 return |
| 249 | 358 content_render_state = render_state.Copy() |
| 250 renderState.errors.extend(contentRenderState.errors) | 359 self._content.Render(content_render_state) |
| 251 renderState.text.append(self._indent_str) | 360 def AddIndentation(text): |
| 252 # TODO: this might introduce an extra \n at the end? need test. | 361 buf = _StringBuilder() |
| 253 renderState.text.append( | 362 buf.Append(self._indent_str) |
| 254 contentRenderState.text.toString().replace('\n', | 363 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) |
| 255 '\n' + self._indent_str)) | 364 buf.Append('\n') |
| 256 renderState.text.append('\n') | 365 return buf.ToString() |
| 257 | 366 render_state.Merge(content_render_state, text_transform=AddIndentation) |
| 258 class BlockNode(DecoratorNode): | 367 |
| 368 class _BlockNode(_DecoratorNode): |
| 259 def __init__(self, content): | 369 def __init__(self, content): |
| 260 DecoratorNode.__init__(self, content) | 370 _DecoratorNode.__init__(self, content) |
| 261 content.trimStartingNewLine() | 371 content.TrimStartingNewLine() |
| 262 content.trimEndingSpaces() | 372 content.TrimEndingSpaces() |
| 263 | 373 |
| 264 def render(self, renderState): | 374 def Render(self, render_state): |
| 265 self._content.render(renderState) | 375 self._content.Render(render_state) |
| 266 | 376 |
| 267 class NodeCollection(object): | 377 class _NodeCollection(object): |
| 268 def __init__(self, nodes): | 378 def __init__(self, nodes): |
| 269 if len(nodes) == 0: | 379 assert nodes |
| 270 raise ValueError() | |
| 271 self._nodes = nodes | 380 self._nodes = nodes |
| 272 | 381 |
| 273 def render(self, renderState): | 382 def Render(self, render_state): |
| 274 for node in self._nodes: | 383 for node in self._nodes: |
| 275 node.render(renderState) | 384 node.Render(render_state) |
| 276 | 385 |
| 277 def startsWithNewLine(self): | 386 def StartsWithNewLine(self): |
| 278 return self._nodes[0].startsWithNewLine() | 387 return self._nodes[0].StartsWithNewLine() |
| 279 | 388 |
| 280 def trimStartingNewLine(self): | 389 def TrimStartingNewLine(self): |
| 281 self._nodes[0].trimStartingNewLine() | 390 self._nodes[0].TrimStartingNewLine() |
| 282 | 391 |
| 283 def trimEndingSpaces(self): | 392 def TrimEndingSpaces(self): |
| 284 return self._nodes[-1].trimEndingSpaces() | 393 return self._nodes[-1].TrimEndingSpaces() |
| 285 | 394 |
| 286 def trimEndingNewLine(self): | 395 def TrimEndingNewLine(self): |
| 287 self._nodes[-1].trimEndingNewLine() | 396 self._nodes[-1].TrimEndingNewLine() |
| 288 | 397 |
| 289 def endsWithEmptyLine(self): | 398 def EndsWithEmptyLine(self): |
| 290 return self._nodes[-1].endsWithEmptyLine() | 399 return self._nodes[-1].EndsWithEmptyLine() |
| 291 | 400 |
| 292 def getStartLine(self): | 401 def GetStartLine(self): |
| 293 return self._nodes[0].getStartLine() | 402 return self._nodes[0].GetStartLine() |
| 294 | 403 |
| 295 def getEndLine(self): | 404 def GetEndLine(self): |
| 296 return self._nodes[-1].getEndLine() | 405 return self._nodes[-1].GetEndLine() |
| 297 | 406 |
| 298 class StringNode(object): | 407 class _StringNode(object): |
| 299 """ Just a string. | 408 ''' Just a string. |
| 300 """ | 409 ''' |
| 301 def __init__(self, string, startLine, endLine): | 410 def __init__(self, string, start_line, end_line): |
| 302 self._string = string | 411 self._string = string |
| 303 self._startLine = startLine | 412 self._start_line = start_line |
| 304 self._endLine = endLine | 413 self._end_line = end_line |
| 305 | 414 |
| 306 def render(self, renderState): | 415 def Render(self, render_state): |
| 307 renderState.text.append(self._string) | 416 render_state.text.Append(self._string) |
| 308 | 417 |
| 309 def startsWithNewLine(self): | 418 def StartsWithNewLine(self): |
| 310 return self._string.startswith('\n') | 419 return self._string.startswith('\n') |
| 311 | 420 |
| 312 def trimStartingNewLine(self): | 421 def TrimStartingNewLine(self): |
| 313 if self.startsWithNewLine(): | 422 if self.StartsWithNewLine(): |
| 314 self._string = self._string[1:] | 423 self._string = self._string[1:] |
| 315 | 424 |
| 316 def trimEndingSpaces(self): | 425 def TrimEndingSpaces(self): |
| 317 originalLength = len(self._string) | 426 original_length = len(self._string) |
| 318 self._string = self._string[:self._lastIndexOfSpaces()] | 427 self._string = self._string[:self._LastIndexOfSpaces()] |
| 319 return originalLength - len(self._string) | 428 return original_length - len(self._string) |
| 320 | 429 |
| 321 def trimEndingNewLine(self): | 430 def TrimEndingNewLine(self): |
| 322 if self._string.endswith('\n'): | 431 if self._string.endswith('\n'): |
| 323 self._string = self._string[:len(self._string) - 1] | 432 self._string = self._string[:len(self._string) - 1] |
| 324 | 433 |
| 325 def endsWithEmptyLine(self): | 434 def EndsWithEmptyLine(self): |
| 326 index = self._lastIndexOfSpaces() | 435 index = self._LastIndexOfSpaces() |
| 327 return index == 0 or self._string[index - 1] == '\n' | 436 return index == 0 or self._string[index - 1] == '\n' |
| 328 | 437 |
| 329 def _lastIndexOfSpaces(self): | 438 def _LastIndexOfSpaces(self): |
| 330 index = len(self._string) | 439 index = len(self._string) |
| 331 while index > 0 and self._string[index - 1] == ' ': | 440 while index > 0 and self._string[index - 1] == ' ': |
| 332 index -= 1 | 441 index -= 1 |
| 333 return index | 442 return index |
| 334 | 443 |
| 335 def getStartLine(self): | 444 def GetStartLine(self): |
| 336 return self._startLine | 445 return self._start_line |
| 337 | 446 |
| 338 def getEndLine(self): | 447 def GetEndLine(self): |
| 339 return self._endLine | 448 return self._end_line |
| 340 | 449 |
| 341 class EscapedVariableNode(LeafNode): | 450 class _EscapedVariableNode(_LeafNode): |
| 342 """ {{foo}} | 451 ''' {{foo}} |
| 343 """ | 452 ''' |
| 344 def __init__(self, id, line): | 453 def __init__(self, id_): |
| 345 LeafNode.__init__(self, line) | 454 _LeafNode.__init__(self, id_.line, id_.line) |
| 346 self._id = id | 455 self._id = id_ |
| 347 | 456 |
| 348 def render(self, renderState): | 457 def Render(self, render_state): |
| 349 value = self._id.resolve(renderState) | 458 value = render_state.contexts.Resolve(self._id.name) |
| 350 if value is None: | 459 if value is None: |
| 460 render_state.AddResolutionError(self._id) |
| 351 return | 461 return |
| 352 | |
| 353 string = value if isinstance(value, basestring) else str(value) | 462 string = value if isinstance(value, basestring) else str(value) |
| 354 renderState.text.append(string.replace('&', '&') | 463 render_state.text.Append(string.replace('&', '&') |
| 355 .replace('<', '<') | 464 .replace('<', '<') |
| 356 .replace('>', '>')) | 465 .replace('>', '>')) |
| 357 | 466 |
| 358 class UnescapedVariableNode(LeafNode): | 467 class _UnescapedVariableNode(_LeafNode): |
| 359 """ {{{foo}}} | 468 ''' {{{foo}}} |
| 360 """ | 469 ''' |
| 361 def __init__(self, id, line): | 470 def __init__(self, id_): |
| 362 LeafNode.__init__(self, line) | 471 _LeafNode.__init__(self, id_.line, id_.line) |
| 363 self._id = id | 472 self._id = id_ |
| 364 | 473 |
| 365 def render(self, renderState): | 474 def Render(self, render_state): |
| 366 value = self._id.resolve(renderState) | 475 value = render_state.contexts.Resolve(self._id.name) |
| 367 if value is None: | 476 if value is None: |
| 477 render_state.AddResolutionError(self._id) |
| 368 return | 478 return |
| 369 renderState.text.append( | 479 string = value if isinstance(value, basestring) else str(value) |
| 370 value if isinstance(value, basestring) else str(value)) | 480 render_state.text.Append(string) |
| 371 | 481 |
| 372 class SectionNode(DecoratorNode): | 482 class _CommentNode(_LeafNode): |
| 373 """ {{#foo}} ... {{/}} | 483 '''{{- This is a comment -}} |
| 374 """ | 484 An empty placeholder node for correct indented rendering behaviour. |
| 375 def __init__(self, id, content): | 485 ''' |
| 376 DecoratorNode.__init__(self, content) | 486 def __init__(self, start_line, end_line): |
| 377 self._id = id | 487 _LeafNode.__init__(self, start_line, end_line) |
| 378 | 488 |
| 379 def render(self, renderState): | 489 def Render(self, render_state): |
| 380 value = self._id.resolve(renderState) | 490 pass |
| 381 if value is None: | 491 |
| 382 return | 492 class _SectionNode(_DecoratorNode): |
| 383 | 493 ''' {{#foo}} ... {{/}} |
| 494 ''' |
| 495 def __init__(self, id_, content): |
| 496 _DecoratorNode.__init__(self, content) |
| 497 self._id = id_ |
| 498 |
| 499 def Render(self, render_state): |
| 500 value = render_state.contexts.Resolve(self._id.name) |
| 384 if isinstance(value, list): | 501 if isinstance(value, list): |
| 385 for item in value: | 502 for item in value: |
| 386 renderState.localContexts.insert(0, item) | 503 # Always push context, even if it's not "valid", since we want to |
| 387 self._content.render(renderState) | 504 # be able to refer to items in a list such as [1,2,3] via @. |
| 388 renderState.localContexts.pop(0) | 505 render_state.contexts.Push(item) |
| 389 elif isinstance(value, dict): | 506 self._content.Render(render_state) |
| 390 renderState.localContexts.insert(0, value) | 507 render_state.contexts.Pop() |
| 391 self._content.render(renderState) | 508 elif hasattr(value, 'get'): |
| 392 renderState.localContexts.pop(0) | 509 render_state.contexts.Push(value) |
| 510 self._content.Render(render_state) |
| 511 render_state.contexts.Pop() |
| 393 else: | 512 else: |
| 394 renderState.addError("{{#", self._id, | 513 render_state.AddResolutionError(self._id) |
| 395 "}} cannot be rendered with a ", type(value)) | 514 |
| 396 | 515 class _VertedSectionNode(_DecoratorNode): |
| 397 class VertedSectionNode(DecoratorNode): | 516 ''' {{?foo}} ... {{/}} |
| 398 """ {{?foo}} ... {{/}} | 517 ''' |
| 399 """ | 518 def __init__(self, id_, content): |
| 400 def __init__(self, id, content): | 519 _DecoratorNode.__init__(self, content) |
| 401 DecoratorNode.__init__(self, content) | 520 self._id = id_ |
| 402 self._id = id | 521 |
| 403 | 522 def Render(self, render_state): |
| 404 def render(self, renderState): | 523 value = render_state.contexts.Resolve(self._id.name) |
| 405 value = self._id.resolve(renderState.inSameContext().disableErrors()) | 524 if _VertedSectionNode.ShouldRender(value): |
| 406 if _VertedSectionNodeShouldRender(value): | 525 render_state.contexts.Push(value) |
| 407 renderState.localContexts.insert(0, value) | 526 self._content.Render(render_state) |
| 408 self._content.render(renderState) | 527 render_state.contexts.Pop() |
| 409 renderState.localContexts.pop(0) | 528 |
| 410 | 529 @staticmethod |
| 411 def _VertedSectionNodeShouldRender(value): | 530 def ShouldRender(value): |
| 412 if value is None: | 531 if value is None: |
| 413 return False | 532 return False |
| 414 if isinstance(value, bool): | 533 if isinstance(value, bool): |
| 415 return value | 534 return value |
| 416 if (isinstance(value, int) or | 535 if isinstance(value, list): |
| 417 isinstance(value, long) or | 536 return len(value) > 0 |
| 418 isinstance(value, float)): | |
| 419 return True | 537 return True |
| 420 if isinstance(value, basestring): | 538 |
| 421 return True | 539 class _InvertedSectionNode(_DecoratorNode): |
| 422 if isinstance(value, list): | 540 ''' {{^foo}} ... {{/}} |
| 423 return len(value) > 0 | 541 ''' |
| 424 if isinstance(value, dict): | 542 def __init__(self, id_, content): |
| 425 return True | 543 _DecoratorNode.__init__(self, content) |
| 426 raise TypeError("Unhandled type %s" % type(value)) | 544 self._id = id_ |
| 427 | 545 |
| 428 class InvertedSectionNode(DecoratorNode): | 546 def Render(self, render_state): |
| 429 """ {{^foo}} ... {{/}} | 547 value = render_state.contexts.Resolve(self._id.name) |
| 430 """ | 548 if not _VertedSectionNode.ShouldRender(value): |
| 431 def __init__(self, id, content): | 549 self._content.Render(render_state) |
| 432 DecoratorNode.__init__(self, content) | 550 |
| 433 self._id = id | 551 class _JsonNode(_LeafNode): |
| 434 | 552 ''' {{*foo}} |
| 435 def render(self, renderState): | 553 ''' |
| 436 value = self._id.resolve(renderState.inSameContext().disableErrors()) | 554 def __init__(self, id_): |
| 437 if not _VertedSectionNodeShouldRender(value): | 555 _LeafNode.__init__(self, id_.line, id_.line) |
| 438 self._content.render(renderState) | 556 self._id = id_ |
| 439 | 557 |
| 440 class JsonNode(LeafNode): | 558 def Render(self, render_state): |
| 441 """ {{*foo}} | 559 value = render_state.contexts.Resolve(self._id.name) |
| 442 """ | |
| 443 def __init__(self, id, line): | |
| 444 LeafNode.__init__(self, line) | |
| 445 self._id = id | |
| 446 | |
| 447 def render(self, renderState): | |
| 448 value = self._id.resolve(renderState) | |
| 449 if value is None: | 560 if value is None: |
| 561 render_state.AddResolutionError(self._id) |
| 450 return | 562 return |
| 451 renderState.text.append(json.dumps(value, separators=(',',':'))) | 563 render_state.text.Append(json.dumps(value, separators=(',',':'))) |
| 452 | 564 |
| 453 class PartialNode(LeafNode): | 565 class _PartialNode(_LeafNode): |
| 454 """ {{+foo}} | 566 ''' {{+foo}} |
| 455 """ | 567 ''' |
| 456 def __init__(self, id, line): | 568 def __init__(self, id_): |
| 457 LeafNode.__init__(self, line) | 569 _LeafNode.__init__(self, id_.line, id_.line) |
| 458 self._id = id | 570 self._id = id_ |
| 459 self._args = None | 571 self._args = None |
| 460 | 572 self._local_context_id = None |
| 461 def render(self, renderState): | 573 |
| 462 value = self._id.resolve(renderState) | 574 def Render(self, render_state): |
| 575 value = render_state.contexts.Resolve(self._id.name) |
| 576 if value is None: |
| 577 render_state.AddResolutionError(self._id) |
| 578 return |
| 463 if not isinstance(value, Handlebar): | 579 if not isinstance(value, Handlebar): |
| 464 renderState.addError(self._id, " didn't resolve to a Handlebar") | 580 render_state.AddResolutionError(self._id) |
| 465 return | 581 return |
| 466 | 582 |
| 467 argContext = [] | 583 partial_render_state = render_state.ForkPartial(value._name, self._id) |
| 468 if len(renderState.localContexts) > 0: | 584 |
| 469 argContext.append(renderState.localContexts[0]) | 585 # TODO: Don't do this. Force callers to do this by specifying an @ argument. |
| 470 | 586 top_local = render_state.contexts.GetTopLocal() |
| 471 if self._args: | 587 if top_local is not None: |
| 472 argContextMap = {} | 588 partial_render_state.contexts.Push(top_local) |
| 473 for key, valueId in self._args.items(): | 589 |
| 474 context = valueId.resolve(renderState) | 590 if self._args is not None: |
| 475 if context: | 591 arg_context = {} |
| 476 argContextMap[key] = context | 592 for key, value_id in self._args.items(): |
| 477 argContext.append(argContextMap) | 593 context = render_state.contexts.Resolve(value_id.name) |
| 478 | 594 if context is not None: |
| 479 partialRenderState = RenderState(renderState.globalContexts, argContext) | 595 arg_context[key] = context |
| 480 value._topNode.render(partialRenderState) | 596 partial_render_state.contexts.Push(arg_context) |
| 481 | 597 |
| 482 text = partialRenderState.text.toString() | 598 if self._local_context_id is not None: |
| 483 if len(text) > 0 and text[-1] == '\n': | 599 local_context = render_state.contexts.Resolve(self._local_context_id.name) |
| 484 text = text[:-1] | 600 if local_context is not None: |
| 485 | 601 partial_render_state.contexts.Push(local_context) |
| 486 renderState.text.append(text) | 602 |
| 487 renderState.errors.extend(partialRenderState.errors) | 603 value._top_node.Render(partial_render_state) |
| 488 | 604 |
| 489 def addArgument(self, key, valueId): | 605 render_state.Merge( |
| 490 if not self._args: | 606 partial_render_state, |
| 607 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) |
| 608 |
| 609 def AddArgument(self, key, id_): |
| 610 if self._args is None: |
| 491 self._args = {} | 611 self._args = {} |
| 492 self._args[key] = valueId | 612 self._args[key] = id_ |
| 493 | 613 |
| 494 # List of tokens in order of longest to shortest, to avoid any prefix matching | 614 def SetLocalContext(self, id_): |
| 495 # issues. | 615 self._local_context_id = id_ |
| 496 TokenValues = [] | 616 |
| 497 | 617 _TOKENS = {} |
| 498 class Token(object): | 618 |
| 499 """ The tokens that can appear in a template. | 619 class _Token(object): |
| 500 """ | 620 ''' The tokens that can appear in a template. |
| 621 ''' |
| 501 class Data(object): | 622 class Data(object): |
| 502 def __init__(self, name, text, clazz): | 623 def __init__(self, name, text, clazz): |
| 503 self.name = name | 624 self.name = name |
| 504 self.text = text | 625 self.text = text |
| 505 self.clazz = clazz | 626 self.clazz = clazz |
| 506 TokenValues.append(self) | 627 _TOKENS[text] = self |
| 507 | 628 |
| 508 def elseNodeClass(self): | 629 def ElseNodeClass(self): |
| 509 if self.clazz == VertedSectionNode: | 630 if self.clazz == _VertedSectionNode: |
| 510 return InvertedSectionNode | 631 return _InvertedSectionNode |
| 511 if self.clazz == InvertedSectionNode: | 632 if self.clazz == _InvertedSectionNode: |
| 512 return VertedSectionNode | 633 return _VertedSectionNode |
| 513 raise ValueError(self.clazz + " can not have an else clause.") | 634 raise ValueError('%s cannot have an else clause.' % self.clazz) |
| 514 | 635 |
| 515 OPEN_START_SECTION = Data("OPEN_START_SECTION" , "{{#", Secti
onNode) | 636 def __str__(self): |
| 516 OPEN_START_VERTED_SECTION = Data("OPEN_START_VERTED_SECTION" , "{{?", Verte
dSectionNode) | 637 return '%s(%s)' % (self.name, self.text) |
| 517 OPEN_START_INVERTED_SECTION = Data("OPEN_START_INVERTED_SECTION", "{{^", Inver
tedSectionNode) | 638 |
| 518 OPEN_START_JSON = Data("OPEN_START_JSON" , "{{*", JsonN
ode) | 639 OPEN_START_SECTION = Data('OPEN_START_SECTION' , '{{#', _Sect
ionNode) |
| 519 OPEN_START_PARTIAL = Data("OPEN_START_PARTIAL" , "{{+", Parti
alNode) | 640 OPEN_START_VERTED_SECTION = Data('OPEN_START_VERTED_SECTION' , '{{?', _Vert
edSectionNode) |
| 520 OPEN_ELSE = Data("OPEN_ELSE" , "{{:", None) | 641 OPEN_START_INVERTED_SECTION = Data('OPEN_START_INVERTED_SECTION', '{{^', _Inve
rtedSectionNode) |
| 521 OPEN_END_SECTION = Data("OPEN_END_SECTION" , "{{/", None) | 642 OPEN_START_JSON = Data('OPEN_START_JSON' , '{{*', _Json
Node) |
| 522 OPEN_UNESCAPED_VARIABLE = Data("OPEN_UNESCAPED_VARIABLE" , "{{{", Unesc
apedVariableNode) | 643 OPEN_START_PARTIAL = Data('OPEN_START_PARTIAL' , '{{+', _Part
ialNode) |
| 523 CLOSE_MUSTACHE3 = Data("CLOSE_MUSTACHE3" , "}}}", None) | 644 OPEN_ELSE = Data('OPEN_ELSE' , '{{:', None) |
| 524 OPEN_COMMENT = Data("OPEN_COMMENT" , "{{-", None) | 645 OPEN_END_SECTION = Data('OPEN_END_SECTION' , '{{/', None) |
| 525 CLOSE_COMMENT = Data("CLOSE_COMMENT" , "-}}", None) | 646 INLINE_END_SECTION = Data('INLINE_END_SECTION' , '/}}', None) |
| 526 OPEN_VARIABLE = Data("OPEN_VARIABLE" , "{{" , Escap
edVariableNode) | 647 OPEN_UNESCAPED_VARIABLE = Data('OPEN_UNESCAPED_VARIABLE' , '{{{', _Unes
capedVariableNode) |
| 527 CLOSE_MUSTACHE = Data("CLOSE_MUSTACHE" , "}}" , None) | 648 CLOSE_MUSTACHE3 = Data('CLOSE_MUSTACHE3' , '}}}', None) |
| 528 CHARACTER = Data("CHARACTER" , "." , None) | 649 OPEN_COMMENT = Data('OPEN_COMMENT' , '{{-', _Comm
entNode) |
| 529 | 650 CLOSE_COMMENT = Data('CLOSE_COMMENT' , '-}}', None) |
| 530 class TokenStream(object): | 651 OPEN_VARIABLE = Data('OPEN_VARIABLE' , '{{' , _Esca
pedVariableNode) |
| 531 """ Tokeniser for template parsing. | 652 CLOSE_MUSTACHE = Data('CLOSE_MUSTACHE' , '}}' , None) |
| 532 """ | 653 CHARACTER = Data('CHARACTER' , '.' , None) |
| 654 |
| 655 class _TokenStream(object): |
| 656 ''' Tokeniser for template parsing. |
| 657 ''' |
| 533 def __init__(self, string): | 658 def __init__(self, string): |
| 534 self._remainder = string | 659 self.next_token = None |
| 535 | 660 self.next_line = _Line(1) |
| 536 self.nextToken = None | 661 self.next_column = 0 |
| 537 self.nextContents = None | 662 self._string = string |
| 538 self.nextLine = Line(1) | 663 self._cursor = 0 |
| 539 self.advance() | 664 self.Advance() |
| 540 | 665 |
| 541 def hasNext(self): | 666 def HasNext(self): |
| 542 return self.nextToken is not None | 667 return self.next_token is not None |
| 543 | 668 |
| 544 def advance(self): | 669 def Advance(self): |
| 545 if self.nextContents == '\n': | 670 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': |
| 546 self.nextLine = Line(self.nextLine.number + 1) | 671 self.next_line = _Line(self.next_line.number + 1) |
| 547 | 672 self.next_column = 0 |
| 548 self.nextToken = None | 673 elif self.next_token is not None: |
| 549 self.nextContents = None | 674 self.next_column += len(self.next_token.text) |
| 550 | 675 |
| 551 if self._remainder == '': | 676 self.next_token = None |
| 677 |
| 678 if self._cursor == len(self._string): |
| 552 return None | 679 return None |
| 553 | 680 assert self._cursor < len(self._string) |
| 554 for token in TokenValues: | 681 |
| 555 if self._remainder.startswith(token.text): | 682 if (self._cursor + 1 < len(self._string) and |
| 556 self.nextToken = token | 683 self._string[self._cursor + 1] in '{}'): |
| 684 self.next_token = ( |
| 685 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or |
| 686 _TOKENS.get(self._string[self._cursor:self._cursor+2])) |
| 687 |
| 688 if self.next_token is None: |
| 689 self.next_token = _Token.CHARACTER |
| 690 |
| 691 self._cursor += len(self.next_token.text) |
| 692 return self |
| 693 |
| 694 def AdvanceOver(self, token): |
| 695 if self.next_token != token: |
| 696 raise ParseException( |
| 697 'Expecting token %s but got %s at line %s' % (token.name, |
| 698 self.next_token.name, |
| 699 self.next_line)) |
| 700 return self.Advance() |
| 701 |
| 702 def AdvanceOverNextString(self, excluded=''): |
| 703 start = self._cursor - len(self.next_token.text) |
| 704 while (self.next_token is _Token.CHARACTER and |
| 705 # Can use -1 here because token length of CHARACTER is 1. |
| 706 self._string[self._cursor - 1] not in excluded): |
| 707 self.Advance() |
| 708 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) |
| 709 return self._string[start:end] |
| 710 |
| 711 def AdvanceToNextWhitespace(self): |
| 712 return self.AdvanceOverNextString(excluded=' \n\r\t') |
| 713 |
| 714 def SkipWhitespace(self): |
| 715 while (self.next_token is _Token.CHARACTER and |
| 716 # Can use -1 here because token length of CHARACTER is 1. |
| 717 self._string[self._cursor - 1] in ' \n\r\t'): |
| 718 self.Advance() |
| 719 |
| 720 class Handlebar(object): |
| 721 ''' A handlebar template. |
| 722 ''' |
| 723 def __init__(self, template, name=None): |
| 724 self.source = template |
| 725 self._name = name |
| 726 tokens = _TokenStream(template) |
| 727 self._top_node = self._ParseSection(tokens) |
| 728 if not self._top_node: |
| 729 raise ParseException('Template is empty') |
| 730 if tokens.HasNext(): |
| 731 raise ParseException('There are still tokens remaining at %s, ' |
| 732 'was there an end-section without a start-section?' |
| 733 % tokens.next_line) |
| 734 |
| 735 def _ParseSection(self, tokens): |
| 736 nodes = [] |
| 737 while tokens.HasNext(): |
| 738 if tokens.next_token in (_Token.OPEN_END_SECTION, |
| 739 _Token.OPEN_ELSE): |
| 740 # Handled after running parseSection within the SECTION cases, so this |
| 741 # is a terminating condition. If there *is* an orphaned |
| 742 # OPEN_END_SECTION, it will be caught by noticing that there are |
| 743 # leftover tokens after termination. |
| 557 break | 744 break |
| 558 | 745 elif tokens.next_token in (_Token.CLOSE_MUSTACHE, |
| 559 if not self.nextToken: | 746 _Token.CLOSE_MUSTACHE3): |
| 560 self.nextToken = Token.CHARACTER | 747 raise ParseException('Orphaned %s at line %s' % (tokens.next_token.name, |
| 561 | 748 tokens.next_line)) |
| 562 self.nextContents = self._remainder[0:len(self.nextToken.text)] | 749 nodes += self._ParseNextOpenToken(tokens) |
| 563 self._remainder = self._remainder[len(self.nextToken.text):] | |
| 564 return self | |
| 565 | |
| 566 def advanceOver(self, token): | |
| 567 if self.nextToken != token: | |
| 568 raise ParseException( | |
| 569 "Expecting token " + token.name + " but got " + self.nextToken.name, | |
| 570 self.nextLine) | |
| 571 return self.advance() | |
| 572 | |
| 573 def advanceOverNextString(self, excluded=''): | |
| 574 buf = StringBuilder() | |
| 575 while self.nextToken == Token.CHARACTER and \ | |
| 576 excluded.find(self.nextContents) == -1: | |
| 577 buf.append(self.nextContents) | |
| 578 self.advance() | |
| 579 return buf.toString() | |
| 580 | |
| 581 def advanceToNextWhitespace(self): | |
| 582 return self.advanceOverNextString(excluded=' \n\r\t') | |
| 583 | |
| 584 def skipWhitespace(self): | |
| 585 while len(self.nextContents) > 0 and \ | |
| 586 ' \n\r\t'.find(self.nextContents) >= 0: | |
| 587 self.advance() | |
| 588 | |
| 589 class Handlebar(object): | |
| 590 """ A handlebar template. | |
| 591 """ | |
| 592 def __init__(self, template): | |
| 593 self.source = template | |
| 594 tokens = TokenStream(template) | |
| 595 self._topNode = self._parseSection(tokens) | |
| 596 if not self._topNode: | |
| 597 raise ParseException("Template is empty", tokens.nextLine) | |
| 598 if tokens.hasNext(): | |
| 599 raise ParseException("There are still tokens remaining, " | |
| 600 "was there an end-section without a start-section:", | |
| 601 tokens.nextLine) | |
| 602 | |
| 603 def _parseSection(self, tokens): | |
| 604 nodes = [] | |
| 605 sectionEnded = False | |
| 606 | |
| 607 while tokens.hasNext() and not sectionEnded: | |
| 608 token = tokens.nextToken | |
| 609 | |
| 610 if token == Token.CHARACTER: | |
| 611 startLine = tokens.nextLine | |
| 612 string = tokens.advanceOverNextString() | |
| 613 nodes.append(StringNode(string, startLine, tokens.nextLine)) | |
| 614 elif token == Token.OPEN_VARIABLE or \ | |
| 615 token == Token.OPEN_UNESCAPED_VARIABLE or \ | |
| 616 token == Token.OPEN_START_JSON: | |
| 617 id = self._openSectionOrTag(tokens) | |
| 618 nodes.append(token.clazz(id, tokens.nextLine)) | |
| 619 elif token == Token.OPEN_START_PARTIAL: | |
| 620 tokens.advance() | |
| 621 id = Identifier(tokens.advanceToNextWhitespace(), | |
| 622 tokens.nextLine) | |
| 623 partialNode = PartialNode(id, tokens.nextLine) | |
| 624 | |
| 625 while tokens.nextToken == Token.CHARACTER: | |
| 626 tokens.skipWhitespace() | |
| 627 key = tokens.advanceOverNextString(excluded=':') | |
| 628 tokens.advance() | |
| 629 partialNode.addArgument( | |
| 630 key, | |
| 631 Identifier(tokens.advanceToNextWhitespace(), | |
| 632 tokens.nextLine)) | |
| 633 | |
| 634 tokens.advanceOver(Token.CLOSE_MUSTACHE) | |
| 635 nodes.append(partialNode) | |
| 636 elif token == Token.OPEN_START_SECTION: | |
| 637 id = self._openSectionOrTag(tokens) | |
| 638 section = self._parseSection(tokens) | |
| 639 self._closeSection(tokens, id) | |
| 640 if section: | |
| 641 nodes.append(SectionNode(id, section)) | |
| 642 elif token == Token.OPEN_START_VERTED_SECTION or \ | |
| 643 token == Token.OPEN_START_INVERTED_SECTION: | |
| 644 id = self._openSectionOrTag(tokens) | |
| 645 section = self._parseSection(tokens) | |
| 646 elseSection = None | |
| 647 if tokens.nextToken == Token.OPEN_ELSE: | |
| 648 self._openElse(tokens, id) | |
| 649 elseSection = self._parseSection(tokens) | |
| 650 self._closeSection(tokens, id) | |
| 651 if section: | |
| 652 nodes.append(token.clazz(id, section)) | |
| 653 if elseSection: | |
| 654 nodes.append(token.elseNodeClass()(id, elseSection)) | |
| 655 elif token == Token.OPEN_COMMENT: | |
| 656 self._advanceOverComment(tokens) | |
| 657 elif token == Token.OPEN_END_SECTION or \ | |
| 658 token == Token.OPEN_ELSE: | |
| 659 # Handled after running parseSection within the SECTION cases, so this i
s a | |
| 660 # terminating condition. If there *is* an orphaned OPEN_END_SECTION, it
will be caught | |
| 661 # by noticing that there are leftover tokens after termination. | |
| 662 sectionEnded = True | |
| 663 elif Token.CLOSE_MUSTACHE: | |
| 664 raise ParseException("Orphaned " + tokens.nextToken.name, | |
| 665 tokens.nextLine) | |
| 666 | 750 |
| 667 for i, node in enumerate(nodes): | 751 for i, node in enumerate(nodes): |
| 668 if isinstance(node, StringNode): | 752 if isinstance(node, _StringNode): |
| 669 continue | 753 continue |
| 670 | 754 |
| 671 previousNode = nodes[i - 1] if i > 0 else None | 755 previous_node = nodes[i - 1] if i > 0 else None |
| 672 nextNode = nodes[i + 1] if i < len(nodes) - 1 else None | 756 next_node = nodes[i + 1] if i < len(nodes) - 1 else None |
| 673 renderedNode = None | 757 rendered_node = None |
| 674 | 758 |
| 675 if node.getStartLine() != node.getEndLine(): | 759 if node.GetStartLine() != node.GetEndLine(): |
| 676 renderedNode = BlockNode(node) | 760 rendered_node = _BlockNode(node) |
| 677 if previousNode: | 761 if previous_node: |
| 678 previousNode.trimEndingSpaces() | 762 previous_node.TrimEndingSpaces() |
| 679 if nextNode: | 763 if next_node: |
| 680 nextNode.trimStartingNewLine() | 764 next_node.TrimStartingNewLine() |
| 681 elif isinstance(node, LeafNode) and \ | 765 elif (isinstance(node, _LeafNode) and |
| 682 (not previousNode or previousNode.endsWithEmptyLine()) and \ | 766 (not previous_node or previous_node.EndsWithEmptyLine()) and |
| 683 (not nextNode or nextNode.startsWithNewLine()): | 767 (not next_node or next_node.StartsWithNewLine())): |
| 684 indentation = 0 | 768 indentation = 0 |
| 685 if previousNode: | 769 if previous_node: |
| 686 indentation = previousNode.trimEndingSpaces() | 770 indentation = previous_node.TrimEndingSpaces() |
| 687 if nextNode: | 771 if next_node: |
| 688 nextNode.trimStartingNewLine() | 772 next_node.TrimStartingNewLine() |
| 689 renderedNode = IndentedNode(node, indentation) | 773 rendered_node = _IndentedNode(node, indentation) |
| 690 else: | 774 else: |
| 691 renderedNode = InlineNode(node) | 775 rendered_node = _InlineNode(node) |
| 692 | 776 |
| 693 nodes[i] = renderedNode | 777 nodes[i] = rendered_node |
| 694 | 778 |
| 695 if len(nodes) == 0: | 779 if len(nodes) == 0: |
| 696 return None | 780 return None |
| 697 if len(nodes) == 1: | 781 if len(nodes) == 1: |
| 698 return nodes[0] | 782 return nodes[0] |
| 699 return NodeCollection(nodes) | 783 return _NodeCollection(nodes) |
| 700 | 784 |
| 701 def _advanceOverComment(self, tokens): | 785 def _ParseNextOpenToken(self, tokens): |
| 702 tokens.advanceOver(Token.OPEN_COMMENT) | 786 next_token = tokens.next_token |
| 787 |
| 788 if next_token is _Token.CHARACTER: |
| 789 start_line = tokens.next_line |
| 790 string = tokens.AdvanceOverNextString() |
| 791 return [_StringNode(string, start_line, tokens.next_line)] |
| 792 elif next_token in (_Token.OPEN_VARIABLE, |
| 793 _Token.OPEN_UNESCAPED_VARIABLE, |
| 794 _Token.OPEN_START_JSON): |
| 795 id_, inline_value_id = self._OpenSectionOrTag(tokens) |
| 796 if inline_value_id is not None: |
| 797 raise ParseException( |
| 798 '%s cannot have an inline value' % id_.GetDescription()) |
| 799 return [next_token.clazz(id_)] |
| 800 elif next_token is _Token.OPEN_START_PARTIAL: |
| 801 tokens.Advance() |
| 802 column_start = tokens.next_column + 1 |
| 803 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), |
| 804 tokens.next_line, |
| 805 column_start) |
| 806 partial_node = _PartialNode(id_) |
| 807 while tokens.next_token is _Token.CHARACTER: |
| 808 tokens.SkipWhitespace() |
| 809 key = tokens.AdvanceOverNextString(excluded=':') |
| 810 tokens.Advance() |
| 811 column_start = tokens.next_column + 1 |
| 812 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), |
| 813 tokens.next_line, |
| 814 column_start) |
| 815 if key == '@': |
| 816 partial_node.SetLocalContext(id_) |
| 817 else: |
| 818 partial_node.AddArgument(key, id_) |
| 819 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 820 return [partial_node] |
| 821 elif next_token is _Token.OPEN_START_SECTION: |
| 822 id_, inline_node = self._OpenSectionOrTag(tokens) |
| 823 nodes = [] |
| 824 if inline_node is None: |
| 825 section = self._ParseSection(tokens) |
| 826 self._CloseSection(tokens, id_) |
| 827 nodes = [] |
| 828 if section is not None: |
| 829 nodes.append(_SectionNode(id_, section)) |
| 830 else: |
| 831 nodes.append(_SectionNode(id_, inline_node)) |
| 832 return nodes |
| 833 elif next_token in (_Token.OPEN_START_VERTED_SECTION, |
| 834 _Token.OPEN_START_INVERTED_SECTION): |
| 835 id_, inline_node = self._OpenSectionOrTag(tokens) |
| 836 nodes = [] |
| 837 if inline_node is None: |
| 838 section = self._ParseSection(tokens) |
| 839 else_section = None |
| 840 if tokens.next_token is _Token.OPEN_ELSE: |
| 841 self._OpenElse(tokens, id_) |
| 842 else_section = self._ParseSection(tokens) |
| 843 self._CloseSection(tokens, id_) |
| 844 if section: |
| 845 nodes.append(next_token.clazz(id_, section)) |
| 846 if else_section: |
| 847 nodes.append(next_token.ElseNodeClass()(id_, else_section)) |
| 848 else: |
| 849 nodes.append(next_token.clazz(id_, inline_node)) |
| 850 return nodes |
| 851 elif next_token is _Token.OPEN_COMMENT: |
| 852 start_line = tokens.next_line |
| 853 self._AdvanceOverComment(tokens) |
| 854 return [_CommentNode(start_line, tokens.next_line)] |
| 855 |
| 856 def _AdvanceOverComment(self, tokens): |
| 857 tokens.AdvanceOver(_Token.OPEN_COMMENT) |
| 703 depth = 1 | 858 depth = 1 |
| 704 while tokens.hasNext() and depth > 0: | 859 while tokens.HasNext() and depth > 0: |
| 705 if tokens.nextToken == Token.OPEN_COMMENT: | 860 if tokens.next_token is _Token.OPEN_COMMENT: |
| 706 depth += 1 | 861 depth += 1 |
| 707 elif tokens.nextToken == Token.CLOSE_COMMENT: | 862 elif tokens.next_token is _Token.CLOSE_COMMENT: |
| 708 depth -= 1 | 863 depth -= 1 |
| 709 tokens.advance() | 864 tokens.Advance() |
| 710 | 865 |
| 711 def _openSectionOrTag(self, tokens): | 866 def _OpenSectionOrTag(self, tokens): |
| 712 openToken = tokens.nextToken | 867 def NextIdentifierArgs(): |
| 713 tokens.advance() | 868 tokens.SkipWhitespace() |
| 714 id = Identifier(tokens.advanceOverNextString(), tokens.nextLine) | 869 line = tokens.next_line |
| 715 if openToken == Token.OPEN_UNESCAPED_VARIABLE: | 870 column = tokens.next_column + 1 |
| 716 tokens.advanceOver(Token.CLOSE_MUSTACHE3) | 871 name = tokens.AdvanceToNextWhitespace() |
| 872 tokens.SkipWhitespace() |
| 873 return (name, line, column) |
| 874 close_token = (_Token.CLOSE_MUSTACHE3 |
| 875 if tokens.next_token is _Token.OPEN_UNESCAPED_VARIABLE else |
| 876 _Token.CLOSE_MUSTACHE) |
| 877 tokens.Advance() |
| 878 id_ = _Identifier(*NextIdentifierArgs()) |
| 879 if tokens.next_token is close_token: |
| 880 tokens.AdvanceOver(close_token) |
| 881 inline_node = None |
| 717 else: | 882 else: |
| 718 tokens.advanceOver(Token.CLOSE_MUSTACHE) | 883 name, line, column = NextIdentifierArgs() |
| 719 return id | 884 tokens.AdvanceOver(_Token.INLINE_END_SECTION) |
| 720 | 885 # Support select other types of nodes, the most useful being partial. |
| 721 def _closeSection(self, tokens, id): | 886 clazz = _UnescapedVariableNode |
| 722 tokens.advanceOver(Token.OPEN_END_SECTION) | 887 if name.startswith('*'): |
| 723 nextString = tokens.advanceOverNextString() | 888 clazz = _JsonNode |
| 724 if nextString != '' and nextString != str(id): | 889 elif name.startswith('+'): |
| 890 clazz = _PartialNode |
| 891 if clazz is not _UnescapedVariableNode: |
| 892 name = name[1:] |
| 893 column += 1 |
| 894 inline_node = clazz(_Identifier(name, line, column)) |
| 895 return (id_, inline_node) |
| 896 |
| 897 def _CloseSection(self, tokens, id_): |
| 898 tokens.AdvanceOver(_Token.OPEN_END_SECTION) |
| 899 next_string = tokens.AdvanceOverNextString() |
| 900 if next_string != '' and next_string != id_.name: |
| 725 raise ParseException( | 901 raise ParseException( |
| 726 "Start section " + str(id) + " doesn't match end " + nextString) | 902 'Start section %s doesn\'t match end %s' % (id_, next_string)) |
| 727 tokens.advanceOver(Token.CLOSE_MUSTACHE) | 903 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 728 | 904 |
| 729 def _openElse(self, tokens, id): | 905 def _OpenElse(self, tokens, id_): |
| 730 tokens.advanceOver(Token.OPEN_ELSE) | 906 tokens.AdvanceOver(_Token.OPEN_ELSE) |
| 731 nextString = tokens.advanceOverNextString() | 907 next_string = tokens.AdvanceOverNextString() |
| 732 if nextString != '' and nextString != str(id): | 908 if next_string != '' and next_string != id_.name: |
| 733 raise ParseException( | 909 raise ParseException( |
| 734 "Start section " + str(id) + " doesn't match else " + nextString) | 910 'Start section %s doesn\'t match else %s' % (id_, next_string)) |
| 735 tokens.advanceOver(Token.CLOSE_MUSTACHE) | 911 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 912 |
| 913 def Render(self, *contexts): |
| 914 '''Renders this template given a variable number of contexts to read out |
| 915 values from (such as those appearing in {{foo}}). |
| 916 ''' |
| 917 name = self._name or '<root>' |
| 918 render_state = _RenderState(name, _Contexts(contexts)) |
| 919 self._top_node.Render(render_state) |
| 920 return render_state.GetResult() |
| 736 | 921 |
| 737 def render(self, *contexts): | 922 def render(self, *contexts): |
| 738 """ Renders this template given a variable number of "contexts" to read | 923 return self.Render(*contexts) |
| 739 out values from (such as those appearing in {{foo}}). | |
| 740 """ | |
| 741 globalContexts = [] | |
| 742 for context in contexts: | |
| 743 globalContexts.append(context) | |
| 744 renderState = RenderState(globalContexts, []) | |
| 745 self._topNode.render(renderState) | |
| 746 return renderState.getResult() | |
| OLD | NEW |