[svn] added new jinja trunk
authorArmin Ronacher <armin.ronacher@active-4.com>
Mon, 26 Feb 2007 21:17:32 +0000 (22:17 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Mon, 26 Feb 2007 21:17:32 +0000 (22:17 +0100)
--HG--
branch : trunk

18 files changed:
jinja/__init__.py [new file with mode: 0644]
jinja/ast.py [new file with mode: 0644]
jinja/datastructure.py [new file with mode: 0644]
jinja/defaults.py [new file with mode: 0644]
jinja/environment.py [new file with mode: 0644]
jinja/exceptions.py [new file with mode: 0644]
jinja/filters.py [new file with mode: 0644]
jinja/lexer.py [new file with mode: 0644]
jinja/nodes.py [new file with mode: 0644]
jinja/parser.py [new file with mode: 0644]
jinja/translators/__init__.py [new file with mode: 0644]
jinja/translators/python.py [new file with mode: 0644]
syntax.html [new file with mode: 0644]
syntax.txt [new file with mode: 0644]
test.html [new file with mode: 0644]
test.py [new file with mode: 0644]
tests/mockup.txt [new file with mode: 0644]
tests/run.py [new file with mode: 0644]

diff --git a/jinja/__init__.py b/jinja/__init__.py
new file mode 100644 (file)
index 0000000..edf3439
--- /dev/null
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+"""
+    Jinja Sandboxed Template Engine
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
diff --git a/jinja/ast.py b/jinja/ast.py
new file mode 100644 (file)
index 0000000..88f18a2
--- /dev/null
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.ast
+    ~~~~~~~~~
+
+    Advance Syntax Tree for jinja.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+class Node(object):
+    """
+    Baseclass of all nodes. For instance checking.
+    """
+    __slots__ = ()
+
+    def __init__(self):
+        raise TypeError('cannot create %r instances' %
+                        self.__class__.__name__)
+
+
+class Expression(list, Node):
+    """
+    Node that helds childnodes. Normally just used temporary.
+    """
+    __slots__ = ()
+
+    def __init__(self, *args, **kwargs):
+        super(Expression, self).__init__(*args, **kwargs)
+
+    def __repr__(self):
+        return 'Expression(%s)' % list.__repr__(self)
+
+
+class Comment(Node):
+    """
+    Node that helds a comment. We keep comments in the data
+    if some translator wants to forward it into the generated
+    code (for example the python translator).
+    """
+    __slots__ = ('pos', 'comment',)
+
+    def __init__(self, pos, comment):
+        self.pos = pos
+        self.comment = comment
+
+    def __repr__(self):
+        return 'Comment(%r, %r)' % (self.pos, self.comment)
+
+
+class Page(Node):
+    """
+    Node that helds all root nodes.
+    """
+    __slots__ = ('filename', 'nodes')
+
+    def __init__(self, filename, nodes):
+        self.filename = filename
+        self.nodes = nodes
+
+    def __repr__(self):
+        return 'Page(%r, %r)' % (
+            self.filename,
+            self.nodes
+        )
+
+
+class Variable(Node):
+    """
+    Node for variables
+    """
+    __slots__ = ('pos', 'expression')
+
+    def __init__(self, pos, expression):
+        self.pos = pos
+        self.expression = expression
+
+    def __repr__(self):
+        return 'Variable(%r)' % self.expression
+
+
+class Data(Node):
+    """
+    Node for data outside of tags.
+    """
+    __slots__ = ('pos', 'data')
+
+    def __init__(self, pos, data):
+        self.pos = pos
+        self.data = data
+
+    def __repr__(self):
+        return 'Data(%d, %r)' % (self.pos, self.data)
+
+
+class Name(Node):
+    """
+    Node for names.
+    """
+    __slots__ = ('pos', 'data')
+
+    def __init__(self, pos, data):
+        self.pos = pos
+        self.data = data
+
+    def __repr__(self):
+        return 'Name(%d, %r)' % (self.pos, self.data)
diff --git a/jinja/datastructure.py b/jinja/datastructure.py
new file mode 100644 (file)
index 0000000..868397c
--- /dev/null
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.datastructure
+    ~~~~~~~~~~~~~~~~~~~
+
+    Module that helds several data types used in the template engine.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+# python2.3 compatibility. do not use this method for anything else
+# then context reversing.
+try:
+    _reversed = reversed
+except NameError:
+    def _reversed(c):
+        return c[::-1]
+
+
+class UndefinedType(object):
+    """
+    An object that does not exist.
+    """
+
+    def __init__(self):
+        try:
+            Undefined
+        except NameError:
+            pass
+        else:
+            raise TypeError('cannot create %r instances' %
+                            self.__class__.__name__)
+
+    def __getitem__(self, arg):
+        return self
+
+    def __iter__(self):
+        return iter(int, 0)
+
+    def __getattr__(self, arg):
+        return self
+
+    def __nonzero__(self):
+        return False
+
+    def __len__(self):
+        return 0
+
+    def __str__(self):
+        return ''
+
+    def __unicode__(self):
+        return u''
+
+    def __int__(self):
+        return 0
+
+    def __float__(self):
+        return 1
+
+
+Undefined = UndefinedType()
+
+
+class Context(object):
+    """
+    Dict like object.
+    """
+
+    __slots__ = ('stack')
+
+    def __init__(*args, **kwargs):
+        try:
+            self = args[0]
+            self.environment = args[1]
+            initial = dict(*args[2:], **kwargs)
+        except:
+            raise TypeError('%r requires environment as first argument. '
+                            'The rest of the arguments are forwarded to '
+                            'the default dict constructor.')
+        initial.update(
+            false=False,
+            true=True,
+            none=None
+        )
+        self._stack = [initial, {}]
+
+    def pop(self):
+        if len(self._stack) <= 2:
+            raise ValueError('cannot pop initial layer')
+        return self._stack.pop()
+
+    def push(self, data=None):
+        self._stack.append(data or {})
+
+    def __getitem__(self, name):
+        for d in _reversed(self._stack):
+            if name in d:
+                return d[name]
+        return Undefined
+
+    def __setitem__(self, name, value):
+        self._stack[-1][name] = value
+
+    def __delitem__(self, name):
+        if name in self._stack[-1]:
+            del self._stack[-1][name]
+
+    def __repr__(self):
+        tmp = {}
+        for d in self._stack:
+            for key, value in d.iteritems():
+                tmp[key] = value
+        return 'Context(%s)' % repr(tmp)
+
+
+class TokenStream(object):
+    """
+    A token stream works like a normal generator just that
+    it supports pushing tokens back to the stream.
+    """
+
+    def __init__(self, generator):
+        self._generator = generator
+        self._pushed = []
+        self.last = (0, 'initial', '')
+
+    def __iter__(self):
+        return self
+
+    def __nonzero__(self):
+        """Are we at the end of the tokenstream?"""
+        if self._pushed:
+            return True
+        try:
+            self.push(self.next())
+        except StopIteration:
+            return False
+        return True
+
+    eos = property(lambda x: not x.__nonzero__(), doc=__nonzero__.__doc__)
+
+    def next(self):
+        """Return the next token from the stream."""
+        if self._pushed:
+            rv = self._pushed.pop()
+        else:
+            rv = self._generator.next()
+        self.last = rv
+        return rv
+
+    def look(self):
+        """Pop and push a token, return it."""
+        token = self.next()
+        self.push(*token)
+        return token
+
+    def fetch_until(self, test, drop_needle=False):
+        """Fetch tokens until a function matches."""
+        try:
+            while True:
+                token = self.next()
+                if test(*token):
+                    if not drop_needle:
+                        self.push(*token)
+                    return
+                else:
+                    yield token
+        except StopIteration:
+            raise IndexError('end of stream reached')
+
+    def push(self, pos, token, data):
+        """Push an yielded token back to the stream."""
+        self._pushed.append((pos, token, data))
diff --git a/jinja/defaults.py b/jinja/defaults.py
new file mode 100644 (file)
index 0000000..4a2d6bb
--- /dev/null
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.defaults
+    ~~~~~~~~~~~~~~
+
+    Jinja default filters and tags.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+
+DEFAULT_TAGS = {}
+
+DEFAULT_FILTERS = {}
diff --git a/jinja/environment.py b/jinja/environment.py
new file mode 100644 (file)
index 0000000..1f1a62c
--- /dev/null
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.environment
+    ~~~~~~~~~~~~~~~~~
+
+    Provides a class that holds runtime and parsing time options.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+from jinja.lexer import Lexer
+from jinja.parser import Parser
+from jinja.exceptions import TagNotFound, FilterNotFound
+from jinja.defaults import DEFAULT_TAGS, DEFAULT_FILTERS
+
+
+class Environment(object):
+    """
+    The jinja environment.
+    """
+
+    def __init__(self,
+                 block_start_string='{%',
+                 block_end_string='%}',
+                 variable_start_string='{{',
+                 variable_end_string='}}',
+                 comment_start_string='{#',
+                 comment_end_string='#}',
+                 template_charset='utf-8',
+                 charset='utf-8',
+                 loader=None,
+                 tags=None,
+                 filters=None):
+
+        # lexer / parser information
+        self.block_start_string = block_start_string
+        self.block_end_string = block_end_string
+        self.variable_start_string = variable_start_string
+        self.variable_end_string = variable_end_string
+        self.comment_start_string = comment_start_string
+        self.comment_end_string = comment_end_string
+
+        # other stuff
+        self.template_charset = template_charset
+        self.charset = charset
+        self.loader = loader
+        self.tags = tags or DEFAULT_TAGS.copy()
+        self.filters = filters or DEFAULT_FILTERS.copy()
+
+        # create lexer
+        self.lexer = Lexer(self)
+
+    def parse(self, source):
+        """Function that creates a new parser and parses the source."""
+        parser = Parser(self, source)
+        return parser.parse_page()
+
+    def get_tag(self, name):
+        """
+        Return the tag for a specific name. Raise a `TagNotFound` exception
+        if a tag with this name is not registered.
+        """
+        if name not in self._tags:
+            raise TagNotFound(name)
+        return self._tags[name]
+
+    def get_filter(self, name):
+        """
+        Return the filter for a given name. Raise a `FilterNotFound` exception
+        if the requested filter is not registered.
+        """
+        if name not in self._filters:
+            raise FilterNotFound(name)
+        return self._filters[name]
+
+    def to_unicode(self, value):
+        """
+        Convert a value to unicode with the rules defined on the environment.
+        """
+        if isinstance(value, unicode):
+            return value
+        else:
+            try:
+                return unicode(value)
+            except UnicodeError:
+                return str(value).decode(self.charset, 'ignore')
diff --git a/jinja/exceptions.py b/jinja/exceptions.py
new file mode 100644 (file)
index 0000000..6156041
--- /dev/null
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.exceptions
+    ~~~~~~~~~~~~~~~~
+
+    Jinja exceptions.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+
+class TemplateError(RuntimeError):
+    pass
+
+
+class TagNotFound(KeyError, TemplateError):
+    """
+    The parser looked for a specific tag in the tag library but was unable to find
+    one.
+    """
+
+    def __init__(self, tagname):
+        super(TagNotFound, self).__init__('The tag %r does not exist.' % tagname)
+        self.tagname = tagname
+
+
+class FilterNotFound(KeyError, TemplateError):
+    """
+    The template engine looked for a filter but was unable to find it.
+    """
+
+    def __init__(self, filtername):
+        super(FilterNotFound, self).__init__('The filter %r does not exist.' % filtername)
+        self.filtername = filtername
+
+
+class TemplateSyntaxError(SyntaxError, TemplateError):
+    """
+    Raised to tell the user that there is a problem with the template.
+    """
+
+    def __init__(self, message, pos):
+        super(TemplateSyntaxError, self).__init__(message)
+        self.pos = pos
+
+
+class TemplateRuntimeError(TemplateError):
+    """
+    Raised by the template engine if a tag encountered an error when
+    rendering.
+    """
+
+    def __init__(self, message, pos):
+        super(TemplateRuntimeError, self).__init__(message)
+        self.pos = pos
diff --git a/jinja/filters.py b/jinja/filters.py
new file mode 100644 (file)
index 0000000..93ef90f
--- /dev/null
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.filters
+    ~~~~~~~~~~~~~
+
+    Bundled jinja filters.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+
+
+def stringfilter(f):
+    """
+    Decorator for filters that just work on unicode objects.
+    """
+    def decorator(*args):
+        def wrapped(env, context, value):
+            args = list(args)
+            for idx, var in enumerate(args):
+                if isinstance(var, str):
+                    args[idx] = env.to_unicode(var)
+            return f(env.to_unicode(value), *args)
+        return wrapped
+    return decorator
+
+
+def do_replace(s, old, new, count=None):
+    """
+    {{ s|replace(old, new, count=None) }}
+
+    Return a copy of s with all occurrences of substring
+    old replaced by new. If the optional argument count is
+    given, only the first count occurrences are replaced.
+    """
+    if count is None:
+        return s.replace(old, new)
+    return s.replace(old, new, count)
+do_replace = stringfilter(do_replace)
+
+
+def do_upper(s):
+    """
+    {{ s|upper }}
+
+    Return a copy of s converted to uppercase.
+    """
+    return s.upper()
+do_upper = stringfilter(do_upper)
+
+
+def do_lower(s):
+    """
+    {{ s|lower }}
+
+    Return a copy of s converted to lowercase.
+    """
+    return s.lower()
+do_lower = stringfilter(do_lower)
+
+
+def do_escape(s, attribute=False):
+    """
+    {{ s|escape(attribute) }}
+
+    XML escape &, <, and > in a string of data. If attribute is
+    True it also converts ``"`` to ``&quot;``
+    """
+    s = s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+    if attribute:
+        s = s.replace('"', "&quot;")
+    return s
+do_escape = stringfilter(do_escape)
+
+
+def do_addslashes(s):
+    """
+    {{ s|addslashes }}
+
+    Adds slashes to s.
+    """
+    return s.encode('utf-8').encode('string-escape').decode('utf-8')
+do_addslashes = stringfilter(do_addslashes)
+
+
+def do_capitalize(s):
+    """
+    {{ s|capitalize }}
+
+    Return a copy of the string s with only its first character
+    capitalized.
+    """
+    return s.capitalize()
+do_capitalize = stringfilter(do_capitalize)
+
+
+def do_title(s):
+    """
+    {{ s|title }}
+
+    Return a titlecased version of s, i.e. words start with uppercase
+    characters, all remaining cased characters have lowercase.
+    """
+    return s.title()
+do_title = stringfilter(do_title)
+
+
+def do_default(default_value=u''):
+    """
+    {{ s|default(default_value='') }}
+
+    In case of s isn't set or True default will return default_value
+    which is '' per default.
+    """
+    return lambda e, c, v: v or default_value
+
+
+def do_join(d=u''):
+    """
+    {{ sequence|join(d='') }}
+
+    Return a string which is the concatenation of the strings in the
+    sequence. The separator between elements is d which is an empty
+    string per default.
+    """
+    def wrapped(env, context, value):
+        d = env.to_unicode(d)
+        return d.join([env.to_unicode(x) for x in value])
+    return wrapped
+
+
+def do_count():
+    """
+    {{ var|count }}
+
+    Return the length of var. In case if getting an integer or float
+    it will convert it into a string an return the length of the new
+    string.
+    If the object doesn't provide a __len__ function it will return
+    zero.st(value)
+        l.reverse()
+        return 
+    """
+    def wrapped(env, context, value):
+        try:
+            if type(value) in (int, float, long):
+                return len(str(var))
+            return len(var)
+        except TypeError:
+            return 0
+    return wrapped
+
+
+def do_odd():
+    """
+    {{ var|odd }}
+
+    Return true if the variable is odd.
+    """
+    return lambda e, c, v: v % 2 == 1
+
+
+def do_even():
+    """
+    {{ var|even }}
+
+    Return true of the variable is even.
+    """
+    return lambda e, c, v: v % 2 == 0
+
+
+def do_reversed():
+    """
+    {{ var|reversed }}
+
+    Return a reversed list of the iterable filtered.
+    """
+    def wrapped(env, context, value):
+        try:
+            return value[::-1]
+        except:
+            l = list(value)
+            l.reverse()
+            return l
+    return wrapped
+
+
+FILTERS = {
+    'replace':              do_replace,
+    'upper':                do_upper,
+    'lower':                do_lower,
+    'escape':               do_escape,
+    'e':                    do_escape,
+    'addslashes':           do_addslashes,
+    'capitalize':           do_capitalize,
+    'title':                do_title,
+    'default':              do_default,
+    'join':                 do_join,
+    'count':                do_count,
+    'odd':                  do_odd,
+    'even':                 do_even,
+    'reversed':             do_reversed
+}
diff --git a/jinja/lexer.py b/jinja/lexer.py
new file mode 100644 (file)
index 0000000..ce2541d
--- /dev/null
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.lexer
+    ~~~~~~~~~~~
+"""
+import re
+from jinja.datastructure import TokenStream
+from jinja.exceptions import TemplateSyntaxError
+
+
+# static regular expressions
+whitespace_re = re.compile(r'\s+(?m)')
+name_re = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*[!?]?')
+string_re = re.compile(r"('([^'\\]*(?:\\.[^'\\]*)*)'"
+                       r'|"([^"\\]*(?:\\.[^"\\]*)*)")(?ms)')
+number_re = re.compile(r'\d+(\.\d+)*')
+
+operator_re = re.compile('(%s)' % '|'.join(
+    isinstance(x, unicode) and str(x) or re.escape(x) for x in [
+    # math operators
+    '+', '-', '*', '/', '%',
+    # braces and parenthesis
+    '[', ']', '(', ')', '{', '}',
+    # attribute access and comparison / logical operators
+    '.', ',', '|', '==', '<', '>', '<=', '>=', '!=',
+    ur'or\b', ur'and\b', ur'not\b'
+]))
+
+
+class Failure(object):
+    """
+    Class that raises a `TemplateSyntaxError` if called.
+    Used by the `Lexer` to specify known errors.
+    """
+
+    def __init__(self, message, cls=TemplateSyntaxError):
+        self.message = message
+        self.error_class = cls
+
+    def __call__(self, position):
+        raise self.error_class(self.message, position)
+
+
+class Lexer(object):
+    """
+    Class that implements a lexer for a given environment. Automatically
+    created by the environment class, usually you don't have to do that.
+    """
+
+    def __init__(self, environment):
+        # shortcuts
+        c = lambda x: re.compile(x, re.M | re.S)
+        e = re.escape
+
+        # parsing rules for tags
+        tag_rules = [
+            (whitespace_re, None, None),
+            (number_re, 'number', None),
+            (operator_re, 'operator', None),
+            (name_re, 'name', None),
+            (string_re, 'string', None)
+        ]
+
+        # global parsing rules
+        self.rules = {
+            'root': [
+                (c('(.*?)(?:(?P<comment_begin>' +
+                    e(environment.comment_start_string) +
+                    ')|(?P<block_begin>' +
+                    e(environment.block_start_string) +
+                    ')|(?P<variable_begin>' +
+                    e(environment.variable_start_string) +
+                   '))'), ('data', '#bygroup'), '#bygroup'),
+                (c('.+'), 'data', None)
+            ],
+            'comment_begin': [
+                (c(r'(.*?)(%s)' % e(environment.comment_end_string)),
+                 ('comment', 'comment_end'), '#pop'),
+                (c('(.)'), (Failure('Missing end of comment tag'),), None)
+            ],
+            'block_begin': [
+                (c(e(environment.block_end_string)), 'block_end', '#pop')
+            ] + tag_rules,
+            'variable_begin': [
+                (c(e(environment.variable_end_string)), 'variable_end',
+                 '#pop')
+            ] + tag_rules
+        }
+
+    def tokenize(self, source):
+        """
+        Simple tokenize function that yields ``(position, type, contents)``
+        tuples. Wrap the generator returned by this function in a
+        `TokenStream` to get real token instances and be able to push tokens
+        back to the stream. That's for example done by the parser.
+        """
+        return TokenStream(self.tokeniter(source))
+
+    def tokeniter(self, source):
+        """
+        This method tokenizes the text and returns the tokens in a generator.
+        Normally it's a better idea to use the `tokenize` function which
+        returns a `TokenStream` but in some situations it can be useful
+        to use this function since it can be marginally faster.
+        """
+        pos = 0
+        stack = ['root']
+        statetokens = self.rules['root']
+        source_length = len(source)
+
+        while True:
+            # tokenizer loop
+            for regex, tokens, new_state in statetokens:
+                m = regex.match(source, pos)
+                if m:
+                    # tuples support more options
+                    if isinstance(tokens, tuple):
+                        for idx, token in enumerate(tokens):
+                            # hidden group
+                            if token is None:
+                                continue
+                            # failure group
+                            elif isinstance(token, Failure):
+                                raise token(m.start(idx + 1))
+                            # bygroup is a bit more complex, in that case we
+                            # yield for the current token the first named
+                            # group that matched
+                            elif token == '#bygroup':
+                                for key, value in m.groupdict().iteritems():
+                                    if value is not None:
+                                        yield m.start(key), key, value
+                                        break
+                                else:
+                                    raise RuntimeError('%r wanted to resolve '
+                                                       'the token dynamically'
+                                                       ' but no group matched'
+                                                       % regex)
+                            # normal group
+                            else:
+                                data = m.group(idx + 1)
+                                if data:
+                                    yield m.start(idx + 1), token, data
+                    # strings as token just are yielded as it, but just
+                    # if the data is not empty
+                    else:
+                        data = m.group()
+                        if tokens is not None:
+                            if data:
+                                yield pos, tokens, data
+                    # fetch new position into new variable so that we can check
+                    # if there is a internal parsing error which would result
+                    # in an infinite loop
+                    pos2 = m.end()
+                    # handle state changes
+                    if new_state is not None:
+                        # remove the uppermost state
+                        if new_state == '#pop':
+                            stack.pop()
+                        # resolve the new state by group checking
+                        elif new_state == '#bygroup':
+                            for key, value in m.groupdict().iteritems():
+                                if value is not None:
+                                    stack.append(key)
+                                    break
+                            else:
+                                raise RuntimeError('%r wanted to resolve the '
+                                                   'new state dynamically but'
+                                                   ' no group matched' %
+                                                   regex)
+                        # direct state name given
+                        else:
+                            stack.append(new_state)
+                        statetokens = self.rules[stack[-1]]
+                    # we are still at the same position and no stack change.
+                    # this means a loop without break condition, avoid that and
+                    # raise error
+                    elif pos2 == pos:
+                        raise RuntimeError('%r yielded empty string without '
+                                           'stack change' % regex)
+                    # publish new function and start again
+                    pos = pos2
+                    break
+            # if loop terminated without break we havn't found a single match
+            # either we are at the end of the file or we have a problem
+            else:
+                # end of text
+                if pos >= source_length:
+                    return
+                # something went wrong
+                raise TemplateSyntaxError('unexpected char %r at %d' %
+                                          (source[pos], pos), pos)
diff --git a/jinja/nodes.py b/jinja/nodes.py
new file mode 100644 (file)
index 0000000..580310a
--- /dev/null
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.nodes
+    ~~~~~~~~~~~
+
+    Additional nodes for jinja. Look like nodes from the ast.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+from compiler.ast import Node
+
+
+class Text(Node):
+    """
+    Node that represents normal text.
+    """
+
+    def __init__(self, pos, text):
+        self.pos = pos
+        self.text = text
+
+    def __repr__(self):
+        return 'Text(%r)' % (self.text,)
+
+
+class NodeList(list, Node):
+    """
+    A node that stores multiple childnodes.
+    """
+
+    def __init__(self, pos, data=None):
+        self.pos = pos
+        list.__init__(self, data or ())
+
+    def __repr__(self):
+        return 'NodeList(%s)' % list.__repr__(self)
+
+
+class ForLoop(Node):
+    """
+    A node that represents a for loop
+    """
+
+    def __init__(self, pos, item, seq, body, else_):
+        self.pos = pos
+        self.item = item
+        self.seq = seq
+        self.body = body
+        self.else_ = else_
+
+    def __repr__(self):
+        return 'ForLoop(%r, %r, %r, %r)' % (
+            self.item,
+            self.seq,
+            self.body,
+            self.else_
+        )
+
+
+class IfCondition(Node):
+    """
+    A node that represents an if condition.
+    """
+
+    def __init__(self, pos, test, body, else_):
+        self.pos = pos
+        self.test = test
+        self.body = body
+        self.else_ = else_
+
+    def __repr__(self):
+        return 'IfCondition(%r, %r, %r)' % (
+            self.test,
+            self.body,
+            self.else_
+        )
+
+
+class Print(Node):
+    """
+    A node that represents variable tags and print calls
+    """
+
+    def __init__(self, pos, variable):
+        self.pos = pos
+        self.variable = variable
+
+    def __repr__(self):
+        return 'Print(%r)' % (self.variable,)
diff --git a/jinja/parser.py b/jinja/parser.py
new file mode 100644 (file)
index 0000000..7ab0fe8
--- /dev/null
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.parser
+    ~~~~~~~~~~~~
+
+    Implements the template parser.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+import re
+from compiler import ast, parse
+from jinja import nodes
+from jinja.datastructure import TokenStream
+from jinja.exceptions import TemplateSyntaxError
+
+
+# callback functions for the subparse method
+end_of_block = lambda p, t, d: t == 'block_end'
+end_of_variable = lambda p, t, d: t == 'variable_end'
+switch_for = lambda p, t, d: t == 'name' and d in ('else', 'endfor')
+end_of_for = lambda p, t, d: t == 'name' and d == 'endfor'
+switch_if = lambda p, t, d: t == 'name' and d in ('else', 'endif')
+end_of_if = lambda p, t, d: t == 'name' and d == 'endif'
+
+
+class Parser(object):
+    """
+    The template parser class.
+
+    Transforms sourcecode into an abstract syntax tree::
+
+        >>> parse("{% for item in seq|reversed %}{{ item }}{% endfor %}")
+        Document(ForLoop(AssignName('item'), Filter(Name('seq'), Name('reversed')),
+                         Print('item'), None))
+        >>> parse("{% if true %}foo{% else %}bar{% endif %}")
+        Document(IfCondition(Name('true'), Data('foo'), Data('bar')))
+        >>> parse("{% if false %}...{% elif 0 > 1 %}...{% else %}...{% endif %}")
+        Document(IfCondition(Name('false'), Data('...'),
+                             IfCondition(Compare('>', Const(0), Const(1)),
+                                         Data('...'), Data('...'))))
+    """
+
+    def __init__(self, environment, source, filename=None):
+        self.environment = environment
+        if isinstance(source, str):
+            source = source.decode(environment.template_charset, 'ignore')
+        self.source = source
+        self.filename = filename
+        self.tokenstream = environment.lexer.tokenize(source)
+        self._parsed = False
+
+        self.directives = {
+            'for':          self.handle_for_directive,
+            'if':           self.handle_if_directive,
+            'print':        self.handle_print_directive
+        }
+
+    def handle_for_directive(self, pos, gen):
+        """
+        Handle a for directive and return a ForLoop node
+        """
+        ast = self.parse_python(pos, gen, 'for %s:pass\nelse:pass')
+        body = self.subparse(switch_for)
+
+        # do we have an else section?
+        if self.tokenstream.next()[2] == 'else':
+            self.close_remaining_block()
+            else_ = self.subparse(end_of_for, True)
+        else:
+            else_ = None
+        self.close_remaining_block()
+
+        return nodes.ForLoop(pos, ast.assign, ast.list, body, else_)
+
+    def handle_if_directive(self, pos, gen):
+        """
+        Handle if/else blocks. elif is not supported by now.
+        """
+        ast = self.parse_python(pos, gen, 'if %s:pass\nelse:pass')
+        body = self.subparse(switch_if)
+
+        # do we have an else section?
+        if self.tokenstream.next()[2] == 'else':
+            self.close_remaining_block()
+            else_ = self.subparse(end_of_if, True)
+        else:
+            else_ = None
+        self.close_remaining_block()
+
+        return nodes.IfCondition(pos, ast.tests[0][0], body, else_)
+
+    def handle_print_directive(self, pos, gen):
+        """
+        Handle {{ foo }} and {% print foo %}.
+        """
+        ast = self.parse_python(pos, gen, 'print_(%s)')
+        # ast is something like Discard(CallFunc(Name('print_'), ...))
+        # so just use the args
+        arguments = ast.expr.args
+        # we only accept one argument
+        if len(arguments) != 1:
+            raise TemplateSyntaxError('invalid argument count for print; '
+                                      'print requires exactly one argument, '
+                                      'got %d.' % len(arguments), pos)
+        return nodes.Print(pos, arguments[0])
+
+    def parse_python(self, pos, gen, template='%s'):
+        """
+        Convert the passed generator into a flat string representing
+        python sourcecode and return an ast node or raise a
+        TemplateSyntaxError.
+        """
+        tokens = []
+        for t_pos, t_token, t_data in gen:
+            if t_token == 'string':
+                tokens.append('u' + t_data)
+            else:
+                tokens.append(t_data)
+        source = '\xef\xbb\xbf' + (template % (u' '.join(tokens)).encode('utf-8'))
+        try:
+            ast = parse(source, 'exec')
+        except SyntaxError, e:
+            raise TemplateSyntaxError(str(e), pos + e.offset - 1)
+        assert len(ast.node.nodes) == 1, 'get %d nodes, 1 expected' % len(ast.node.nodes)
+        return ast.node.nodes[0]
+
+    def parse(self):
+        """
+        Parse the template and return a nodelist.
+        """
+        return self.subparse(None)
+
+    def subparse(self, test, drop_needle=False):
+        """
+        Helper function used to parse the sourcecode until the test
+        function which is passed a tuple in the form (pos, token, data)
+        returns True. In that case the current token is pushed back to
+        the tokenstream and the generator ends.
+
+        The test function is only called for the first token after a
+        block tag. Variable tags are *not* aliases for {% print %} in
+        that case.
+
+        If drop_needle is True the needle_token is removed from the tokenstream.
+        """
+        def finish():
+            """Helper function to remove unused nodelists."""
+            if len(result) == 1:
+                return result[0]
+            return result
+
+        pos = self.tokenstream.last[0]
+        result = nodes.NodeList(pos)
+        for pos, token, data in self.tokenstream:
+            # this token marks the begin or a variable section.
+            # parse everything till the end of it.
+            if token == 'variable_begin':
+                gen = self.tokenstream.fetch_until(end_of_variable, True)
+                result.append(self.directives['print'](pos, gen))
+
+            # this token marks the start of a block. like for variables
+            # just parse everything until the end of the block
+            elif token == 'block_begin':
+                gen = self.tokenstream.fetch_until(end_of_block, True)
+                try:
+                    pos, token, data = gen.next()
+                except StopIteration:
+                    raise TemplateSyntaxError('unexpected end of block', pos)
+
+                # first token *must* be a name token
+                if token != 'name':
+                    raise TemplateSyntaxError('unexpected %r token' % token, pos)
+
+                # if a test function is passed to subparse we check if we
+                # reached the end of such a requested block.
+                if test is not None and test(pos, token, data):
+                    if not drop_needle:
+                        self.tokenstream.push(pos, token, data)
+                    return finish()
+
+                # the first token tells us which directive we want to call.
+                # if if doesn't match any existing directive it's like a
+                # template syntax error.
+                if data in self.directives:
+                    node = self.directives[data](pos, gen)
+                else:
+                    raise TemplateSyntaxError('unknown directive %r' % data, pos)
+                result.append(node)
+
+            # here the only token we should get is "data". all other
+            # tokens just exist in block or variable sections. (if the
+            # tokenizer is not brocken)
+            elif token == 'data':
+                result.append(nodes.Text(pos, data))
+
+            # so this should be unreachable code
+            else:
+                raise AssertionError('unexpected token %r' % token)
+
+        # still here and a test function is provided? raise and error
+        if test is not None:
+            raise TemplateSyntaxError('unexpected end of template', pos)
+        return finish()
+
+    def close_remaining_block(self):
+        """
+        If we opened a block tag because one of our tags requires an end
+        tag we can use this method to drop the rest of the block from
+        the stream. If the next token isn't the block end we throw an
+        error.
+        """
+        pos = self.tokenstream.last[0]
+        try:
+            pos, token, data = self.tokenstream.next()
+        except StopIteration:
+            raise TemplateSyntaxError('missing closing tag', pos)
+        if token != 'block_end':
+            raise TemplateSyntaxError('expected close tag, found %r' % token, pos)
diff --git a/jinja/translators/__init__.py b/jinja/translators/__init__.py
new file mode 100644 (file)
index 0000000..b92b79e
--- /dev/null
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.translators
+    ~~~~~~~~~~~~~~~~~
+
+    The submodules of this module provide translators for the jinja ast
+    which basically just is the python ast with a few more nodes.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
diff --git a/jinja/translators/python.py b/jinja/translators/python.py
new file mode 100644 (file)
index 0000000..927f892
--- /dev/null
@@ -0,0 +1,351 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja.translators.python
+    ~~~~~~~~~~~~~~~~~~~~~~~~
+
+    This module translates a jinja ast into python code.
+
+    :copyright: 2006 by Armin Ronacher.
+    :license: BSD, see LICENSE for more details.
+"""
+from compiler import ast
+from jinja import nodes
+
+
+class PythonTranslator(object):
+    """
+    Pass this translator a ast tree to get valid python code.
+    """
+
+    def __init__(self, environment, node):
+        self.environment = environment
+        self.node = node
+        self.indention = 0
+        self.last_pos = 0
+
+        self.handlers = {
+            # jinja nodes
+            nodes.Text:             self.handle_template_text,
+            nodes.NodeList:         self.handle_node_list,
+            nodes.ForLoop:          self.handle_for_loop,
+            nodes.IfCondition:      self.handle_if_condition,
+            nodes.Print:            self.handle_print,
+            # used python nodes
+            ast.Name:               self.handle_name,
+            ast.AssName:            self.handle_name,
+            ast.Compare:            self.handle_compare,
+            ast.Const:              self.handle_const,
+            ast.Subscript:          self.handle_subscript,
+            ast.Getattr:            self.handle_getattr,
+            ast.AssTuple:           self.handle_ass_tuple,
+            ast.Bitor:              self.handle_bitor,
+            ast.CallFunc:           self.handle_call_func,
+            ast.Add:                self.handle_add,
+            ast.Sub:                self.handle_sub,
+            ast.Div:                self.handle_div,
+            ast.Mul:                self.handle_mul,
+            ast.Mod:                self.handle_mod,
+            ast.UnarySub:           self.handle_unary_sub,
+            ast.Power:              self.handle_power,
+            ast.Dict:               self.handle_dict,
+            ast.List:               self.handle_list,
+            ast.Tuple:              self.handle_list,
+            ast.And:                self.handle_and,
+            ast.Or:                 self.handle_or,
+            ast.Not:                self.handle_not
+        }
+
+    def indent(self, text):
+        """
+        Indent the current text.
+        """
+        return (' ' * (self.indention * 4)) + text
+
+    def handle_node(self, node):
+        """
+        Handle one node
+        """
+        if node.__class__ in self.handlers:
+            out = self.handlers[node.__class__](node)
+        else:
+            raise AssertionError('unhandled node %r' % node.__class__)
+        return out
+
+    # -- jinja nodes
+
+    def handle_node_list(self, node):
+        """
+        In some situations we might have a node list. It's just
+        a collection of multiple statements.
+        """
+        self.last_pos = node.pos
+        buf = []
+        for n in node:
+            buf.append(self.handle_node(n))
+        return '\n'.join(buf)
+
+    def handle_for_loop(self, node):
+        """
+        Handle a for loop. Pretty basic, just that we give the else
+        clause a different behavior.
+        """
+        self.last_pos = node.pos
+        buf = []
+        write = lambda x: buf.append(self.indent(x))
+        write('context.push()')
+        write('parent_loop = context[\'loop\']')
+        write('loop_data = None')
+        write('for (loop_data, %s) in environment.iterate(%s):' % (
+            self.handle_node(node.item),
+            self.handle_node(node.seq)
+        ))
+        self.indention += 1
+        write('loop_data.parent = parent_loop')
+        write('context[\'loop\'] = loop_data')
+        buf.append(self.handle_node(node.body))
+        self.indention -= 1
+        if node.else_ is not None:
+            write('if loop_data is None:')
+            self.indention += 1
+            buf.append(self.handle_node(node.else_))
+            self.indention -= 1
+        write('context.pop()')
+        return '\n'.join(buf)
+
+    def handle_if_condition(self, node):
+        """
+        Handle an if condition node.
+        """
+        self.last_pos = node.pos
+        buf = []
+        write = lambda x: buf.append(self.indent(x))
+        write('if %s:' % self.handle_node(node.test))
+        self.indention += 1
+        buf.append(self.handle_node(node.body))
+        self.indention -= 1
+        if node.else_ is not None:
+            write('else:')
+            self.indention += 1
+            buf.append(self.handle_node(node.else_))
+            self.indention -= 1
+        return '\n'.join(buf)
+
+    def handle_print(self, node):
+        """
+        Handle a print statement.
+        """
+        self.last_pos = node.pos
+        return self.indent('write_var(%s)' % self.handle_node(node.variable))
+
+    def handle_template_text(self, node):
+        """
+        Handle data around nodes.
+        """
+        self.last_pos = node.pos
+        return self.indent('write(%r)' % node.text)
+
+    # -- python nodes
+
+    def handle_name(self, node):
+        """
+        Handle name assignments and name retreivement.
+        """
+        return 'context[%r]' % node.name
+
+    def handle_compare(self, node):
+        """
+        Any sort of comparison
+        """
+        buf = []
+        buf.append(self.handle_node(node.expr))
+        for op, n in node.ops:
+            buf.append(op)
+            buf.append(self.handle_node(n))
+        return ' '.join(buf)
+
+    def handle_const(self, node):
+        """
+        Constant values in expressions.
+        """
+        return repr(node.value)
+
+    def handle_subscript(self, node):
+        """
+        Handle variable based attribute access foo['bar'].
+        """
+        if len(node.subs) != 1:
+            raise TemplateSyntaxError('attribute access requires one argument',
+                                      self.last_pos)
+        assert node.flags != 'OP_DELETE', 'wtf? do we support that?'
+        return 'get_attribute(%s, %s)' % (
+            self.handle_node(node.expr),
+            self.handle_node(node.subs[0])
+        )
+
+    def handle_getattr(self, node):
+        """
+        Handle hardcoded attribute access. foo.bar
+        """
+        return 'get_attribute(%s, %r)' % (
+            self.handle_node(node.expr),
+            node.attrname
+        )
+
+    def handle_ass_tuple(self, node):
+        """
+        Tuple unpacking loops.
+        """
+        return '(%s)' % ', '.join([self.handle_node(n) for n in node.nodes])
+
+    def handle_bitor(self, node):
+        """
+        We use the pipe operator for filtering.
+        """
+        return 'environment.apply_filters(%s, %r)' % (
+            self.handle_node(node.nodes[0]),
+            [self.handle_node(n) for n in node.nodes[1:]]
+        )
+
+    def handle_call_func(self, node):
+        """
+        Handle function calls.
+        """
+        args = []
+        kwargs = {}
+        star_args = dstar_args = None
+        if node.star_args is not None:
+            star_args = self.handle_node(node.star_args)
+        if node.dstar_args is not None:
+            dstar_args = self.handle_node(node.dstar_args)
+        for arg in node.args:
+            if arg.__class__ is ast.Keyword:
+                kwargs[arg.name] = self.handle_node(arg.expr)
+            else:
+                args.append(self.handle_node(arg))
+        return 'environment.call_function(%s, [%s], {%s}, %s, %s)' % (
+            self.handle_node(node.node),
+            ', '.join(args),
+            ', '.join(['%r: %s' % i for i in kwargs.iteritems()]),
+            star_args,
+            dstar_args
+        )
+
+    def handle_add(self, node):
+        """
+        Add two items.
+        """
+        return '(%s + %s)' % (
+            self.handle_node(node.left),
+            self.handle_node(node.right)
+        )
+
+    def handle_sub(self, node):
+        """
+        Sub two items.
+        """
+        return '(%s - %s)' % (
+            self.handle_node(node.left),
+            self.handle_node(node.right)
+        )
+
+    def handle_div(self, node):
+        """
+        Divide two items.
+        """
+        return '(%s / %s)' % (
+            self.handle_node(node.left),
+            self.handle_node(node.right)
+        )
+
+    def handle_mul(self, node):
+        """
+        Multiply two items.
+        """
+        return '(%s * %s)' % (
+            self.handle_node(node.left),
+            self.handle_node(node.right)
+        )
+
+    def handle_mod(self, node):
+        """
+        Apply modulo.
+        """
+        return '(%s %% %s)' % (
+            self.handle_node(node.left),
+            self.handle_node(node.right)
+        )
+
+    def handle_unary_sub(self, node):
+        """
+        Make a number negative.
+        """
+        return '(-%s)' % self.handle_node(node.expr)
+
+    def handle_power(self, node):
+        """
+        handle foo**bar
+        """
+        return '(%s**%s)' % (
+            self.handle_node(node.left),
+            self.handle_node(node.right)
+        )
+
+    def handle_dict(self, node):
+        """
+        Dict constructor syntax.
+        """
+        return '{%s}' % ', '.join([
+            '%s: %s' % (
+                self.handle_node(key),
+                self.handle_node(value)
+            ) for key, value in node.items
+        ])
+
+    def handle_list(self, node):
+        """
+        We don't know tuples, tuples are lists for jinja.
+        """
+        return '[%s]' % ', '.join([
+            self.handle_node(n) for n in nodes
+        ])
+
+    def handle_and(self, node):
+        """
+        Handle foo and bar.
+        """
+        return ' and '.join([
+            self.handle_node(n) for n in node.nodes
+        ])
+
+    def handle_or(self, node):
+        """
+        handle foo or bar.
+        """
+        return ' or '.join([
+            self.handle_node(n) for n in self.nodse
+        ])
+
+    def handle_not(self, node):
+        """
+        handle not operator.
+        """
+        return 'not %s' % self.handle_node(node.expr)
+
+    def translate(self):
+        self.indention = 1
+        lines = [
+            'def generate(environment, context, write, write_var=None):',
+            '    """This function was automatically generated by',
+            '    the jinja python translator. do not edit."""',
+            '    if write_var is None:',
+            '        write_var = write'
+        ]
+        lines.append(self.handle_node(self.node))
+        return '\n'.join(lines)
+
+
+def translate(environment, node):
+    """
+    Do the translation to python.
+    """
+    return PythonTranslator(environment, node).translate()
diff --git a/syntax.html b/syntax.html
new file mode 100644 (file)
index 0000000..bc10e08
--- /dev/null
@@ -0,0 +1,61 @@
+Jinja 1.0 Syntax
+================
+
+<ul>
+{% for char in my_string|upper|replace(" ", "") %}
+    <li>{{ loop.index }} - {{ char|e }}</li>
+{% endfor %}
+</ul>
+
+{{ variable|strip(" ")|escape|replace("a", "b") }}
+
+{% if item == 42 %}
+    ...
+{% endif %}
+
+{% if item|odd? %}
+    applies the odd? filter which returns true if the
+    item in is odd.
+{% endif %}
+
+{{ item|e(true) }} -- escape the variable for attributes
+
+<ul>
+{% for item in seq %}
+    <li>{{ item.current|e }}
+        {% if item.items %}<ul>{% recurse item.items %}</ul>{% endif %}
+    </li>
+{% endfor %}
+</ul>
+
+How a Filter Looks Like
+=======================
+
+def replace(search, repl):
+    def wrapped(env, value):
+        return env.to_unicode(value).replace(search, repl)
+    return wrapped
+
+
+def escape(attr=False):
+    def wrapped(env, value):
+        return cgi.escape(env.to_unicode(value), attr)
+    return wrapped
+
+
+def odd():
+    return lambda env, value: value % 2 == 1
+odd.__name__ = 'odd?'
+
+
+@stringfilter
+def replace(value, search, repl):
+    return value.replace(search, repl)
+
+
+def stringfilter(f):
+    def decorator(*args):
+        def wrapped(env, value):
+            return f(env.to_unicode(value), *args)
+        return wrapped
+    return decorator
diff --git a/syntax.txt b/syntax.txt
new file mode 100644 (file)
index 0000000..381ec74
--- /dev/null
@@ -0,0 +1,22 @@
+For Loops
+---------
+
+Simple Syntax::
+
+    {% for <item> in <seq> %}
+        <data>
+    {% endfor %}
+
+Item Unpacking::
+
+    {% for <item1>, <item2> in <seq> %}
+        <data>
+    {% endfor %}
+
+With else block::
+    
+    {% for <item> in <seq> %}
+        <data>
+    {% else %}
+        <data>
+    {% endfor %}
diff --git a/test.html b/test.html
new file mode 100644 (file)
index 0000000..f846b06
--- /dev/null
+++ b/test.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Frameset//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>{% block "page_title" %}Untitled{% endblock %} | My Webpage</title>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <link rel="stylesheet" href="screen.css" type="text/css" media="screen" />
+    <link rel="stylesheet" href="print.css" type="text/css" media="print" />
+    {% block html_head %}{% endblock %}
+  </head>
+  <body>
+    <div id="header">
+      <h1>My Webpage</h1>
+      <ul id="navigation">
+        {% for item in page_navigation %}
+          <li><a href="{{ item.url|e }}"{% if item.active %}
+            class="active"{% endif %}>{{ item.caption|e }}</a></li>
+        {% endfor %}
+      </ul>
+    </div>
+    <div id="body">
+      {% block body %}
+        content goes here.
+      {% endblock %}
+    </div>
+    <div id="sidebar">
+      <h2>Who is online?</h2>
+      <ul>
+      {% for user in online_users %}
+        <li class="row_{% cycle 'even', 'odd' %}"><a href="{{ user.url|e }}">{{ user.username|e }}</a></li>
+      {% endfor %}
+      </ul>
+      <h2>Sitemap</h2>
+      {% macro draw_sitemap(items) %}
+        <ul>
+        {% for item in items %}
+          <li>{{ item.name|e }}{% if item.items %}{{ draw_sitemap(item.items) }}{% endif %}</li>
+        {% endfor %}
+        </ul>
+      {% endmacro %}
+      {{ draw_sitemap(sitemap) }}
+      <!-- alternative solution -->
+      <ul>
+      {% for item in items %}
+        <li>{{ item.name|e }}{% if item.items %}<ul>{% recurse item.items %}</ul>{% endif %}</li>
+      {% endfor %}
+      </ul>
+    </div>
+    <div id="footer">
+      &copy; Copyrigh by yourself.
+    </div>
+  </body>
+</html>
diff --git a/test.py b/test.py
new file mode 100644 (file)
index 0000000..e090dac
--- /dev/null
+++ b/test.py
@@ -0,0 +1,7 @@
+from jinja.environment import Environment
+
+e = Environment()
+
+def test(x):
+    for pos, token, data in e.lexer.tokenize(x):
+        print '%-8d%-30r%-40r' % (pos, token, data)
diff --git a/tests/mockup.txt b/tests/mockup.txt
new file mode 100644 (file)
index 0000000..ff7e5b2
--- /dev/null
@@ -0,0 +1,153 @@
+source index.html::
+
+    {% page extends='layout.html', defaultfilter='autoescape',
+            charset='utf-8' %}
+
+    {% def title %}Index | {{ super.title }}{% enddef %}
+    {% def body %}
+      <h1>Index</h1>
+      <p>
+        This is just a mockup template so that you can see how the new
+        jinja syntax looks like. It also shows the python output of this
+        template when compiled.
+      </p>
+      <ul>
+        {% for user in users %}
+          <li class="{% cycle 'row1', 'row2' %}">{{ user.username }}</li>
+        {% endfor %}
+      </ul>
+    {% enddef %}
+
+source layout.html::
+
+    {% page defaultfilter='autoescape', charset='utf-8' %}
+    <html>
+      <head>
+        <title>{% block title %}My Webpage{% endblock %}</title>
+      </head>
+      <body>
+        <div class="header">
+          <h1>My Webpage</h1>
+          <h2>{{ self.title }}</h2>
+        </div>
+        <div class="body">
+          {{ self.body }}
+        </div>
+        <div class="footer">
+          {% block footer %}
+            Copyright 2007 by the Pocoo Team.
+          {% endblock %}
+        </div>
+      </body>
+    </html>
+
+generated python code::
+
+    from jinja.runtime import Template, Markup
+
+    class Layer1Template(Template):
+
+        defaultfilter = 'autoescape'
+
+        def p_title(self):
+            yield Markup(u'My Webpage')
+
+        def p_footer(self):
+            yield Markup(u'\n        Copyright 2007 by the Pocoo Team.\n      ')
+
+        def p_outer_body(self):
+            yield Markup(u'<html>\n  <head>\n    <title>\n')
+            for i in self.resolve('self.title'):
+                yield i
+            yield Markup(u'</title>\n  </head>\n  <body>\n    <div class="header">\n'
+                         u'      <h1>My Webpage</h1>\n      <h2>')
+            for i in self.resolve('self.title'):
+                yield i
+            yield Markup(u'</h2>\n    </div>\n    <div class="body">\n      ')
+            for i in self.resolve('self.body'):
+                yield i
+            yield Markup(u'\n    </div>\n    <div class="footer">\n      ')
+            for i in self.resolve('self.footer'):
+                yield i
+            yield Markup(u'\n    </div>\n  </body>\n</html')
+
+    class Layer2Template(Layer1Template):
+
+        defaultfilter = 'autoescape'
+
+        def p_title(self):
+            yield Markup(u'Index | ')
+            for i in self.resolve('super.title'):
+                yield i
+
+        def p_body(self):
+            yield Markup(u'\n  <h1>Index</h1>\n  <p>\n    This is just a mockup '
+                         u'template so that you can see how the new jinja syntax '
+                         u'looks like. It also shows the python output of this '
+                         u'template when compiled.\n  </p>\n  <ul>\n    ')
+            iteration_1 = self.start_iteration(['user'], 'users')
+            for _ in iteration_1:
+                yield Markup(u'\n      <li class="')
+                for i in self.cycle(u'row1', u'row2'):
+                    yield i
+                yield Markup(u'">')
+                for i in self.resolve('user.username'):
+                    yield i
+                yield Markup(u'</li>\n      ')
+            iteration_1.close()
+
+    def get_template(context):
+        return Layer2Template(context)
+
+generated javascript code::
+
+    (function(c) {
+        var m = function(x) { return new Jinja.Markup(x) };
+
+        var l1 = function() {};
+        l1.defaultfilter = 'autoescape';
+        l1.prototype.p_title = function() {
+            this.yield(m('My Webpage'));
+        };
+        l1.prototype.p_footer = function() {
+            this.yield(m('\n        Copyright 2007 by the Pocoo Team.\n      '));
+        };
+        l1.prototype.p_outer_body = function() {
+            this.yield(m('<html>\n  <head>\n    <title>\n'));
+            this.resolve('self.title');
+            this.yield(m('</title>\n  </head>\n  <body>\n    <div class="header">\n'
+                         '      <h1>My Webpage</h1>\n      <h2>'));
+            this.resolve('self.title');
+            this.yield(m('</h2>\n    </div>\n    <div class="body">\n      '));
+            this.resolve('self.body');
+            this.yield(m('\n    </div>\n    <div class="footer">\n      '));
+            this.resolve('self.footer');
+            this.yield(m('\n    </div>\n  </body>\n</html'));
+        };
+
+        var l2 = function() {};
+        l2.defaultfilter = 'autoescape';
+        l2.prototype.p_title = function() {
+            this.yield(m('Index | '));
+            this.resolve('super.title');
+        };
+        l2.prototype.p_body = function() {
+            this.yield(m('\n  <h1>Index</h1>\n  <p>\n    This is just a mockup '
+                         'template so that you can see how the new jinja syntax '
+                         'looks like. It also shows the python output of this '
+                         'template when compiled.\n  </p>\n  <ul>\n    '));
+            var iteration_1 = this.startIteration(['user'], 'users');
+            while (iteration_1.next()) {
+                this.yield(m('\n      <li class="'));
+                this.cycle('row1', 'row2');
+                this.yield(m('">'));
+                this.resolve('user.username');
+                this.yield('</li>\n      ');
+            };
+            iteration_1.close();
+        };
+
+        return function(context) {
+            return new Jinja.Template(context, l1, l2);
+        };
+    })();
diff --git a/tests/run.py b/tests/run.py
new file mode 100644 (file)
index 0000000..b323b64
--- /dev/null
@@ -0,0 +1,15 @@
+from jinja import Environment, FileSystemLoader
+
+env = Environment(
+    loader=FileSystemLoader(['.'], suffix='')
+)
+env.filters['odd?'] = lambda c, x: x % 2 == 1
+env.filters['even?'] = lambda x, x: x % 2 == 0
+
+t = env.get_template('index.html')
+print t.render(
+    users=[
+        dict(username='foo', user_id=1),
+        dict(username='bar', user_id=2)
+    ]
+)