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, |
(...skipping 25 matching lines...) Expand all Loading... |
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 def _SafeStr(obj): | |
47 return obj if isinstance(obj, basestring) else str(obj) | |
48 | |
49 class ParseException(Exception): | 46 class ParseException(Exception): |
50 """ Exception thrown while parsing the template. | 47 """ Exception thrown while parsing the template. |
51 """ | 48 """ |
52 def __init__(self, error, line): | 49 def __init__(self, error, line): |
53 Exception.__init__(self, "%s (line %s)" % (error, line.number)) | 50 Exception.__init__(self, "%s (line %s)" % (error, line.number)) |
54 | 51 |
55 class RenderResult(object): | 52 class RenderResult(object): |
56 """ Result of a render operation. | 53 """ Result of a render operation. |
57 """ | 54 """ |
58 def __init__(self, text, errors): | 55 def __init__(self, text, errors): |
59 self.text = text; | 56 self.text = text; |
60 self.errors = errors | 57 self.errors = errors |
61 | 58 |
62 class StringBuilder(object): | 59 class StringBuilder(object): |
63 """ 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 |
64 this file to Python. | 61 this file to Python. |
65 """ | 62 """ |
66 def __init__(self): | 63 def __init__(self): |
67 self._buf = [] | 64 self._buf = [] |
68 self._length = 0 | |
69 | 65 |
70 def __len__(self): | 66 def __len__(self): |
71 return self._length | 67 self._Collapse() |
| 68 return len(self._buf[0]) |
72 | 69 |
73 def append(self, obj): | 70 def append(self, string): |
74 string = _SafeStr(obj) | |
75 self._buf.append(string) | 71 self._buf.append(string) |
76 self._length += len(string) | |
77 | 72 |
78 def toString(self): | 73 def toString(self): |
79 return u''.join(self._buf) | 74 self._Collapse() |
| 75 return self._buf[0] |
80 | 76 |
81 def __str__(self): | 77 def __str__(self): |
82 return self.toString() | 78 return self.toString() |
83 | 79 |
| 80 def _Collapse(self): |
| 81 self._buf = [u''.join(self._buf)] |
| 82 |
84 class RenderState(object): | 83 class RenderState(object): |
85 """ The state of a render call. | 84 """ The state of a render call. |
86 """ | 85 """ |
87 def __init__(self, globalContexts, localContexts): | 86 def __init__(self, globalContexts, localContexts): |
88 self.globalContexts = globalContexts | 87 self.globalContexts = globalContexts |
89 self.localContexts = localContexts | 88 self.localContexts = localContexts |
90 self.text = StringBuilder() | 89 self.text = StringBuilder() |
91 self.errors = [] | 90 self.errors = [] |
92 self._errorsDisabled = False | 91 self._errorsDisabled = False |
93 | 92 |
94 def inSameContext(self): | 93 def inSameContext(self): |
95 return RenderState(self.globalContexts, self.localContexts) | 94 return RenderState(self.globalContexts, self.localContexts) |
96 | 95 |
97 def getFirstContext(self): | 96 def getFirstContext(self): |
98 if len(self.localContexts) > 0: | 97 if len(self.localContexts) > 0: |
99 return self.localContexts[0] | 98 return self.localContexts[0] |
100 if len(self.globalContexts) > 0: | 99 if len(self.globalContexts) > 0: |
101 return self.globalContexts[0] | 100 return self.globalContexts[0] |
102 return None | 101 return None |
103 | 102 |
104 def disableErrors(self): | 103 def disableErrors(self): |
105 self._errorsDisabled = True | 104 self._errorsDisabled = True |
106 return self | 105 return self |
107 | 106 |
108 def addError(self, *messages): | 107 def addError(self, *messages): |
109 if self._errorsDisabled: | 108 if self._errorsDisabled: |
110 return self | 109 return self |
111 buf = StringBuilder() | 110 buf = StringBuilder() |
112 for message in messages: | 111 for message in messages: |
113 buf.append(message) | 112 buf.append(str(message)) |
114 self.errors.append(buf.toString()) | 113 self.errors.append(buf.toString()) |
115 return self | 114 return self |
116 | 115 |
117 def getResult(self): | 116 def getResult(self): |
118 return RenderResult(self.text.toString(), self.errors); | 117 return RenderResult(self.text.toString(), self.errors); |
119 | 118 |
120 class Identifier(object): | 119 class Identifier(object): |
121 """ An identifier of the form "@", "foo.bar.baz", or "@.foo.bar.baz". | 120 """ An identifier of the form "@", "foo.bar.baz", or "@.foo.bar.baz". |
122 """ | 121 """ |
123 def __init__(self, name, line): | 122 def __init__(self, name, line): |
(...skipping 13 matching lines...) Expand all Loading... |
137 self._path = name.split('.') | 136 self._path = name.split('.') |
138 | 137 |
139 def resolve(self, renderState): | 138 def resolve(self, renderState): |
140 if self._isThis: | 139 if self._isThis: |
141 return renderState.getFirstContext() | 140 return renderState.getFirstContext() |
142 | 141 |
143 if self._startsWithThis: | 142 if self._startsWithThis: |
144 return self._resolveFromContext(renderState.getFirstContext()) | 143 return self._resolveFromContext(renderState.getFirstContext()) |
145 | 144 |
146 resolved = self._resolveFromContexts(renderState.localContexts) | 145 resolved = self._resolveFromContexts(renderState.localContexts) |
147 if resolved == None: | 146 if resolved is None: |
148 resolved = self._resolveFromContexts(renderState.globalContexts) | 147 resolved = self._resolveFromContexts(renderState.globalContexts) |
149 if resolved == None: | 148 if resolved is None: |
150 renderState.addError("Couldn't resolve identifier ", self._path) | 149 renderState.addError("Couldn't resolve identifier ", self._path) |
151 return resolved | 150 return resolved |
152 | 151 |
153 def _resolveFromContexts(self, contexts): | 152 def _resolveFromContexts(self, contexts): |
154 for context in contexts: | 153 for context in contexts: |
155 resolved = self._resolveFromContext(context) | 154 resolved = self._resolveFromContext(context) |
156 if resolved != None: | 155 if resolved is not None: |
157 return resolved | 156 return resolved |
158 return None | 157 return None |
159 | 158 |
160 def _resolveFromContext(self, context): | 159 def _resolveFromContext(self, context): |
161 result = context | 160 result = context |
162 for next in self._path: | 161 for next in self._path: |
163 # Only require that contexts provide a get method, meaning that callers | 162 # Only require that contexts provide a get method, meaning that callers |
164 # can provide dict-like contexts (for example, to populate values lazily). | 163 # can provide dict-like contexts (for example, to populate values lazily). |
165 if result == None or not getattr(result, "get", None): | 164 if result is None or not getattr(result, "get", None): |
166 return None | 165 return None |
167 result = result.get(next) | 166 result = result.get(next) |
168 return result | 167 return result |
169 | 168 |
170 def __str__(self): | 169 def __str__(self): |
171 if self._isThis: | 170 if self._isThis: |
172 return '@' | 171 return '@' |
173 name = '.'.join(self._path) | 172 name = '.'.join(self._path) |
174 return ('@.' + name) if self._startsWithThis else name | 173 return ('@.' + name) if self._startsWithThis else name |
175 | 174 |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
229 | 228 |
230 class InlineNode(DecoratorNode): | 229 class InlineNode(DecoratorNode): |
231 def __init__(self, content): | 230 def __init__(self, content): |
232 DecoratorNode.__init__(self, content) | 231 DecoratorNode.__init__(self, content) |
233 | 232 |
234 def render(self, renderState): | 233 def render(self, renderState): |
235 contentRenderState = renderState.inSameContext() | 234 contentRenderState = renderState.inSameContext() |
236 self._content.render(contentRenderState) | 235 self._content.render(contentRenderState) |
237 | 236 |
238 renderState.errors.extend(contentRenderState.errors) | 237 renderState.errors.extend(contentRenderState.errors) |
239 | 238 renderState.text.append( |
240 for c in contentRenderState.text.toString(): | 239 contentRenderState.text.toString().replace('\n', '')) |
241 if c != '\n': | |
242 renderState.text.append(c) | |
243 | 240 |
244 class IndentedNode(DecoratorNode): | 241 class IndentedNode(DecoratorNode): |
245 def __init__(self, content, indentation): | 242 def __init__(self, content, indentation): |
246 DecoratorNode.__init__(self, content) | 243 DecoratorNode.__init__(self, content) |
247 self._indentation = indentation | 244 self._indent_str = ' ' * indentation |
248 | 245 |
249 def render(self, renderState): | 246 def render(self, renderState): |
250 contentRenderState = renderState.inSameContext() | 247 contentRenderState = renderState.inSameContext() |
251 self._content.render(contentRenderState) | 248 self._content.render(contentRenderState) |
252 | 249 |
253 renderState.errors.extend(contentRenderState.errors) | 250 renderState.errors.extend(contentRenderState.errors) |
254 | 251 renderState.text.append(self._indent_str) |
255 self._indent(renderState.text) | 252 # TODO: this might introduce an extra \n at the end? need test. |
256 for i, c in enumerate(contentRenderState.text.toString()): | 253 renderState.text.append( |
257 renderState.text.append(c) | 254 contentRenderState.text.toString().replace('\n', |
258 if c == '\n' and i < len(contentRenderState.text) - 1: | 255 '\n' + self._indent_str)) |
259 self._indent(renderState.text) | |
260 renderState.text.append('\n') | 256 renderState.text.append('\n') |
261 | 257 |
262 def _indent(self, buf): | |
263 buf.append(' ' * self._indentation) | |
264 | |
265 class BlockNode(DecoratorNode): | 258 class BlockNode(DecoratorNode): |
266 def __init__(self, content): | 259 def __init__(self, content): |
267 DecoratorNode.__init__(self, content) | 260 DecoratorNode.__init__(self, content) |
268 content.trimStartingNewLine() | 261 content.trimStartingNewLine() |
269 content.trimEndingSpaces() | 262 content.trimEndingSpaces() |
270 | 263 |
271 def render(self, renderState): | 264 def render(self, renderState): |
272 self._content.render(renderState) | 265 self._content.render(renderState) |
273 | 266 |
274 class NodeCollection(object): | 267 class NodeCollection(object): |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
347 | 340 |
348 class EscapedVariableNode(LeafNode): | 341 class EscapedVariableNode(LeafNode): |
349 """ {{foo}} | 342 """ {{foo}} |
350 """ | 343 """ |
351 def __init__(self, id, line): | 344 def __init__(self, id, line): |
352 LeafNode.__init__(self, line) | 345 LeafNode.__init__(self, line) |
353 self._id = id | 346 self._id = id |
354 | 347 |
355 def render(self, renderState): | 348 def render(self, renderState): |
356 value = self._id.resolve(renderState) | 349 value = self._id.resolve(renderState) |
357 if value != None: | 350 if value is None: |
358 self._appendEscapedHtml(renderState.text, _SafeStr(value)) | 351 return |
359 | 352 |
360 def _appendEscapedHtml(self, escaped, unescaped): | 353 string = value if isinstance(value, basestring) else str(value) |
361 for c in unescaped: | 354 renderState.text.append(string.replace('&', '&') |
362 if c == '<': | 355 .replace('<', '<') |
363 escaped.append("<") | 356 .replace('>', '>')) |
364 elif c == '>': | |
365 escaped.append(">") | |
366 elif c == '&': | |
367 escaped.append("&") | |
368 else: | |
369 escaped.append(c) | |
370 | 357 |
371 class UnescapedVariableNode(LeafNode): | 358 class UnescapedVariableNode(LeafNode): |
372 """ {{{foo}}} | 359 """ {{{foo}}} |
373 """ | 360 """ |
374 def __init__(self, id, line): | 361 def __init__(self, id, line): |
375 LeafNode.__init__(self, line) | 362 LeafNode.__init__(self, line) |
376 self._id = id | 363 self._id = id |
377 | 364 |
378 def render(self, renderState): | 365 def render(self, renderState): |
379 value = self._id.resolve(renderState) | 366 value = self._id.resolve(renderState) |
380 if value != None: | 367 if value is None: |
381 renderState.text.append(value) | 368 return |
| 369 renderState.text.append( |
| 370 value if isinstance(value, basestring) else str(value)) |
382 | 371 |
383 class SectionNode(DecoratorNode): | 372 class SectionNode(DecoratorNode): |
384 """ {{#foo}} ... {{/}} | 373 """ {{#foo}} ... {{/}} |
385 """ | 374 """ |
386 def __init__(self, id, content): | 375 def __init__(self, id, content): |
387 DecoratorNode.__init__(self, content) | 376 DecoratorNode.__init__(self, content) |
388 self._id = id | 377 self._id = id |
389 | 378 |
390 def render(self, renderState): | 379 def render(self, renderState): |
391 value = self._id.resolve(renderState) | 380 value = self._id.resolve(renderState) |
392 if value == None: | 381 if value is None: |
393 return | 382 return |
394 | 383 |
395 if isinstance(value, list): | 384 if isinstance(value, list): |
396 for item in value: | 385 for item in value: |
397 renderState.localContexts.insert(0, item) | 386 renderState.localContexts.insert(0, item) |
398 self._content.render(renderState) | 387 self._content.render(renderState) |
399 renderState.localContexts.pop(0) | 388 renderState.localContexts.pop(0) |
400 elif isinstance(value, dict): | 389 elif isinstance(value, dict): |
401 renderState.localContexts.insert(0, value) | 390 renderState.localContexts.insert(0, value) |
402 self._content.render(renderState) | 391 self._content.render(renderState) |
(...skipping 10 matching lines...) Expand all Loading... |
413 self._id = id | 402 self._id = id |
414 | 403 |
415 def render(self, renderState): | 404 def render(self, renderState): |
416 value = self._id.resolve(renderState.inSameContext().disableErrors()) | 405 value = self._id.resolve(renderState.inSameContext().disableErrors()) |
417 if _VertedSectionNodeShouldRender(value): | 406 if _VertedSectionNodeShouldRender(value): |
418 renderState.localContexts.insert(0, value) | 407 renderState.localContexts.insert(0, value) |
419 self._content.render(renderState) | 408 self._content.render(renderState) |
420 renderState.localContexts.pop(0) | 409 renderState.localContexts.pop(0) |
421 | 410 |
422 def _VertedSectionNodeShouldRender(value): | 411 def _VertedSectionNodeShouldRender(value): |
423 if value == None: | 412 if value is None: |
424 return False | 413 return False |
425 if isinstance(value, bool): | 414 if isinstance(value, bool): |
426 return value | 415 return value |
427 if (isinstance(value, int) or | 416 if (isinstance(value, int) or |
428 isinstance(value, long) or | 417 isinstance(value, long) or |
429 isinstance(value, float)): | 418 isinstance(value, float)): |
430 return True | 419 return True |
431 if isinstance(value, basestring): | 420 if isinstance(value, basestring): |
432 return True | 421 return True |
433 if isinstance(value, list): | 422 if isinstance(value, list): |
(...skipping 16 matching lines...) Expand all Loading... |
450 | 439 |
451 class JsonNode(LeafNode): | 440 class JsonNode(LeafNode): |
452 """ {{*foo}} | 441 """ {{*foo}} |
453 """ | 442 """ |
454 def __init__(self, id, line): | 443 def __init__(self, id, line): |
455 LeafNode.__init__(self, line) | 444 LeafNode.__init__(self, line) |
456 self._id = id | 445 self._id = id |
457 | 446 |
458 def render(self, renderState): | 447 def render(self, renderState): |
459 value = self._id.resolve(renderState) | 448 value = self._id.resolve(renderState) |
460 if value != None: | 449 if value is None: |
461 renderState.text.append(json.dumps(value, separators=(',',':'))) | 450 return |
| 451 renderState.text.append(json.dumps(value, separators=(',',':'))) |
462 | 452 |
463 class PartialNode(LeafNode): | 453 class PartialNode(LeafNode): |
464 """ {{+foo}} | 454 """ {{+foo}} |
465 """ | 455 """ |
466 def __init__(self, id, line): | 456 def __init__(self, id, line): |
467 LeafNode.__init__(self, line) | 457 LeafNode.__init__(self, line) |
468 self._id = id | 458 self._id = id |
469 self._args = None | 459 self._args = None |
470 | 460 |
471 def render(self, renderState): | 461 def render(self, renderState): |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
542 """ | 532 """ |
543 def __init__(self, string): | 533 def __init__(self, string): |
544 self._remainder = string | 534 self._remainder = string |
545 | 535 |
546 self.nextToken = None | 536 self.nextToken = None |
547 self.nextContents = None | 537 self.nextContents = None |
548 self.nextLine = Line(1) | 538 self.nextLine = Line(1) |
549 self.advance() | 539 self.advance() |
550 | 540 |
551 def hasNext(self): | 541 def hasNext(self): |
552 return self.nextToken != None | 542 return self.nextToken is not None |
553 | 543 |
554 def advance(self): | 544 def advance(self): |
555 if self.nextContents == '\n': | 545 if self.nextContents == '\n': |
556 self.nextLine = Line(self.nextLine.number + 1) | 546 self.nextLine = Line(self.nextLine.number + 1) |
557 | 547 |
558 self.nextToken = None | 548 self.nextToken = None |
559 self.nextContents = None | 549 self.nextContents = None |
560 | 550 |
561 if self._remainder == '': | 551 if self._remainder == '': |
562 return None | 552 return None |
(...skipping 184 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
747 def render(self, *contexts): | 737 def render(self, *contexts): |
748 """ Renders this template given a variable number of "contexts" to read | 738 """ Renders this template given a variable number of "contexts" to read |
749 out values from (such as those appearing in {{foo}}). | 739 out values from (such as those appearing in {{foo}}). |
750 """ | 740 """ |
751 globalContexts = [] | 741 globalContexts = [] |
752 for context in contexts: | 742 for context in contexts: |
753 globalContexts.append(context) | 743 globalContexts.append(context) |
754 renderState = RenderState(globalContexts, []) | 744 renderState = RenderState(globalContexts, []) |
755 self._topNode.render(renderState) | 745 self._topNode.render(renderState) |
756 return renderState.getResult() | 746 return renderState.getResult() |
OLD | NEW |