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 | |
---|