more docs and fixed bug in parser that assigned lineno for ExprStmt wrong
authorArmin Ronacher <armin.ronacher@active-4.com>
Mon, 28 Apr 2008 14:14:03 +0000 (16:14 +0200)
committerArmin Ronacher <armin.ronacher@active-4.com>
Mon, 28 Apr 2008 14:14:03 +0000 (16:14 +0200)
--HG--
branch : trunk

docs/conf.py
docs/jinjaext.py
docs/templates.rst
jinja2/filters.py
jinja2/parser.py

index 4c984d263b7ded484866aed396af67765c0ea26c..237b61b82c5a37b26c7325eabe0b99b97bbb4988 100644 (file)
@@ -67,7 +67,7 @@ today_fmt = '%B %d, %Y'
 #show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'autumn'
+pygments_style = 'jinjaext.JinjaStyle'
 
 
 # Options for HTML output
@@ -132,7 +132,14 @@ latex_documents = [
 ]
 
 # Additional stuff for the LaTeX preamble.
-latex_preamble = ''
+latex_preamble = '''
+\usepackage{palatino}
+\definecolor{TitleColor}{rgb}{0.7,0,0}
+\definecolor{InnerLinkColor}{rgb}{0.7,0,0}
+\definecolor{OuterLinkColor}{rgb}{0.8,0,0}
+\definecolor{VerbatimColor}{rgb}{0.98,0.98,0.98}
+\definecolor{VerbatimBorderColor}{rgb}{0.8,0.8,0.8}
+'''
 
 # Documents to append as an appendix to all manuals.
 #latex_appendices = []
