cmlenz@1: # -*- coding: utf-8 -*- cmlenz@1: # cmlenz@66: # Copyright (C) 2006 Edgewall Software cmlenz@1: # All rights reserved. cmlenz@1: # cmlenz@1: # This software is licensed as described in the file COPYING, which cmlenz@1: # you should have received as part of this distribution. The terms cmlenz@66: # are also available at http://markup.edgewall.org/wiki/License. cmlenz@1: # cmlenz@1: # This software consists of voluntary contributions made by many cmlenz@1: # individuals. For the exact contribution history, see the revision cmlenz@66: # history and logs, available at http://markup.edgewall.org/log/. cmlenz@1: cmlenz@82: """Implementation of the template engine.""" cmlenz@1: cmlenz@70: try: cmlenz@70: from collections import deque cmlenz@70: except ImportError: cmlenz@70: class deque(list): cmlenz@70: def appendleft(self, x): self.insert(0, x) cmlenz@70: def popleft(self): return self.pop(0) cmlenz@1: import compiler cmlenz@1: import os cmlenz@1: import re cmlenz@1: from StringIO import StringIO cmlenz@1: cmlenz@145: from markup.core import Attributes, Namespace, Stream, StreamEventKind, _ensure cmlenz@145: from markup.core import START, END, START_NS, END_NS, TEXT, COMMENT cmlenz@1: from markup.eval import Expression cmlenz@69: from markup.input import XMLParser cmlenz@14: from markup.path import Path cmlenz@1: cmlenz@150: __all__ = ['BadDirectiveError', 'TemplateError', 'TemplateSyntaxError', cmlenz@150: 'TemplateNotFound', 'Template', 'TemplateLoader'] cmlenz@1: cmlenz@1: cmlenz@1: class TemplateError(Exception): cmlenz@1: """Base exception class for errors related to template processing.""" cmlenz@1: cmlenz@1: cmlenz@1: class TemplateSyntaxError(TemplateError): cmlenz@1: """Exception raised when an expression in a template causes a Python syntax cmlenz@1: error.""" cmlenz@1: cmlenz@1: def __init__(self, message, filename='', lineno=-1, offset=-1): cmlenz@1: if isinstance(message, SyntaxError) and message.lineno is not None: cmlenz@1: message = str(message).replace(' (line %d)' % message.lineno, '') cmlenz@80: message = '%s (%s, line %d)' % (message, filename, lineno) cmlenz@1: TemplateError.__init__(self, message) cmlenz@1: self.filename = filename cmlenz@1: self.lineno = lineno cmlenz@1: self.offset = offset cmlenz@1: cmlenz@1: cmlenz@1: class BadDirectiveError(TemplateSyntaxError): cmlenz@1: """Exception raised when an unknown directive is encountered when parsing cmlenz@1: a template. cmlenz@1: cmlenz@1: An unknown directive is any attribute using the namespace for directives, cmlenz@1: with a local name that doesn't match any registered directive. cmlenz@1: """ cmlenz@1: cmlenz@1: def __init__(self, name, filename='', lineno=-1): cmlenz@80: msg = 'bad directive "%s" (%s, line %d)' % (name.localname, filename, cmlenz@80: lineno) cmlenz@80: TemplateSyntaxError.__init__(self, msg, filename, lineno) cmlenz@1: cmlenz@1: cmlenz@1: class TemplateNotFound(TemplateError): cmlenz@1: """Exception raised when a specific template file could not be found.""" cmlenz@1: cmlenz@1: def __init__(self, name, search_path): cmlenz@1: TemplateError.__init__(self, 'Template "%s" not found' % name) cmlenz@1: self.search_path = search_path cmlenz@1: cmlenz@1: cmlenz@1: class Context(object): cmlenz@95: """Container for template input data. cmlenz@1: cmlenz@95: A context provides a stack of scopes (represented by dictionaries). cmlenz@95: cmlenz@95: Template directives such as loops can push a new scope on the stack with cmlenz@95: data that should only be available inside the loop. When the loop cmlenz@95: terminates, that scope can get popped off the stack again. cmlenz@1: cmlenz@1: >>> ctxt = Context(one='foo', other=1) cmlenz@1: >>> ctxt.get('one') cmlenz@1: 'foo' cmlenz@1: >>> ctxt.get('other') cmlenz@1: 1 cmlenz@95: >>> ctxt.push(dict(one='frost')) cmlenz@1: >>> ctxt.get('one') cmlenz@1: 'frost' cmlenz@1: >>> ctxt.get('other') cmlenz@1: 1 cmlenz@1: >>> ctxt.pop() cmlenz@95: {'one': 'frost'} cmlenz@1: >>> ctxt.get('one') cmlenz@1: 'foo' cmlenz@1: """ cmlenz@1: cmlenz@1: def __init__(self, **data): cmlenz@70: self.frames = deque([data]) cmlenz@95: self.pop = self.frames.popleft cmlenz@95: self.push = self.frames.appendleft cmlenz@157: self._match_templates = [] cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@70: return repr(self.frames) cmlenz@1: cmlenz@1: def __setitem__(self, key, value): cmlenz@95: """Set a variable in the current scope.""" cmlenz@70: self.frames[0][key] = value cmlenz@1: cmlenz@1: def get(self, key): cmlenz@95: """Get a variable's value, starting at the current scope and going cmlenz@95: upward. cmlenz@29: """ cmlenz@70: for frame in self.frames: cmlenz@1: if key in frame: cmlenz@1: return frame[key] cmlenz@70: __getitem__ = get cmlenz@1: cmlenz@95: def push(self, data): cmlenz@95: """Push a new scope on the stack.""" cmlenz@1: cmlenz@1: def pop(self): cmlenz@95: """Pop the top-most scope from the stack.""" cmlenz@1: cmlenz@1: cmlenz@1: class Directive(object): cmlenz@1: """Abstract base class for template directives. cmlenz@1: cmlenz@54: A directive is basically a callable that takes three positional arguments: cmlenz@54: `ctxt` is the template data context, `stream` is an iterable over the cmlenz@54: events that the directive applies to, and `directives` is is a list of cmlenz@54: other directives on the same stream that need to be applied. cmlenz@1: cmlenz@1: Directives can be "anonymous" or "registered". Registered directives can be cmlenz@1: applied by the template author using an XML attribute with the cmlenz@1: corresponding name in the template. Such directives should be subclasses of cmlenz@31: this base class that can be instantiated with the value of the directive cmlenz@31: attribute as parameter. cmlenz@1: cmlenz@1: Anonymous directives are simply functions conforming to the protocol cmlenz@1: described above, and can only be applied programmatically (for example by cmlenz@1: template filters). cmlenz@1: """ cmlenz@1: __slots__ = ['expr'] cmlenz@1: cmlenz@81: def __init__(self, value, filename=None, lineno=-1, offset=-1): cmlenz@81: try: cmlenz@81: self.expr = value and Expression(value, filename, lineno) or None cmlenz@81: except SyntaxError, err: cmlenz@166: err.msg += ' in expression "%s" of "%s" directive' % (value, cmlenz@172: self.tagname) cmlenz@81: raise TemplateSyntaxError(err, filename, lineno, cmlenz@81: offset + (err.offset or 0)) cmlenz@1: cmlenz@53: def __call__(self, stream, ctxt, directives): cmlenz@1: raise NotImplementedError cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@1: expr = '' cmlenz@1: if self.expr is not None: cmlenz@1: expr = ' "%s"' % self.expr.source cmlenz@1: return '<%s%s>' % (self.__class__.__name__, expr) cmlenz@1: cmlenz@172: def tagname(self): cmlenz@172: """Return the local tag name of the directive as it is used in cmlenz@172: templates. cmlenz@172: """ cmlenz@166: return self.__class__.__name__.lower().replace('directive', '') cmlenz@172: tagname = property(tagname) cmlenz@166: cmlenz@78: cmlenz@78: def _apply_directives(stream, ctxt, directives): cmlenz@161: """Apply the given directives to the stream.""" cmlenz@78: if directives: cmlenz@78: stream = directives[0](iter(stream), ctxt, directives[1:]) cmlenz@78: return stream cmlenz@53: cmlenz@1: cmlenz@1: class AttrsDirective(Directive): cmlenz@1: """Implementation of the `py:attrs` template directive. cmlenz@1: cmlenz@1: The value of the `py:attrs` attribute should be a dictionary. The keys and cmlenz@1: values of that dictionary will be added as attributes to the element: cmlenz@1: cmlenz@61: >>> tmpl = Template('''''') cmlenz@149: >>> print tmpl.generate(foo={'class': 'collapse'}) cmlenz@1: cmlenz@1: cmlenz@1: If the value evaluates to `None` (or any other non-truth value), no cmlenz@1: attributes are added: cmlenz@1: cmlenz@149: >>> print tmpl.generate(foo=None) cmlenz@1: cmlenz@1: """ cmlenz@50: __slots__ = [] cmlenz@50: cmlenz@53: def __call__(self, stream, ctxt, directives): cmlenz@53: def _generate(): cmlenz@53: kind, (tag, attrib), pos = stream.next() cmlenz@53: attrs = self.expr.evaluate(ctxt) cmlenz@53: if attrs: cmlenz@53: attrib = Attributes(attrib[:]) cmlenz@77: if isinstance(attrs, Stream): cmlenz@77: try: cmlenz@77: attrs = iter(attrs).next() cmlenz@77: except StopIteration: cmlenz@77: attrs = [] cmlenz@77: elif not isinstance(attrs, list): # assume it's a dict cmlenz@53: attrs = attrs.items() cmlenz@53: for name, value in attrs: cmlenz@53: if value is None: cmlenz@53: attrib.remove(name) cmlenz@53: else: cmlenz@53: attrib.set(name, unicode(value).strip()) cmlenz@53: yield kind, (tag, attrib), pos cmlenz@53: for event in stream: cmlenz@53: yield event cmlenz@77: cmlenz@78: return _apply_directives(_generate(), ctxt, directives) cmlenz@1: cmlenz@1: cmlenz@1: class ContentDirective(Directive): cmlenz@1: """Implementation of the `py:content` template directive. cmlenz@1: cmlenz@1: This directive replaces the content of the element with the result of cmlenz@1: evaluating the value of the `py:content` attribute: cmlenz@1: cmlenz@61: >>> tmpl = Template('''''') cmlenz@149: >>> print tmpl.generate(bar='Bye') cmlenz@1: cmlenz@1: """ cmlenz@50: __slots__ = [] cmlenz@50: cmlenz@50: def __call__(self, stream, ctxt, directives): cmlenz@53: def _generate(): cmlenz@50: kind, data, pos = stream.next() cmlenz@101: if kind is START: cmlenz@50: yield kind, data, pos # emit start tag cmlenz@69: yield EXPR, self.expr, pos cmlenz@50: previous = stream.next() cmlenz@50: for event in stream: cmlenz@50: previous = event cmlenz@50: if previous is not None: cmlenz@50: yield previous cmlenz@87: cmlenz@78: return _apply_directives(_generate(), ctxt, directives) cmlenz@1: cmlenz@1: cmlenz@1: class DefDirective(Directive): cmlenz@1: """Implementation of the `py:def` template directive. cmlenz@1: cmlenz@1: This directive can be used to create "Named Template Functions", which cmlenz@1: are template snippets that are not actually output during normal cmlenz@1: processing, but rather can be expanded from expressions in other places cmlenz@1: in the template. cmlenz@1: cmlenz@1: A named template function can be used just like a normal Python function cmlenz@1: from template expressions: cmlenz@1: cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ...

