Extending stencil

Stencil allows you to easily add new tags and filters.

Filters

As noted in Custom filters, you can easily register new filter functions.

Here is an example of adding an escape filter:

escape_html = lambda text: (
    text.replace('&', '&')
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace('"', "&quot;")
        .replace("'", "&#x27;")
)


_js_escapes = {
    ord('\\'): '\\u005C',
    ord("'"): '\\u0027',
    ord('"'): '\\u0022',
    ord('>'): '\\u003E',
    ord('<'): '\\u003C',
    ord('&'): '\\u0026',
    ord('='): '\\u003D',
    ord('-'): '\\u002D',
    ord(';'): '\\u003B',
    ord('\u2028'): '\\u2028',
    ord('\u2029'): '\\u2029'
}

# Escape every ASCII character with a value less than 32.
_js_escapes.update((ord('%c' % z), '\\u%04X' % z) for z in range(32))


escape_js = lambda text: text.translate(_js_escapes)

def escape(value, mode='html'):
    if mode == 'html':
        return escape_html(value)
    elif mode == 'js':
        return escape_js(value)
    raise ValueError('Invalid escape mode: %r' % mode)

stencil.FILTERS['escape'] = escape

And we use it in our template:

<input type="text" value="{{ value|escape }}">

Now we can use it:

>>> from stencil import TemplateLoader, Context
>>> ctx = Context({'value': '<script>alert("BOO");</script>'})
>>> tmp.render(ctx)
u'<input type="text" value="&lt;script&gt;alert(&quot;BOO&quot;);&lt;/script&gt;">'

Tags

All tags derive from the stencil.BlockNode class, and self-register with stencil on declaration.

from stencil import BlockNode

class MyTag(BlockNode):
    name = 'my'  # This is matched in {% my %}

When stencil finds a tag matching this name, it will call the BlockNode.parse classmethod, passing it the rest of the tag content, and the template instance. This method must return a BlockNode sub-class instance.

class MyTag(BlockNode):

    @classmethod
    def parse(cls, content, parser):
        return cls(content)

The default action is to just return an instance of the class, passed the tag content.

When a template is rendered, a blocks render method will be called, passed a Context instance, and a file-like object to output to.

Tags with children

Some tags contain child nodes (e.g. for, if, block).

To do this they build a Nodelist:

class MyBlock(BlockNode):

    @classmethod
    def parse(self, content, parser):
        nodelist = parser.parse_nodelist({'endmyblock',})
        return cls(nodelist)

This will consume tags until it reaches one with a name found in the list. The tags are added to a Nodelist instance, except the matching one which it stored in Nodelist.endnode.

A Nodelist can be rendered easily by calling their render method, which works just like a BlockNode.

nodelist.render(context, output)

Expressions

To have an argument resolved as an expression, use the parse_expression function. This will parse then value passed, and construct an Expression instance.

Then in render, call Expression.resolve(context) to get its value.

For more fine grained parsing, and to parse key=expr syntax, use a Tokens class.

tokens = Tokens(content)

This provides several useful methods:

value = tokens.parse_argument()

Parses a single argument, be it a string, float or int literal, or a lookup. The result is suitable for passing as the second argument to resolve_lookup, or as the first to Expression.

value = resolve_lookup(value)
value, filters = tokens.parse_filter_expression()

Parse a filter expression, returning a value (as from parse_argument, and a list of (filter name, *args) tuples.

kwargs = tokens.parse_kwargs()

Parse key=filter-expression sequences, and construct a dict of key: Expression() items.

tokens.assert_end()

Asserts the current token to be parsed is an end marker, or raises and assertion error with a message showing where the token was.