index c0dfe44c782ad101d287a137ddc15deb3ef3070f..bd4cab644a9bd52402836fd31e1cea6ae005a37e 100644 (file)
     :copyright: Copyright 2008 by Armin Ronacher.
     :license: BSD.
 """
+import re
 import inspect
+from types import BuiltinFunctionType
 from docutils import nodes
 from docutils.statemachine import ViewList
 from sphinx.ext.autodoc import prepare_docstring
 
 
-def format_filter(name, aliases, func):
-    try:
-        argspec = inspect.getargspec(func)
-    except:
+from pygments.style import Style
+from pygments.token import Keyword, Name, Comment, String, Error, \
+     Number, Operator, Generic
+
+
+class JinjaStyle(Style):
+    title = 'Jinja Style'
+    default_style = ""
+    styles = {
+        Comment:                    'italic #aaaaaa',
+        Comment.Preproc:            'noitalic #B11414',
+        Comment.Special:            'italic #505050',
+
+        Keyword:                    'bold #B80000',
+        Keyword.Type:               '#808080',
+
+        Operator.Word:              '#333333',
+
+        Name.Builtin:               '#333333',
+        Name.Function:              '#333333',
+        Name.Class:                 'bold #333333',
+        Name.Namespace:             'bold #333333',
+        Name.Entity:                'bold #363636',
+        Name.Attribute:             '#686868',
+        Name.Tag:                   'bold #686868',
+        Name.Decorator:             '#686868',
+
+        String:                     '#BE9B5D',
+        Number:                     '#FF0000',
+
+        Generic.Heading:            'bold #000080',
+        Generic.Subheading:         'bold #800080',
+        Generic.Deleted:            '#aa0000',
+        Generic.Inserted:           '#00aa00',
+        Generic.Error:              '#aa0000',
+        Generic.Emph:               'italic',
+        Generic.Strong:             'bold',
+        Generic.Prompt:             '#555555',
+        Generic.Output:             '#888888',
+        Generic.Traceback:          '#aa0000',
+
+        Error:                      '#F00 bg:#FAA'
+    }
+
+_sig_re = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*(\(.*?\))')
+
+
+def format_function(name, aliases, func):
+    lines = inspect.getdoc(func).splitlines()
+    signature = '()'
+    if isinstance(func, BuiltinFunctionType):
+        match = _sig_re.match(lines[0])
+        if match is not None:
+            del lines[:1 + bool(lines and not lines[0])]
+            signature = match.group(1)
+    else:
         try:
-            argspec = inspect.getargspec(func.__init__)
+            argspec = inspect.getargspec(func)
+            if getattr(func, 'environmentfilter', False) or \
+               getattr(func, 'contextfilter', False):
+                del argspec[0][0]
+            signature = inspect.formatargspec(*argspec)
         except:
-            try:
-                argspec = inspect.getargspec(func.__new__)
-            except:
-                return []
-        del argspec[0][0]
-    if getattr(func, 'environmentfilter', False) or \
-       getattr(func, 'contextfilter', False):
-        del argspec[0][0]
-    signature = inspect.formatargspec(*argspec)
+            pass
     result = ['.. function:: %s%s' % (name, signature), '']
-    for line in inspect.getdoc(func).splitlines():
-        result.append('    ' + line)
+    result.extend('    ' + line for line in lines)
     if aliases:
         result.extend(('', '    :aliases: %s' % ', '.join(
                       '``%s``' % x for x in sorted(aliases))))
     return result
 
 
-def jinja_filters(dirname, arguments, options, content, lineno,
-                  content_offset, block_text, state, state_machine):
-    from jinja2.defaults import DEFAULT_FILTERS
-    mapping = {}
-    for name, func in DEFAULT_FILTERS.iteritems():
-        mapping.setdefault(func, []).append(name)
-    filters = []
-    for func, names in mapping.iteritems():
-        aliases = sorted(names, key=lambda x: len(x))
-        name = aliases.pop()
-        filters.append((name, aliases, func))
-    filters.sort()
+def dump_functions(mapping):
+    def directive(dirname, arguments, options, content, lineno,
+                      content_offset, block_text, state, state_machine):
+        reverse_mapping = {}
+        for name, func in mapping.iteritems():
+            reverse_mapping.setdefault(func, []).append(name)
+        filters = []
+        for func, names in reverse_mapping.iteritems():
+            aliases = sorted(names, key=lambda x: len(x))
+            name = aliases.pop()
+            filters.append((name, aliases, func))
+        filters.sort()
+
+        result = ViewList()
+        for name, aliases, func in filters:
+            for item in format_function(name, aliases, func):
+                result.append(item, '<jinjaext>')
+
+        node = nodes.paragraph()
+        state.nested_parse(result, content_offset, node)
+        return node.children
+    return directive
 
-    result = ViewList()
-    for name, aliases, func in filters:
-        for item in format_filter(name, aliases, func):
-            result.append(item, '<jinjaext>')
 
-    node = nodes.paragraph()
-    state.nested_parse(result, content_offset, node)
-    return node.children
+from jinja2.defaults import DEFAULT_FILTERS, DEFAULT_TESTS
+jinja_filters = dump_functions(DEFAULT_FILTERS)
+jinja_tests = dump_functions(DEFAULT_TESTS)
 
 
 def setup(app):
-    app.add_directive('jinjafilters', jinja_filters, 1, (0, 0, 0))
+    app.add_directive('jinjafilters', jinja_filters, 0, (0, 0, 0))
+    app.add_directive('jinjatests', jinja_tests, 0, (0, 0, 0))
index a3b2325f8804e1698fe7f868deaaff3c061996c6..8171f79a00ce05be2e0b8663b90eba9adc0629d8 100644 (file)
@@ -89,7 +89,7 @@ applied to the next.
 around the arguments, like a function call.  This example will join a list
 by spaces:  ``{{ list|join(', ') }}``.
 
-The `builtin-filters`_ below describes all the builtin filters.
+The :ref:`builtin-filters` below describes all the builtin filters.
 
 
 Tests
@@ -110,7 +110,7 @@ expressions do the same:
     {% if loop.index is divisibleby 3 %}
     {% if loop.index is divisibleby(3) %}
 
-The `builtin-tests`_ below descibes all the builtin tests.
+The :ref:`builtin-tests` below descibes all the builtin tests.
 
 
 Comments
@@ -267,7 +267,7 @@ The default configuaration is no automatic escaping for various reasons:
     escaped HTML.
 
 Working with Manual Escaping
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 If manual escaping is enabled it's **your** responsibility to escape
 variables if needed.  What to escape?  If you have a variable that *may*
@@ -277,28 +277,111 @@ HTML.  Escaping works by piping the variable through the ``|e`` filter:
 ``{{ user.username|e }}``.
 
 Working with Automatic Escaping
--------------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 When automatic escaping is enabled everything is escaped by default except
 for values explicitly marked as safe.  Those can either be marked by the
 application or in the template by using the `|safe` filter.  The main
-problem with this approach is that python itself doesn't have the concept
+problem with this approach is that Python itself doesn't have the concept
 of tainted values so the information if a value is safe or unsafe can get
 lost.  If the information is lost escaping will take place which means that
 you could end up with double escaped contents.
 
 Double escaping is easy to avoid however, just relay on the tools Jinja2
-provides and don't use builtin python constructs such as the string modulo
+provides and don't use builtin Python constructs such as the string modulo
 operator.
 
 Functions returning template data (macros, `super`, `self.BLOCKNAME`) return
 safe markup always.
 
 String literals in templates with automatic escaping are considered unsafe
-too.  The reason for this is that the safe string is an extension to python
+too.  The reason for this is that the safe string is an extension to Python
 and not every library will work properly with it.
 
 
+List of Control Structures
+--------------------------
+
+A control structure refers to all those things that control the flow of a
+program - conditionals (i.e. if/elif/else), for-loops, as well as things like
+macros and blocks.  Control structures appear inside ``{% ... %}`` blocks
+in the default syntax.
+
+For Loops
+~~~~~~~~~
+
+Loop over each item in a sequece.  For example, to display a list of users
+provided in a variable called `users`:
+
+.. sourcecode:: html+jinja
+
+    <h1>Members</h1>
+    <ul>
+    {% for user in users %}
+      <li>{{ user.username|e }}</li>
+    {% endfor %}
+    </ul>
+
+Inside of a for loop block you can access some special variables:
+
++-----------------------+---------------------------------------------------+
+| `loop.index`          | The current iteration of the loop. (1 indexed)    |
++-----------------------+---------------------------------------------------+
+| `loop.index0`         | The current iteration of the loop. (0 indexed)    |
++-----------------------+---------------------------------------------------+
+| `loop.revindex`       | The number of iterations from the end of the loop |
+|                       | (1 indexed)                                       |
++-----------------------+---------------------------------------------------+
+| `loop.revindex0`      | The number of iterations from the end of the loop |
+|                       | (0 indexed)                                       |
++-----------------------+---------------------------------------------------+
+| `loop.first`          | True if first iteration.                          |
++-----------------------+---------------------------------------------------+
+| `loop.last`           | True if last iteration.                           |
++-----------------------+---------------------------------------------------+
+| `loop.length`         | The number of items in the sequence.              |
++-----------------------+---------------------------------------------------+
+| `loop.cycle`          | A helper function to cycle between a list of      |
+|                       | sequences.  See the explanation below.            |
++-----------------------+---------------------------------------------------+
+
+Within a for-loop, it's psosible to cycle among a list of strings/variables
+each time through the loop by using the special `loop.cycle` helper:
+
+.. sourcecode:: html+jinja
+
+    {% for row in rows %}
+        <li class="{{ loop.cycle('odd', 'even') }}">{{ row }}</li>
+    {% endfor %}
+
+Unlike in Python it's not possible to `break` or `continue` in a loop.  You
+can however filter the sequence during iteration which allows you to skip
+items.  The following example skips all the users which are hidden:
+
+.. sourcecode:: html+jinja
+
+    {% for user in users if not user.hidden %}
+        <li>{{ user.username|e }}</li>
+    {% endfor %}
+
+The advantage is that the special `loop` variable will count correctly thus
+not counting the users not iterated over.
+
+If no iteration took place because the sequence was empty or the filtering
+removed all the items from the sequence you can render a replacement block
+by using `else`:
+
+.. sourcecode:: html+jinja
+
+    <ul>
+    {% for user in users %}
+        <li>{{ user.username|e }}</li>
+    {% else %}
+        <li><em>no users found</em></li>
+    {% endif %}
+    </ul>
+    
+
 .. _builtin-filters:
 
 List of Builtin Filters
@@ -312,4 +395,4 @@ List of Builtin Filters
 List of Builtin Tests
 ---------------------
 
-bleh
+.. jinjatests::
index e8cf5aafd9efb411a655d3e80e30b7dc82794058..4c9766c9754b1e52157b7b8cd0534b16758cdafb 100644 (file)
@@ -90,7 +90,7 @@ def do_lower(s):
 
 
 @environmentfilter
-def do_xmlattr(_environment, *args, **kwargs):
+def do_xmlattr(_environment, d, autospace=True):
     """Create an SGML/XML attribute string based on the items in a dict.
     All values that are neither `none` nor `undefined` are automatically
     escaped:
