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

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

Issue 10697056: Extensions Docs Server: Handlebar updates (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 5 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 import json 15 import json
16 import re 16 import re
17 17
18 """ Handlebar templates are logicless templates inspired by ctemplate (or more 18 """ Handlebar templates are mostly-logicless templates inspired by ctemplate
19 specifically mustache templates) then taken in their own direction because I 19 (or more specifically mustache templates) then taken in their own direction
20 found those to be inadequate. 20 because I found those to be inadequate.
21 21
22 from handlebar import Handlebar 22 from handlebar import Handlebar
23 23
24 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world') 24 template = Handlebar('hello {{#foo}}{{bar}}{{/}} world')
25 input = { 25 input = {
26 'foo': [ 26 'foo': [
27 { 'bar': 1 }, 27 { 'bar': 1 },
28 { 'bar': 2 }, 28 { 'bar': 2 },
29 { 'bar': 3 } 29 { 'bar': 3 }
30 ] 30 ]
31 } 31 }
32 print(template.render(input).text) 32 print(template.render(input).text)
33 33
34 Handlebar will use get() on contexts to return values, so to create custom 34 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 35 getters (e.g. something that populates values lazily from keys) just add
36 a get() method. 36 a get() method.
37 37
38 class CustomContext(object): 38 class CustomContext(object):
39 def get(self, key): 39 def get(self, key):
40 return 10 40 return 10
41 41
42 # Any time {{ }} is used, will fill it with 10. 42 # Any time {{ }} is used, will fill it with 10.
43 print(Handlebar('hello {{world}}').render(CustomContext()).text) 43 print(Handlebar('hello {{world}}').render(CustomContext()).text)
44 """ 44 """
45 45
46 class ParseException(Exception): 46 class ParseException(Exception):
47 """ Exception thrown while parsing the template. 47 """ Exception thrown while parsing the template.
48 """ 48 """
49 def __init__(self, message): 49 def __init__(self, error, line):
50 Exception.__init__(self, message) 50 Exception.__init__(self, "%s (line %s)" % (error, line.number))
51 51
52 class RenderResult(object): 52 class RenderResult(object):
53 """ Result of a render operation. 53 """ Result of a render operation.
54 """ 54 """
55 def __init__(self, text, errors): 55 def __init__(self, text, errors):
56 self.text = text; 56 self.text = text;
57 self.errors = errors 57 self.errors = errors
58 58
59 class StringBuilder(object): 59 class StringBuilder(object):
60 """ Mimics Java's StringBuilder for easy porting from the Java version of 60 """ Mimics Java's StringBuilder for easy porting from the Java version of
61 this file to Python. 61 this file to Python.
62 """ 62 """
63 def __init__(self): 63 def __init__(self):
64 self._buf = [] 64 self._buf = []
65 65
66 def __len__(self):
67 return len(self._buf)
68
66 def append(self, obj): 69 def append(self, obj):
67 self._buf.append(str(obj)) 70 self._buf.append(str(obj))
68 71
69 def toString(self): 72 def toString(self):
70 return ''.join(self._buf) 73 return ''.join(self._buf)
71 74
72 class PathIdentifier(object): 75 class RenderState(object):
73 """ An identifier of the form "foo.bar.baz". 76 """ The state of a render call.
74 """ 77 """
75 def __init__(self, name): 78 def __init__(self, globalContexts, localContexts):
76 if name == '': 79 self.globalContexts = globalContexts
77 raise ParseException("Cannot have empty identifiers") 80 self.localContexts = localContexts
78 if not re.match('^[a-zA-Z0-9._]*$', name): 81 self.text = StringBuilder()
79 raise ParseException(name + " is not a valid identifier") 82 self.errors = []
80 self.path = name.split(".") 83 self._errorsDisabled = False
81 84
82 def resolve(self, contexts, errors): 85 def inSameContext(self):
83 resolved = None 86 return RenderState(self.globalContexts, self.localContexts)
87
88 def getFirstContext(self):
89 if len(self.localContexts) > 0:
90 return self.localContexts[0]
91 elif len(self.globalContexts) > 0:
92 return self.globalContexts[0]
93 return None
94
95 def disableErrors(self):
96 self._errorsDisabled = True
97 return self
98
99 def addError(self, *messages):
100 if self._errorsDisabled:
101 return self
102 buf = StringBuilder()
103 for message in messages:
104 buf.append(message)
105 self.errors.append(buf.toString())
106 return self
107
108 def getResult(self):
109 return RenderResult(self.text.toString(), self.errors);
110
111 class Identifier(object):
112 """ An identifier of the form "@", "foo.bar.baz", or "@.foo.bar.baz".
113 """
114 def __init__(self, name, line):
115 self._isThis = (name == '@')
116 if self._isThis:
117 self._startsWithThis = False
118 self._path = []
119 return
120
121 thisDot = '@.'
122 self._startsWithThis = name.startswith(thisDot)
123 if self._startsWithThis:
124 name = name[len(thisDot):]
125
126 if not re.match('^[a-zA-Z0-9._]+$', name):
127 raise ParseException(name + " is not a valid identifier", line)
128 self._path = name.split('.')
129
130 def resolve(self, renderState):
131 if self._isThis:
132 return renderState.getFirstContext()
133
134 if self._startsWithThis:
135 return self._resolveFromContext(renderState.getFirstContext())
136
137 resolved = self._resolveFromContexts(renderState.localContexts)
138 if not resolved:
139 resolved = self._resolveFromContexts(renderState.globalContexts)
140 if not resolved:
141 renderState.addError("Couldn't resolve identifier ", self._path)
142 return resolved
143
144 def _resolveFromContexts(self, contexts):
84 for context in contexts: 145 for context in contexts:
85 if not context: 146 resolved = self._resolveFromContext(context)
86 continue
87 resolved = self._resolveFrom(context)
88 if resolved: 147 if resolved:
89 return resolved 148 return resolved
90 _RenderError(errors, "Couldn't resolve identifier ", self.path, " in ", cont exts)
91 return None 149 return None
92 150
93 def _resolveFrom(self, context): 151 def _resolveFromContext(self, context):
94 result = context 152 result = context
95 for next in self.path: 153 for next in self._path:
96 # Only require that contexts provide a get method, meaning that callers 154 # Only require that contexts provide a get method, meaning that callers
97 # can provide dict-like contexts (for example, to populate values lazily). 155 # can provide dict-like contexts (for example, to populate values lazily).
98 if not result or not getattr(result, "get", None): 156 if not result or not getattr(result, "get", None):
99 return None 157 return None
100 result = result.get(next) 158 result = result.get(next)
101 return result 159 return result
102 160
103 def __str__(self): 161 def __str__(self):
104 return '.'.join(self.path) 162 if self._isThis:
163 return '@'
164 name = '.'.join(self._path)
165 return ('@.' + name) if self._startsWithThis else name
105 166
106 class ThisIdentifier(object): 167 class Line(object):
107 """ An identifier of the form "@". 168 def __init__(self, number):
108 """ 169 self.number = number
109 def resolve(self, contexts, errors):
110 return contexts[0]
111 170
112 def __str__(self): 171 class LeafNode(object):
113 return '@' 172 def trimLeadingNewLine(self):
173 pass
114 174
115 class SelfClosingNode(object): 175 def trimTrailingSpaces(self):
116 """ Nodes which are "self closing", e.g. {{foo}}, {{*foo}}. 176 return 0
117 """
118 def init(self, id):
119 self.id = id
120 177
121 class HasChildrenNode(object): 178 def trimTrailingNewLine(self):
122 """ Nodes which are not self closing, and have 0..n children. 179 pass
123 """ 180
124 def init(self, id, children): 181 def trailsWithEmptyLine(self):
125 self.id = id 182 return False
126 self.children = children 183
184 class DecoratorNode(object):
185 def __init__(self, content):
186 self._content = content
187
188 def trimLeadingNewLine(self):
189 self._content.trimLeadingNewLine()
190
191 def trimTrailingSpaces(self):
192 return self._content.trimTrailingSpaces()
193
194 def trimTrailingNewLine(self):
195 self._content.trimTrailingNewLine()
196
197 def trailsWithEmptyLine(self):
198 return self._content.trailsWithEmptyLine()
199
200 class InlineNode(DecoratorNode):
201 def __init__(self, content):
202 DecoratorNode.__init__(self, content)
203
204 def render(self, renderState):
205 contentRenderState = renderState.inSameContext()
206 self._content.render(contentRenderState)
207
208 renderState.errors.extend(contentRenderState.errors)
209
210 for c in contentRenderState.text.toString():
211 if c != '\n':
212 renderState.text.append(c)
213
214 class IndentedNode(DecoratorNode):
215 def __init__(self, content, indentation):
216 DecoratorNode.__init__(self, content)
217 self._indentation = indentation
218
219 def render(self, renderState):
220 contentRenderState = renderState.inSameContext()
221 self._content.render(contentRenderState)
222
223 renderState.errors.extend(contentRenderState.errors)
224
225 self._indent(renderState.text)
226 for i, c in enumerate(contentRenderState.text.toString()):
227 renderState.text.append(c)
228 if c == '\n' and i < len(renderState.text) - 1:
229 self._indent(renderState.text)
230 renderState.text.append('\n')
231
232 def _indent(self, buf):
233 buf.append(' ' * self._indentation)
234
235 class BlockNode(DecoratorNode):
236 def __init__(self, content):
237 DecoratorNode.__init__(self, content)
238 content.trimLeadingNewLine()
239 content.trimTrailingSpaces()
240
241 def render(self, renderState):
242 self._content.render(renderState)
243
244 class NodeCollection(object):
245 def __init__(self, nodes):
246 self._nodes = nodes
247
248 def render(self, renderState):
249 for node in self._nodes:
250 node.render(renderState)
251
252 def trimLeadingNewLine(self):
253 if len(self._nodes) > 0:
254 self._nodes[0].trimLeadingNewLine()
255
256 def trimTrailingSpaces(self):
257 if len(self._nodes) > 0:
258 return self._nodes[-1].trimTrailingSpaces()
259 return 0
260
261 def trimTrailingNewLine(self):
262 if len(self._nodes) > 0:
263 self._nodes[-1].trimTrailingNewLine()
264
265 def trailsWithEmptyLine(self):
266 if len(self._nodes) > 0:
267 return self._nodes[-1].trailsWithEmptyLine()
268 return False
127 269
128 class StringNode(object): 270 class StringNode(object):
129 """ Just a string. 271 """ Just a string.
130 """ 272 """
131 def __init__(self, string): 273 def __init__(self, string):
132 self.string = string 274 self._string = string
133 275
134 def render(self, buf, contexts, errors): 276 def render(self, renderState):
135 buf.append(self.string) 277 renderState.text.append(self._string)
136 278
137 class EscapedVariableNode(SelfClosingNode): 279 def trimLeadingNewLine(self):
280 if self._string.startswith('\n'):
281 self._string = self._string[1:]
282
283 def trimTrailingSpaces(self):
284 originalLength = len(self._string)
285 self._string = self._string[:self._lastIndexOfSpaces()]
286 return originalLength - len(self._string)
287
288 def trimTrailingNewLine(self):
289 if self._string.endswith('\n'):
290 self._string = self._string[:len(self._string) - 1]
291
292 def trailsWithEmptyLine(self):
293 index = self._lastIndexOfSpaces()
294 return index == 0 or self._string[index - 1] == '\n'
295
296 def _lastIndexOfSpaces(self):
297 index = len(self._string)
298 while index > 0 and self._string[index - 1] == ' ':
299 index -= 1
300 return index
301
302 class EscapedVariableNode(LeafNode):
138 """ {{foo}} 303 """ {{foo}}
139 """ 304 """
140 def render(self, buf, contexts, errors): 305 def __init__(self, id):
141 value = self.id.resolve(contexts, errors) 306 self._id = id
307
308 def render(self, renderState):
309 value = self._id.resolve(renderState)
142 if value: 310 if value:
143 buf.append(self._htmlEscape(str(value))) 311 self._appendEscapedHtml(renderState.text, str(value))
144 312
145 def _htmlEscape(self, unescaped): 313 def _appendEscapedHtml(self, escaped, unescaped):
146 escaped = StringBuilder()
147 for c in unescaped: 314 for c in unescaped:
148 if c == '<': 315 if c == '<':
149 escaped.append("&lt;") 316 escaped.append("&lt;")
150 elif c == '>': 317 elif c == '>':
151 escaped.append("&gt;") 318 escaped.append("&gt;")
152 elif c == '&': 319 elif c == '&':
153 escaped.append("&amp;") 320 escaped.append("&amp;")
154 else: 321 else:
155 escaped.append(c) 322 escaped.append(c)
156 return escaped.toString()
157 323
158 class UnescapedVariableNode(SelfClosingNode): 324 class UnescapedVariableNode(LeafNode):
159 """ {{{foo}}} 325 """ {{{foo}}}
160 """ 326 """
161 def render(self, buf, contexts, errors): 327 def __init__(self, id):
162 value = self.id.resolve(contexts, errors) 328 self._id = id
329
330 def render(self, renderState):
331 value = self._id.resolve(renderState)
163 if value: 332 if value:
164 buf.append(value) 333 renderState.text.append(value)
165 334
166 class SectionNode(HasChildrenNode): 335 class SectionNode(DecoratorNode):
167 """ {{#foo}} ... {{/}} 336 """ {{#foo}} ... {{/}}
168 """ 337 """
169 def render(self, buf, contexts, errors): 338 def __init__(self, id, content):
170 value = self.id.resolve(contexts, errors) 339 DecoratorNode.__init__(self, content)
340 self._id = id
341
342 def render(self, renderState):
343 value = self._id.resolve(renderState)
171 if not value: 344 if not value:
172 return 345 return
173 346
174 type_ = type(value) 347 type_ = type(value)
175 if value == None: 348 if value == None:
176 pass 349 pass
177 elif type_ == list: 350 elif type_ == list:
178 for item in value: 351 for item in value:
179 contexts.insert(0, item) 352 renderState.localContexts.insert(0, item)
180 _RenderNodes(buf, self.children, contexts, errors) 353 self._content.render(renderState)
181 contexts.pop(0) 354 renderState.localContexts.pop(0)
182 elif type_ == dict: 355 elif type_ == dict:
183 contexts.insert(0, value) 356 renderState.localContexts.insert(0, value)
184 _RenderNodes(buf, self.children, contexts, errors) 357 self._content.render(renderState)
185 contexts.pop(0) 358 renderState.localContexts.pop(0)
186 else: 359 else:
187 _RenderError(errors, "{{#", self.id, "}} cannot be rendered with a ", type _) 360 renderState.addError("{{#", self._id,
361 "}} cannot be rendered with a ", type_)
188 362
189 class VertedSectionNode(HasChildrenNode): 363 class VertedSectionNode(DecoratorNode):
190 """ {{?foo}} ... {{/}} 364 """ {{?foo}} ... {{/}}
191 """ 365 """
192 def render(self, buf, contexts, errors): 366 def __init__(self, id, content):
193 value = self.id.resolve(contexts, errors) 367 DecoratorNode.__init__(self, content)
368 self._id = id
369
370 def render(self, renderState):
371 value = self._id.resolve(renderState)
194 if value and _VertedSectionNodeShouldRender(value): 372 if value and _VertedSectionNodeShouldRender(value):
195 contexts.insert(0, value) 373 renderState.localContexts.insert(0, value)
196 _RenderNodes(buf, self.children, contexts, errors) 374 self._content.render(renderState)
197 contexts.pop(0) 375 renderState.localContexts.pop(0)
198 376
199 def _VertedSectionNodeShouldRender(value): 377 def _VertedSectionNodeShouldRender(value):
200 type_ = type(value) 378 type_ = type(value)
201 if value == None: 379 if value == None:
202 return False 380 return False
203 elif type_ == bool: 381 elif type_ == bool:
204 return value 382 return value
205 elif type_ == int or type_ == float: 383 elif type_ == int or type_ == float:
206 return value > 0 384 return value > 0
207 elif type_ == str or type_ == unicode: 385 elif type_ == str or type_ == unicode:
208 return value != '' 386 return value != ''
209 elif type_ == list or type_ == dict: 387 elif type_ == list or type_ == dict:
210 return len(value) > 0 388 return len(value) > 0
211 raise TypeError("Unhandled type: " + str(type_)) 389 raise TypeError("Unhandled type: " + str(type_))
212 390
213 class InvertedSectionNode(HasChildrenNode): 391 class InvertedSectionNode(DecoratorNode):
214 """ {{^foo}} ... {{/}} 392 """ {{^foo}} ... {{/}}
215 """ 393 """
216 def render(self, buf, contexts, errors): 394 def __init__(self, id, content):
217 value = self.id.resolve(contexts, errors) 395 DecoratorNode.__init__(self, content)
396 self._id = id
397
398 def render(self, renderState):
399 value = self._id.resolve(renderState)
218 if not value or not _VertedSectionNodeShouldRender(value): 400 if not value or not _VertedSectionNodeShouldRender(value):
219 _RenderNodes(buf, self.children, contexts, errors) 401 self._content.render(renderState)
220 402
221 class JsonNode(SelfClosingNode): 403 class JsonNode(LeafNode):
222 """ {{*foo}} 404 """ {{*foo}}
223 """ 405 """
224 def render(self, buf, contexts, errors): 406 def __init__(self, id):
225 value = self.id.resolve(contexts, errors) 407 self._id = id
408
409 def render(self, renderState):
410 value = self._id.resolve(renderState)
226 if value: 411 if value:
227 buf.append(json.dumps(value, separators=(',',':'))) 412 renderState.text.append(json.dumps(value, separators=(',',':')))
228 413
229 class PartialNode(SelfClosingNode): 414 class PartialNode(LeafNode):
230 """ {{+foo}} 415 """ {{+foo}}
231 """ 416 """
232 def render(self, buf, contexts, errors): 417 def __init__(self, id):
233 value = self.id.resolve(contexts, errors) 418 self._id = id
419 self._args = None
420
421 def render(self, renderState):
422 value = self._id.resolve(renderState)
234 if not isinstance(value, Handlebar): 423 if not isinstance(value, Handlebar):
235 _RenderError(errors, id, " didn't resolve to a Handlebar") 424 renderState.addError(self._id, " didn't resolve to a Handlebar")
236 return 425 return
237 _RenderNodes(buf, value.nodes, contexts, errors)
238 426
239 class SwitchNode(object): 427 argContext = []
240 """ {{:foo}} 428 if len(renderState.localContexts) > 0:
241 """ 429 argContext.append(renderState.localContexts[0])
242 def __init__(self, id):
243 self.id = id
244 self._cases = {}
245 430
246 def addCase(self, caseValue, caseNode): 431 if self._args:
247 self._cases[caseValue] = caseNode 432 argContextMap = {}
433 for key, valueId in self._args.items():
434 context = valueId.resolve(renderState)
435 if context:
436 argContextMap[key] = context
437 argContext.append(argContextMap)
248 438
249 def render(self, buf, contexts, errors): 439 partialRenderState = RenderState(renderState.globalContexts, argContext)
250 value = self.id.resolve(contexts, errors) 440 value._topNode.render(partialRenderState)
251 if not value:
252 _RenderError(errors, id, " didn't resolve to any value")
253 return
254 if not (type(value) == str or type(value) == unicode):
255 _RenderError(errors, id, " didn't resolve to a String")
256 return
257 caseNode = self._cases.get(value)
258 if caseNode:
259 caseNode.render(buf, contexts, errors)
260 441
261 class CaseNode(object): 442 text = partialRenderState.text.toString()
262 """ {{=foo}} 443 lastIndex = len(text) - 1
263 """ 444 if lastIndex >= 0 and text[lastIndex] == '\n':
264 def __init__(self, children): 445 text = text[:lastIndex]
265 self.children = children
266 446
267 def render(self, buf, contexts, errors): 447 renderState.text.append(text)
268 for child in self.children: 448 renderState.errors.extend(partialRenderState.errors)
269 child.render(buf, contexts, errors) 449
450 def addArgument(self, key, valueId):
451 if not self._args:
452 self._args = {}
453 self._args[key] = valueId
270 454
271 class Token(object): 455 class Token(object):
272 """ The tokens that can appear in a template. 456 """ The tokens that can appear in a template.
273 """ 457 """
274 class Data(object): 458 class Data(object):
275 def __init__(self, name, text, clazz): 459 def __init__(self, name, text, clazz):
276 self.name = name 460 self.name = name
277 self.text = text 461 self.text = text
278 self.clazz = clazz 462 self.clazz = clazz
279 463
280 OPEN_START_SECTION = Data("OPEN_START_SECTION" , "{{#", Secti onNode) 464 OPEN_START_SECTION = Data("OPEN_START_SECTION" , "{{#", Secti onNode)
281 OPEN_START_VERTED_SECTION = Data("OPEN_START_VERTED_SECTION" , "{{?", Verte dSectionNode) 465 OPEN_START_VERTED_SECTION = Data("OPEN_START_VERTED_SECTION" , "{{?", Verte dSectionNode)
282 OPEN_START_INVERTED_SECTION = Data("OPEN_START_INVERTED_SECTION", "{{^", Inver tedSectionNode) 466 OPEN_START_INVERTED_SECTION = Data("OPEN_START_INVERTED_SECTION", "{{^", Inver tedSectionNode)
283 OPEN_START_JSON = Data("OPEN_START_JSON" , "{{*", JsonN ode) 467 OPEN_START_JSON = Data("OPEN_START_JSON" , "{{*", JsonN ode)
284 OPEN_START_PARTIAL = Data("OPEN_START_PARTIAL" , "{{+", Parti alNode) 468 OPEN_START_PARTIAL = Data("OPEN_START_PARTIAL" , "{{+", Parti alNode)
285 OPEN_START_SWITCH = Data("OPEN_START_SWITCH" , "{{:", Switc hNode)
286 OPEN_CASE = Data("OPEN_CASE" , "{{=", CaseN ode)
287 OPEN_END_SECTION = Data("OPEN_END_SECTION" , "{{/", None) 469 OPEN_END_SECTION = Data("OPEN_END_SECTION" , "{{/", None)
288 OPEN_UNESCAPED_VARIABLE = Data("OPEN_UNESCAPED_VARIABLE" , "{{{", Unesc apedVariableNode) 470 OPEN_UNESCAPED_VARIABLE = Data("OPEN_UNESCAPED_VARIABLE" , "{{{", Unesc apedVariableNode)
289 CLOSE_MUSTACHE3 = Data("CLOSE_MUSTACHE3" , "}}}", None) 471 CLOSE_MUSTACHE3 = Data("CLOSE_MUSTACHE3" , "}}}", None)
472 OPEN_COMMENT = Data("OPEN_COMMENT" , "{{-", None)
473 CLOSE_COMMENT = Data("CLOSE_COMMENT" , "-}}", None)
290 OPEN_VARIABLE = Data("OPEN_VARIABLE" , "{{" , Escap edVariableNode) 474 OPEN_VARIABLE = Data("OPEN_VARIABLE" , "{{" , Escap edVariableNode)
291 CLOSE_MUSTACHE = Data("CLOSE_MUSTACHE" , "}}" , None) 475 CLOSE_MUSTACHE = Data("CLOSE_MUSTACHE" , "}}" , None)
292 CHARACTER = Data("CHARACTER" , "." , Strin gNode) 476 CHARACTER = Data("CHARACTER" , "." , None)
293 477
294 # List of tokens in order of longest to shortest, to avoid any prefix matching 478 # List of tokens in order of longest to shortest, to avoid any prefix matching
295 # issues. 479 # issues.
296 _tokenList = [ 480 _tokenList = [
297 Token.OPEN_START_SECTION, 481 Token.OPEN_START_SECTION,
298 Token.OPEN_START_VERTED_SECTION, 482 Token.OPEN_START_VERTED_SECTION,
299 Token.OPEN_START_INVERTED_SECTION, 483 Token.OPEN_START_INVERTED_SECTION,
300 Token.OPEN_START_JSON, 484 Token.OPEN_START_JSON,
301 Token.OPEN_START_PARTIAL, 485 Token.OPEN_START_PARTIAL,
302 Token.OPEN_START_SWITCH,
303 Token.OPEN_CASE,
304 Token.OPEN_END_SECTION, 486 Token.OPEN_END_SECTION,
305 Token.OPEN_UNESCAPED_VARIABLE, 487 Token.OPEN_UNESCAPED_VARIABLE,
306 Token.CLOSE_MUSTACHE3, 488 Token.CLOSE_MUSTACHE3,
489 Token.OPEN_COMMENT,
490 Token.CLOSE_COMMENT,
307 Token.OPEN_VARIABLE, 491 Token.OPEN_VARIABLE,
308 Token.CLOSE_MUSTACHE, 492 Token.CLOSE_MUSTACHE,
309 Token.CHARACTER 493 Token.CHARACTER
310 ] 494 ]
311 495
312 class TokenStream(object): 496 class TokenStream(object):
313 """ Tokeniser for template parsing. 497 """ Tokeniser for template parsing.
314 """ 498 """
315 def __init__(self, string): 499 def __init__(self, string):
500 self._remainder = string
501
316 self.nextToken = None 502 self.nextToken = None
317 self._remainder = string 503 self.nextContents = None
318 self._nextContents = None 504 self.nextLine = Line(1)
319 self.advance() 505 self.advance()
320 506
321 def hasNext(self): 507 def hasNext(self):
322 return self.nextToken != None 508 return self.nextToken != None
323 509
324 def advanceOver(self, token): 510 def advance(self):
325 if self.nextToken != token: 511 if self.nextContents == '\n':
326 raise ParseException( 512 self.nextLine = Line(self.nextLine.number + 1)
327 "Expecting token " + token.name + " but got " + self.nextToken.name)
328 return self.advance()
329 513
330 def advance(self):
331 self.nextToken = None 514 self.nextToken = None
332 self._nextContents = None 515 self.nextContents = None
333 516
334 if self._remainder == '': 517 if self._remainder == '':
335 return None 518 return None
336 519
337 for token in _tokenList: 520 for token in _tokenList:
338 if self._remainder.startswith(token.text): 521 if self._remainder.startswith(token.text):
339 self.nextToken = token 522 self.nextToken = token
340 break 523 break
341 524
342 if self.nextToken == None: 525 if self.nextToken == None:
343 self.nextToken = Token.CHARACTER 526 self.nextToken = Token.CHARACTER
344 527
345 self._nextContents = self._remainder[0:len(self.nextToken.text)] 528 self.nextContents = self._remainder[0:len(self.nextToken.text)]
346 self._remainder = self._remainder[len(self.nextToken.text):] 529 self._remainder = self._remainder[len(self.nextToken.text):]
347 return self.nextToken 530 return self
348 531
349 def nextString(self): 532 def advanceOver(self, token):
533 if self.nextToken != token:
534 raise ParseException(
535 "Expecting token " + token.name + " but got " + self.nextToken.name,
536 self.nextLine)
537 return self.advance()
538
539 def advanceOverNextString(self, excluded=''):
350 buf = StringBuilder() 540 buf = StringBuilder()
351 while self.nextToken == Token.CHARACTER: 541 while self.nextToken == Token.CHARACTER and \
352 buf.append(self._nextContents) 542 excluded.find(self.nextContents) == -1:
543 buf.append(self.nextContents)
353 self.advance() 544 self.advance()
354 return buf.toString() 545 return buf.toString()
355 546
356 def _CreateIdentifier(path):
357 if path == '@':
358 return ThisIdentifier()
359 else:
360 return PathIdentifier(path)
361
362 class Handlebar(object): 547 class Handlebar(object):
363 """ A handlebar template. 548 """ A handlebar template.
364 """ 549 """
365 def __init__(self, template): 550 def __init__(self, template):
366 self.nodes = [] 551 self.source = template
367 self._parseTemplate(template, self.nodes)
368
369 def _parseTemplate(self, template, nodes):
370 tokens = TokenStream(template) 552 tokens = TokenStream(template)
371 self._parseSection(tokens, nodes) 553 self._topNode = self._parseSection(tokens, None)
372 if tokens.hasNext(): 554 if tokens.hasNext():
373 raise ParseException("There are still tokens remaining, " 555 raise ParseException("There are still tokens remaining, "
374 "was there an end-section without a start-section:") 556 "was there an end-section without a start-section:",
557 tokens.nextLine)
375 558
376 def _parseSection(self, tokens, nodes): 559 def _parseSection(self, tokens, previousNode):
560 nodes = []
377 sectionEnded = False 561 sectionEnded = False
562
378 while tokens.hasNext() and not sectionEnded: 563 while tokens.hasNext() and not sectionEnded:
379 next = tokens.nextToken 564 token = tokens.nextToken
380 if next == Token.CHARACTER: 565 node = None
381 nodes.append(StringNode(tokens.nextString()))
382 elif next == Token.OPEN_VARIABLE or \
383 next == Token.OPEN_UNESCAPED_VARIABLE or \
384 next == Token.OPEN_START_JSON or \
385 next == Token.OPEN_START_PARTIAL:
386 token = tokens.nextToken
387 id = self._openSection(tokens)
388 node = token.clazz()
389 node.init(id)
390 nodes.append(node)
391 elif next == Token.OPEN_START_SECTION or \
392 next == Token.OPEN_START_VERTED_SECTION or \
393 next == Token.OPEN_START_INVERTED_SECTION:
394 token = tokens.nextToken
395 id = self._openSection(tokens)
396 566
397 children = [] 567 if token == Token.CHARACTER:
398 self._parseSection(tokens, children) 568 node = StringNode(tokens.advanceOverNextString())
569 elif token == Token.OPEN_VARIABLE or \
570 token == Token.OPEN_UNESCAPED_VARIABLE or \
571 token == Token.OPEN_START_JSON:
572 id = self._openSectionOrTag(tokens)
573 node = token.clazz(id)
574 elif token == Token.OPEN_START_PARTIAL:
575 tokens.advance()
576 id = Identifier(tokens.advanceOverNextString(excluded=' '),
577 tokens.nextLine)
578 partialNode = PartialNode(id)
579
580 while tokens.nextToken == Token.CHARACTER:
581 tokens.advance()
582 key = tokens.advanceOverNextString(excluded=':')
583 tokens.advance()
584 partialNode.addArgument(
585 key,
586 Identifier(tokens.advanceOverNextString(excluded=' '),
587 tokens.nextLine))
588
589 tokens.advanceOver(Token.CLOSE_MUSTACHE)
590 node = partialNode
591 elif token == Token.OPEN_START_SECTION or \
592 token == Token.OPEN_START_VERTED_SECTION or \
593 token == Token.OPEN_START_INVERTED_SECTION:
594 startLine = tokens.nextLine
595
596 id = self._openSectionOrTag(tokens)
597 section = self._parseSection(tokens, previousNode)
399 self._closeSection(tokens, id) 598 self._closeSection(tokens, id)
400 599
401 node = token.clazz() 600 node = token.clazz(id, section)
402 node.init(id, children)
403 nodes.append(node)
404 elif next == Token.OPEN_START_SWITCH:
405 id = self._openSection(tokens)
406 while tokens.nextToken == Token.CHARACTER:
407 tokens.advanceOver(Token.CHARACTER)
408 601
409 switchNode = SwitchNode(id) 602 if startLine.number != tokens.nextLine.number:
410 nodes.append(switchNode) 603 node = BlockNode(node)
411 604 if previousNode:
412 while tokens.hasNext() and tokens.nextToken == Token.OPEN_CASE: 605 previousNode.trimTrailingSpaces();
413 tokens.advanceOver(Token.OPEN_CASE) 606 if tokens.nextContents == '\n':
414 caseValue = tokens.nextString() 607 tokens.advance()
415 tokens.advanceOver(Token.CLOSE_MUSTACHE) 608 elif token == Token.OPEN_COMMENT:
416 609 self._advanceOverComment(tokens)
417 caseChildren = [] 610 elif token == Token.OPEN_END_SECTION:
418 self._parseSection(tokens, caseChildren)
419
420 switchNode.addCase(caseValue, CaseNode(caseChildren))
421
422 self._closeSection(tokens, id)
423 elif next == Token.OPEN_CASE:
424 # See below.
425 sectionEnded = True
426 elif next == Token.OPEN_END_SECTION:
427 # Handled after running parseSection within the SECTION cases, so this i s a 611 # Handled after running parseSection within the SECTION cases, so this i s a
428 # terminating condition. If there *is* an orphaned OPEN_END_SECTION, it will be caught 612 # terminating condition. If there *is* an orphaned OPEN_END_SECTION, it will be caught
429 # by noticing that there are leftover tokens after termination. 613 # by noticing that there are leftover tokens after termination.
430 sectionEnded = True 614 sectionEnded = True
431 elif Token.CLOSE_MUSTACHE: 615 elif Token.CLOSE_MUSTACHE:
432 raise ParseException("Orphaned " + tokens.nextToken.name) 616 raise ParseException("Orphaned " + tokens.nextToken.name,
617 tokens.nextLine)
618
619 if not node:
620 continue
433 621
434 def _openSection(self, tokens): 622 if not isinstance(node, StringNode) and \
623 not isinstance(node, BlockNode):
624 if (not previousNode or previousNode.trailsWithEmptyLine()) and \
625 (not tokens.hasNext() or tokens.nextContents == '\n'):
626 indentation = 0
627 if previousNode:
628 indentation = previousNode.trimTrailingSpaces()
629 tokens.advance()
630 node = IndentedNode(node, indentation)
631 else:
632 node = InlineNode(node)
633
634 previousNode = node
635 nodes.append(node)
636
637 if len(nodes) == 1:
638 return nodes[0]
639 return NodeCollection(nodes)
640
641 def _advanceOverComment(self, tokens):
642 tokens.advanceOver(Token.OPEN_COMMENT)
643 depth = 1
644 while tokens.hasNext() and depth > 0:
645 if tokens.nextToken == Token.OPEN_COMMENT:
646 depth += 1
647 elif tokens.nextToken == Token.CLOSE_COMMENT:
648 depth -= 1
649 tokens.advance()
650
651 def _openSectionOrTag(self, tokens):
435 openToken = tokens.nextToken 652 openToken = tokens.nextToken
436 tokens.advance() 653 tokens.advance()
437 id = _CreateIdentifier(tokens.nextString()) 654 id = Identifier(tokens.advanceOverNextString(), tokens.nextLine)
438 if openToken == Token.OPEN_UNESCAPED_VARIABLE: 655 if openToken == Token.OPEN_UNESCAPED_VARIABLE:
439 tokens.advanceOver(Token.CLOSE_MUSTACHE3) 656 tokens.advanceOver(Token.CLOSE_MUSTACHE3)
440 else: 657 else:
441 tokens.advanceOver(Token.CLOSE_MUSTACHE) 658 tokens.advanceOver(Token.CLOSE_MUSTACHE)
442 return id 659 return id
443 660
444 def _closeSection(self, tokens, id): 661 def _closeSection(self, tokens, id):
445 tokens.advanceOver(Token.OPEN_END_SECTION) 662 tokens.advanceOver(Token.OPEN_END_SECTION)
446 nextString = tokens.nextString() 663 nextString = tokens.advanceOverNextString()
447 if nextString != '' and str(id) != nextString: 664 if nextString != '' and nextString != str(id):
448 raise ParseException( 665 raise ParseException(
449 "Start section " + str(id) + " doesn't match end section " + nextStrin g) 666 "Start section " + str(id) + " doesn't match end section " + nextStrin g)
450 tokens.advanceOver(Token.CLOSE_MUSTACHE) 667 tokens.advanceOver(Token.CLOSE_MUSTACHE)
451 668
452 def render(self, *contexts): 669 def render(self, *contexts):
453 """ Renders this template given a variable number of "contexts" to read 670 """ Renders this template given a variable number of "contexts" to read
454 out values from (such as those appearing in {{foo}}). 671 out values from (such as those appearing in {{foo}}).
455 """ 672 """
456 contextDeque = [] 673 globalContexts = []
457 for context in contexts: 674 for context in contexts:
458 contextDeque.append(context) 675 globalContexts.append(context)
459 buf = StringBuilder() 676 renderState = RenderState(globalContexts, [])
460 errors = [] 677 self._topNode.render(renderState)
461 _RenderNodes(buf, self.nodes, contextDeque, errors) 678 return renderState.getResult()
462 return RenderResult(buf.toString(), errors)
463
464 def _RenderNodes(buf, nodes, contexts, errors):
465 for node in nodes:
466 node.render(buf, contexts, errors)
467
468 def _RenderError(errors, *messages):
469 if not errors:
470 return
471 buf = StringBuilder()
472 for message in messages:
473 buf.append(message)
474 errors.append(buf.toString())
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