Welcome to Stencil’s documentation!¶
Contents:
Writing Templates¶
Templates are just plain text files, with special notation (called ‘tags’) to indicate where the engine should take action.
There are 3 basic types of tags:
- var
- block
- comment
Expressions¶
At several points in the syntax, an Expression can be used.
An expression can be:
- an integer
- a float
- a string
- an expression
All expressions start by looking up their first element in the Context
. For
example the expression name
will look for Context[‘name’].
After that, they behave just like Python. You can do a dict or list lookup using [], or call a function using (). Note, however, that function invocations are limited to only positional arguments.
Comments¶
Comment tags are discarded during parsing, and are only for the benefit of future template editors. They have no impact on rendering performance.
Variables¶
Var tags are used to display the values of variables. They look like this:
Hello {{ expr }}
Block Tags¶
Block tags perform some action, may render some output, and may “contain” other tags.
{% include 'another.html' %}
Built In Tags¶
for¶
The for
tag allows you to loop over a sequence.
{% for x in expr %}
…
{% endfor %}
The for
tag also support and else
block. It will be used if sequence
to be iterated is empty.
{% for x in empty_list %}
…
{% else %}
Nothing to show.
{% endfor %}
if¶
The if
tag allows for simple flow control based on a truthy test.
{% if expr %}
Success!
{% endif %}
It also supports negative cases:
{% if not expr %}
Failure!
{% endif %}
And, like the for
tag, it supports an else
block:
{% if expr %}
Success!
{% else %}
Failure!
{% endif %}
“Truthiness” is based on the Python concept. Here are some things that are “truthy”:
- True
- non-empty strings
- non-empty lists or dicts
- non-zero values
Conversely, things that are “falsy” are:
- False
- empty strings
- 0 and 0.0
- empty lists and dicts
include¶
The include
tag lets you render another template inline, using the current
context.
{% include expr %}
Additionally, you can pass extra expressions to be added to the context whilst the other template is being rendered.
{% include form_field.html field=current_field %}
load¶
This tag lets you load other code modules to add new tags to use in this template. See Tags for more details.
{% load 'myproject.tags' %}
The value passed is a Python import path.
extends and block¶
The extends
tag allows the use of template inheritance. A base template
can denote blocks
of content which can be overridden by templates which
extend
it.
Caution
The extends
tag only works properly if it is the very first thing in
your template.
Say we have the following base template:
<!DOCTYPE html>
<html lan="en">
<head>
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="/static/css/base.css">
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
<h1>{% block header %}Welcome!{% endblock %}</h1>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2016 Me!</p>
</footer>
{% block footer_scripts %}{% endblock %}
</body>
</html>
Now, when rendered itself, it will show as:
<!DOCTYPE html>
<html lan="en">
<head>
<title>Welcome!</title>
<link rel="stylesheet" type="text/css" href="/static/css/base.css">
</head>
<body>
<header>
<h1>Welcome!</h1>
</header>
<main>
</main>
<footer>
<p>© 2016 Me!</p>
</footer>
</body>
</html>
However, if we write another template which extends this one, we just have to
write now the blocks
we want to override:
{% extends 'base.html' %}
{% block title %}My Title!{% endblock %}
{% block content %}
Welcome to my first page!
{% endblock %}
This will override only the two given blocks content.
Any content outside of block
tags will be ignored.
with¶
Using with
you can temporarily assign new values in the context from
expressions. This can help avoid repeated work.
{% with url=make_url(page) %}
<a href="{{ url }}" class="link {% if is_current_url(url) %}current{% endif %}">{{ page.title }}</a>
{% endwith %}
case/when¶
Allows switching between multiple blocks of template depending on the value of a variable.
{% case foo.bar %}
{% when 1 %}
You got one!
{% when 2 %}
You got two!
{% else %}
You got some!
{% endcase %}
The optional {% else %} clause is used if no when cases match.
Using Templates¶
To use stencil templates there is very little to do.
From Strings¶
To build a template from a string, just create a stencil.Template
instance:
>>> from stencil import Template
>>> t = Template('''Hello, {{name}}!''')
And to render it:
>>> t.render({'name': 'Bob'})
'Hello, Bob!'
From a file¶
First you’ll need to create a TemplateLoader
, passing it a list of paths to
search for templates.
>>> from stencil import TemplateLoader
>>> loader = TemplateLoader(['templates/'])
You can ask it to load a template freshly calling TemplateLoader.load
>>> t = loader.load('base.html')
The TemplateLoader
can also cache loaded, parsed templates if you treat it
as a dict:
>>> t = loader['base.html']
# Loads template from file.
>>> s = loader['base.html']
# Returns the same template instance.
Context¶
When rendering a template, you need to pass it a Context
- this is the
limit of information the template can access.
When instantiating a Context, you can pass it the information you want available to the template.
>>> ctx = stencil.Context({'a': True})
Rendering¶
Finally, to render a template, call its render()
method, passing a context.
>>> output = t.render(ctx)
Additionally, you can pass a file-like object for the template to write into:
>>> with open('output.html', 'w') as fout:
... t.render(ctx, fout)
Escaping¶
By default, all variables (e.g. {{ var }}
) will be escaped, using
html.escape
.
Values can be marked as “safe”, and thus not requiring escaping, by wrapping
them in stencil.SafeStr
.
Alternatively, any object whose __safe__
attribute is Truethy will not be
escaped.
Alternate Escaping¶
You can override the escaping function used when constructing the Context
.
>>> ctx = Context({...}, escape=my_escape)
Extending stencil¶
Stencil allows you to easily add new tags.
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 Expression.parse()
function. This will parse then value passed, and construct an Expression
instance.
Then in render, call the expression’s .resolve(context)
to get its value.
For more fine grained parsing, and to parse key=expr
syntax, create an
Expression
instance:
expr = Expression(content)
This provides several useful methods:
value = tokens._parse()
Parses a single argument, be it a string, float or int literal, or a lookup.
kwargs = expr.parse_kwargs()
Parse key=expression sequences, and construct a dict of key: Expression() items.
Why?¶
There are plenty of template engines in Python, and I’ve even written my own powerful, super-fast one (knights-templater), so why write another?
I was experimenting with AWS’ Serverless concept, and was saddened to learn it only supports Python 2.7 currently. I wanted templating, but felt back-porting K-T to Py2 just wasn’t warranted.
So I figured, why not see how small I can make a functional template language?
Apparently, “under 400 lines of code” is the answer…
Since then AWS Lambda picked up support for Python3, and stencil has grown considerably - in features, more than code size.