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