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@1: """Template engine that is compatible with Kid (http://kid.lesscode.org) to a cmlenz@1: certain extent. cmlenz@1: cmlenz@1: Differences include: cmlenz@1: * No generation of Python code for a template; the template is "interpreted" cmlenz@1: * No support for processing instructions cmlenz@1: * Expressions are evaluated in a more flexible manner, meaning you can use e.g. cmlenz@1: attribute access notation to access items in a dictionary, etc cmlenz@1: * Use of XInclude and match templates instead of Kid's py:extends/py:layout cmlenz@1: directives cmlenz@1: * Real (thread-safe) search path support cmlenz@1: * No dependency on ElementTree (due to the lack of pos info) cmlenz@1: * The original pos of parse events is kept throughout the processing cmlenz@1: pipeline, so that errors can be tracked back to a specific line/column in cmlenz@1: the template file cmlenz@1: * py:match directives use (basic) XPath expressions to match against input cmlenz@1: nodes, making match templates more powerful while keeping the syntax simple cmlenz@1: cmlenz@1: Todo items: cmlenz@1: * Improved error reporting cmlenz@1: * Support for list comprehensions and generator expressions in expressions cmlenz@1: cmlenz@1: Random thoughts: cmlenz@1: * Is there any need to support py:extends and/or py:layout? cmlenz@1: * Could we generate byte code from expressions? cmlenz@1: """ cmlenz@1: cmlenz@1: import compiler cmlenz@1: import os cmlenz@21: import posixpath cmlenz@1: import re cmlenz@1: from StringIO import StringIO cmlenz@1: cmlenz@18: from markup.core import Attributes, Namespace, Stream, StreamEventKind cmlenz@69: from markup.core import START, END, START_NS, END_NS, TEXT cmlenz@1: from markup.eval import Expression cmlenz@69: from markup.input import XMLParser cmlenz@14: from markup.path import Path cmlenz@1: cmlenz@1: __all__ = ['Context', 'BadDirectiveError', 'TemplateError', cmlenz@1: 'TemplateSyntaxError', 'TemplateNotFound', 'Template', cmlenz@1: '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@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@1: TemplateSyntaxError.__init__(self, 'Bad directive "%s"' % name.localname, cmlenz@1: 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@1: """A container for template input data. cmlenz@1: cmlenz@1: A context provides a stack of scopes. Template directives such as loops can cmlenz@1: push a new scope on the stack with data that should only be available cmlenz@1: inside the loop. When the loop terminates, that scope can get popped off cmlenz@1: 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@1: >>> ctxt.push(one='frost') cmlenz@1: >>> ctxt.get('one') cmlenz@1: 'frost' cmlenz@1: >>> ctxt.get('other') cmlenz@1: 1 cmlenz@1: >>> ctxt.pop() cmlenz@1: >>> ctxt.get('one') cmlenz@1: 'foo' cmlenz@1: """ cmlenz@1: cmlenz@1: def __init__(self, **data): cmlenz@1: self.stack = [data] cmlenz@1: cmlenz@1: def __getitem__(self, key): cmlenz@29: """Get a variable's value, starting at the current context frame and cmlenz@29: going upward. cmlenz@1: """ cmlenz@1: return self.get(key) cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@1: return repr(self.stack) cmlenz@1: cmlenz@1: def __setitem__(self, key, value): cmlenz@1: """Set a variable in the current context.""" cmlenz@1: self.stack[0][key] = value cmlenz@1: cmlenz@1: def get(self, key): cmlenz@29: """Get a variable's value, starting at the current context frame and cmlenz@29: going upward. cmlenz@29: """ cmlenz@1: for frame in self.stack: cmlenz@1: if key in frame: cmlenz@1: return frame[key] cmlenz@1: cmlenz@1: def push(self, **data): cmlenz@29: """Push a new context frame on the stack.""" cmlenz@1: self.stack.insert(0, data) cmlenz@1: cmlenz@1: def pop(self): cmlenz@29: """Pop the top-most context frame from the stack. cmlenz@29: cmlenz@29: If the stack is empty, an `AssertionError` is raised. cmlenz@29: """ cmlenz@1: assert self.stack, 'Pop from empty context stack' cmlenz@1: self.stack.pop(0) 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@29: def __init__(self, value): cmlenz@1: self.expr = value and Expression(value) or None 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@53: def _apply_directives(self, stream, ctxt, directives): cmlenz@53: if directives: cmlenz@53: stream = directives[0](iter(stream), ctxt, directives[1:]) cmlenz@53: 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@1: >>> ctxt = Context(foo={'class': 'collapse'}) cmlenz@61: >>> tmpl = Template('''''') cmlenz@1: >>> print tmpl.generate(ctxt) 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@1: >>> ctxt = Context(foo=None) cmlenz@1: >>> print tmpl.generate(ctxt) 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@53: if 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@53: return self._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@1: >>> ctxt = Context(bar='Bye') cmlenz@61: >>> tmpl = Template('''''') cmlenz@1: >>> print tmpl.generate(ctxt) 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@50: if kind is Stream.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@53: return self._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@1: >>> ctxt = Context(bar='Bye') cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ...

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

cmlenz@1: ... ${echo('hi', name='you')} cmlenz@1: ...
''') cmlenz@1: >>> print tmpl.generate(ctxt) cmlenz@1:
cmlenz@1:

