Chris Craik | b2cbf15 | 2015-07-28 16:26:29 -0700 | [diff] [blame^] | 1 | """ |
| 2 | A small templating language |
| 3 | |
| 4 | This implements a small templating language for use internally in |
| 5 | Paste and Paste Script. This language implements if/elif/else, |
| 6 | for/continue/break, expressions, and blocks of Python code. The |
| 7 | syntax is:: |
| 8 | |
| 9 | {{any expression (function calls etc)}} |
| 10 | {{any expression | filter}} |
| 11 | {{for x in y}}...{{endfor}} |
| 12 | {{if x}}x{{elif y}}y{{else}}z{{endif}} |
| 13 | {{py:x=1}} |
| 14 | {{py: |
| 15 | def foo(bar): |
| 16 | return 'baz' |
| 17 | }} |
| 18 | {{default var = default_value}} |
| 19 | {{# comment}} |
| 20 | |
| 21 | You use this with the ``Template`` class or the ``sub`` shortcut. |
| 22 | The ``Template`` class takes the template string and the name of |
| 23 | the template (for errors) and a default namespace. Then (like |
| 24 | ``string.Template``) you can call the ``tmpl.substitute(**kw)`` |
| 25 | method to make a substitution (or ``tmpl.substitute(a_dict)``). |
| 26 | |
| 27 | ``sub(content, **kw)`` substitutes the template immediately. You |
| 28 | can use ``__name='tmpl.html'`` to set the name of the template. |
| 29 | |
| 30 | If there are syntax errors ``TemplateError`` will be raised. |
| 31 | """ |
| 32 | |
| 33 | import re |
| 34 | import six |
| 35 | import sys |
| 36 | import cgi |
| 37 | from six.moves.urllib.parse import quote |
| 38 | from paste.util.looper import looper |
| 39 | |
| 40 | __all__ = ['TemplateError', 'Template', 'sub', 'HTMLTemplate', |
| 41 | 'sub_html', 'html', 'bunch'] |
| 42 | |
| 43 | token_re = re.compile(r'\{\{|\}\}') |
| 44 | in_re = re.compile(r'\s+in\s+') |
| 45 | var_re = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) |
| 46 | |
| 47 | class TemplateError(Exception): |
| 48 | """Exception raised while parsing a template |
| 49 | """ |
| 50 | |
| 51 | def __init__(self, message, position, name=None): |
| 52 | self.message = message |
| 53 | self.position = position |
| 54 | self.name = name |
| 55 | |
| 56 | def __str__(self): |
| 57 | msg = '%s at line %s column %s' % ( |
| 58 | self.message, self.position[0], self.position[1]) |
| 59 | if self.name: |
| 60 | msg += ' in %s' % self.name |
| 61 | return msg |
| 62 | |
| 63 | class _TemplateContinue(Exception): |
| 64 | pass |
| 65 | |
| 66 | class _TemplateBreak(Exception): |
| 67 | pass |
| 68 | |
| 69 | class Template(object): |
| 70 | |
| 71 | default_namespace = { |
| 72 | 'start_braces': '{{', |
| 73 | 'end_braces': '}}', |
| 74 | 'looper': looper, |
| 75 | } |
| 76 | |
| 77 | default_encoding = 'utf8' |
| 78 | |
| 79 | def __init__(self, content, name=None, namespace=None): |
| 80 | self.content = content |
| 81 | self._unicode = isinstance(content, six.text_type) |
| 82 | self.name = name |
| 83 | self._parsed = parse(content, name=name) |
| 84 | if namespace is None: |
| 85 | namespace = {} |
| 86 | self.namespace = namespace |
| 87 | |
| 88 | def from_filename(cls, filename, namespace=None, encoding=None): |
| 89 | f = open(filename, 'rb') |
| 90 | c = f.read() |
| 91 | f.close() |
| 92 | if encoding: |
| 93 | c = c.decode(encoding) |
| 94 | return cls(content=c, name=filename, namespace=namespace) |
| 95 | |
| 96 | from_filename = classmethod(from_filename) |
| 97 | |
| 98 | def __repr__(self): |
| 99 | return '<%s %s name=%r>' % ( |
| 100 | self.__class__.__name__, |
| 101 | hex(id(self))[2:], self.name) |
| 102 | |
| 103 | def substitute(self, *args, **kw): |
| 104 | if args: |
| 105 | if kw: |
| 106 | raise TypeError( |
| 107 | "You can only give positional *or* keyword arguments") |
| 108 | if len(args) > 1: |
| 109 | raise TypeError( |
| 110 | "You can only give on positional argument") |
| 111 | kw = args[0] |
| 112 | ns = self.default_namespace.copy() |
| 113 | ns.update(self.namespace) |
| 114 | ns.update(kw) |
| 115 | result = self._interpret(ns) |
| 116 | return result |
| 117 | |
| 118 | def _interpret(self, ns): |
| 119 | __traceback_hide__ = True |
| 120 | parts = [] |
| 121 | self._interpret_codes(self._parsed, ns, out=parts) |
| 122 | return ''.join(parts) |
| 123 | |
| 124 | def _interpret_codes(self, codes, ns, out): |
| 125 | __traceback_hide__ = True |
| 126 | for item in codes: |
| 127 | if isinstance(item, six.string_types): |
| 128 | out.append(item) |
| 129 | else: |
| 130 | self._interpret_code(item, ns, out) |
| 131 | |
| 132 | def _interpret_code(self, code, ns, out): |
| 133 | __traceback_hide__ = True |
| 134 | name, pos = code[0], code[1] |
| 135 | if name == 'py': |
| 136 | self._exec(code[2], ns, pos) |
| 137 | elif name == 'continue': |
| 138 | raise _TemplateContinue() |
| 139 | elif name == 'break': |
| 140 | raise _TemplateBreak() |
| 141 | elif name == 'for': |
| 142 | vars, expr, content = code[2], code[3], code[4] |
| 143 | expr = self._eval(expr, ns, pos) |
| 144 | self._interpret_for(vars, expr, content, ns, out) |
| 145 | elif name == 'cond': |
| 146 | parts = code[2:] |
| 147 | self._interpret_if(parts, ns, out) |
| 148 | elif name == 'expr': |
| 149 | parts = code[2].split('|') |
| 150 | base = self._eval(parts[0], ns, pos) |
| 151 | for part in parts[1:]: |
| 152 | func = self._eval(part, ns, pos) |
| 153 | base = func(base) |
| 154 | out.append(self._repr(base, pos)) |
| 155 | elif name == 'default': |
| 156 | var, expr = code[2], code[3] |
| 157 | if var not in ns: |
| 158 | result = self._eval(expr, ns, pos) |
| 159 | ns[var] = result |
| 160 | elif name == 'comment': |
| 161 | return |
| 162 | else: |
| 163 | assert 0, "Unknown code: %r" % name |
| 164 | |
| 165 | def _interpret_for(self, vars, expr, content, ns, out): |
| 166 | __traceback_hide__ = True |
| 167 | for item in expr: |
| 168 | if len(vars) == 1: |
| 169 | ns[vars[0]] = item |
| 170 | else: |
| 171 | if len(vars) != len(item): |
| 172 | raise ValueError( |
| 173 | 'Need %i items to unpack (got %i items)' |
| 174 | % (len(vars), len(item))) |
| 175 | for name, value in zip(vars, item): |
| 176 | ns[name] = value |
| 177 | try: |
| 178 | self._interpret_codes(content, ns, out) |
| 179 | except _TemplateContinue: |
| 180 | continue |
| 181 | except _TemplateBreak: |
| 182 | break |
| 183 | |
| 184 | def _interpret_if(self, parts, ns, out): |
| 185 | __traceback_hide__ = True |
| 186 | # @@: if/else/else gets through |
| 187 | for part in parts: |
| 188 | assert not isinstance(part, six.string_types) |
| 189 | name, pos = part[0], part[1] |
| 190 | if name == 'else': |
| 191 | result = True |
| 192 | else: |
| 193 | result = self._eval(part[2], ns, pos) |
| 194 | if result: |
| 195 | self._interpret_codes(part[3], ns, out) |
| 196 | break |
| 197 | |
| 198 | def _eval(self, code, ns, pos): |
| 199 | __traceback_hide__ = True |
| 200 | try: |
| 201 | value = eval(code, ns) |
| 202 | return value |
| 203 | except: |
| 204 | exc_info = sys.exc_info() |
| 205 | e = exc_info[1] |
| 206 | if getattr(e, 'args'): |
| 207 | arg0 = e.args[0] |
| 208 | else: |
| 209 | arg0 = str(e) |
| 210 | e.args = (self._add_line_info(arg0, pos),) |
| 211 | six.reraise(exc_info[0], e, exc_info[2]) |
| 212 | |
| 213 | def _exec(self, code, ns, pos): |
| 214 | __traceback_hide__ = True |
| 215 | try: |
| 216 | six.exec_(code, ns) |
| 217 | except: |
| 218 | exc_info = sys.exc_info() |
| 219 | e = exc_info[1] |
| 220 | e.args = (self._add_line_info(e.args[0], pos),) |
| 221 | six.reraise(exc_info[0], e, exc_info[2]) |
| 222 | |
| 223 | def _repr(self, value, pos): |
| 224 | __traceback_hide__ = True |
| 225 | try: |
| 226 | if value is None: |
| 227 | return '' |
| 228 | if self._unicode: |
| 229 | try: |
| 230 | value = six.text_type(value) |
| 231 | except UnicodeDecodeError: |
| 232 | value = str(value) |
| 233 | else: |
| 234 | value = str(value) |
| 235 | except: |
| 236 | exc_info = sys.exc_info() |
| 237 | e = exc_info[1] |
| 238 | e.args = (self._add_line_info(e.args[0], pos),) |
| 239 | six.reraise(exc_info[0], e, exc_info[2]) |
| 240 | else: |
| 241 | if self._unicode and isinstance(value, six.binary_type): |
| 242 | if not self.decode_encoding: |
| 243 | raise UnicodeDecodeError( |
| 244 | 'Cannot decode str value %r into unicode ' |
| 245 | '(no default_encoding provided)' % value) |
| 246 | value = value.decode(self.default_encoding) |
| 247 | elif not self._unicode and isinstance(value, six.text_type): |
| 248 | if not self.decode_encoding: |
| 249 | raise UnicodeEncodeError( |
| 250 | 'Cannot encode unicode value %r into str ' |
| 251 | '(no default_encoding provided)' % value) |
| 252 | value = value.encode(self.default_encoding) |
| 253 | return value |
| 254 | |
| 255 | |
| 256 | def _add_line_info(self, msg, pos): |
| 257 | msg = "%s at line %s column %s" % ( |
| 258 | msg, pos[0], pos[1]) |
| 259 | if self.name: |
| 260 | msg += " in file %s" % self.name |
| 261 | return msg |
| 262 | |
| 263 | def sub(content, **kw): |
| 264 | name = kw.get('__name') |
| 265 | tmpl = Template(content, name=name) |
| 266 | return tmpl.substitute(kw) |
| 267 | |
| 268 | def paste_script_template_renderer(content, vars, filename=None): |
| 269 | tmpl = Template(content, name=filename) |
| 270 | return tmpl.substitute(vars) |
| 271 | |
| 272 | class bunch(dict): |
| 273 | |
| 274 | def __init__(self, **kw): |
| 275 | for name, value in kw.items(): |
| 276 | setattr(self, name, value) |
| 277 | |
| 278 | def __setattr__(self, name, value): |
| 279 | self[name] = value |
| 280 | |
| 281 | def __getattr__(self, name): |
| 282 | try: |
| 283 | return self[name] |
| 284 | except KeyError: |
| 285 | raise AttributeError(name) |
| 286 | |
| 287 | def __getitem__(self, key): |
| 288 | if 'default' in self: |
| 289 | try: |
| 290 | return dict.__getitem__(self, key) |
| 291 | except KeyError: |
| 292 | return dict.__getitem__(self, 'default') |
| 293 | else: |
| 294 | return dict.__getitem__(self, key) |
| 295 | |
| 296 | def __repr__(self): |
| 297 | items = [ |
| 298 | (k, v) for k, v in self.items()] |
| 299 | items.sort() |
| 300 | return '<%s %s>' % ( |
| 301 | self.__class__.__name__, |
| 302 | ' '.join(['%s=%r' % (k, v) for k, v in items])) |
| 303 | |
| 304 | ############################################################ |
| 305 | ## HTML Templating |
| 306 | ############################################################ |
| 307 | |
| 308 | class html(object): |
| 309 | def __init__(self, value): |
| 310 | self.value = value |
| 311 | def __str__(self): |
| 312 | return self.value |
| 313 | def __repr__(self): |
| 314 | return '<%s %r>' % ( |
| 315 | self.__class__.__name__, self.value) |
| 316 | |
| 317 | def html_quote(value): |
| 318 | if value is None: |
| 319 | return '' |
| 320 | if not isinstance(value, six.string_types): |
| 321 | if hasattr(value, '__unicode__'): |
| 322 | value = unicode(value) |
| 323 | else: |
| 324 | value = str(value) |
| 325 | value = cgi.escape(value, 1) |
| 326 | if isinstance(value, unicode): |
| 327 | value = value.encode('ascii', 'xmlcharrefreplace') |
| 328 | return value |
| 329 | |
| 330 | def url(v): |
| 331 | if not isinstance(v, six.string_types): |
| 332 | if hasattr(v, '__unicode__'): |
| 333 | v = unicode(v) |
| 334 | else: |
| 335 | v = str(v) |
| 336 | if isinstance(v, unicode): |
| 337 | v = v.encode('utf8') |
| 338 | return quote(v) |
| 339 | |
| 340 | def attr(**kw): |
| 341 | kw = kw.items() |
| 342 | kw.sort() |
| 343 | parts = [] |
| 344 | for name, value in kw: |
| 345 | if value is None: |
| 346 | continue |
| 347 | if name.endswith('_'): |
| 348 | name = name[:-1] |
| 349 | parts.append('%s="%s"' % (html_quote(name), html_quote(value))) |
| 350 | return html(' '.join(parts)) |
| 351 | |
| 352 | class HTMLTemplate(Template): |
| 353 | |
| 354 | default_namespace = Template.default_namespace.copy() |
| 355 | default_namespace.update(dict( |
| 356 | html=html, |
| 357 | attr=attr, |
| 358 | url=url, |
| 359 | )) |
| 360 | |
| 361 | def _repr(self, value, pos): |
| 362 | plain = Template._repr(self, value, pos) |
| 363 | if isinstance(value, html): |
| 364 | return plain |
| 365 | else: |
| 366 | return html_quote(plain) |
| 367 | |
| 368 | def sub_html(content, **kw): |
| 369 | name = kw.get('__name') |
| 370 | tmpl = HTMLTemplate(content, name=name) |
| 371 | return tmpl.substitute(kw) |
| 372 | |
| 373 | |
| 374 | ############################################################ |
| 375 | ## Lexing and Parsing |
| 376 | ############################################################ |
| 377 | |
| 378 | def lex(s, name=None, trim_whitespace=True): |
| 379 | """ |
| 380 | Lex a string into chunks: |
| 381 | |
| 382 | >>> lex('hey') |
| 383 | ['hey'] |
| 384 | >>> lex('hey {{you}}') |
| 385 | ['hey ', ('you', (1, 7))] |
| 386 | >>> lex('hey {{') |
| 387 | Traceback (most recent call last): |
| 388 | ... |
| 389 | TemplateError: No }} to finish last expression at line 1 column 7 |
| 390 | >>> lex('hey }}') |
| 391 | Traceback (most recent call last): |
| 392 | ... |
| 393 | TemplateError: }} outside expression at line 1 column 7 |
| 394 | >>> lex('hey {{ {{') |
| 395 | Traceback (most recent call last): |
| 396 | ... |
| 397 | TemplateError: {{ inside expression at line 1 column 10 |
| 398 | |
| 399 | """ |
| 400 | in_expr = False |
| 401 | chunks = [] |
| 402 | last = 0 |
| 403 | last_pos = (1, 1) |
| 404 | for match in token_re.finditer(s): |
| 405 | expr = match.group(0) |
| 406 | pos = find_position(s, match.end()) |
| 407 | if expr == '{{' and in_expr: |
| 408 | raise TemplateError('{{ inside expression', position=pos, |
| 409 | name=name) |
| 410 | elif expr == '}}' and not in_expr: |
| 411 | raise TemplateError('}} outside expression', position=pos, |
| 412 | name=name) |
| 413 | if expr == '{{': |
| 414 | part = s[last:match.start()] |
| 415 | if part: |
| 416 | chunks.append(part) |
| 417 | in_expr = True |
| 418 | else: |
| 419 | chunks.append((s[last:match.start()], last_pos)) |
| 420 | in_expr = False |
| 421 | last = match.end() |
| 422 | last_pos = pos |
| 423 | if in_expr: |
| 424 | raise TemplateError('No }} to finish last expression', |
| 425 | name=name, position=last_pos) |
| 426 | part = s[last:] |
| 427 | if part: |
| 428 | chunks.append(part) |
| 429 | if trim_whitespace: |
| 430 | chunks = trim_lex(chunks) |
| 431 | return chunks |
| 432 | |
| 433 | statement_re = re.compile(r'^(?:if |elif |else |for |py:)') |
| 434 | single_statements = ['endif', 'endfor', 'continue', 'break'] |
| 435 | trail_whitespace_re = re.compile(r'\n[\t ]*$') |
| 436 | lead_whitespace_re = re.compile(r'^[\t ]*\n') |
| 437 | |
| 438 | def trim_lex(tokens): |
| 439 | r""" |
| 440 | Takes a lexed set of tokens, and removes whitespace when there is |
| 441 | a directive on a line by itself: |
| 442 | |
| 443 | >>> tokens = lex('{{if x}}\nx\n{{endif}}\ny', trim_whitespace=False) |
| 444 | >>> tokens |
| 445 | [('if x', (1, 3)), '\nx\n', ('endif', (3, 3)), '\ny'] |
| 446 | >>> trim_lex(tokens) |
| 447 | [('if x', (1, 3)), 'x\n', ('endif', (3, 3)), 'y'] |
| 448 | """ |
| 449 | for i in range(len(tokens)): |
| 450 | current = tokens[i] |
| 451 | if isinstance(tokens[i], six.string_types): |
| 452 | # we don't trim this |
| 453 | continue |
| 454 | item = current[0] |
| 455 | if not statement_re.search(item) and item not in single_statements: |
| 456 | continue |
| 457 | if not i: |
| 458 | prev = '' |
| 459 | else: |
| 460 | prev = tokens[i-1] |
| 461 | if i+1 >= len(tokens): |
| 462 | next = '' |
| 463 | else: |
| 464 | next = tokens[i+1] |
| 465 | if (not isinstance(next, six.string_types) |
| 466 | or not isinstance(prev, six.string_types)): |
| 467 | continue |
| 468 | if ((not prev or trail_whitespace_re.search(prev)) |
| 469 | and (not next or lead_whitespace_re.search(next))): |
| 470 | if prev: |
| 471 | m = trail_whitespace_re.search(prev) |
| 472 | # +1 to leave the leading \n on: |
| 473 | prev = prev[:m.start()+1] |
| 474 | tokens[i-1] = prev |
| 475 | if next: |
| 476 | m = lead_whitespace_re.search(next) |
| 477 | next = next[m.end():] |
| 478 | tokens[i+1] = next |
| 479 | return tokens |
| 480 | |
| 481 | |
| 482 | def find_position(string, index): |
| 483 | """Given a string and index, return (line, column)""" |
| 484 | leading = string[:index].splitlines() |
| 485 | return (len(leading), len(leading[-1])+1) |
| 486 | |
| 487 | def parse(s, name=None): |
| 488 | r""" |
| 489 | Parses a string into a kind of AST |
| 490 | |
| 491 | >>> parse('{{x}}') |
| 492 | [('expr', (1, 3), 'x')] |
| 493 | >>> parse('foo') |
| 494 | ['foo'] |
| 495 | >>> parse('{{if x}}test{{endif}}') |
| 496 | [('cond', (1, 3), ('if', (1, 3), 'x', ['test']))] |
| 497 | >>> parse('series->{{for x in y}}x={{x}}{{endfor}}') |
| 498 | ['series->', ('for', (1, 11), ('x',), 'y', ['x=', ('expr', (1, 27), 'x')])] |
| 499 | >>> parse('{{for x, y in z:}}{{continue}}{{endfor}}') |
| 500 | [('for', (1, 3), ('x', 'y'), 'z', [('continue', (1, 21))])] |
| 501 | >>> parse('{{py:x=1}}') |
| 502 | [('py', (1, 3), 'x=1')] |
| 503 | >>> parse('{{if x}}a{{elif y}}b{{else}}c{{endif}}') |
| 504 | [('cond', (1, 3), ('if', (1, 3), 'x', ['a']), ('elif', (1, 12), 'y', ['b']), ('else', (1, 23), None, ['c']))] |
| 505 | |
| 506 | Some exceptions:: |
| 507 | |
| 508 | >>> parse('{{continue}}') |
| 509 | Traceback (most recent call last): |
| 510 | ... |
| 511 | TemplateError: continue outside of for loop at line 1 column 3 |
| 512 | >>> parse('{{if x}}foo') |
| 513 | Traceback (most recent call last): |
| 514 | ... |
| 515 | TemplateError: No {{endif}} at line 1 column 3 |
| 516 | >>> parse('{{else}}') |
| 517 | Traceback (most recent call last): |
| 518 | ... |
| 519 | TemplateError: else outside of an if block at line 1 column 3 |
| 520 | >>> parse('{{if x}}{{for x in y}}{{endif}}{{endfor}}') |
| 521 | Traceback (most recent call last): |
| 522 | ... |
| 523 | TemplateError: Unexpected endif at line 1 column 25 |
| 524 | >>> parse('{{if}}{{endif}}') |
| 525 | Traceback (most recent call last): |
| 526 | ... |
| 527 | TemplateError: if with no expression at line 1 column 3 |
| 528 | >>> parse('{{for x y}}{{endfor}}') |
| 529 | Traceback (most recent call last): |
| 530 | ... |
| 531 | TemplateError: Bad for (no "in") in 'x y' at line 1 column 3 |
| 532 | >>> parse('{{py:x=1\ny=2}}') |
| 533 | Traceback (most recent call last): |
| 534 | ... |
| 535 | TemplateError: Multi-line py blocks must start with a newline at line 1 column 3 |
| 536 | """ |
| 537 | tokens = lex(s, name=name) |
| 538 | result = [] |
| 539 | while tokens: |
| 540 | next, tokens = parse_expr(tokens, name) |
| 541 | result.append(next) |
| 542 | return result |
| 543 | |
| 544 | def parse_expr(tokens, name, context=()): |
| 545 | if isinstance(tokens[0], six.string_types): |
| 546 | return tokens[0], tokens[1:] |
| 547 | expr, pos = tokens[0] |
| 548 | expr = expr.strip() |
| 549 | if expr.startswith('py:'): |
| 550 | expr = expr[3:].lstrip(' \t') |
| 551 | if expr.startswith('\n'): |
| 552 | expr = expr[1:] |
| 553 | else: |
| 554 | if '\n' in expr: |
| 555 | raise TemplateError( |
| 556 | 'Multi-line py blocks must start with a newline', |
| 557 | position=pos, name=name) |
| 558 | return ('py', pos, expr), tokens[1:] |
| 559 | elif expr in ('continue', 'break'): |
| 560 | if 'for' not in context: |
| 561 | raise TemplateError( |
| 562 | 'continue outside of for loop', |
| 563 | position=pos, name=name) |
| 564 | return (expr, pos), tokens[1:] |
| 565 | elif expr.startswith('if '): |
| 566 | return parse_cond(tokens, name, context) |
| 567 | elif (expr.startswith('elif ') |
| 568 | or expr == 'else'): |
| 569 | raise TemplateError( |
| 570 | '%s outside of an if block' % expr.split()[0], |
| 571 | position=pos, name=name) |
| 572 | elif expr in ('if', 'elif', 'for'): |
| 573 | raise TemplateError( |
| 574 | '%s with no expression' % expr, |
| 575 | position=pos, name=name) |
| 576 | elif expr in ('endif', 'endfor'): |
| 577 | raise TemplateError( |
| 578 | 'Unexpected %s' % expr, |
| 579 | position=pos, name=name) |
| 580 | elif expr.startswith('for '): |
| 581 | return parse_for(tokens, name, context) |
| 582 | elif expr.startswith('default '): |
| 583 | return parse_default(tokens, name, context) |
| 584 | elif expr.startswith('#'): |
| 585 | return ('comment', pos, tokens[0][0]), tokens[1:] |
| 586 | return ('expr', pos, tokens[0][0]), tokens[1:] |
| 587 | |
| 588 | def parse_cond(tokens, name, context): |
| 589 | start = tokens[0][1] |
| 590 | pieces = [] |
| 591 | context = context + ('if',) |
| 592 | while 1: |
| 593 | if not tokens: |
| 594 | raise TemplateError( |
| 595 | 'Missing {{endif}}', |
| 596 | position=start, name=name) |
| 597 | if (isinstance(tokens[0], tuple) |
| 598 | and tokens[0][0] == 'endif'): |
| 599 | return ('cond', start) + tuple(pieces), tokens[1:] |
| 600 | next, tokens = parse_one_cond(tokens, name, context) |
| 601 | pieces.append(next) |
| 602 | |
| 603 | def parse_one_cond(tokens, name, context): |
| 604 | (first, pos), tokens = tokens[0], tokens[1:] |
| 605 | content = [] |
| 606 | if first.endswith(':'): |
| 607 | first = first[:-1] |
| 608 | if first.startswith('if '): |
| 609 | part = ('if', pos, first[3:].lstrip(), content) |
| 610 | elif first.startswith('elif '): |
| 611 | part = ('elif', pos, first[5:].lstrip(), content) |
| 612 | elif first == 'else': |
| 613 | part = ('else', pos, None, content) |
| 614 | else: |
| 615 | assert 0, "Unexpected token %r at %s" % (first, pos) |
| 616 | while 1: |
| 617 | if not tokens: |
| 618 | raise TemplateError( |
| 619 | 'No {{endif}}', |
| 620 | position=pos, name=name) |
| 621 | if (isinstance(tokens[0], tuple) |
| 622 | and (tokens[0][0] == 'endif' |
| 623 | or tokens[0][0].startswith('elif ') |
| 624 | or tokens[0][0] == 'else')): |
| 625 | return part, tokens |
| 626 | next, tokens = parse_expr(tokens, name, context) |
| 627 | content.append(next) |
| 628 | |
| 629 | def parse_for(tokens, name, context): |
| 630 | first, pos = tokens[0] |
| 631 | tokens = tokens[1:] |
| 632 | context = ('for',) + context |
| 633 | content = [] |
| 634 | assert first.startswith('for ') |
| 635 | if first.endswith(':'): |
| 636 | first = first[:-1] |
| 637 | first = first[3:].strip() |
| 638 | match = in_re.search(first) |
| 639 | if not match: |
| 640 | raise TemplateError( |
| 641 | 'Bad for (no "in") in %r' % first, |
| 642 | position=pos, name=name) |
| 643 | vars = first[:match.start()] |
| 644 | if '(' in vars: |
| 645 | raise TemplateError( |
| 646 | 'You cannot have () in the variable section of a for loop (%r)' |
| 647 | % vars, position=pos, name=name) |
| 648 | vars = tuple([ |
| 649 | v.strip() for v in first[:match.start()].split(',') |
| 650 | if v.strip()]) |
| 651 | expr = first[match.end():] |
| 652 | while 1: |
| 653 | if not tokens: |
| 654 | raise TemplateError( |
| 655 | 'No {{endfor}}', |
| 656 | position=pos, name=name) |
| 657 | if (isinstance(tokens[0], tuple) |
| 658 | and tokens[0][0] == 'endfor'): |
| 659 | return ('for', pos, vars, expr, content), tokens[1:] |
| 660 | next, tokens = parse_expr(tokens, name, context) |
| 661 | content.append(next) |
| 662 | |
| 663 | def parse_default(tokens, name, context): |
| 664 | first, pos = tokens[0] |
| 665 | assert first.startswith('default ') |
| 666 | first = first.split(None, 1)[1] |
| 667 | parts = first.split('=', 1) |
| 668 | if len(parts) == 1: |
| 669 | raise TemplateError( |
| 670 | "Expression must be {{default var=value}}; no = found in %r" % first, |
| 671 | position=pos, name=name) |
| 672 | var = parts[0].strip() |
| 673 | if ',' in var: |
| 674 | raise TemplateError( |
| 675 | "{{default x, y = ...}} is not supported", |
| 676 | position=pos, name=name) |
| 677 | if not var_re.search(var): |
| 678 | raise TemplateError( |
| 679 | "Not a valid variable name for {{default}}: %r" |
| 680 | % var, position=pos, name=name) |
| 681 | expr = parts[1].strip() |
| 682 | return ('default', pos, var, expr), tokens[1:] |
| 683 | |
| 684 | _fill_command_usage = """\ |
| 685 | %prog [OPTIONS] TEMPLATE arg=value |
| 686 | |
| 687 | Use py:arg=value to set a Python value; otherwise all values are |
| 688 | strings. |
| 689 | """ |
| 690 | |
| 691 | def fill_command(args=None): |
| 692 | import sys, optparse, pkg_resources, os |
| 693 | if args is None: |
| 694 | args = sys.argv[1:] |
| 695 | dist = pkg_resources.get_distribution('Paste') |
| 696 | parser = optparse.OptionParser( |
| 697 | version=str(dist), |
| 698 | usage=_fill_command_usage) |
| 699 | parser.add_option( |
| 700 | '-o', '--output', |
| 701 | dest='output', |
| 702 | metavar="FILENAME", |
| 703 | help="File to write output to (default stdout)") |
| 704 | parser.add_option( |
| 705 | '--html', |
| 706 | dest='use_html', |
| 707 | action='store_true', |
| 708 | help="Use HTML style filling (including automatic HTML quoting)") |
| 709 | parser.add_option( |
| 710 | '--env', |
| 711 | dest='use_env', |
| 712 | action='store_true', |
| 713 | help="Put the environment in as top-level variables") |
| 714 | options, args = parser.parse_args(args) |
| 715 | if len(args) < 1: |
| 716 | print('You must give a template filename') |
| 717 | print(dir(parser)) |
| 718 | assert 0 |
| 719 | template_name = args[0] |
| 720 | args = args[1:] |
| 721 | vars = {} |
| 722 | if options.use_env: |
| 723 | vars.update(os.environ) |
| 724 | for value in args: |
| 725 | if '=' not in value: |
| 726 | print('Bad argument: %r' % value) |
| 727 | sys.exit(2) |
| 728 | name, value = value.split('=', 1) |
| 729 | if name.startswith('py:'): |
| 730 | name = name[:3] |
| 731 | value = eval(value) |
| 732 | vars[name] = value |
| 733 | if template_name == '-': |
| 734 | template_content = sys.stdin.read() |
| 735 | template_name = '<stdin>' |
| 736 | else: |
| 737 | f = open(template_name, 'rb') |
| 738 | template_content = f.read() |
| 739 | f.close() |
| 740 | if options.use_html: |
| 741 | TemplateClass = HTMLTemplate |
| 742 | else: |
| 743 | TemplateClass = Template |
| 744 | template = TemplateClass(template_content, name=template_name) |
| 745 | result = template.substitute(vars) |
| 746 | if options.output: |
| 747 | f = open(options.output, 'wb') |
| 748 | f.write(result) |
| 749 | f.close() |
| 750 | else: |
| 751 | sys.stdout.write(result) |
| 752 | |
| 753 | if __name__ == '__main__': |
| 754 | from paste.util.template import fill_command |
| 755 | fill_command() |
| 756 | |
| 757 | |