| 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? | 15 # TODO: New name, not "handlebar". |
| 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. \{{, \{{-. | 16 # TODO: Escaping control characters somehow. e.g. \{{, \{{-. |
| 22 # TODO: Dump warnings-so-far into the output. | |
| 23 | 17 |
| 24 import json | 18 import json |
| 25 import re | 19 import re |
| 26 | 20 |
| 27 '''Handlebar templates are data binding templates more-than-loosely inspired by | 21 '''Handlebar templates are data binding templates more-than-loosely inspired by |
| 28 ctemplate. Use like: | 22 ctemplate. Use like: |
| 29 | 23 |
| 30 from handlebar import Handlebar | 24 from handlebar import Handlebar |
| 31 | 25 |
| 32 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') | 26 template = Handlebar('hello {{#foo bar/}} world') |
| 33 input = { | 27 input = { |
| 34 'foo': [ | 28 'foo': [ |
| 35 { 'bar': 1 }, | 29 { 'bar': 1 }, |
| 36 { 'bar': 2 }, | 30 { 'bar': 2 }, |
| 37 { 'bar': 3 } | 31 { 'bar': 3 } |
| 38 ] | 32 ] |
| 39 } | 33 } |
| 40 print(template.render(input).text) | 34 print(template.render(input).text) |
| 41 | 35 |
| 42 Handlebar will use get() on contexts to return values, so to create custom | 36 Handlebar will use get() on contexts to return values, so to create custom |
| (...skipping 15 matching lines...) Expand all Loading... |
| 58 Exception.__init__(self, error) | 52 Exception.__init__(self, error) |
| 59 | 53 |
| 60 class RenderResult(object): | 54 class RenderResult(object): |
| 61 '''The result of a render operation. | 55 '''The result of a render operation. |
| 62 ''' | 56 ''' |
| 63 def __init__(self, text, errors): | 57 def __init__(self, text, errors): |
| 64 self.text = text; | 58 self.text = text; |
| 65 self.errors = errors | 59 self.errors = errors |
| 66 | 60 |
| 67 def __repr__(self): | 61 def __repr__(self): |
| 68 return '%s(text=%s, errors=%s)' % ( | 62 return '%s(text=%s, errors=%s)' % (type(self).__name__, |
| 69 self.__class__.__name__, self.text, self.errors) | 63 self.text, |
| 64 self.errors) |
| 70 | 65 |
| 71 def __str__(self): | 66 def __str__(self): |
| 72 return repr(self) | 67 return repr(self) |
| 73 | 68 |
| 74 class _StringBuilder(object): | 69 class _StringBuilder(object): |
| 75 '''Efficiently builds strings. | 70 '''Efficiently builds strings. |
| 76 ''' | 71 ''' |
| 77 def __init__(self): | 72 def __init__(self): |
| 78 self._buf = [] | 73 self._buf = [] |
| 79 | 74 |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 153 def Push(self, context): | 148 def Push(self, context): |
| 154 self._nodes.append(_Contexts._Node(context)) | 149 self._nodes.append(_Contexts._Node(context)) |
| 155 | 150 |
| 156 def Pop(self): | 151 def Pop(self): |
| 157 node = self._nodes.pop() | 152 node = self._nodes.pop() |
| 158 assert len(self._nodes) >= self._first_local | 153 assert len(self._nodes) >= self._first_local |
| 159 for found_key in node.GetKeys(): | 154 for found_key in node.GetKeys(): |
| 160 # [0] is the stack of nodes that |found_key| has been found in. | 155 # [0] is the stack of nodes that |found_key| has been found in. |
| 161 self._value_info[found_key][0].pop() | 156 self._value_info[found_key][0].pop() |
| 162 | 157 |
| 163 def GetTopLocal(self): | |
| 164 if len(self._nodes) == self._first_local: | |
| 165 return None | |
| 166 return self._nodes[-1]._value | |
| 167 | |
| 168 def Resolve(self, path): | 158 def Resolve(self, path): |
| 169 # This method is only efficient at finding |key|; if |tail| has a value (and | 159 # This method is only efficient at finding |key|; if |tail| has a value (and |
| 170 # |key| evaluates to an indexable value) we'll need to descend into that. | 160 # |key| evaluates to an indexable value) we'll need to descend into that. |
| 171 key, tail = path.split('.', 1) if '.' in path else (path, None) | 161 key, tail = path.split('.', 1) if '.' in path else (path, None) |
| 172 | 162 found = self._FindNodeValue(key) |
| 173 if key == '@': | |
| 174 found = self._nodes[-1]._value | |
| 175 else: | |
| 176 found = self._FindNodeValue(key) | |
| 177 | |
| 178 if tail is None: | 163 if tail is None: |
| 179 return found | 164 return found |
| 180 | |
| 181 for part in tail.split('.'): | 165 for part in tail.split('.'): |
| 182 if not hasattr(found, 'get'): | 166 if not hasattr(found, 'get'): |
| 183 return None | 167 return None |
| 184 found = found.get(part) | 168 found = found.get(part) |
| 185 return found | 169 return found |
| 186 | 170 |
| 171 def Scope(self, context, fn, *args): |
| 172 self.Push(context) |
| 173 try: |
| 174 return fn(*args) |
| 175 finally: |
| 176 self.Pop() |
| 177 |
| 187 def _FindNodeValue(self, key): | 178 def _FindNodeValue(self, key): |
| 188 # |found_node_list| will be all the nodes that |key| has been found in. | 179 # |found_node_list| will be all the nodes that |key| has been found in. |
| 189 # |checked_node_set| are those that have been checked. | 180 # |checked_node_set| are those that have been checked. |
| 190 info = self._value_info.get(key) | 181 info = self._value_info.get(key) |
| 191 if info is None: | 182 if info is None: |
| 192 info = ([], set()) | 183 info = ([], set()) |
| 193 self._value_info[key] = info | 184 self._value_info[key] = info |
| 194 found_node_list, checked_node_set = info | 185 found_node_list, checked_node_set = info |
| 195 | 186 |
| 196 # Check all the nodes not yet checked for |key|. | 187 # Check all the nodes not yet checked for |key|. |
| (...skipping 21 matching lines...) Expand all Loading... |
| 218 self.id_ = id_ | 209 self.id_ = id_ |
| 219 | 210 |
| 220 def __init__(self, entries=[]): | 211 def __init__(self, entries=[]): |
| 221 self.entries = entries | 212 self.entries = entries |
| 222 | 213 |
| 223 def Descend(self, name, id_): | 214 def Descend(self, name, id_): |
| 224 descended = list(self.entries) | 215 descended = list(self.entries) |
| 225 descended.append(_Stack.Entry(name, id_)) | 216 descended.append(_Stack.Entry(name, id_)) |
| 226 return _Stack(entries=descended) | 217 return _Stack(entries=descended) |
| 227 | 218 |
| 219 class _InternalContext(object): |
| 220 def __init__(self): |
| 221 self._render_state = None |
| 222 |
| 223 def SetRenderState(self, render_state): |
| 224 self._render_state = render_state |
| 225 |
| 226 def get(self, key): |
| 227 if key == 'errors': |
| 228 errors = self._render_state._errors |
| 229 return '\n'.join(errors) if errors else None |
| 230 return None |
| 231 |
| 228 class _RenderState(object): | 232 class _RenderState(object): |
| 229 '''The state of a render call. | 233 '''The state of a render call. |
| 230 ''' | 234 ''' |
| 231 def __init__(self, name, contexts, _stack=_Stack()): | 235 def __init__(self, name, contexts, _stack=_Stack()): |
| 232 self.text = _StringBuilder() | 236 self.text = _StringBuilder() |
| 233 self.contexts = contexts | 237 self.contexts = contexts |
| 234 self._name = name | 238 self._name = name |
| 235 self._errors = [] | 239 self._errors = [] |
| 236 self._stack = _stack | 240 self._stack = _stack |
| 237 | 241 |
| 238 def AddResolutionError(self, id_): | 242 def AddResolutionError(self, id_, description=None): |
| 239 self._errors.append( | 243 message = id_.CreateResolutionErrorMessage(self._name, stack=self._stack) |
| 240 id_.CreateResolutionErrorMessage(self._name, stack=self._stack)) | 244 if description is not None: |
| 245 message = '%s (%s)' % (message, description) |
| 246 self._errors.append(message) |
| 241 | 247 |
| 242 def Copy(self): | 248 def Copy(self): |
| 243 return _RenderState( | 249 return _RenderState( |
| 244 self._name, self.contexts, _stack=self._stack) | 250 self._name, self.contexts, _stack=self._stack) |
| 245 | 251 |
| 246 def ForkPartial(self, custom_name, id_): | 252 def ForkPartial(self, custom_name, id_): |
| 247 name = custom_name or id_.name | 253 name = custom_name or id_.name |
| 248 return _RenderState(name, | 254 return _RenderState(name, |
| 249 self.contexts.CreateFromGlobals(), | 255 self.contexts.CreateFromGlobals(), |
| 250 _stack=self._stack.Descend(name, id_)) | 256 _stack=self._stack.Descend(name, id_)) |
| 251 | 257 |
| 252 def Merge(self, render_state, text_transform=None): | 258 def Merge(self, render_state, text_transform=None): |
| 253 self._errors.extend(render_state._errors) | 259 self._errors.extend(render_state._errors) |
| 254 text = render_state.text.ToString() | 260 text = render_state.text.ToString() |
| 255 if text_transform is not None: | 261 if text_transform is not None: |
| 256 text = text_transform(text) | 262 text = text_transform(text) |
| 257 self.text.Append(text) | 263 self.text.Append(text) |
| 258 | 264 |
| 259 def GetResult(self): | 265 def GetResult(self): |
| 260 return RenderResult(self.text.ToString(), self._errors); | 266 return RenderResult(self.text.ToString(), self._errors); |
| 261 | 267 |
| 262 class _Identifier(object): | 268 class _Identifier(object): |
| 263 ''' An identifier of the form '@', 'foo.bar.baz', or '@.foo.bar.baz'. | 269 '''An identifier of the form 'foo', 'foo.bar.baz', 'foo-bar.baz', etc. |
| 264 ''' | 270 ''' |
| 271 _VALID_ID_MATCHER = re.compile(r'^[a-zA-Z0-9@_/-]+$') |
| 272 |
| 265 def __init__(self, name, line, column): | 273 def __init__(self, name, line, column): |
| 266 self.name = name | 274 self.name = name |
| 267 self.line = line | 275 self.line = line |
| 268 self.column = column | 276 self.column = column |
| 269 if name == '': | 277 if name == '': |
| 270 raise ParseException('Empty identifier %s' % self.GetDescription()) | 278 raise ParseException('Empty identifier %s' % self.GetDescription()) |
| 271 for part in name.split('.'): | 279 for part in name.split('.'): |
| 272 if part != '@' and not re.match('^[a-zA-Z0-9_/-]+$', part): | 280 if not _Identifier._VALID_ID_MATCHER.match(part): |
| 273 raise ParseException('Invalid identifier %s' % self.GetDescription()) | 281 raise ParseException('Invalid identifier %s' % self.GetDescription()) |
| 274 | 282 |
| 275 def GetDescription(self): | 283 def GetDescription(self): |
| 276 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) | 284 return '\'%s\' at line %s column %s' % (self.name, self.line, self.column) |
| 277 | 285 |
| 278 def CreateResolutionErrorMessage(self, name, stack=None): | 286 def CreateResolutionErrorMessage(self, name, stack=None): |
| 279 message = _StringBuilder() | 287 message = _StringBuilder() |
| 280 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), | 288 message.Append('Failed to resolve %s in %s\n' % (self.GetDescription(), |
| 281 name)) | 289 name)) |
| 282 if stack is not None: | 290 if stack is not None: |
| 283 for entry in stack.entries: | 291 for entry in reversed(stack.entries): |
| 284 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), | 292 message.Append(' included as %s in %s\n' % (entry.id_.GetDescription(), |
| 285 entry.name)) | 293 entry.name)) |
| 286 return message.ToString() | 294 return message.ToString().strip() |
| 287 | 295 |
| 288 def __repr__(self): | 296 def __repr__(self): |
| 289 return self.name | 297 return self.name |
| 290 | 298 |
| 291 def __str__(self): | 299 def __str__(self): |
| 292 return repr(self) | 300 return repr(self) |
| 293 | 301 |
| 294 class _Line(object): | 302 class _Node(object): pass |
| 295 def __init__(self, number): | |
| 296 self.number = number | |
| 297 | 303 |
| 298 def __repr__(self): | 304 class _LeafNode(_Node): |
| 299 return str(self.number) | |
| 300 | |
| 301 def __str__(self): | |
| 302 return repr(self) | |
| 303 | |
| 304 class _LeafNode(object): | |
| 305 def __init__(self, start_line, end_line): | 305 def __init__(self, start_line, end_line): |
| 306 self._start_line = start_line | 306 self._start_line = start_line |
| 307 self._end_line = end_line | 307 self._end_line = end_line |
| 308 | 308 |
| 309 def StartsWithNewLine(self): | 309 def StartsWithNewLine(self): |
| 310 return False | 310 return False |
| 311 | 311 |
| 312 def TrimStartingNewLine(self): | 312 def TrimStartingNewLine(self): |
| 313 pass | 313 pass |
| 314 | 314 |
| 315 def TrimEndingSpaces(self): | 315 def TrimEndingSpaces(self): |
| 316 return 0 | 316 return 0 |
| 317 | 317 |
| 318 def TrimEndingNewLine(self): | 318 def TrimEndingNewLine(self): |
| 319 pass | 319 pass |
| 320 | 320 |
| 321 def EndsWithEmptyLine(self): | 321 def EndsWithEmptyLine(self): |
| 322 return False | 322 return False |
| 323 | 323 |
| 324 def GetStartLine(self): | 324 def GetStartLine(self): |
| 325 return self._start_line | 325 return self._start_line |
| 326 | 326 |
| 327 def GetEndLine(self): | 327 def GetEndLine(self): |
| 328 return self._end_line | 328 return self._end_line |
| 329 | 329 |
| 330 class _DecoratorNode(object): | 330 def __str__(self): |
| 331 return repr(self) |
| 332 |
| 333 class _DecoratorNode(_Node): |
| 331 def __init__(self, content): | 334 def __init__(self, content): |
| 332 self._content = content | 335 self._content = content |
| 333 | 336 |
| 334 def StartsWithNewLine(self): | 337 def StartsWithNewLine(self): |
| 335 return self._content.StartsWithNewLine() | 338 return self._content.StartsWithNewLine() |
| 336 | 339 |
| 337 def TrimStartingNewLine(self): | 340 def TrimStartingNewLine(self): |
| 338 self._content.TrimStartingNewLine() | 341 self._content.TrimStartingNewLine() |
| 339 | 342 |
| 340 def TrimEndingSpaces(self): | 343 def TrimEndingSpaces(self): |
| 341 return self._content.TrimEndingSpaces() | 344 return self._content.TrimEndingSpaces() |
| 342 | 345 |
| 343 def TrimEndingNewLine(self): | 346 def TrimEndingNewLine(self): |
| 344 self._content.TrimEndingNewLine() | 347 self._content.TrimEndingNewLine() |
| 345 | 348 |
| 346 def EndsWithEmptyLine(self): | 349 def EndsWithEmptyLine(self): |
| 347 return self._content.EndsWithEmptyLine() | 350 return self._content.EndsWithEmptyLine() |
| 348 | 351 |
| 349 def GetStartLine(self): | 352 def GetStartLine(self): |
| 350 return self._content.GetStartLine() | 353 return self._content.GetStartLine() |
| 351 | 354 |
| 352 def GetEndLine(self): | 355 def GetEndLine(self): |
| 353 return self._content.GetEndLine() | 356 return self._content.GetEndLine() |
| 354 | 357 |
| 355 def __repr__(self): | 358 def __repr__(self): |
| 356 return str(self._content) | 359 return str(self._content) |
| 357 | 360 |
| 358 def __str__(self): | 361 def __str__(self): |
| 359 return repr(self) | 362 return repr(self) |
| 360 | 363 |
| 361 class _InlineNode(_DecoratorNode): | 364 class _InlineNode(_DecoratorNode): |
| 362 def __init__(self, content): | 365 def __init__(self, content): |
| 363 _DecoratorNode.__init__(self, content) | 366 _DecoratorNode.__init__(self, content) |
| 364 | 367 |
| 365 def Render(self, render_state): | 368 def Render(self, render_state): |
| 366 content_render_state = render_state.Copy() | 369 content_render_state = render_state.Copy() |
| 367 self._content.Render(content_render_state) | 370 self._content.Render(content_render_state) |
| 368 render_state.Merge(content_render_state, | 371 render_state.Merge(content_render_state, |
| 369 text_transform=lambda text: text.replace('\n', '')) | 372 text_transform=lambda text: text.replace('\n', '')) |
| 370 | 373 |
| 371 class _IndentedNode(_DecoratorNode): | 374 class _IndentedNode(_DecoratorNode): |
| 372 def __init__(self, content, indentation): | 375 def __init__(self, content, indentation): |
| 373 _DecoratorNode.__init__(self, content) | 376 _DecoratorNode.__init__(self, content) |
| 374 self._indent_str = ' ' * indentation | 377 self._indent_str = ' ' * indentation |
| 375 | 378 |
| 376 def Render(self, render_state): | 379 def Render(self, render_state): |
| 377 if isinstance(self._content, _CommentNode): | 380 if isinstance(self._content, _CommentNode): |
| 378 return | 381 return |
| 379 content_render_state = render_state.Copy() | 382 def inlinify(text): |
| 380 self._content.Render(content_render_state) | 383 if len(text) == 0: # avoid rendering a blank line |
| 381 def AddIndentation(text): | 384 return '' |
| 382 buf = _StringBuilder() | 385 buf = _StringBuilder() |
| 383 buf.Append(self._indent_str) | 386 buf.Append(self._indent_str) |
| 384 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) | 387 buf.Append(text.replace('\n', '\n%s' % self._indent_str)) |
| 385 buf.Append('\n') | 388 if not text.endswith('\n'): # partials will often already end in a \n |
| 389 buf.Append('\n') |
| 386 return buf.ToString() | 390 return buf.ToString() |
| 387 render_state.Merge(content_render_state, text_transform=AddIndentation) | 391 content_render_state = render_state.Copy() |
| 392 self._content.Render(content_render_state) |
| 393 render_state.Merge(content_render_state, text_transform=inlinify) |
| 388 | 394 |
| 389 class _BlockNode(_DecoratorNode): | 395 class _BlockNode(_DecoratorNode): |
| 390 def __init__(self, content): | 396 def __init__(self, content): |
| 391 _DecoratorNode.__init__(self, content) | 397 _DecoratorNode.__init__(self, content) |
| 392 content.TrimStartingNewLine() | 398 content.TrimStartingNewLine() |
| 393 content.TrimEndingSpaces() | 399 content.TrimEndingSpaces() |
| 394 | 400 |
| 395 def Render(self, render_state): | 401 def Render(self, render_state): |
| 396 self._content.Render(render_state) | 402 self._content.Render(render_state) |
| 397 | 403 |
| 398 class _NodeCollection(object): | 404 class _NodeCollection(_Node): |
| 399 def __init__(self, nodes): | 405 def __init__(self, nodes): |
| 400 assert nodes | 406 assert nodes |
| 401 self._nodes = nodes | 407 self._nodes = nodes |
| 402 | 408 |
| 403 def Render(self, render_state): | 409 def Render(self, render_state): |
| 404 for node in self._nodes: | 410 for node in self._nodes: |
| 405 node.Render(render_state) | 411 node.Render(render_state) |
| 406 | 412 |
| 407 def StartsWithNewLine(self): | 413 def StartsWithNewLine(self): |
| 408 return self._nodes[0].StartsWithNewLine() | 414 return self._nodes[0].StartsWithNewLine() |
| (...skipping 12 matching lines...) Expand all Loading... |
| 421 | 427 |
| 422 def GetStartLine(self): | 428 def GetStartLine(self): |
| 423 return self._nodes[0].GetStartLine() | 429 return self._nodes[0].GetStartLine() |
| 424 | 430 |
| 425 def GetEndLine(self): | 431 def GetEndLine(self): |
| 426 return self._nodes[-1].GetEndLine() | 432 return self._nodes[-1].GetEndLine() |
| 427 | 433 |
| 428 def __repr__(self): | 434 def __repr__(self): |
| 429 return ''.join(str(node) for node in self._nodes) | 435 return ''.join(str(node) for node in self._nodes) |
| 430 | 436 |
| 431 def __str__(self): | 437 class _StringNode(_Node): |
| 432 return repr(self) | 438 '''Just a string. |
| 433 | |
| 434 class _StringNode(object): | |
| 435 ''' Just a string. | |
| 436 ''' | 439 ''' |
| 437 def __init__(self, string, start_line, end_line): | 440 def __init__(self, string, start_line, end_line): |
| 438 self._string = string | 441 self._string = string |
| 439 self._start_line = start_line | 442 self._start_line = start_line |
| 440 self._end_line = end_line | 443 self._end_line = end_line |
| 441 | 444 |
| 442 def Render(self, render_state): | 445 def Render(self, render_state): |
| 443 render_state.text.Append(self._string) | 446 render_state.text.Append(self._string) |
| 444 | 447 |
| 445 def StartsWithNewLine(self): | 448 def StartsWithNewLine(self): |
| (...skipping 24 matching lines...) Expand all Loading... |
| 470 | 473 |
| 471 def GetStartLine(self): | 474 def GetStartLine(self): |
| 472 return self._start_line | 475 return self._start_line |
| 473 | 476 |
| 474 def GetEndLine(self): | 477 def GetEndLine(self): |
| 475 return self._end_line | 478 return self._end_line |
| 476 | 479 |
| 477 def __repr__(self): | 480 def __repr__(self): |
| 478 return self._string | 481 return self._string |
| 479 | 482 |
| 480 def __str__(self): | |
| 481 return repr(self) | |
| 482 | |
| 483 class _EscapedVariableNode(_LeafNode): | 483 class _EscapedVariableNode(_LeafNode): |
| 484 ''' {{foo}} | 484 '''{{foo}} |
| 485 ''' | 485 ''' |
| 486 def __init__(self, id_): | 486 def __init__(self, id_): |
| 487 _LeafNode.__init__(self, id_.line, id_.line) | 487 _LeafNode.__init__(self, id_.line, id_.line) |
| 488 self._id = id_ | 488 self._id = id_ |
| 489 | 489 |
| 490 def Render(self, render_state): | 490 def Render(self, render_state): |
| 491 value = render_state.contexts.Resolve(self._id.name) | 491 value = render_state.contexts.Resolve(self._id.name) |
| 492 if value is None: | 492 if value is None: |
| 493 render_state.AddResolutionError(self._id) | 493 render_state.AddResolutionError(self._id) |
| 494 return | 494 return |
| 495 string = value if isinstance(value, basestring) else str(value) | 495 string = value if isinstance(value, basestring) else str(value) |
| 496 render_state.text.Append(string.replace('&', '&') | 496 render_state.text.Append(string.replace('&', '&') |
| 497 .replace('<', '<') | 497 .replace('<', '<') |
| 498 .replace('>', '>')) | 498 .replace('>', '>')) |
| 499 | 499 |
| 500 def __repr__(self): | 500 def __repr__(self): |
| 501 return '{{%s}}' % self._id | 501 return '{{%s}}' % self._id |
| 502 | 502 |
| 503 def __str__(self): | |
| 504 return repr(self) | |
| 505 | |
| 506 class _UnescapedVariableNode(_LeafNode): | 503 class _UnescapedVariableNode(_LeafNode): |
| 507 ''' {{{foo}}} | 504 '''{{{foo}}} |
| 508 ''' | 505 ''' |
| 509 def __init__(self, id_): | 506 def __init__(self, id_): |
| 510 _LeafNode.__init__(self, id_.line, id_.line) | 507 _LeafNode.__init__(self, id_.line, id_.line) |
| 511 self._id = id_ | 508 self._id = id_ |
| 512 | 509 |
| 513 def Render(self, render_state): | 510 def Render(self, render_state): |
| 514 value = render_state.contexts.Resolve(self._id.name) | 511 value = render_state.contexts.Resolve(self._id.name) |
| 515 if value is None: | 512 if value is None: |
| 516 render_state.AddResolutionError(self._id) | 513 render_state.AddResolutionError(self._id) |
| 517 return | 514 return |
| 518 string = value if isinstance(value, basestring) else str(value) | 515 string = value if isinstance(value, basestring) else str(value) |
| 519 render_state.text.Append(string) | 516 render_state.text.Append(string) |
| 520 | 517 |
| 521 def __repr__(self): | 518 def __repr__(self): |
| 522 return '{{{%s}}}' % self._id | 519 return '{{{%s}}}' % self._id |
| 523 | 520 |
| 524 def __str__(self): | |
| 525 return repr(self) | |
| 526 | |
| 527 class _CommentNode(_LeafNode): | 521 class _CommentNode(_LeafNode): |
| 528 '''{{- This is a comment -}} | 522 '''{{- This is a comment -}} |
| 529 An empty placeholder node for correct indented rendering behaviour. | 523 An empty placeholder node for correct indented rendering behaviour. |
| 530 ''' | 524 ''' |
| 531 def __init__(self, start_line, end_line): | 525 def __init__(self, start_line, end_line): |
| 532 _LeafNode.__init__(self, start_line, end_line) | 526 _LeafNode.__init__(self, start_line, end_line) |
| 533 | 527 |
| 534 def Render(self, render_state): | 528 def Render(self, render_state): |
| 535 pass | 529 pass |
| 536 | 530 |
| 537 def __repr__(self): | 531 def __repr__(self): |
| 538 return '<comment>' | 532 return '<comment>' |
| 539 | 533 |
| 540 def __str__(self): | |
| 541 return repr(self) | |
| 542 | |
| 543 class _SectionNode(_DecoratorNode): | 534 class _SectionNode(_DecoratorNode): |
| 544 ''' {{#foo}} ... {{/}} | 535 '''{{#var:foo}} ... {{/foo}} |
| 545 ''' | 536 ''' |
| 546 def __init__(self, id_, content): | 537 def __init__(self, bind_to, id_, content): |
| 547 _DecoratorNode.__init__(self, content) | 538 _DecoratorNode.__init__(self, content) |
| 539 self._bind_to = bind_to |
| 548 self._id = id_ | 540 self._id = id_ |
| 549 | 541 |
| 550 def Render(self, render_state): | 542 def Render(self, render_state): |
| 551 value = render_state.contexts.Resolve(self._id.name) | 543 value = render_state.contexts.Resolve(self._id.name) |
| 552 if isinstance(value, list): | 544 if isinstance(value, list): |
| 553 for item in value: | 545 for item in value: |
| 554 # Always push context, even if it's not "valid", since we want to | 546 if self._bind_to is not None: |
| 555 # be able to refer to items in a list such as [1,2,3] via @. | 547 render_state.contexts.Scope({self._bind_to.name: item}, |
| 556 render_state.contexts.Push(item) | 548 self._content.Render, render_state) |
| 557 self._content.Render(render_state) | 549 else: |
| 558 render_state.contexts.Pop() | 550 self._content.Render(render_state) |
| 559 elif hasattr(value, 'get'): | 551 elif hasattr(value, 'get'): |
| 560 render_state.contexts.Push(value) | 552 if self._bind_to is not None: |
| 561 self._content.Render(render_state) | 553 render_state.contexts.Scope({self._bind_to.name: value}, |
| 562 render_state.contexts.Pop() | 554 self._content.Render, render_state) |
| 555 else: |
| 556 render_state.contexts.Scope(value, self._content.Render, render_state) |
| 563 else: | 557 else: |
| 564 render_state.AddResolutionError(self._id) | 558 render_state.AddResolutionError(self._id) |
| 565 | 559 |
| 566 def __repr__(self): | 560 def __repr__(self): |
| 567 return '{{#%s}}%s{{/%s}}' % ( | 561 return '{{#%s}}%s{{/%s}}' % ( |
| 568 self._id, _DecoratorNode.__repr__(self), self._id) | 562 self._id, _DecoratorNode.__repr__(self), self._id) |
| 569 | 563 |
| 570 def __str__(self): | |
| 571 return repr(self) | |
| 572 | |
| 573 class _VertedSectionNode(_DecoratorNode): | 564 class _VertedSectionNode(_DecoratorNode): |
| 574 ''' {{?foo}} ... {{/}} | 565 '''{{?var:foo}} ... {{/foo}} |
| 575 ''' | 566 ''' |
| 576 def __init__(self, id_, content): | 567 def __init__(self, bind_to, id_, content): |
| 577 _DecoratorNode.__init__(self, content) | 568 _DecoratorNode.__init__(self, content) |
| 569 self._bind_to = bind_to |
| 578 self._id = id_ | 570 self._id = id_ |
| 579 | 571 |
| 580 def Render(self, render_state): | 572 def Render(self, render_state): |
| 581 value = render_state.contexts.Resolve(self._id.name) | 573 value = render_state.contexts.Resolve(self._id.name) |
| 582 if _VertedSectionNode.ShouldRender(value): | 574 if _VertedSectionNode.ShouldRender(value): |
| 583 render_state.contexts.Push(value) | 575 if self._bind_to is not None: |
| 584 self._content.Render(render_state) | 576 render_state.contexts.Scope({self._bind_to.name: value}, |
| 585 render_state.contexts.Pop() | 577 self._content.Render, render_state) |
| 578 else: |
| 579 self._content.Render(render_state) |
| 586 | 580 |
| 587 def __repr__(self): | 581 def __repr__(self): |
| 588 return '{{?%s}}%s{{/%s}}' % ( | 582 return '{{?%s}}%s{{/%s}}' % ( |
| 589 self._id, _DecoratorNode.__repr__(self), self._id) | 583 self._id, _DecoratorNode.__repr__(self), self._id) |
| 590 | 584 |
| 591 def __str__(self): | |
| 592 return repr(self) | |
| 593 | |
| 594 @staticmethod | 585 @staticmethod |
| 595 def ShouldRender(value): | 586 def ShouldRender(value): |
| 596 if value is None: | 587 if value is None: |
| 597 return False | 588 return False |
| 598 if isinstance(value, bool): | 589 if isinstance(value, bool): |
| 599 return value | 590 return value |
| 600 if isinstance(value, list): | 591 if isinstance(value, list): |
| 601 return len(value) > 0 | 592 return len(value) > 0 |
| 602 return True | 593 return True |
| 603 | 594 |
| 604 class _InvertedSectionNode(_DecoratorNode): | 595 class _InvertedSectionNode(_DecoratorNode): |
| 605 ''' {{^foo}} ... {{/}} | 596 '''{{^foo}} ... {{/foo}} |
| 606 ''' | 597 ''' |
| 607 def __init__(self, id_, content): | 598 def __init__(self, bind_to, id_, content): |
| 608 _DecoratorNode.__init__(self, content) | 599 _DecoratorNode.__init__(self, content) |
| 600 if bind_to is not None: |
| 601 raise ParseException('{{^%s:%s}} does not support variable binding' |
| 602 % (bind_to, id_)) |
| 609 self._id = id_ | 603 self._id = id_ |
| 610 | 604 |
| 611 def Render(self, render_state): | 605 def Render(self, render_state): |
| 612 value = render_state.contexts.Resolve(self._id.name) | 606 value = render_state.contexts.Resolve(self._id.name) |
| 613 if not _VertedSectionNode.ShouldRender(value): | 607 if not _VertedSectionNode.ShouldRender(value): |
| 614 self._content.Render(render_state) | 608 self._content.Render(render_state) |
| 615 | 609 |
| 616 def __repr__(self): | 610 def __repr__(self): |
| 617 return '{{^%s}}%s{{/%s}}' % ( | 611 return '{{^%s}}%s{{/%s}}' % ( |
| 618 self._id, _DecoratorNode.__repr__(self), self._id) | 612 self._id, _DecoratorNode.__repr__(self), self._id) |
| 619 | 613 |
| 620 def __str__(self): | 614 class _AssertionNode(_LeafNode): |
| 621 return repr(self) | 615 '''{{!foo Some comment about foo}} |
| 616 ''' |
| 617 def __init__(self, id_, description): |
| 618 _LeafNode.__init__(self, id_.line, id_.line) |
| 619 self._id = id_ |
| 620 self._description = description |
| 621 |
| 622 def Render(self, render_state): |
| 623 if render_state.contexts.Resolve(self._id.name) is None: |
| 624 render_state.AddResolutionError(self._id, description=self._description) |
| 625 |
| 626 def __repr__(self): |
| 627 return '{{!%s %s}}' % (self._id, self._description) |
| 622 | 628 |
| 623 class _JsonNode(_LeafNode): | 629 class _JsonNode(_LeafNode): |
| 624 ''' {{*foo}} | 630 '''{{*foo}} |
| 625 ''' | 631 ''' |
| 626 def __init__(self, id_): | 632 def __init__(self, id_): |
| 627 _LeafNode.__init__(self, id_.line, id_.line) | 633 _LeafNode.__init__(self, id_.line, id_.line) |
| 628 self._id = id_ | 634 self._id = id_ |
| 629 | 635 |
| 630 def Render(self, render_state): | 636 def Render(self, render_state): |
| 631 value = render_state.contexts.Resolve(self._id.name) | 637 value = render_state.contexts.Resolve(self._id.name) |
| 632 if value is None: | 638 if value is None: |
| 633 render_state.AddResolutionError(self._id) | 639 render_state.AddResolutionError(self._id) |
| 634 return | 640 return |
| 635 render_state.text.Append(json.dumps(value, separators=(',',':'))) | 641 render_state.text.Append(json.dumps(value, separators=(',',':'))) |
| 636 | 642 |
| 637 def __repr__(self): | 643 def __repr__(self): |
| 638 return '{{*%s}}' % self._id | 644 return '{{*%s}}' % self._id |
| 639 | 645 |
| 640 def __str__(self): | 646 class _PartialNode(_LeafNode): |
| 641 return repr(self) | 647 '''{{+var:foo}} ... {{/foo}} |
| 648 ''' |
| 649 def __init__(self, bind_to, id_, content): |
| 650 _LeafNode.__init__(self, id_.line, id_.line) |
| 651 self._bind_to = bind_to |
| 652 self._id = id_ |
| 653 self._content = content |
| 654 self._resolved_args = None |
| 655 self._args = None |
| 656 self._pass_through_id = None |
| 642 | 657 |
| 643 class _PartialNode(_LeafNode): | 658 @classmethod |
| 644 ''' {{+foo}} | 659 def Inline(cls, id_): |
| 645 ''' | 660 return cls(None, id_, None) |
| 646 def __init__(self, id_): | |
| 647 _LeafNode.__init__(self, id_.line, id_.line) | |
| 648 self._id = id_ | |
| 649 self._args = None | |
| 650 self._local_context_id = None | |
| 651 | 661 |
| 652 def Render(self, render_state): | 662 def Render(self, render_state): |
| 653 value = render_state.contexts.Resolve(self._id.name) | 663 value = render_state.contexts.Resolve(self._id.name) |
| 654 if value is None: | 664 if value is None: |
| 655 render_state.AddResolutionError(self._id) | 665 render_state.AddResolutionError(self._id) |
| 656 return | 666 return |
| 657 if not isinstance(value, Handlebar): | 667 if not isinstance(value, (Handlebar, _Node)): |
| 658 render_state.AddResolutionError(self._id) | 668 render_state.AddResolutionError(self._id, description='not a partial') |
| 659 return | 669 return |
| 660 | 670 |
| 661 partial_render_state = render_state.ForkPartial(value._name, self._id) | 671 if isinstance(value, Handlebar): |
| 672 node, name = value._top_node, value._name |
| 673 else: |
| 674 node, name = value, None |
| 662 | 675 |
| 663 # TODO: Don't do this. Force callers to do this by specifying an @ argument. | 676 partial_render_state = render_state.ForkPartial(name, self._id) |
| 664 top_local = render_state.contexts.GetTopLocal() | |
| 665 if top_local is not None: | |
| 666 partial_render_state.contexts.Push(top_local) | |
| 667 | 677 |
| 678 arg_context = {} |
| 679 if self._pass_through_id is not None: |
| 680 context = render_state.contexts.Resolve(self._pass_through_id.name) |
| 681 if context is not None: |
| 682 arg_context[self._pass_through_id.name] = context |
| 683 if self._resolved_args is not None: |
| 684 arg_context.update(self._resolved_args) |
| 668 if self._args is not None: | 685 if self._args is not None: |
| 669 arg_context = {} | 686 def resolve_args(args): |
| 670 for key, value_id in self._args.items(): | 687 resolved = {} |
| 671 context = render_state.contexts.Resolve(value_id.name) | 688 for key, value in args.iteritems(): |
| 672 if context is not None: | 689 if isinstance(value, dict): |
| 673 arg_context[key] = context | 690 assert len(value.keys()) == 1 |
| 691 inner_id, inner_args = value.items()[0] |
| 692 inner_partial = render_state.contexts.Resolve(inner_id.name) |
| 693 if inner_partial is not None: |
| 694 context = _PartialNode(None, inner_id, inner_partial) |
| 695 context.SetResolvedArguments(resolve_args(inner_args)) |
| 696 resolved[key] = context |
| 697 else: |
| 698 context = render_state.contexts.Resolve(value.name) |
| 699 if context is not None: |
| 700 resolved[key] = context |
| 701 return resolved |
| 702 arg_context.update(resolve_args(self._args)) |
| 703 if self._bind_to and self._content: |
| 704 arg_context[self._bind_to.name] = self._content |
| 705 if arg_context: |
| 674 partial_render_state.contexts.Push(arg_context) | 706 partial_render_state.contexts.Push(arg_context) |
| 675 | 707 |
| 676 if self._local_context_id is not None: | 708 node.Render(partial_render_state) |
| 677 local_context = render_state.contexts.Resolve(self._local_context_id.name) | |
| 678 if local_context is not None: | |
| 679 partial_render_state.contexts.Push(local_context) | |
| 680 | |
| 681 value._top_node.Render(partial_render_state) | |
| 682 | 709 |
| 683 render_state.Merge( | 710 render_state.Merge( |
| 684 partial_render_state, | 711 partial_render_state, |
| 685 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) | 712 text_transform=lambda text: text[:-1] if text.endswith('\n') else text) |
| 686 | 713 |
| 687 def AddArgument(self, key, id_): | 714 def SetResolvedArguments(self, args): |
| 688 if self._args is None: | 715 self._resolved_args = args |
| 689 self._args = {} | |
| 690 self._args[key] = id_ | |
| 691 | 716 |
| 692 def SetLocalContext(self, id_): | 717 def SetArguments(self, args): |
| 693 self._local_context_id = id_ | 718 self._args = args |
| 719 |
| 720 def PassThroughArgument(self, id_): |
| 721 self._pass_through_id = id_ |
| 694 | 722 |
| 695 def __repr__(self): | 723 def __repr__(self): |
| 696 return '{{+%s}}' % self._id | 724 return '{{+%s}}' % self._id |
| 697 | 725 |
| 698 def __str__(self): | |
| 699 return repr(self) | |
| 700 | |
| 701 _TOKENS = {} | 726 _TOKENS = {} |
| 702 | 727 |
| 703 class _Token(object): | 728 class _Token(object): |
| 704 ''' The tokens that can appear in a template. | 729 '''The tokens that can appear in a template. |
| 705 ''' | 730 ''' |
| 706 class Data(object): | 731 class Data(object): |
| 707 def __init__(self, name, text, clazz): | 732 def __init__(self, name, text, clazz): |
| 708 self.name = name | 733 self.name = name |
| 709 self.text = text | 734 self.text = text |
| 710 self.clazz = clazz | 735 self.clazz = clazz |
| 711 _TOKENS[text] = self | 736 _TOKENS[text] = self |
| 712 | 737 |
| 713 def ElseNodeClass(self): | 738 def ElseNodeClass(self): |
| 714 if self.clazz == _VertedSectionNode: | 739 if self.clazz == _VertedSectionNode: |
| 715 return _InvertedSectionNode | 740 return _InvertedSectionNode |
| 716 if self.clazz == _InvertedSectionNode: | 741 if self.clazz == _InvertedSectionNode: |
| 717 return _VertedSectionNode | 742 return _VertedSectionNode |
| 718 raise ValueError('%s cannot have an else clause.' % self.clazz) | 743 return None |
| 719 | 744 |
| 720 OPEN_START_SECTION = Data('OPEN_START_SECTION' , '{{#', _Sect
ionNode) | 745 def __repr__(self): |
| 721 OPEN_START_VERTED_SECTION = Data('OPEN_START_VERTED_SECTION' , '{{?', _Vert
edSectionNode) | 746 return self.name |
| 722 OPEN_START_INVERTED_SECTION = Data('OPEN_START_INVERTED_SECTION', '{{^', _Inve
rtedSectionNode) | 747 |
| 723 OPEN_START_JSON = Data('OPEN_START_JSON' , '{{*', _Json
Node) | 748 def __str__(self): |
| 724 OPEN_START_PARTIAL = Data('OPEN_START_PARTIAL' , '{{+', _Part
ialNode) | 749 return repr(self) |
| 725 OPEN_ELSE = Data('OPEN_ELSE' , '{{:', None) | 750 |
| 726 OPEN_END_SECTION = Data('OPEN_END_SECTION' , '{{/', None) | 751 OPEN_START_SECTION = Data( |
| 727 INLINE_END_SECTION = Data('INLINE_END_SECTION' , '/}}', None) | 752 'OPEN_START_SECTION' , '{{#', _SectionNode) |
| 728 OPEN_UNESCAPED_VARIABLE = Data('OPEN_UNESCAPED_VARIABLE' , '{{{', _Unes
capedVariableNode) | 753 OPEN_START_VERTED_SECTION = Data( |
| 729 CLOSE_MUSTACHE3 = Data('CLOSE_MUSTACHE3' , '}}}', None) | 754 'OPEN_START_VERTED_SECTION' , '{{?', _VertedSectionNode) |
| 730 OPEN_COMMENT = Data('OPEN_COMMENT' , '{{-', _Comm
entNode) | 755 OPEN_START_INVERTED_SECTION = Data( |
| 731 CLOSE_COMMENT = Data('CLOSE_COMMENT' , '-}}', None) | 756 'OPEN_START_INVERTED_SECTION', '{{^', _InvertedSectionNode) |
| 732 OPEN_VARIABLE = Data('OPEN_VARIABLE' , '{{' , _Esca
pedVariableNode) | 757 OPEN_ASSERTION = Data( |
| 733 CLOSE_MUSTACHE = Data('CLOSE_MUSTACHE' , '}}' , None) | 758 'OPEN_ASSERTION' , '{{!', _AssertionNode) |
| 734 CHARACTER = Data('CHARACTER' , '.' , None) | 759 OPEN_JSON = Data( |
| 760 'OPEN_JSON' , '{{*', _JsonNode) |
| 761 OPEN_PARTIAL = Data( |
| 762 'OPEN_PARTIAL' , '{{+', _PartialNode) |
| 763 OPEN_ELSE = Data( |
| 764 'OPEN_ELSE' , '{{:', None) |
| 765 OPEN_END_SECTION = Data( |
| 766 'OPEN_END_SECTION' , '{{/', None) |
| 767 INLINE_END_SECTION = Data( |
| 768 'INLINE_END_SECTION' , '/}}', None) |
| 769 OPEN_UNESCAPED_VARIABLE = Data( |
| 770 'OPEN_UNESCAPED_VARIABLE' , '{{{', _UnescapedVariableNode) |
| 771 CLOSE_MUSTACHE3 = Data( |
| 772 'CLOSE_MUSTACHE3' , '}}}', None) |
| 773 OPEN_COMMENT = Data( |
| 774 'OPEN_COMMENT' , '{{-', _CommentNode) |
| 775 CLOSE_COMMENT = Data( |
| 776 'CLOSE_COMMENT' , '-}}', None) |
| 777 OPEN_VARIABLE = Data( |
| 778 'OPEN_VARIABLE' , '{{' , _EscapedVariableNode) |
| 779 CLOSE_MUSTACHE = Data( |
| 780 'CLOSE_MUSTACHE' , '}}' , None) |
| 781 CHARACTER = Data( |
| 782 'CHARACTER' , '.' , None) |
| 735 | 783 |
| 736 class _TokenStream(object): | 784 class _TokenStream(object): |
| 737 ''' Tokeniser for template parsing. | 785 '''Tokeniser for template parsing. |
| 738 ''' | 786 ''' |
| 739 def __init__(self, string): | 787 def __init__(self, string): |
| 740 self.next_token = None | 788 self.next_token = None |
| 741 self.next_line = _Line(1) | 789 self.next_line = 1 |
| 742 self.next_column = 0 | 790 self.next_column = 0 |
| 743 self._string = string | 791 self._string = string |
| 744 self._cursor = 0 | 792 self._cursor = 0 |
| 745 self.Advance() | 793 self.Advance() |
| 746 | 794 |
| 747 def HasNext(self): | 795 def HasNext(self): |
| 748 return self.next_token is not None | 796 return self.next_token is not None |
| 749 | 797 |
| 798 def NextCharacter(self): |
| 799 if self.next_token is _Token.CHARACTER: |
| 800 return self._string[self._cursor - 1] |
| 801 return None |
| 802 |
| 750 def Advance(self): | 803 def Advance(self): |
| 751 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': | 804 if self._cursor > 0 and self._string[self._cursor - 1] == '\n': |
| 752 self.next_line = _Line(self.next_line.number + 1) | 805 self.next_line += 1 |
| 753 self.next_column = 0 | 806 self.next_column = 0 |
| 754 elif self.next_token is not None: | 807 elif self.next_token is not None: |
| 755 self.next_column += len(self.next_token.text) | 808 self.next_column += len(self.next_token.text) |
| 756 | 809 |
| 757 self.next_token = None | 810 self.next_token = None |
| 758 | 811 |
| 759 if self._cursor == len(self._string): | 812 if self._cursor == len(self._string): |
| 760 return None | 813 return None |
| 761 assert self._cursor < len(self._string) | 814 assert self._cursor < len(self._string) |
| 762 | 815 |
| 763 if (self._cursor + 1 < len(self._string) and | 816 if (self._cursor + 1 < len(self._string) and |
| 764 self._string[self._cursor + 1] in '{}'): | 817 self._string[self._cursor + 1] in '{}'): |
| 765 self.next_token = ( | 818 self.next_token = ( |
| 766 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or | 819 _TOKENS.get(self._string[self._cursor:self._cursor+3]) or |
| 767 _TOKENS.get(self._string[self._cursor:self._cursor+2])) | 820 _TOKENS.get(self._string[self._cursor:self._cursor+2])) |
| 768 | 821 |
| 769 if self.next_token is None: | 822 if self.next_token is None: |
| 770 self.next_token = _Token.CHARACTER | 823 self.next_token = _Token.CHARACTER |
| 771 | 824 |
| 772 self._cursor += len(self.next_token.text) | 825 self._cursor += len(self.next_token.text) |
| 773 return self | 826 return self |
| 774 | 827 |
| 775 def AdvanceOver(self, token): | 828 def AdvanceOver(self, token, description=None): |
| 776 if self.next_token != token: | 829 parse_error = None |
| 777 raise ParseException( | 830 if not self.next_token: |
| 778 'Expecting token %s but got %s at line %s' % (token.name, | 831 parse_error = 'Reached EOF but expected %s' % token.name |
| 779 self.next_token.name, | 832 elif self.next_token is not token: |
| 780 self.next_line)) | 833 parse_error = 'Expecting token %s but got %s at line %s' % ( |
| 834 token.name, self.next_token.name, self.next_line) |
| 835 if parse_error: |
| 836 parse_error += ' %s' % description or '' |
| 837 raise ParseException(parse_error) |
| 781 return self.Advance() | 838 return self.Advance() |
| 782 | 839 |
| 840 def AdvanceOverSeparator(self, char, description=None): |
| 841 self.SkipWhitespace() |
| 842 next_char = self.NextCharacter() |
| 843 if next_char != char: |
| 844 parse_error = 'Expected \'%s\'. got \'%s\'' % (char, next_char) |
| 845 if description is not None: |
| 846 parse_error += ' (%s)' % description |
| 847 raise ParseException(parse_error) |
| 848 self.AdvanceOver(_Token.CHARACTER) |
| 849 self.SkipWhitespace() |
| 850 |
| 783 def AdvanceOverNextString(self, excluded=''): | 851 def AdvanceOverNextString(self, excluded=''): |
| 784 start = self._cursor - len(self.next_token.text) | 852 start = self._cursor - len(self.next_token.text) |
| 785 while (self.next_token is _Token.CHARACTER and | 853 while (self.next_token is _Token.CHARACTER and |
| 786 # Can use -1 here because token length of CHARACTER is 1. | 854 # Can use -1 here because token length of CHARACTER is 1. |
| 787 self._string[self._cursor - 1] not in excluded): | 855 self._string[self._cursor - 1] not in excluded): |
| 788 self.Advance() | 856 self.Advance() |
| 789 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) | 857 end = self._cursor - (len(self.next_token.text) if self.next_token else 0) |
| 790 return self._string[start:end] | 858 return self._string[start:end] |
| 791 | 859 |
| 792 def AdvanceToNextWhitespace(self): | 860 def AdvanceToNextWhitespace(self): |
| 793 return self.AdvanceOverNextString(excluded=' \n\r\t') | 861 return self.AdvanceOverNextString(excluded=' \n\r\t') |
| 794 | 862 |
| 795 def SkipWhitespace(self): | 863 def SkipWhitespace(self): |
| 796 while (self.next_token is _Token.CHARACTER and | 864 while (self.next_token is _Token.CHARACTER and |
| 797 # Can use -1 here because token length of CHARACTER is 1. | 865 # Can use -1 here because token length of CHARACTER is 1. |
| 798 self._string[self._cursor - 1] in ' \n\r\t'): | 866 self._string[self._cursor - 1] in ' \n\r\t'): |
| 799 self.Advance() | 867 self.Advance() |
| 800 | 868 |
| 869 def __repr__(self): |
| 870 return '%s(next_token=%s, remainder=%s)' % (type(self).__name__, |
| 871 self.next_token, |
| 872 self._string[self._cursor:]) |
| 873 |
| 874 def __str__(self): |
| 875 return repr(self) |
| 876 |
| 801 class Handlebar(object): | 877 class Handlebar(object): |
| 802 ''' A handlebar template. | 878 '''A handlebar template. |
| 803 ''' | 879 ''' |
| 804 def __init__(self, template, name=None): | 880 def __init__(self, template, name=None): |
| 805 self.source = template | 881 self.source = template |
| 806 self._name = name | 882 self._name = name |
| 807 tokens = _TokenStream(template) | 883 tokens = _TokenStream(template) |
| 808 self._top_node = self._ParseSection(tokens) | 884 self._top_node = self._ParseSection(tokens) |
| 809 if not self._top_node: | 885 if not self._top_node: |
| 810 raise ParseException('Template is empty') | 886 raise ParseException('Template is empty') |
| 811 if tokens.HasNext(): | 887 if tokens.HasNext(): |
| 812 raise ParseException('There are still tokens remaining at %s, ' | 888 raise ParseException('There are still tokens remaining at %s, ' |
| 813 'was there an end-section without a start-section?' | 889 'was there an end-section without a start-section?' % |
| 814 % tokens.next_line) | 890 tokens.next_line) |
| 815 | 891 |
| 816 def _ParseSection(self, tokens): | 892 def _ParseSection(self, tokens): |
| 817 nodes = [] | 893 nodes = [] |
| 818 while tokens.HasNext(): | 894 while tokens.HasNext(): |
| 819 if tokens.next_token in (_Token.OPEN_END_SECTION, | 895 if tokens.next_token in (_Token.OPEN_END_SECTION, |
| 820 _Token.OPEN_ELSE): | 896 _Token.OPEN_ELSE): |
| 821 # Handled after running parseSection within the SECTION cases, so this | 897 # Handled after running parseSection within the SECTION cases, so this |
| 822 # is a terminating condition. If there *is* an orphaned | 898 # is a terminating condition. If there *is* an orphaned |
| 823 # OPEN_END_SECTION, it will be caught by noticing that there are | 899 # OPEN_END_SECTION, it will be caught by noticing that there are |
| 824 # leftover tokens after termination. | 900 # leftover tokens after termination. |
| (...skipping 11 matching lines...) Expand all Loading... |
| 836 previous_node = nodes[i - 1] if i > 0 else None | 912 previous_node = nodes[i - 1] if i > 0 else None |
| 837 next_node = nodes[i + 1] if i < len(nodes) - 1 else None | 913 next_node = nodes[i + 1] if i < len(nodes) - 1 else None |
| 838 rendered_node = None | 914 rendered_node = None |
| 839 | 915 |
| 840 if node.GetStartLine() != node.GetEndLine(): | 916 if node.GetStartLine() != node.GetEndLine(): |
| 841 rendered_node = _BlockNode(node) | 917 rendered_node = _BlockNode(node) |
| 842 if previous_node: | 918 if previous_node: |
| 843 previous_node.TrimEndingSpaces() | 919 previous_node.TrimEndingSpaces() |
| 844 if next_node: | 920 if next_node: |
| 845 next_node.TrimStartingNewLine() | 921 next_node.TrimStartingNewLine() |
| 846 elif (isinstance(node, _LeafNode) and | 922 elif ((not previous_node or previous_node.EndsWithEmptyLine()) and |
| 847 (not previous_node or previous_node.EndsWithEmptyLine()) and | |
| 848 (not next_node or next_node.StartsWithNewLine())): | 923 (not next_node or next_node.StartsWithNewLine())): |
| 849 indentation = 0 | 924 indentation = 0 |
| 850 if previous_node: | 925 if previous_node: |
| 851 indentation = previous_node.TrimEndingSpaces() | 926 indentation = previous_node.TrimEndingSpaces() |
| 852 if next_node: | 927 if next_node: |
| 853 next_node.TrimStartingNewLine() | 928 next_node.TrimStartingNewLine() |
| 854 rendered_node = _IndentedNode(node, indentation) | 929 rendered_node = _IndentedNode(node, indentation) |
| 855 else: | 930 else: |
| 856 rendered_node = _InlineNode(node) | 931 rendered_node = _InlineNode(node) |
| 857 | 932 |
| 858 nodes[i] = rendered_node | 933 nodes[i] = rendered_node |
| 859 | 934 |
| 860 if len(nodes) == 0: | 935 if len(nodes) == 0: |
| 861 return None | 936 return None |
| 862 if len(nodes) == 1: | 937 if len(nodes) == 1: |
| 863 return nodes[0] | 938 return nodes[0] |
| 864 return _NodeCollection(nodes) | 939 return _NodeCollection(nodes) |
| 865 | 940 |
| 866 def _ParseNextOpenToken(self, tokens): | 941 def _ParseNextOpenToken(self, tokens): |
| 867 next_token = tokens.next_token | 942 next_token = tokens.next_token |
| 868 | 943 |
| 869 if next_token is _Token.CHARACTER: | 944 if next_token is _Token.CHARACTER: |
| 945 # Plain strings. |
| 870 start_line = tokens.next_line | 946 start_line = tokens.next_line |
| 871 string = tokens.AdvanceOverNextString() | 947 string = tokens.AdvanceOverNextString() |
| 872 return [_StringNode(string, start_line, tokens.next_line)] | 948 return [_StringNode(string, start_line, tokens.next_line)] |
| 873 elif next_token in (_Token.OPEN_VARIABLE, | 949 elif next_token in (_Token.OPEN_VARIABLE, |
| 874 _Token.OPEN_UNESCAPED_VARIABLE, | 950 _Token.OPEN_UNESCAPED_VARIABLE, |
| 875 _Token.OPEN_START_JSON): | 951 _Token.OPEN_JSON): |
| 876 id_, inline_value_id = self._OpenSectionOrTag(tokens) | 952 # Inline nodes that don't take arguments. |
| 877 if inline_value_id is not None: | 953 tokens.Advance() |
| 878 raise ParseException( | 954 close_token = (_Token.CLOSE_MUSTACHE3 |
| 879 '%s cannot have an inline value' % id_.GetDescription()) | 955 if next_token is _Token.OPEN_UNESCAPED_VARIABLE else |
| 956 _Token.CLOSE_MUSTACHE) |
| 957 id_ = self._NextIdentifier(tokens) |
| 958 tokens.AdvanceOver(close_token) |
| 880 return [next_token.clazz(id_)] | 959 return [next_token.clazz(id_)] |
| 881 elif next_token is _Token.OPEN_START_PARTIAL: | 960 elif next_token is _Token.OPEN_ASSERTION: |
| 961 # Inline nodes that take arguments. |
| 882 tokens.Advance() | 962 tokens.Advance() |
| 883 column_start = tokens.next_column + 1 | 963 id_ = self._NextIdentifier(tokens) |
| 884 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), | 964 node = next_token.clazz(id_, tokens.AdvanceOverNextString()) |
| 885 tokens.next_line, | 965 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 886 column_start) | 966 return [node] |
| 887 partial_node = _PartialNode(id_) | 967 elif next_token in (_Token.OPEN_PARTIAL, |
| 888 while tokens.next_token is _Token.CHARACTER: | 968 _Token.OPEN_START_SECTION, |
| 969 _Token.OPEN_START_VERTED_SECTION, |
| 970 _Token.OPEN_START_INVERTED_SECTION): |
| 971 # Block nodes, though they may have inline syntax like {{#foo bar /}}. |
| 972 tokens.Advance() |
| 973 bind_to, id_ = None, self._NextIdentifier(tokens) |
| 974 if tokens.NextCharacter() == ':': |
| 975 # This section has the format {{#bound:id}} as opposed to just {{id}}. |
| 976 # That is, |id_| is actually the identifier to bind what the section |
| 977 # is producing, not the identifier of where to find that content. |
| 978 tokens.AdvanceOverSeparator(':') |
| 979 bind_to, id_ = id_, self._NextIdentifier(tokens) |
| 980 partial_args = None |
| 981 if next_token is _Token.OPEN_PARTIAL: |
| 982 partial_args = self._ParsePartialNodeArgs(tokens) |
| 983 if tokens.next_token is not _Token.CLOSE_MUSTACHE: |
| 984 # Inline syntax for partial types. |
| 985 if bind_to is not None: |
| 986 raise ParseException( |
| 987 'Cannot bind %s to a self-closing partial' % bind_to) |
| 988 tokens.AdvanceOver(_Token.INLINE_END_SECTION) |
| 989 partial_node = _PartialNode.Inline(id_) |
| 990 partial_node.SetArguments(partial_args) |
| 991 return [partial_node] |
| 992 elif tokens.next_token is not _Token.CLOSE_MUSTACHE: |
| 993 # Inline syntax for non-partial types. Support select node types: |
| 994 # variables, partials, JSON. |
| 995 line, column = tokens.next_line, (tokens.next_column + 1) |
| 996 name = tokens.AdvanceToNextWhitespace() |
| 997 clazz = _UnescapedVariableNode |
| 998 if name.startswith('*'): |
| 999 clazz = _JsonNode |
| 1000 elif name.startswith('+'): |
| 1001 clazz = _PartialNode.Inline |
| 1002 if clazz is not _UnescapedVariableNode: |
| 1003 name = name[1:] |
| 1004 column += 1 |
| 1005 inline_node = clazz(_Identifier(name, line, column)) |
| 1006 if isinstance(inline_node, _PartialNode): |
| 1007 inline_node.SetArguments(self._ParsePartialNodeArgs(tokens)) |
| 1008 if bind_to is not None: |
| 1009 inline_node.PassThroughArgument(bind_to) |
| 889 tokens.SkipWhitespace() | 1010 tokens.SkipWhitespace() |
| 890 key = tokens.AdvanceOverNextString(excluded=':') | 1011 tokens.AdvanceOver(_Token.INLINE_END_SECTION) |
| 891 tokens.Advance() | 1012 return [next_token.clazz(bind_to, id_, inline_node)] |
| 892 column_start = tokens.next_column + 1 | 1013 # Block syntax. |
| 893 id_ = _Identifier(tokens.AdvanceToNextWhitespace(), | |
| 894 tokens.next_line, | |
| 895 column_start) | |
| 896 if key == '@': | |
| 897 partial_node.SetLocalContext(id_) | |
| 898 else: | |
| 899 partial_node.AddArgument(key, id_) | |
| 900 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) | 1014 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 901 return [partial_node] | 1015 section = self._ParseSection(tokens) |
| 902 elif next_token is _Token.OPEN_START_SECTION: | 1016 else_node_class = next_token.ElseNodeClass() # may not have one |
| 903 id_, inline_node = self._OpenSectionOrTag(tokens) | 1017 else_section = None |
| 1018 if (else_node_class is not None and |
| 1019 tokens.next_token is _Token.OPEN_ELSE): |
| 1020 self._OpenElse(tokens, id_) |
| 1021 else_section = self._ParseSection(tokens) |
| 1022 self._CloseSection(tokens, id_) |
| 904 nodes = [] | 1023 nodes = [] |
| 905 if inline_node is None: | 1024 if section is not None: |
| 906 section = self._ParseSection(tokens) | 1025 node = next_token.clazz(bind_to, id_, section) |
| 907 self._CloseSection(tokens, id_) | 1026 if partial_args: |
| 908 nodes = [] | 1027 node.SetArguments(partial_args) |
| 909 if section is not None: | 1028 nodes.append(node) |
| 910 nodes.append(_SectionNode(id_, section)) | 1029 if else_section is not None: |
| 911 else: | 1030 nodes.append(else_node_class(bind_to, id_, else_section)) |
| 912 nodes.append(_SectionNode(id_, inline_node)) | |
| 913 return nodes | |
| 914 elif next_token in (_Token.OPEN_START_VERTED_SECTION, | |
| 915 _Token.OPEN_START_INVERTED_SECTION): | |
| 916 id_, inline_node = self._OpenSectionOrTag(tokens) | |
| 917 nodes = [] | |
| 918 if inline_node is None: | |
| 919 section = self._ParseSection(tokens) | |
| 920 else_section = None | |
| 921 if tokens.next_token is _Token.OPEN_ELSE: | |
| 922 self._OpenElse(tokens, id_) | |
| 923 else_section = self._ParseSection(tokens) | |
| 924 self._CloseSection(tokens, id_) | |
| 925 if section: | |
| 926 nodes.append(next_token.clazz(id_, section)) | |
| 927 if else_section: | |
| 928 nodes.append(next_token.ElseNodeClass()(id_, else_section)) | |
| 929 else: | |
| 930 nodes.append(next_token.clazz(id_, inline_node)) | |
| 931 return nodes | 1031 return nodes |
| 932 elif next_token is _Token.OPEN_COMMENT: | 1032 elif next_token is _Token.OPEN_COMMENT: |
| 1033 # Comments. |
| 933 start_line = tokens.next_line | 1034 start_line = tokens.next_line |
| 934 self._AdvanceOverComment(tokens) | 1035 self._AdvanceOverComment(tokens) |
| 935 return [_CommentNode(start_line, tokens.next_line)] | 1036 return [_CommentNode(start_line, tokens.next_line)] |
| 936 | 1037 |
| 937 def _AdvanceOverComment(self, tokens): | 1038 def _AdvanceOverComment(self, tokens): |
| 938 tokens.AdvanceOver(_Token.OPEN_COMMENT) | 1039 tokens.AdvanceOver(_Token.OPEN_COMMENT) |
| 939 depth = 1 | 1040 depth = 1 |
| 940 while tokens.HasNext() and depth > 0: | 1041 while tokens.HasNext() and depth > 0: |
| 941 if tokens.next_token is _Token.OPEN_COMMENT: | 1042 if tokens.next_token is _Token.OPEN_COMMENT: |
| 942 depth += 1 | 1043 depth += 1 |
| 943 elif tokens.next_token is _Token.CLOSE_COMMENT: | 1044 elif tokens.next_token is _Token.CLOSE_COMMENT: |
| 944 depth -= 1 | 1045 depth -= 1 |
| 945 tokens.Advance() | 1046 tokens.Advance() |
| 946 | 1047 |
| 947 def _OpenSectionOrTag(self, tokens): | |
| 948 def NextIdentifierArgs(): | |
| 949 tokens.SkipWhitespace() | |
| 950 line = tokens.next_line | |
| 951 column = tokens.next_column + 1 | |
| 952 name = tokens.AdvanceToNextWhitespace() | |
| 953 tokens.SkipWhitespace() | |
| 954 return (name, line, column) | |
| 955 close_token = (_Token.CLOSE_MUSTACHE3 | |
| 956 if tokens.next_token is _Token.OPEN_UNESCAPED_VARIABLE else | |
| 957 _Token.CLOSE_MUSTACHE) | |
| 958 tokens.Advance() | |
| 959 id_ = _Identifier(*NextIdentifierArgs()) | |
| 960 if tokens.next_token is close_token: | |
| 961 tokens.AdvanceOver(close_token) | |
| 962 inline_node = None | |
| 963 else: | |
| 964 name, line, column = NextIdentifierArgs() | |
| 965 tokens.AdvanceOver(_Token.INLINE_END_SECTION) | |
| 966 # Support select other types of nodes, the most useful being partial. | |
| 967 clazz = _UnescapedVariableNode | |
| 968 if name.startswith('*'): | |
| 969 clazz = _JsonNode | |
| 970 elif name.startswith('+'): | |
| 971 clazz = _PartialNode | |
| 972 if clazz is not _UnescapedVariableNode: | |
| 973 name = name[1:] | |
| 974 column += 1 | |
| 975 inline_node = clazz(_Identifier(name, line, column)) | |
| 976 return (id_, inline_node) | |
| 977 | |
| 978 def _CloseSection(self, tokens, id_): | 1048 def _CloseSection(self, tokens, id_): |
| 979 tokens.AdvanceOver(_Token.OPEN_END_SECTION) | 1049 tokens.AdvanceOver(_Token.OPEN_END_SECTION, |
| 1050 description='to match %s' % id_.GetDescription()) |
| 980 next_string = tokens.AdvanceOverNextString() | 1051 next_string = tokens.AdvanceOverNextString() |
| 981 if next_string != '' and next_string != id_.name: | 1052 if next_string != '' and next_string != id_.name: |
| 982 raise ParseException( | 1053 raise ParseException( |
| 983 'Start section %s doesn\'t match end %s' % (id_, next_string)) | 1054 'Start section %s doesn\'t match end %s' % (id_, next_string)) |
| 984 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) | 1055 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 985 | 1056 |
| 986 def _OpenElse(self, tokens, id_): | 1057 def _OpenElse(self, tokens, id_): |
| 987 tokens.AdvanceOver(_Token.OPEN_ELSE) | 1058 tokens.AdvanceOver(_Token.OPEN_ELSE) |
| 988 next_string = tokens.AdvanceOverNextString() | 1059 next_string = tokens.AdvanceOverNextString() |
| 989 if next_string != '' and next_string != id_.name: | 1060 if next_string != '' and next_string != id_.name: |
| 990 raise ParseException( | 1061 raise ParseException( |
| 991 'Start section %s doesn\'t match else %s' % (id_, next_string)) | 1062 'Start section %s doesn\'t match else %s' % (id_, next_string)) |
| 992 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) | 1063 tokens.AdvanceOver(_Token.CLOSE_MUSTACHE) |
| 993 | 1064 |
| 994 def Render(self, *contexts): | 1065 def _ParsePartialNodeArgs(self, tokens): |
| 1066 args = {} |
| 1067 tokens.SkipWhitespace() |
| 1068 while (tokens.next_token is _Token.CHARACTER and |
| 1069 tokens.NextCharacter() != ')'): |
| 1070 key = tokens.AdvanceOverNextString(excluded=':') |
| 1071 tokens.AdvanceOverSeparator(':') |
| 1072 if tokens.NextCharacter() == '(': |
| 1073 tokens.AdvanceOverSeparator('(') |
| 1074 inner_id = self._NextIdentifier(tokens) |
| 1075 inner_args = self._ParsePartialNodeArgs(tokens) |
| 1076 tokens.AdvanceOverSeparator(')') |
| 1077 args[key] = {inner_id: inner_args} |
| 1078 else: |
| 1079 args[key] = self._NextIdentifier(tokens) |
| 1080 return args or None |
| 1081 |
| 1082 def _NextIdentifier(self, tokens): |
| 1083 tokens.SkipWhitespace() |
| 1084 column_start = tokens.next_column + 1 |
| 1085 id_ = _Identifier(tokens.AdvanceOverNextString(excluded=' \n\r\t:()'), |
| 1086 tokens.next_line, |
| 1087 column_start) |
| 1088 tokens.SkipWhitespace() |
| 1089 return id_ |
| 1090 |
| 1091 def Render(self, *user_contexts): |
| 995 '''Renders this template given a variable number of contexts to read out | 1092 '''Renders this template given a variable number of contexts to read out |
| 996 values from (such as those appearing in {{foo}}). | 1093 values from (such as those appearing in {{foo}}). |
| 997 ''' | 1094 ''' |
| 998 name = self._name or '<root>' | 1095 name = self._name or '<root>' |
| 999 render_state = _RenderState(name, _Contexts(contexts)) | 1096 internal_context = _InternalContext() |
| 1097 render_state = _RenderState( |
| 1098 name, _Contexts([{'_': internal_context}] + list(user_contexts))) |
| 1099 internal_context.SetRenderState(render_state) |
| 1000 self._top_node.Render(render_state) | 1100 self._top_node.Render(render_state) |
| 1001 return render_state.GetResult() | 1101 return render_state.GetResult() |
| 1002 | 1102 |
| 1003 def render(self, *contexts): | 1103 def render(self, *contexts): |
| 1004 return self.Render(*contexts) | 1104 return self.Render(*contexts) |
| 1005 | 1105 |
| 1106 def __eq__(self, other): |
| 1107 return self.source == other.source and self._name == other._name |
| 1108 |
| 1109 def __ne__(self, other): |
| 1110 return not (self == other) |
| 1111 |
| 1006 def __repr__(self): | 1112 def __repr__(self): |
| 1007 return str('%s(%s)' % (self.__class__.__name__, self._top_node)) | 1113 return str('%s(%s)' % (type(self).__name__, self._top_node)) |
| 1008 | 1114 |
| 1009 def __str__(self): | 1115 def __str__(self): |
| 1010 return repr(self) | 1116 return repr(self) |
| OLD | NEW |