cmlenz@1: ... ${greeting}, ${name}! cmlenz@1: ...

cmlenz@90: ... ${echo('Hi', name='you')} cmlenz@1: ...
''') cmlenz@149: >>> print tmpl.generate(bar='Bye') cmlenz@1:
cmlenz@1:

cmlenz@90: Hi, you! cmlenz@1:

cmlenz@1:
cmlenz@1: cmlenz@90: If a function does not require parameters, the parenthesis can be omitted cmlenz@90: both when defining and when calling it: cmlenz@90: cmlenz@61: >>> tmpl = Template('''
cmlenz@90: ...

cmlenz@90: ... Hello, world! cmlenz@1: ...

cmlenz@90: ... ${helloworld} cmlenz@1: ...
''') cmlenz@149: >>> print tmpl.generate(bar='Bye') cmlenz@1:
cmlenz@1:

cmlenz@90: Hello, world! cmlenz@1:

cmlenz@1:
cmlenz@1: """ cmlenz@154: __slots__ = ['name', 'args', 'defaults'] cmlenz@1: cmlenz@65: ATTRIBUTE = 'function' cmlenz@65: cmlenz@81: def __init__(self, args, filename=None, lineno=-1, offset=-1): cmlenz@81: Directive.__init__(self, None, filename, lineno, offset) cmlenz@1: ast = compiler.parse(args, 'eval').node cmlenz@1: self.args = [] cmlenz@1: self.defaults = {} cmlenz@1: if isinstance(ast, compiler.ast.CallFunc): cmlenz@1: self.name = ast.node.name cmlenz@1: for arg in ast.args: cmlenz@1: if isinstance(arg, compiler.ast.Keyword): cmlenz@1: self.args.append(arg.name) cmlenz@165: self.defaults[arg.name] = Expression(arg.expr, filename, cmlenz@165: lineno) cmlenz@1: else: cmlenz@1: self.args.append(arg.name) cmlenz@1: else: cmlenz@1: self.name = ast.name cmlenz@1: cmlenz@50: def __call__(self, stream, ctxt, directives): cmlenz@154: stream = list(stream) cmlenz@1: cmlenz@154: def function(*args, **kwargs): cmlenz@154: scope = {} cmlenz@154: args = list(args) # make mutable cmlenz@154: for name in self.args: cmlenz@154: if args: cmlenz@154: scope[name] = args.pop(0) cmlenz@154: else: cmlenz@165: if name in kwargs: cmlenz@165: val = kwargs.pop(name) cmlenz@165: else: cmlenz@165: val = self.defaults.get(name).evaluate(ctxt) cmlenz@165: scope[name] = val cmlenz@154: ctxt.push(scope) cmlenz@154: for event in _apply_directives(stream, ctxt, directives): cmlenz@154: yield event cmlenz@154: ctxt.pop() cmlenz@154: try: cmlenz@154: function.__name__ = self.name cmlenz@154: except TypeError: cmlenz@154: # Function name can't be set in Python 2.3 cmlenz@154: pass cmlenz@154: cmlenz@154: # Store the function reference in the bottom context frame so that it cmlenz@154: # doesn't get popped off before processing the template has finished cmlenz@154: ctxt.frames[-1][self.name] = function cmlenz@154: cmlenz@154: return [] cmlenz@1: cmlenz@172: def __repr__(self): cmlenz@172: return '<%s "%s">' % (self.__class__.__name__, self.name) cmlenz@172: cmlenz@1: cmlenz@1: class ForDirective(Directive): cmlenz@31: """Implementation of the `py:for` template directive for repeating an cmlenz@31: element based on an iterable in the context data. cmlenz@1: cmlenz@61: >>> tmpl = Template('''''') cmlenz@149: >>> print tmpl.generate(items=[1, 2, 3]) cmlenz@1: cmlenz@1: """ cmlenz@1: __slots__ = ['targets'] cmlenz@1: cmlenz@65: ATTRIBUTE = 'each' cmlenz@65: cmlenz@81: def __init__(self, value, filename=None, lineno=-1, offset=-1): cmlenz@29: targets, value = value.split(' in ', 1) cmlenz@1: self.targets = [str(name.strip()) for name in targets.split(',')] cmlenz@140: Directive.__init__(self, value.strip(), filename, lineno, offset) cmlenz@1: cmlenz@50: def __call__(self, stream, ctxt, directives): cmlenz@53: iterable = self.expr.evaluate(ctxt) cmlenz@101: if iterable is None: cmlenz@101: return cmlenz@101: cmlenz@101: scope = {} cmlenz@101: stream = list(stream) cmlenz@101: targets = self.targets cmlenz@140: single = len(targets) == 1 cmlenz@101: for item in iter(iterable): cmlenz@140: if single: cmlenz@101: scope[targets[0]] = item cmlenz@101: else: cmlenz@101: for idx, name in enumerate(targets): cmlenz@101: scope[name] = item[idx] cmlenz@101: ctxt.push(scope) cmlenz@101: for event in _apply_directives(stream, ctxt, directives): cmlenz@101: yield event cmlenz@101: ctxt.pop() cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@1: return '<%s "%s in %s">' % (self.__class__.__name__, cmlenz@1: ', '.join(self.targets), self.expr.source) cmlenz@1: cmlenz@1: cmlenz@1: class IfDirective(Directive): cmlenz@31: """Implementation of the `py:if` template directive for conditionally cmlenz@31: excluding elements from being output. cmlenz@1: cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ... ${bar} cmlenz@1: ...
''') cmlenz@149: >>> print tmpl.generate(foo=True, bar='Hello') cmlenz@1:
cmlenz@1: Hello cmlenz@1:
cmlenz@1: """ cmlenz@50: __slots__ = [] cmlenz@50: cmlenz@65: ATTRIBUTE = 'test' cmlenz@65: cmlenz@50: def __call__(self, stream, ctxt, directives): cmlenz@1: if self.expr.evaluate(ctxt): cmlenz@78: return _apply_directives(stream, ctxt, directives) cmlenz@1: return [] cmlenz@1: cmlenz@1: cmlenz@1: class MatchDirective(Directive): cmlenz@1: """Implementation of the `py:match` template directive. cmlenz@14: cmlenz@61: >>> tmpl = Template('''
cmlenz@17: ... cmlenz@1: ... Hello ${select('@name')} cmlenz@1: ... cmlenz@1: ... cmlenz@1: ...
''') cmlenz@14: >>> print tmpl.generate() cmlenz@1:
cmlenz@1: cmlenz@1: Hello Dude cmlenz@1: cmlenz@1:
cmlenz@1: """ cmlenz@1: __slots__ = ['path', 'stream'] cmlenz@1: cmlenz@65: ATTRIBUTE = 'path' cmlenz@65: cmlenz@81: def __init__(self, value, filename=None, lineno=-1, offset=-1): cmlenz@81: Directive.__init__(self, None, filename, lineno, offset) cmlenz@139: self.path = Path(value, filename, lineno) cmlenz@1: self.stream = [] cmlenz@1: cmlenz@50: def __call__(self, stream, ctxt, directives): cmlenz@1: self.stream = list(stream) cmlenz@38: ctxt._match_templates.append((self.path.test(ignore_context=True), cmlenz@50: self.path, self.stream, directives)) cmlenz@1: return [] cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@14: return '<%s "%s">' % (self.__class__.__name__, self.path.source) cmlenz@1: cmlenz@1: cmlenz@1: class ReplaceDirective(Directive): cmlenz@1: """Implementation of the `py:replace` template directive. cmlenz@1: cmlenz@31: This directive replaces the element with the result of evaluating the cmlenz@31: value of the `py:replace` attribute: cmlenz@31: cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ... Hello cmlenz@1: ...
''') cmlenz@149: >>> print tmpl.generate(bar='Bye') cmlenz@1:
cmlenz@1: Bye cmlenz@1:
cmlenz@1: cmlenz@1: This directive is equivalent to `py:content` combined with `py:strip`, cmlenz@1: providing a less verbose way to achieve the same effect: cmlenz@1: cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ... Hello cmlenz@1: ...
''') cmlenz@149: >>> print tmpl.generate(bar='Bye') cmlenz@1:
cmlenz@1: Bye cmlenz@1:
cmlenz@1: """ cmlenz@50: __slots__ = [] cmlenz@50: cmlenz@54: def __call__(self, stream, ctxt, directives): cmlenz@1: kind, data, pos = stream.next() cmlenz@69: yield EXPR, self.expr, pos cmlenz@1: cmlenz@1: cmlenz@1: class StripDirective(Directive): cmlenz@1: """Implementation of the `py:strip` template directive. cmlenz@1: cmlenz@1: When the value of the `py:strip` attribute evaluates to `True`, the element cmlenz@1: is stripped from the output cmlenz@1: cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ...
foo
cmlenz@1: ...
''') cmlenz@14: >>> print tmpl.generate() cmlenz@1:
cmlenz@1: foo cmlenz@1:
cmlenz@1: cmlenz@37: Leaving the attribute value empty is equivalent to a truth value. cmlenz@1: cmlenz@1: This directive is particulary interesting for named template functions or cmlenz@1: match templates that do not generate a top-level element: cmlenz@1: cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ...
cmlenz@1: ... ${what} cmlenz@1: ...
cmlenz@1: ... ${echo('foo')} cmlenz@1: ...
''') cmlenz@14: >>> print tmpl.generate() cmlenz@1:
cmlenz@1: foo cmlenz@1:
cmlenz@1: """ cmlenz@50: __slots__ = [] cmlenz@50: cmlenz@54: def __call__(self, stream, ctxt, directives): cmlenz@78: def _generate(): cmlenz@78: if self.expr: cmlenz@78: strip = self.expr.evaluate(ctxt) cmlenz@78: else: cmlenz@78: strip = True cmlenz@78: if strip: cmlenz@78: stream.next() # skip start tag cmlenz@78: previous = stream.next() cmlenz@78: for event in stream: cmlenz@78: yield previous cmlenz@78: previous = event cmlenz@78: else: cmlenz@78: for event in stream: cmlenz@78: yield event cmlenz@78: cmlenz@78: return _apply_directives(_generate(), ctxt, directives) cmlenz@1: cmlenz@1: mgood@44: class ChooseDirective(Directive): mgood@44: """Implementation of the `py:choose` directive for conditionally selecting mgood@44: one of several body elements to display. cmlenz@53: mgood@44: If the `py:choose` expression is empty the expressions of nested `py:when` mgood@44: directives are tested for truth. The first true `py:when` body is output. cmlenz@53: If no `py:when` directive is matched then the fallback directive cmlenz@53: `py:otherwise` will be used. cmlenz@53: cmlenz@61: >>> tmpl = Template('''
mgood@44: ... 0 mgood@44: ... 1 cmlenz@53: ... 2 mgood@44: ...
''') cmlenz@149: >>> print tmpl.generate() mgood@44:
mgood@44: 1 mgood@44:
cmlenz@53: mgood@44: If the `py:choose` directive contains an expression, the nested `py:when` cmlenz@53: directives are tested for equality to the `py:choose` expression: cmlenz@53: cmlenz@61: >>> tmpl = Template('''
mgood@44: ... 1 mgood@44: ... 2 mgood@44: ...
''') cmlenz@149: >>> print tmpl.generate() mgood@44:
mgood@44: 2 mgood@44:
cmlenz@53: mgood@44: Behavior is undefined if a `py:choose` block contains content outside a mgood@44: `py:when` or `py:otherwise` block. Behavior is also undefined if a mgood@44: `py:otherwise` occurs before `py:when` blocks. mgood@44: """ cmlenz@50: __slots__ = ['matched', 'value'] mgood@44: cmlenz@65: ATTRIBUTE = 'test' cmlenz@65: cmlenz@53: def __call__(self, stream, ctxt, directives): mgood@44: if self.expr: mgood@44: self.value = self.expr.evaluate(ctxt) mgood@44: self.matched = False cmlenz@95: ctxt.push(dict(_choose=self)) cmlenz@78: for event in _apply_directives(stream, ctxt, directives): mgood@44: yield event mgood@44: ctxt.pop() mgood@44: mgood@44: mgood@44: class WhenDirective(Directive): mgood@44: """Implementation of the `py:when` directive for nesting in a parent with cmlenz@50: the `py:choose` directive. cmlenz@50: cmlenz@50: See the documentation of `py:choose` for usage. mgood@44: """ cmlenz@65: cmlenz@65: ATTRIBUTE = 'test' cmlenz@65: cmlenz@54: def __call__(self, stream, ctxt, directives): cmlenz@50: choose = ctxt['_choose'] cmlenz@166: if not choose: cmlenz@166: raise TemplateSyntaxError('when directives can only be used inside ' cmlenz@166: 'a choose directive', *stream.next()[2]) mgood@44: if choose.matched: mgood@44: return [] mgood@44: value = self.expr.evaluate(ctxt) mgood@44: try: mgood@44: if value == choose.value: mgood@44: choose.matched = True cmlenz@78: return _apply_directives(stream, ctxt, directives) mgood@44: except AttributeError: mgood@44: if value: mgood@44: choose.matched = True cmlenz@78: return _apply_directives(stream, ctxt, directives) mgood@44: return [] mgood@44: mgood@44: mgood@44: class OtherwiseDirective(Directive): mgood@44: """Implementation of the `py:otherwise` directive for nesting in a parent cmlenz@50: with the `py:choose` directive. cmlenz@50: cmlenz@50: See the documentation of `py:choose` for usage. mgood@44: """ cmlenz@54: def __call__(self, stream, ctxt, directives): cmlenz@50: choose = ctxt['_choose'] cmlenz@166: if not choose: cmlenz@166: raise TemplateSyntaxError('an otherwise directive can only be used ' cmlenz@166: 'inside a choose directive', cmlenz@166: *stream.next()[2]) mgood@44: if choose.matched: mgood@44: return [] mgood@44: choose.matched = True cmlenz@78: return _apply_directives(stream, ctxt, directives) mgood@44: mgood@44: cmlenz@104: class WithDirective(Directive): cmlenz@104: """Implementation of the `py:with` template directive, which allows cmlenz@104: shorthand access to variables and expressions. cmlenz@104: cmlenz@104: >>> tmpl = Template('''
cmlenz@104: ... $x $y $z cmlenz@104: ...
''') cmlenz@149: >>> print tmpl.generate(x=42) cmlenz@104:
cmlenz@104: 42 7 52 cmlenz@104:
cmlenz@104: """ cmlenz@104: __slots__ = ['vars'] cmlenz@104: cmlenz@104: ATTRIBUTE = 'vars' cmlenz@104: cmlenz@104: def __init__(self, value, filename=None, lineno=-1, offset=-1): cmlenz@104: Directive.__init__(self, None, filename, lineno, offset) cmlenz@104: self.vars = [] cmlenz@104: try: cmlenz@104: for stmt in value.split(';'): cmlenz@104: name, value = stmt.split('=', 1) cmlenz@104: self.vars.append((name.strip(), cmlenz@104: Expression(value.strip(), filename, lineno))) cmlenz@104: except SyntaxError, err: cmlenz@104: raise TemplateSyntaxError(err, filename, lineno, cmlenz@104: offset + (err.offset or 0)) cmlenz@104: cmlenz@104: def __call__(self, stream, ctxt, directives): cmlenz@120: ctxt.push(dict([(name, expr.evaluate(ctxt, nocall=True)) cmlenz@104: for name, expr in self.vars])) cmlenz@104: for event in _apply_directives(stream, ctxt, directives): cmlenz@104: yield event cmlenz@104: ctxt.pop() cmlenz@104: cmlenz@104: def __repr__(self): cmlenz@104: return '<%s "%s">' % (self.__class__.__name__, cmlenz@104: '; '.join(['%s = %s' % (name, expr.source) cmlenz@104: for name, expr in self.vars])) cmlenz@104: cmlenz@104: cmlenz@1: class Template(object): cmlenz@1: """Can parse a template and transform it into the corresponding output cmlenz@1: based on context data. cmlenz@1: """ cmlenz@61: NAMESPACE = Namespace('http://markup.edgewall.org/') cmlenz@1: cmlenz@17: EXPR = StreamEventKind('EXPR') # an expression cmlenz@17: SUB = StreamEventKind('SUB') # a "subprogram" cmlenz@10: cmlenz@1: directives = [('def', DefDirective), cmlenz@1: ('match', MatchDirective), cmlenz@120: ('when', WhenDirective), cmlenz@120: ('otherwise', OtherwiseDirective), cmlenz@1: ('for', ForDirective), cmlenz@1: ('if', IfDirective), cmlenz@53: ('choose', ChooseDirective), cmlenz@104: ('with', WithDirective), cmlenz@1: ('replace', ReplaceDirective), cmlenz@1: ('content', ContentDirective), cmlenz@1: ('attrs', AttrsDirective), cmlenz@50: ('strip', StripDirective)] cmlenz@1: _dir_by_name = dict(directives) cmlenz@1: _dir_order = [directive[1] for directive in directives] cmlenz@1: cmlenz@21: def __init__(self, source, basedir=None, filename=None): cmlenz@1: """Initialize a template from either a string or a file-like object.""" cmlenz@1: if isinstance(source, basestring): cmlenz@1: self.source = StringIO(source) cmlenz@1: else: cmlenz@1: self.source = source cmlenz@21: self.basedir = basedir cmlenz@172: self.filename = filename cmlenz@21: if basedir and filename: cmlenz@21: self.filepath = os.path.join(basedir, filename) cmlenz@21: else: cmlenz@172: self.filepath = None cmlenz@1: cmlenz@23: self.filters = [] cmlenz@1: self.parse() cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@21: return '<%s "%s">' % (self.__class__.__name__, self.filename) cmlenz@1: cmlenz@1: def parse(self): cmlenz@1: """Parse the template. cmlenz@1: cmlenz@1: The parsing stage parses the XML template and constructs a list of cmlenz@1: directives that will be executed in the render stage. The input is cmlenz@1: split up into literal output (markup that does not depend on the cmlenz@1: context data) and actual directives (commands or variable cmlenz@1: substitution). cmlenz@1: """ cmlenz@1: stream = [] # list of events of the "compiled" template cmlenz@1: dirmap = {} # temporary mapping of directives to elements cmlenz@1: ns_prefix = {} cmlenz@1: depth = 0 cmlenz@1: cmlenz@21: for kind, data, pos in XMLParser(self.source, filename=self.filename): cmlenz@1: cmlenz@69: if kind is START_NS: cmlenz@1: # Strip out the namespace declaration for template directives cmlenz@1: prefix, uri = data cmlenz@1: if uri == self.NAMESPACE: cmlenz@1: ns_prefix[prefix] = uri cmlenz@1: else: cmlenz@1: stream.append((kind, data, pos)) cmlenz@1: cmlenz@69: elif kind is END_NS: cmlenz@1: if data in ns_prefix: cmlenz@1: del ns_prefix[data] cmlenz@1: else: cmlenz@1: stream.append((kind, data, pos)) cmlenz@1: cmlenz@69: elif kind is START: cmlenz@1: # Record any directive attributes in start tags cmlenz@1: tag, attrib = data cmlenz@1: directives = [] cmlenz@65: strip = False cmlenz@65: cmlenz@65: if tag in self.NAMESPACE: cmlenz@65: cls = self._dir_by_name.get(tag.localname) cmlenz@65: if cls is None: cmlenz@65: raise BadDirectiveError(tag, pos[0], pos[1]) cmlenz@66: value = attrib.get(getattr(cls, 'ATTRIBUTE', None), '') cmlenz@81: directives.append(cls(value, *pos)) cmlenz@65: strip = True cmlenz@65: cmlenz@1: new_attrib = [] cmlenz@1: for name, value in attrib: cmlenz@18: if name in self.NAMESPACE: cmlenz@1: cls = self._dir_by_name.get(name.localname) cmlenz@1: if cls is None: cmlenz@65: raise BadDirectiveError(name, pos[0], pos[1]) cmlenz@81: directives.append(cls(value, *pos)) cmlenz@1: else: cmlenz@75: if value: cmlenz@75: value = list(self._interpolate(value, *pos)) cmlenz@81: if len(value) == 1 and value[0][0] is TEXT: cmlenz@81: value = value[0][1] cmlenz@75: else: cmlenz@75: value = [(TEXT, u'', pos)] cmlenz@1: new_attrib.append((name, value)) cmlenz@65: cmlenz@1: if directives: cmlenz@50: directives.sort(lambda a, b: cmp(self._dir_order.index(a.__class__), cmlenz@50: self._dir_order.index(b.__class__))) cmlenz@65: dirmap[(depth, tag)] = (directives, len(stream), strip) cmlenz@1: cmlenz@1: stream.append((kind, (tag, Attributes(new_attrib)), pos)) cmlenz@1: depth += 1 cmlenz@1: cmlenz@69: elif kind is END: cmlenz@1: depth -= 1 cmlenz@1: stream.append((kind, data, pos)) cmlenz@1: cmlenz@1: # If there have have directive attributes with the corresponding cmlenz@1: # start tag, move the events inbetween into a "subprogram" cmlenz@1: if (depth, data) in dirmap: cmlenz@65: directives, start_offset, strip = dirmap.pop((depth, data)) cmlenz@1: substream = stream[start_offset:] cmlenz@65: if strip: cmlenz@65: substream = substream[1:-1] cmlenz@69: stream[start_offset:] = [(SUB, (directives, substream), cmlenz@69: pos)] cmlenz@1: cmlenz@69: elif kind is TEXT: cmlenz@1: for kind, data, pos in self._interpolate(data, *pos): cmlenz@1: stream.append((kind, data, pos)) cmlenz@1: cmlenz@89: elif kind is COMMENT: cmlenz@89: if not data.lstrip().startswith('!'): cmlenz@89: stream.append((kind, data, pos)) cmlenz@89: cmlenz@1: else: cmlenz@1: stream.append((kind, data, pos)) cmlenz@1: cmlenz@1: self.stream = stream cmlenz@1: cmlenz@1: _FULL_EXPR_RE = re.compile(r'(? 0: cmlenz@73: kind, data, pos = stream.next() cmlenz@73: if kind is START: cmlenz@73: depth += 1 cmlenz@73: elif kind is END: cmlenz@73: depth -= 1 cmlenz@73: content.append((kind, data, pos)) cmlenz@73: test(kind, data, pos) cmlenz@17: cmlenz@23: content = list(self._flatten(content, ctxt)) cmlenz@95: select = lambda path: Stream(content).select(path) cmlenz@95: ctxt.push(dict(select=select)) cmlenz@36: cmlenz@78: template = _apply_directives(template, ctxt, directives) cmlenz@69: for event in self._match(self._eval(template, ctxt), cmlenz@69: ctxt, match_templates[:idx] + cmlenz@69: match_templates[idx + 1:]): cmlenz@35: yield event cmlenz@116: cmlenz@35: ctxt.pop() cmlenz@17: break cmlenz@69: cmlenz@69: else: # no matches cmlenz@17: yield kind, data, pos cmlenz@17: cmlenz@1: cmlenz@69: EXPR = Template.EXPR cmlenz@69: SUB = Template.SUB cmlenz@69: cmlenz@69: cmlenz@1: class TemplateLoader(object): cmlenz@1: """Responsible for loading templates from files on the specified search cmlenz@1: path. cmlenz@1: cmlenz@1: >>> import tempfile cmlenz@1: >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template') cmlenz@1: >>> os.write(fd, '

