cmlenz@336: # -*- coding: utf-8 -*- cmlenz@336: # cmlenz@902: # Copyright (C) 2006-2009 Edgewall Software cmlenz@336: # All rights reserved. cmlenz@336: # cmlenz@336: # This software is licensed as described in the file COPYING, which cmlenz@336: # you should have received as part of this distribution. The terms cmlenz@336: # are also available at http://genshi.edgewall.org/wiki/License. cmlenz@336: # cmlenz@336: # This software consists of voluntary contributions made by many cmlenz@336: # individuals. For the exact contribution history, see the revision cmlenz@336: # history and logs, available at http://genshi.edgewall.org/log/. cmlenz@336: cmlenz@820: """Plain text templating engine. cmlenz@820: cmlenz@820: This module implements two template language syntaxes, at least for a certain cmlenz@820: transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines cmlenz@820: a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other cmlenz@820: hand is inspired by the syntax of the Django template language, which has more cmlenz@820: explicit delimiting of directives, and is more flexible with regards to cmlenz@820: white space and line breaks. cmlenz@820: cmlenz@820: In a future release, `OldTextTemplate` will be phased out in favor of cmlenz@820: `NewTextTemplate`, as the names imply. Therefore the new syntax is strongly cmlenz@820: recommended for new projects, and existing projects may want to migrate to the cmlenz@820: new syntax to remain compatible with future Genshi releases. cmlenz@820: """ cmlenz@336: cmlenz@336: import re cmlenz@336: cmlenz@820: from genshi.core import TEXT cmlenz@820: from genshi.template.base import BadDirectiveError, Template, \ cmlenz@820: TemplateSyntaxError, EXEC, INCLUDE, SUB cmlenz@820: from genshi.template.eval import Suite cmlenz@336: from genshi.template.directives import * cmlenz@820: from genshi.template.directives import Directive cmlenz@500: from genshi.template.interpolation import interpolate cmlenz@500: cmlenz@820: __all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate'] cmlenz@500: __docformat__ = 'restructuredtext en' cmlenz@336: cmlenz@336: cmlenz@820: class NewTextTemplate(Template): cmlenz@820: r"""Implementation of a simple text-based template engine. This class will cmlenz@820: replace `OldTextTemplate` in a future release. cmlenz@336: cmlenz@820: It uses a more explicit delimiting style for directives: instead of the old cmlenz@820: style which required putting directives on separate lines that were prefixed cmlenz@820: with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs cmlenz@820: (by default ``{% ... %}`` and ``{# ... #}``, respectively). cmlenz@820: cmlenz@820: Variable substitution uses the same interpolation syntax as for markup cmlenz@820: languages: simple references are prefixed with a dollar sign, more complex cmlenz@820: expression enclosed in curly braces. cmlenz@820: cmlenz@820: >>> tmpl = NewTextTemplate('''Dear $name, cmlenz@820: ... cmlenz@820: ... {# This is a comment #} cmlenz@820: ... We have the following items for you: cmlenz@820: ... {% for item in items %} cmlenz@820: ... * ${'Item %d' % item} cmlenz@820: ... {% end %} cmlenz@820: ... ''') cmlenz@902: >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) cmlenz@820: Dear Joe, cmlenz@820: cmlenz@820: cmlenz@820: We have the following items for you: cmlenz@820: cmlenz@820: * Item 1 cmlenz@820: cmlenz@820: * Item 2 cmlenz@820: cmlenz@820: * Item 3 cmlenz@820: cmlenz@820: cmlenz@820: cmlenz@820: By default, no spaces or line breaks are removed. If a line break should cmlenz@820: not be included in the output, prefix it with a backslash: cmlenz@820: cmlenz@820: >>> tmpl = NewTextTemplate('''Dear $name, cmlenz@820: ... cmlenz@820: ... {# This is a comment #}\ cmlenz@820: ... We have the following items for you: cmlenz@820: ... {% for item in items %}\ cmlenz@820: ... * $item cmlenz@820: ... {% end %}\ cmlenz@820: ... ''') cmlenz@902: >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) cmlenz@820: Dear Joe, cmlenz@820: cmlenz@820: We have the following items for you: cmlenz@820: * 1 cmlenz@820: * 2 cmlenz@820: * 3 cmlenz@820: cmlenz@820: cmlenz@820: Backslashes are also used to escape the start delimiter of directives and cmlenz@820: comments: cmlenz@820: cmlenz@820: >>> tmpl = NewTextTemplate('''Dear $name, cmlenz@820: ... cmlenz@820: ... \{# This is a comment #} cmlenz@820: ... We have the following items for you: cmlenz@820: ... {% for item in items %}\ cmlenz@820: ... * $item cmlenz@820: ... {% end %}\ cmlenz@820: ... ''') cmlenz@902: >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) cmlenz@820: Dear Joe, cmlenz@820: cmlenz@820: {# This is a comment #} cmlenz@820: We have the following items for you: cmlenz@820: * 1 cmlenz@820: * 2 cmlenz@820: * 3 cmlenz@820: cmlenz@820: cmlenz@820: :since: version 0.5 cmlenz@820: """ cmlenz@820: directives = [('def', DefDirective), cmlenz@820: ('when', WhenDirective), cmlenz@820: ('otherwise', OtherwiseDirective), cmlenz@820: ('for', ForDirective), cmlenz@820: ('if', IfDirective), cmlenz@820: ('choose', ChooseDirective), cmlenz@820: ('with', WithDirective)] cmlenz@820: serializer = 'text' cmlenz@820: cmlenz@820: _DIRECTIVE_RE = r'((? offset: cmlenz@820: text = _escape_sub(_escape_repl, source[offset:start]) cmlenz@820: for kind, data, pos in interpolate(text, self.filepath, lineno, cmlenz@820: lookup=self.lookup): cmlenz@820: stream.append((kind, data, pos)) cmlenz@820: lineno += len(text.splitlines()) cmlenz@820: cmlenz@820: lineno += len(source[start:end].splitlines()) cmlenz@820: command, value = mo.group(2, 3) cmlenz@820: cmlenz@820: if command == 'include': cmlenz@820: pos = (self.filename, lineno, 0) cmlenz@820: value = list(interpolate(value, self.filepath, lineno, 0, cmlenz@820: lookup=self.lookup)) cmlenz@820: if len(value) == 1 and value[0][0] is TEXT: cmlenz@820: value = value[0][1] cmlenz@820: stream.append((INCLUDE, (value, None, []), pos)) cmlenz@820: cmlenz@820: elif command == 'python': cmlenz@820: if not self.allow_exec: cmlenz@820: raise TemplateSyntaxError('Python code blocks not allowed', cmlenz@820: self.filepath, lineno) cmlenz@820: try: cmlenz@820: suite = Suite(value, self.filepath, lineno, cmlenz@820: lookup=self.lookup) cmlenz@820: except SyntaxError, err: cmlenz@820: raise TemplateSyntaxError(err, self.filepath, cmlenz@820: lineno + (err.lineno or 1) - 1) cmlenz@820: pos = (self.filename, lineno, 0) cmlenz@820: stream.append((EXEC, suite, pos)) cmlenz@820: cmlenz@820: elif command == 'end': cmlenz@820: depth -= 1 cmlenz@820: if depth in dirmap: cmlenz@820: directive, start_offset = dirmap.pop(depth) cmlenz@820: substream = stream[start_offset:] cmlenz@820: stream[start_offset:] = [(SUB, ([directive], substream), cmlenz@820: (self.filepath, lineno, 0))] cmlenz@820: cmlenz@820: elif command: cmlenz@820: cls = self.get_directive(command) cmlenz@820: if cls is None: cmlenz@820: raise BadDirectiveError(command) cmlenz@902: directive = 0, cls, value, None, (self.filepath, lineno, 0) cmlenz@820: dirmap[depth] = (directive, len(stream)) cmlenz@820: depth += 1 cmlenz@820: cmlenz@820: offset = end cmlenz@820: cmlenz@820: if offset < len(source): cmlenz@820: text = _escape_sub(_escape_repl, source[offset:]) cmlenz@820: for kind, data, pos in interpolate(text, self.filepath, lineno, cmlenz@820: lookup=self.lookup): cmlenz@820: stream.append((kind, data, pos)) cmlenz@820: cmlenz@820: return stream cmlenz@820: cmlenz@820: cmlenz@820: class OldTextTemplate(Template): cmlenz@820: """Legacy implementation of the old syntax text-based templates. This class cmlenz@820: is provided in a transition phase for backwards compatibility. New code cmlenz@820: should use the `NewTextTemplate` class and the improved syntax it provides. cmlenz@820: cmlenz@820: >>> tmpl = OldTextTemplate('''Dear $name, cmlenz@336: ... cmlenz@336: ... We have the following items for you: cmlenz@336: ... #for item in items cmlenz@336: ... * $item cmlenz@336: ... #end cmlenz@336: ... cmlenz@336: ... All the best, cmlenz@336: ... Foobar''') cmlenz@902: >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) cmlenz@336: Dear Joe, cmlenz@336: cmlenz@336: We have the following items for you: cmlenz@336: * 1 cmlenz@336: * 2 cmlenz@336: * 3 cmlenz@336: cmlenz@336: All the best, cmlenz@336: Foobar cmlenz@336: """ cmlenz@336: directives = [('def', DefDirective), cmlenz@336: ('when', WhenDirective), cmlenz@336: ('otherwise', OtherwiseDirective), cmlenz@336: ('for', ForDirective), cmlenz@336: ('if', IfDirective), cmlenz@336: ('choose', ChooseDirective), cmlenz@336: ('with', WithDirective)] cmlenz@820: serializer = 'text' cmlenz@336: cmlenz@395: _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(? offset: cmlenz@336: text = source[offset:start] cmlenz@820: for kind, data, pos in interpolate(text, self.filepath, lineno, cmlenz@500: lookup=self.lookup): cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: lineno += len(text.splitlines()) cmlenz@336: cmlenz@336: text = source[start:end].lstrip()[1:] cmlenz@336: lineno += len(text.splitlines()) cmlenz@336: directive = text.split(None, 1) cmlenz@336: if len(directive) > 1: cmlenz@336: command, value = directive cmlenz@336: else: cmlenz@336: command, value = directive[0], None cmlenz@336: cmlenz@336: if command == 'end': cmlenz@336: depth -= 1 cmlenz@336: if depth in dirmap: cmlenz@336: directive, start_offset = dirmap.pop(depth) cmlenz@336: substream = stream[start_offset:] cmlenz@336: stream[start_offset:] = [(SUB, ([directive], substream), cmlenz@336: (self.filepath, lineno, 0))] cmlenz@500: elif command == 'include': cmlenz@500: pos = (self.filename, lineno, 0) cmlenz@820: stream.append((INCLUDE, (value.strip(), None, []), pos)) cmlenz@336: elif command != '#': cmlenz@820: cls = self.get_directive(command) cmlenz@336: if cls is None: cmlenz@336: raise BadDirectiveError(command) cmlenz@902: directive = 0, cls, value, None, (self.filepath, lineno, 0) cmlenz@336: dirmap[depth] = (directive, len(stream)) cmlenz@336: depth += 1 cmlenz@336: cmlenz@336: offset = end cmlenz@336: cmlenz@336: if offset < len(source): cmlenz@336: text = source[offset:].replace('\\#', '#') cmlenz@820: for kind, data, pos in interpolate(text, self.filepath, lineno, cmlenz@500: lookup=self.lookup): cmlenz@336: stream.append((kind, data, pos)) cmlenz@336: cmlenz@336: return stream cmlenz@820: cmlenz@820: cmlenz@820: TextTemplate = OldTextTemplate