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