@@ -111,14 +111,14 @@ def do_xmlattr(_environment, *args, **kwargs):
         </ul>
 
     As you can see it automatically prepends a space in front of the item
-    if the filter returned something.
+    if the filter returned something unless the second parameter is `False`.
     """
     rv = u' '.join(
         u'%s="%s"' % (escape(key), escape(value))
         for key, value in dict(*args, **kwargs).iteritems()
         if value is not None and not isinstance(value, Undefined)
     )
-    if rv:
+    if autospace and rv:
         rv = u' ' + rv
     if _environment.autoescape:
         rv = Markup(rv)
@@ -310,10 +310,7 @@ def do_urlize(value, trim_url_limit=None, nofollow=False):
 
 
 def do_indent(s, width=4, indentfirst=False):
-    """
-    {{ s|indent[ width[ indentfirst[ usetab]]] }}
-
-    Return a copy of the passed string, each line indented by
+    """Return a copy of the passed string, each line indented by
     4 spaces. The first line is not indented. If you want to
     change the number of spaces or indent the first line too
     you can pass additional parameters to the filter:
@@ -330,8 +327,7 @@ def do_indent(s, width=4, indentfirst=False):
 
 
 def do_truncate(s, length=255, killwords=False, end='...'):
-    """
-    Return a truncated copy of the string. The length is specified
+    """Return a truncated copy of the string. The length is specified
     with the first parameter which defaults to ``255``. If the second
     parameter is ``true`` the filter will cut the text at length. Otherwise
     it will try to save the last word. If the text was in fact
@@ -597,6 +593,37 @@ class _GroupTuple(tuple):
         return tuple.__new__(cls, (key, list(value)))
 
 
+def do_list(value):
+    """Convert the value into a list.  If it was a string the returned list
+    will be a list of characters.
+    """
+    return list(value)
+
+
+def do_mark_safe(value):
+    """Mark the value as safe which means that in an environment with automatic
+    escaping enabled this variable will not be escaped.
+    """
+    return Markup(value)
+
+
+def do_reverse(value):
+    """Reverse the object or return an iterator the iterates over it the other
+    way round.
+    """
+    if isinstance(value, basestring):
+        return value[::-1]
+    try:
+        return reversed(value)
+    except TypeError:
+        try:
+            rv = list(value)
+            rv.reverse()
+            return rv
+        except TypeError:
+            raise FilterArgumentError('argument must be iterable')
+
+
 FILTERS = {
     'replace':              do_replace,
     'upper':                do_upper,
@@ -612,7 +639,7 @@ FILTERS = {
     'count':                len,
     'dictsort':             do_dictsort,
     'length':               len,
-    'reverse':              reversed,
+    'reverse':              do_reverse,
     'center':               do_center,
     'indent':               do_indent,
     'title':                do_title,
@@ -628,7 +655,7 @@ FILTERS = {
     'int':                  do_int,
     'float':                do_float,
     'string':               soft_unicode,
-    'list':                 list,
+    'list':                 do_list,
     'urlize':               do_urlize,
     'format':               do_format,
     'trim':                 do_trim,
@@ -640,6 +667,6 @@ FILTERS = {
     'round':                do_round,
     'sort':                 do_sort,
     'groupby':              do_groupby,
-    'safe':                 Markup,
+    'safe':                 do_mark_safe,
     'xmlattr':              do_xmlattr
 }
index 8a0eb6a53ea0f845b330954a8b0d868accd36576..8bc1307d3d3b048c662f04e7158e7472c4361595 100644 (file)
@@ -52,7 +52,7 @@ class Parser(object):
             ext = self.extensions.get(self.stream.current.value)
             if ext is not None:
                 return ext(self)
-        lineno = self.stream.current
+        lineno = self.stream.current.lineno
         expr = self.parse_tuple()
         if self.stream.current.type == 'assign':
             result = self.parse_assign(expr)