cmlenz@1: hi, you! cmlenz@1:

cmlenz@1:
cmlenz@1: cmlenz@1: >>> ctxt = Context(bar='Bye') cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ...

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

cmlenz@1: ...
cmlenz@1: ...
''') cmlenz@1: >>> print tmpl.generate(ctxt) cmlenz@1:
cmlenz@1:

cmlenz@1: hello, world! cmlenz@1:

cmlenz@1:
cmlenz@1: """ cmlenz@50: __slots__ = ['name', 'args', 'defaults', 'stream', 'directives'] cmlenz@1: cmlenz@65: ATTRIBUTE = 'function' cmlenz@65: cmlenz@29: def __init__(self, args): cmlenz@29: Directive.__init__(self, None) 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@1: self.defaults[arg.name] = arg.expr.value cmlenz@1: else: cmlenz@1: self.args.append(arg.name) cmlenz@1: else: cmlenz@1: self.name = ast.name cmlenz@50: self.stream, self.directives = [], [] cmlenz@1: cmlenz@50: def __call__(self, stream, ctxt, directives): cmlenz@1: self.stream = list(stream) cmlenz@50: self.directives = directives cmlenz@1: ctxt[self.name] = lambda *args, **kwargs: self._exec(ctxt, *args, cmlenz@1: **kwargs) cmlenz@1: return [] cmlenz@1: cmlenz@1: def _exec(self, ctxt, *args, **kwargs): cmlenz@1: scope = {} cmlenz@1: args = list(args) # make mutable cmlenz@1: for name in self.args: cmlenz@1: if args: cmlenz@1: scope[name] = args.pop(0) cmlenz@1: else: cmlenz@1: scope[name] = kwargs.pop(name, self.defaults.get(name)) cmlenz@1: ctxt.push(**scope) cmlenz@69: for event in self._apply_directives(self.stream, ctxt, self.directives): cmlenz@1: yield event cmlenz@1: ctxt.pop() cmlenz@1: 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@1: >>> ctxt = Context(items=[1, 2, 3]) cmlenz@61: >>> tmpl = Template('''''') cmlenz@1: >>> print tmpl.generate(ctxt) cmlenz@1: cmlenz@1: """ cmlenz@1: __slots__ = ['targets'] cmlenz@1: cmlenz@65: ATTRIBUTE = 'each' cmlenz@65: cmlenz@29: def __init__(self, value): cmlenz@29: targets, value = value.split(' in ', 1) cmlenz@1: self.targets = [str(name.strip()) for name in targets.split(',')] cmlenz@29: Directive.__init__(self, value) cmlenz@1: cmlenz@50: def __call__(self, stream, ctxt, directives): cmlenz@53: iterable = self.expr.evaluate(ctxt) cmlenz@1: if iterable is not None: cmlenz@1: stream = list(stream) cmlenz@1: for item in iter(iterable): cmlenz@1: if len(self.targets) == 1: cmlenz@1: item = [item] cmlenz@1: scope = {} cmlenz@1: for idx, name in enumerate(self.targets): cmlenz@1: scope[name] = item[idx] cmlenz@1: ctxt.push(**scope) cmlenz@53: for event in self._apply_directives(stream, ctxt, directives): cmlenz@1: yield event cmlenz@1: 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@1: >>> ctxt = Context(foo=True, bar='Hello') cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ... ${bar} cmlenz@1: ...
''') cmlenz@1: >>> print tmpl.generate(ctxt) 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@53: return self._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@29: def __init__(self, value): cmlenz@29: Directive.__init__(self, None) cmlenz@14: self.path = Path(value) 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@1: >>> ctxt = Context(bar='Bye') cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ... Hello cmlenz@1: ...
''') cmlenz@1: >>> print tmpl.generate(ctxt) 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@1: >>> ctxt = Context(bar='Bye') cmlenz@61: >>> tmpl = Template('''
cmlenz@1: ... Hello cmlenz@1: ...
''') cmlenz@1: >>> print tmpl.generate(ctxt) 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@1: if self.expr: cmlenz@1: strip = self.expr.evaluate(ctxt) cmlenz@1: else: cmlenz@1: strip = True cmlenz@53: stream = self._apply_directives(stream, ctxt, directives) cmlenz@1: if strip: cmlenz@1: stream.next() # skip start tag cmlenz@1: previous = stream.next() cmlenz@1: for event in stream: cmlenz@1: yield previous cmlenz@1: previous = event cmlenz@1: else: cmlenz@1: for event in stream: cmlenz@1: yield event 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: mgood@44: >>> ctxt = Context() cmlenz@61: >>> tmpl = Template('''
mgood@44: ... 0 mgood@44: ... 1 cmlenz@53: ... 2 mgood@44: ...
''') mgood@44: >>> print tmpl.generate(ctxt) 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: ...
''') mgood@44: >>> print tmpl.generate(ctxt) 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@50: ctxt.push(_choose=self) cmlenz@53: for event in self._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'] 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@53: return self._apply_directives(stream, ctxt, directives) mgood@44: except AttributeError: mgood@44: if value: mgood@44: choose.matched = True cmlenz@53: return self._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'] mgood@44: if choose.matched: mgood@44: return [] mgood@44: choose.matched = True cmlenz@53: return self._apply_directives(stream, ctxt, directives) mgood@44: mgood@44: 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@1: ('for', ForDirective), cmlenz@1: ('if', IfDirective), cmlenz@50: ('when', WhenDirective), cmlenz@50: ('otherwise', OtherwiseDirective), cmlenz@53: ('choose', ChooseDirective), 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@1: self.filename = filename or '' cmlenz@21: if basedir and filename: cmlenz@21: self.filepath = os.path.join(basedir, filename) cmlenz@21: else: cmlenz@21: self.filepath = '' 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@66: directives.append(cls(value)) 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@65: directives.append(cls(value)) cmlenz@1: else: cmlenz@1: value = list(self._interpolate(value, *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@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@69: ev = stream.next() cmlenz@69: depth += {START: 1, END: -1}.get(ev[0], 0) cmlenz@69: content.append(ev) cmlenz@69: test(*ev) cmlenz@17: cmlenz@23: content = list(self._flatten(content, ctxt)) cmlenz@35: ctxt.push(select=lambda path: Stream(content).select(path)) cmlenz@36: cmlenz@50: if directives: cmlenz@50: template = directives[0](iter(template), ctxt, cmlenz@50: directives[1:]) 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@35: ctxt.pop() cmlenz@17: 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@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@21: filename = posixpath.join(posixpath.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@1: fileobj = file(filepath, 'rt') 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)