Mercurial > genshi > mirror
diff genshi/template/core.py @ 336:7763f7aec949 trunk
Refactoring: `genshi.template` is now a package, it was getting way to crowded in that file.
author | cmlenz |
---|---|
date | Wed, 08 Nov 2006 15:50:15 +0000 |
parents | |
children | d98a77b6094e 2aa7ca37ae6a |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/genshi/template/core.py @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +try: + from collections import deque +except ImportError: + class deque(list): + def appendleft(self, x): self.insert(0, x) + def popleft(self): return self.pop(0) +import os +import re +from StringIO import StringIO + +from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure +from genshi.template.eval import Expression + +__all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError', + 'TemplateSyntaxError', 'BadDirectiveError'] + + +class TemplateError(Exception): + """Base exception class for errors related to template processing.""" + + +class TemplateRuntimeError(TemplateError): + """Exception raised when an the evualation of a Python expression in a + template causes an error.""" + + def __init__(self, message, filename='<string>', lineno=-1, offset=-1): + self.msg = message + message = '%s (%s, line %d)' % (self.msg, filename, lineno) + TemplateError.__init__(self, message) + self.filename = filename + self.lineno = lineno + self.offset = offset + + +class TemplateSyntaxError(TemplateError): + """Exception raised when an expression in a template causes a Python syntax + error.""" + + def __init__(self, message, filename='<string>', lineno=-1, offset=-1): + if isinstance(message, SyntaxError) and message.lineno is not None: + message = str(message).replace(' (line %d)' % message.lineno, '') + self.msg = message + message = '%s (%s, line %d)' % (self.msg, filename, lineno) + TemplateError.__init__(self, message) + self.filename = filename + self.lineno = lineno + self.offset = offset + + +class BadDirectiveError(TemplateSyntaxError): + """Exception raised when an unknown directive is encountered when parsing + a template. + + An unknown directive is any attribute using the namespace for directives, + with a local name that doesn't match any registered directive. + """ + + def __init__(self, name, filename='<string>', lineno=-1): + message = 'bad directive "%s"' % name + TemplateSyntaxError.__init__(self, message, filename, lineno) + + +class Context(object): + """Container for template input data. + + A context provides a stack of scopes (represented by dictionaries). + + Template directives such as loops can push a new scope on the stack with + data that should only be available inside the loop. When the loop + terminates, that scope can get popped off the stack again. + + >>> ctxt = Context(one='foo', other=1) + >>> ctxt.get('one') + 'foo' + >>> ctxt.get('other') + 1 + >>> ctxt.push(dict(one='frost')) + >>> ctxt.get('one') + 'frost' + >>> ctxt.get('other') + 1 + >>> ctxt.pop() + {'one': 'frost'} + >>> ctxt.get('one') + 'foo' + """ + + def __init__(self, **data): + self.frames = deque([data]) + self.pop = self.frames.popleft + self.push = self.frames.appendleft + self._match_templates = [] + + def __repr__(self): + return repr(list(self.frames)) + + def __setitem__(self, key, value): + """Set a variable in the current scope.""" + self.frames[0][key] = value + + def _find(self, key, default=None): + """Retrieve a given variable's value and the frame it was found in. + + Intented for internal use by directives. + """ + for frame in self.frames: + if key in frame: + return frame[key], frame + return default, None + + def get(self, key, default=None): + """Get a variable's value, starting at the current scope and going + upward. + """ + for frame in self.frames: + if key in frame: + return frame[key] + return default + __getitem__ = get + + def push(self, data): + """Push a new scope on the stack.""" + + def pop(self): + """Pop the top-most scope from the stack.""" + + +class Directive(object): + """Abstract base class for template directives. + + A directive is basically a callable that takes three positional arguments: + `ctxt` is the template data context, `stream` is an iterable over the + events that the directive applies to, and `directives` is is a list of + other directives on the same stream that need to be applied. + + Directives can be "anonymous" or "registered". Registered directives can be + applied by the template author using an XML attribute with the + corresponding name in the template. Such directives should be subclasses of + this base class that can be instantiated with the value of the directive + attribute as parameter. + + Anonymous directives are simply functions conforming to the protocol + described above, and can only be applied programmatically (for example by + template filters). + """ + __slots__ = ['expr'] + + def __init__(self, value, namespaces=None, filename=None, lineno=-1, + offset=-1): + try: + self.expr = value and Expression(value, filename, lineno) or None + except SyntaxError, err: + err.msg += ' in expression "%s" of "%s" directive' % (value, + self.tagname) + raise TemplateSyntaxError(err, filename, lineno, + offset + (err.offset or 0)) + + def __call__(self, stream, ctxt, directives): + raise NotImplementedError + + def __repr__(self): + expr = '' + if self.expr is not None: + expr = ' "%s"' % self.expr.source + return '<%s%s>' % (self.__class__.__name__, expr) + + def tagname(self): + """Return the local tag name of the directive as it is used in + templates. + """ + return self.__class__.__name__.lower().replace('directive', '') + tagname = property(tagname) + + +def _apply_directives(stream, ctxt, directives): + """Apply the given directives to the stream.""" + if directives: + stream = directives[0](iter(stream), ctxt, directives[1:]) + return stream + + +class TemplateMeta(type): + """Meta class for templates.""" + + def __new__(cls, name, bases, d): + if 'directives' in d: + d['_dir_by_name'] = dict(d['directives']) + d['_dir_order'] = [directive[1] for directive in d['directives']] + + return type.__new__(cls, name, bases, d) + + +class Template(object): + """Abstract template base class. + + This class implements most of the template processing model, but does not + specify the syntax of templates. + """ + __metaclass__ = TemplateMeta + + EXPR = StreamEventKind('EXPR') # an expression + SUB = StreamEventKind('SUB') # a "subprogram" + + def __init__(self, source, basedir=None, filename=None, loader=None, + encoding=None): + """Initialize a template from either a string or a file-like object.""" + if isinstance(source, basestring): + self.source = StringIO(source) + else: + self.source = source + self.basedir = basedir + self.filename = filename + if basedir and filename: + self.filepath = os.path.join(basedir, filename) + else: + self.filepath = filename + + self.filters = [self._flatten, self._eval] + + self.stream = self._parse(encoding) + + def __repr__(self): + return '<%s "%s">' % (self.__class__.__name__, self.filename) + + def _parse(self, encoding): + """Parse the template. + + The parsing stage parses the template and constructs a list of + directives that will be executed in the render stage. The input is + split up into literal output (text that does not depend on the context + data) and directives or expressions. + """ + raise NotImplementedError + + _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}', re.DOTALL) + _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z_][a-zA-Z0-9_\.]*)') + + def _interpolate(cls, text, basedir=None, filename=None, lineno=-1, + offset=0): + """Parse the given string and extract expressions. + + This method returns a list containing both literal text and `Expression` + objects. + + @param text: the text to parse + @param lineno: the line number at which the text was found (optional) + @param offset: the column number at which the text starts in the source + (optional) + """ + filepath = filename + if filepath and basedir: + filepath = os.path.join(basedir, filepath) + def _interpolate(text, patterns, lineno=lineno, offset=offset): + for idx, grp in enumerate(patterns.pop(0).split(text)): + if idx % 2: + try: + yield EXPR, Expression(grp.strip(), filepath, lineno), \ + (filename, lineno, offset) + except SyntaxError, err: + raise TemplateSyntaxError(err, filepath, lineno, + offset + (err.offset or 0)) + elif grp: + if patterns: + for result in _interpolate(grp, patterns[:]): + yield result + else: + yield TEXT, grp.replace('$$', '$'), \ + (filename, lineno, offset) + if '\n' in grp: + lines = grp.splitlines() + lineno += len(lines) - 1 + offset += len(lines[-1]) + else: + offset += len(grp) + return _interpolate(text, [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE]) + _interpolate = classmethod(_interpolate) + + def generate(self, *args, **kwargs): + """Apply the template to the given context data. + + Any keyword arguments are made available to the template as context + data. + + Only one positional argument is accepted: if it is provided, it must be + an instance of the `Context` class, and keyword arguments are ignored. + This calling style is used for internal processing. + + @return: a markup event stream representing the result of applying + the template to the context data. + """ + if args: + assert len(args) == 1 + ctxt = args[0] + if ctxt is None: + ctxt = Context(**kwargs) + assert isinstance(ctxt, Context) + else: + ctxt = Context(**kwargs) + + stream = self.stream + for filter_ in self.filters: + stream = filter_(iter(stream), ctxt) + return Stream(stream) + + def _eval(self, stream, ctxt): + """Internal stream filter that evaluates any expressions in `START` and + `TEXT` events. + """ + filters = (self._flatten, self._eval) + + for kind, data, pos in stream: + + if kind is START and data[1]: + # Attributes may still contain expressions in start tags at + # this point, so do some evaluation + tag, attrib = data + new_attrib = [] + for name, substream in attrib: + if isinstance(substream, basestring): + value = substream + else: + values = [] + for subkind, subdata, subpos in self._eval(substream, + ctxt): + if subkind is TEXT: + values.append(subdata) + value = [x for x in values if x is not None] + if not value: + continue + new_attrib.append((name, u''.join(value))) + yield kind, (tag, Attrs(new_attrib)), pos + + elif kind is EXPR: + result = data.evaluate(ctxt) + if result is not None: + # First check for a string, otherwise the iterable test below + # succeeds, and the string will be chopped up into individual + # characters + if isinstance(result, basestring): + yield TEXT, result, pos + elif hasattr(result, '__iter__'): + substream = _ensure(result) + for filter_ in filters: + substream = filter_(substream, ctxt) + for event in substream: + yield event + else: + yield TEXT, unicode(result), pos + + else: + yield kind, data, pos + + def _flatten(self, stream, ctxt): + """Internal stream filter that expands `SUB` events in the stream.""" + for event in stream: + if event[0] is SUB: + # This event is a list of directives and a list of nested + # events to which those directives should be applied + directives, substream = event[1] + substream = _apply_directives(substream, ctxt, directives) + for event in self._flatten(substream, ctxt): + yield event + else: + yield event + + +EXPR = Template.EXPR +SUB = Template.SUB