| OLD | NEW |
| 1 # Copyright 2012 Benjamin Kalman | 1 # Copyright 2012 Benjamin Kalman |
| 2 # | 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. | 4 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at | 5 # You may obtain a copy of the License at |
| 6 # | 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # | 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. | 13 # limitations under the License. |
| 14 | 14 |
| 15 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("<") | 316 escaped.append("<") |
| 150 elif c == '>': | 317 elif c == '>': |
| 151 escaped.append(">") | 318 escaped.append(">") |
| 152 elif c == '&': | 319 elif c == '&': |
| 153 escaped.append("&") | 320 escaped.append("&") |
| 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()) | |
| OLD | NEW |