$var

') cmlenz@1: 11 cmlenz@1: >>> os.close(fd) cmlenz@1: cmlenz@1: The template loader accepts a list of directory paths that are then used cmlenz@1: when searching for template files, in the given order: cmlenz@1: cmlenz@1: >>> loader = TemplateLoader([os.path.dirname(path)]) cmlenz@1: cmlenz@1: The `load()` method first checks the template cache whether the requested cmlenz@1: template has already been loaded. If not, it attempts to locate the cmlenz@1: template file, and returns the corresponding `Template` object: cmlenz@1: cmlenz@1: >>> template = loader.load(os.path.basename(path)) cmlenz@1: >>> isinstance(template, Template) cmlenz@1: True cmlenz@1: cmlenz@1: Template instances are cached: requesting a template with the same name cmlenz@1: results in the same instance being returned: cmlenz@1: cmlenz@1: >>> loader.load(os.path.basename(path)) is template cmlenz@1: True cmlenz@152: cmlenz@152: >>> os.remove(path) cmlenz@1: """ cmlenz@1: def __init__(self, search_path=None, auto_reload=False): cmlenz@1: """Create the template laoder. cmlenz@1: cmlenz@1: @param search_path: a list of absolute path names that should be cmlenz@1: searched for template files cmlenz@1: @param auto_reload: whether to check the last modification time of cmlenz@1: template files, and reload them if they have changed cmlenz@1: """ cmlenz@1: self.search_path = search_path cmlenz@1: if self.search_path is None: cmlenz@1: self.search_path = [] cmlenz@1: self.auto_reload = auto_reload cmlenz@1: self._cache = {} cmlenz@1: self._mtime = {} cmlenz@1: cmlenz@21: def load(self, filename, relative_to=None): cmlenz@1: """Load the template with the given name. cmlenz@1: cmlenz@22: If the `filename` parameter is relative, this method searches the search cmlenz@22: path trying to locate a template matching the given name. If the file cmlenz@22: name is an absolute path, the search path is not bypassed. cmlenz@1: cmlenz@22: If requested template is not found, a `TemplateNotFound` exception is cmlenz@22: raised. Otherwise, a `Template` object is returned that represents the cmlenz@22: parsed template. cmlenz@22: cmlenz@22: Template instances are cached to avoid having to parse the same cmlenz@22: template file more than once. Thus, subsequent calls of this method cmlenz@22: with the same template file name will return the same `Template` cmlenz@22: object (unless the `auto_reload` option is enabled and the file was cmlenz@22: changed since the last parse.) cmlenz@1: cmlenz@21: If the `relative_to` parameter is provided, the `filename` is cmlenz@21: interpreted as being relative to that path. cmlenz@21: cmlenz@1: @param filename: the relative path of the template file to load cmlenz@21: @param relative_to: the filename of the template from which the new cmlenz@21: template is being loaded, or `None` if the template is being loaded cmlenz@21: directly cmlenz@1: """ cmlenz@69: from markup.filters import IncludeFilter cmlenz@69: cmlenz@21: if relative_to: cmlenz@153: filename = os.path.join(os.path.dirname(relative_to), filename) cmlenz@1: filename = os.path.normpath(filename) cmlenz@22: cmlenz@22: # First check the cache to avoid reparsing the same file cmlenz@1: try: cmlenz@1: tmpl = self._cache[filename] cmlenz@1: if not self.auto_reload or \ cmlenz@21: os.path.getmtime(tmpl.filepath) == self._mtime[filename]: cmlenz@1: return tmpl cmlenz@1: except KeyError: cmlenz@1: pass cmlenz@22: cmlenz@22: # Bypass the search path if the filename is absolute cmlenz@22: search_path = self.search_path cmlenz@22: if os.path.isabs(filename): cmlenz@22: search_path = [os.path.dirname(filename)] cmlenz@22: cmlenz@22: for dirname in search_path: cmlenz@1: filepath = os.path.join(dirname, filename) cmlenz@1: try: cmlenz@133: fileobj = open(filepath, 'U') cmlenz@1: try: cmlenz@21: tmpl = Template(fileobj, basedir=dirname, filename=filename) cmlenz@17: tmpl.filters.append(IncludeFilter(self)) cmlenz@1: finally: cmlenz@1: fileobj.close() cmlenz@1: self._cache[filename] = tmpl cmlenz@1: self._mtime[filename] = os.path.getmtime(filepath) cmlenz@1: return tmpl cmlenz@1: except IOError: cmlenz@1: continue cmlenz@1: raise TemplateNotFound(filename, self.search_path)