Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(87)

Side by Side Diff: third_party/handlebar/handlebar.py

Issue 14991011: Merge 200687 "Docserver: update third_party/handlebar. handlebar..." (Closed) Base URL: svn://svn.chromium.org/chrome/branches/1453/src/
Patch Set: Created 7 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/handlebar/README.chromium ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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('<', '&lt;') 464 .replace('<', '&lt;')
356 .replace('>', '&gt;')) 465 .replace('>', '&gt;'))
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()
OLDNEW
« no previous file with comments | « third_party/handlebar/README.chromium ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698