YATL Template Language
py4web uses two distinct template languages for rendering dynamic HTML pages that contain Python code:
yatl (Yet Another Template Language) , which is considered the original reference implementation
Renoir, which is a newer and faster implementation of yatl with additional functionality
Since Renoir
does not include HTML helpers (see next chapter), py4web by default uses the Renoir
module for rendering templates and the yatl
module for helpers,
plus some minor trickery to make them work together seamlessly.
py4web also uses double square brackets [[ ... ]]
to escape Python code embedded in HTML, unless specified otherwise.
The advantage of using square brackets instead of angle brackets is that it’s transparent to all common HTML editors. This allows the developer to use those editors to create py4web templates.
Warning
Be careful not to mix Python code square brackets with other square brackets! For example, you’ll soon see syntax like this:
[[items = ['a', 'b', 'c']]] # this gives "Internal Server Error" [[items = ['a', 'b', 'c'] ]] # this works
It’s mandatory to add a space after the first closed bracket for separating the list from the Python code square brackets.
Since the developer is embedding Python code into HTML, the document
should be indented according to HTML rules, and not Python rules.
Therefore, we allow un-indented Python inside the [[ ... ]]
tags.
But since Python normally uses indentation to delimit blocks of code, we
need a different way to delimit them; this is why the py4web template
language makes use of the Python keyword pass
.
A code block starts with a line ending with a colon and ends with a
line beginning with pass
. The keyword pass
is not necessary
when the end of the block is obvious from the context.
Here is an example:
[[
if i == 0:
response.write('i is 0')
else:
response.write('i is not 0')
pass
]]
Note that pass
is a Python keyword, not a py4web keyword. Some
Python editors, such as Emacs, use the keyword pass
to signify the
division of blocks and use it to re-indent code automatically.
The py4web template language does exactly the same. When it finds something like:
<html><body>
[[for x in range(10):]][[=x]] hello <br />[[pass]]
</body></html>
it translates it into a program:
response.write("""<html><body>""", escape=False)
for x in range(10):
response.write(x)
response.write(""" hello <br />""", escape=False)
response.write("""</body></html>""", escape=False)
response.write
writes to the response body.
When there is an error in a py4web template, the error report shows the generated template code, not the actual template as written by the developer. This helps the developer debug the code by highlighting the actual code that is executed (which is something that can be debugged with an HTML editor or the DOM inspector of the browser).
Also note that:
[[=x]]
generates
response.write(x)
Variables injected into the HTML in this way are escaped by default. The
escaping is ignored if x
is an XML
object, even if escape is set
to True
(see XML later for details).
Here is an example that introduces the H1
helper:
[[=H1(i)]]
which is translated to:
response.write(H1(i))
upon evaluation, the H1
object and its components are recursively
serialized, escaped and written to the response body. The tags generated
by H1
and inner HTML are not escaped. This mechanism guarantees that
all text — and only text — displayed on the web page is always escaped,
thus preventing XSS vulnerabilities. At the same time, the code is
simple and easy to debug.
The method response.write(obj, escape=True)
takes two arguments, the
object to be written and whether it has to be escaped (set to True
by default). If obj
has an .xml()
method, it is called and the
result written to the response body (the escape
argument is
ignored). Otherwise it uses the object’s __str__
method to serialize
it and, if the escape argument is True
, escapes it. All built-in
helper objects (H1
in the example) are objects that know how to
serialize themselves via the .xml()
method.
This is all done transparently.
Note
While the response object used inside the controllers is a
full bottle.response
object, inside the yatl templates it is
replaced by a dummy object (yatl.template.DummyResponse
).
This object is quite different, and much simpler: it only has a write method!
Also, you never need to (and never should) call the response.write
method explicitly.
Basic syntax
The py4web template language supports all Python control structures.
Here we provide some examples of each of them. They can be nested
according to usual programming practice.
You can easily test them by copying the _scaffold app (see
Copying the _scaffold app) and then editing the file
new_app/template/index.html
.
for...in
In templates you can loop over any iterable object:
[[items = ['a', 'b', 'c'] ]]
<ul>
[[for item in items:]]<li>[[=item]]</li>[[pass]]
</ul>
which produces:
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
Here items
is any iterable object such as a Python list, Python
tuple, or Rows object, or any object that is implemented as an iterator.
The elements displayed are first serialized and escaped.
while
You can create a loop using the while keyword:
[[k = 3]]
<ul>
[[while k > 0:]]<li>[[=k]][[k = k - 1]]</li>[[pass]]
</ul>
which produces:
<ul>
<li>3</li>
<li>2</li>
<li>1</li>
</ul>
if...elif...else
You can use conditional clauses:
[[
import random
k = random.randint(0, 100)
]]
<h2>
[[=k]]
[[if k % 2:]]is odd[[else:]]is even[[pass]]
</h2>
which produces:
<h2>
45 is odd
</h2>
Since it is obvious that else
closes the first if
block, there
is no need for a pass
statement, and using one would be incorrect.
However, you must explicitly close the else
block with a pass
.
Recall that in Python “else if” is written elif
as in the following
example:
[[
import random
k = random.randint(0, 100)
]]
<h2>
[[=k]]
[[if k % 4 == 0:]]is divisible by 4
[[elif k % 2 == 0:]]is even
[[else:]]is odd
[[pass]]
</h2>
It produces:
<h2>
64 is divisible by 4
</h2>
try...except...else...finally
It is also possible to use try...except
statements in templates with one
caveat. Consider the following example:
[[try:]]
Hello [[= 1 / 0]]
[[except:]]
division by zero
[[else:]]
no division by zero
[[finally:]]
<br />
[[pass]]
It will produce the following output:
Hello division by zero
<br />
This example illustrates that all output generated before an exception occurs is rendered (including output that preceded the exception) inside the try block. “Hello” is written because it precedes the exception.
def...return
The py4web template language allows the developer to define and implement functions that can return any Python object or a text/html string. Here we consider two examples:
[[def itemize1(link): return LI(A(link, _href="http://" + link))]]
<ul>
[[=itemize1('www.google.com')]]
</ul>
produces the following output:
<ul>
<li><a href="http://www.google.com">www.google.com</a></li>
</ul>
The function itemize1
returns a helper object that is inserted at
the location where the function is called.
Consider now the following code:
[[def itemize2(link):]]
<li><a href="http://[[=link]]">[[=link]]</a></li>
[[return]]
<ul>
[[itemize2('www.google.com')]]
</ul>
It produces exactly the same output as above. In this case, the function
itemize2
represents a piece of HTML that is going to replace the
py4web tag where the function is called. Notice that there is no ‘=’ in
front of the call to itemize2
, since the function does not return
the text, but it writes it directly into the response.
There is one caveat: functions defined inside a template must terminate with
a return
statement, or the automatic indentation will fail.
Information workflow
For dynamically modifying the workflow of the information there are custom commands available:
extend
, include
, block
and super
. Note that they are special template
directives, not Python commands.
In addition, you can use normal Python functions inside templates.
extend
and include
Templates can extend and include other templates in a tree-like structure.
For example, we can think of a template “index.html” that extends “layout.html” and includes “body.html”. At the same time, “layout.html” may include “header.html” and “footer.html”.
The root of the tree is what we call a layout template. Just like any other HTML template file, you can edit it from the command line or using the py4web Dashboard. The file name “layout.html” is just a convention.
Here is a minimalist page that extends the “layout.html” template and includes the “page.html” template:
<!--minimalist_page.html-->
[[extend 'layout.html']]
<h1>Hello World</h1>
[[include 'page.html']]
The extended layout file must contain an [[include]]
directive,
something like:
<!--layout.html-->
<html>
<head>
<title>Page Title</title>
</head>
<body>
[[include]]
</body>
</html>
When the template is called, the extended (layout) template is loaded, and the
calling template replaces the [[include]]
directive inside the layout.
If you don’t write the [[include]]
directive inside the layout, then it will
be included at the beginning of the file. Also, if you use multiple [[extend]]
directives only the last one will be processed.
Processing continues recursively until all extend
and include
directives have been processed. The resulting template is then
translated into Python code.
Note, when an application is bytecode compiled, it is this Python code that is compiled, not the original template files themselves. So, the bytecode compiled version of a given template is a single .pyc file that includes the Python code not just for the original template file, but for its entire tree of extended and included templates.
Any content or code that precedes the [[extend ...]]
directive will
be inserted (and therefore executed) before the beginning of the
extended template’s content/code. Although this is not typically used to
insert actual HTML content before the extended template’s content, it can be
useful as a means to define variables or functions that you want to make
available to the extended template. For example, consider a template
“index.html”:
<!--index.html-->
[[sidebar_enabled=True]]
[[extend 'layout.html']]
<h1>Home Page</h1>
and an excerpt from “layout.html”:
<!--layout.html-->
[[include]]
[[if sidebar_enabled:]]
<div id="sidebar">
Sidebar Content
</div>
[[pass]]
Because the sidebar_enabled
assignment in “index.html” comes before
the extend
, that line gets inserted before the beginning of
“layout.html”, making sidebar_enabled
available anywhere within the
“layout.html” code.
It is also worth pointing out that the variables returned by the controller function are available not only in the function’s main template, but in all of its extended and included templates as well.
Extending using variables
The argument of an extend
or include
(i.e., the extended or
included template name) can be a Python variable (though not a Python
expression). However, this imposes a limitation – templates that use
variables in extend
or include
statements cannot be bytecode
compiled. As noted above, bytecode-compiled templates include the entire
tree of extended and included templates, so the specific extended and
included templates must be known at compile time, which is not possible if
the template names are variables (whose values are not determined until run
time). Because bytecode compiling templates can provide a significant speed
boost, using variables in extend
and include
should generally be
avoided if possible.
In some cases, an alternative to using a variable in an include
is
simply to place regular [[include ...]]
directives inside an
if...else
block.
[[if some_condition:]]
[[include 'this_template.html']]
[[else:]]
[[include 'that_template.html']]
[[pass]]
The above code does not present any problem for bytecode compilation
because no variables are involved. Note, however, that the bytecode
compiled template will actually include the Python code for both
“this_template.html” and “that_template.html”, though only the code for one of
those templates will be executed, depending on the value of
some_condition
.
Keep in mind, this only works for include
– you cannot place
[[extend ...]]
directives inside if...else
blocks.
Layouts are used to encapsulate page commonality (headers, footers, menus), and though they are not mandatory, they will make your application easier to write and maintain.
Template Functions
Consider this “layout.html”:
<!--layout.html-->
<html>
<body>
[[include]]
<div class="sidebar">
[[if 'mysidebar' in globals():]][[mysidebar()]][[else:]]
my default sidebar
[[pass]]
</div>
</body>
</html>
and this extending template
[[def mysidebar():]]
my new sidebar!!!
[[return]]
[[extend 'layout.html']]
Hello World!!!
Notice the function is defined before the [[extend...]]
statement –
this results in the function being created before the “layout.html” code
is executed, so the function can be called anywhere within
“layout.html”, even before the [[include]]
. Also notice the function
is included in the extended template without the =
prefix.
The code generates the following output:
<html>
<body>
Hello World!!!
<div class="sidebar">
my new sidebar!!!
</div>
</body>
</html>
Notice that the function is defined in HTML (although it could also
contain Python code) so that response.write
is used to write its
content (the function does not return the content). This is why the
layout calls the template function using [[mysidebar()]]
rather than
[[=mysidebar()]]
. Functions defined in this way can take arguments.
block
and super
The main way to make a template more modular is by using
[[block ...]]
s and this mechanism is an alternative to the
mechanism discussed in the previous section.
To understand how this works, consider apps based on the scaffolding app
welcome, which has a template layout.html. This template is extended by the template
default/index.html
via [[extend 'layout.html']]
. The contents of
layout.html predefine certain blocks with certain default content, and
these are therefore included into default/index.html.
You can override these default content blocks by enclosing your new content inside the same block name. The location of the block in the layout.html is not changed, but the contents is.
Here is a simplified version. Imagine this is “layout.html”:
<html>
<body>
[[include]]
<div class="sidebar">
[[block mysidebar]]
my default sidebar (this content to be replaced)
[[end]]
</div>
</body>
</html>
and this is a simple extending template default/index.html
:
[[extend 'layout.html']]
Hello World!!!
[[block mysidebar]]
my new sidebar!!!
[[end]]
It generates the following output, where the content is provided by the over-riding block in the extending template, yet the enclosing DIV and class comes from layout.html. This allows consistency across templates:
<html>
<body>
Hello World!!!
<div class="sidebar">
my new sidebar!!!
</div>
</body>
</html>
The real layout.html defines a number of useful blocks, and you can easily add more to match the layout your desire.
You can have many blocks, and if a block is present in the extended template
but not in the extending template, the content of the extended template is used.
Also, notice that unlike with functions, it is not necessary to define
blocks before the [[extend ...]]
– even if defined after the
extend
, they can be used to make substitutions anywhere in the
extended template.
Inside a block, you can use the expression [[super]]
to include the
content of the parent. For example, if we replace the above extending
template with:
[[extend 'layout.html']]
Hello World!!!
[[block mysidebar]]
[[super]]
my new sidebar!!!
[[end]]
we get:
<html>
<body>
Hello World!!!
<div class="sidebar">
my default sidebar
my new sidebar!
</div>
</body>
</html>
Page layout standard structure
Default page layout
The “templates/layout.html” that currently ships with the py4web _scaffold application is quite complex but it has the following structure:
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <base href="[[=URL('static')]]/">
5 <meta name="viewport" content="width=device-width, initial-scale=1">
6 <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAA=="/>
7 <link rel="stylesheet" href="css/no.css">
8 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" integrity="sha512-1PKOgIY59xJ8Co8+NE6FZ+LOAZKjy+KY8iq0G4B3CyeY6wYHN3yt9PW0XpSriVlkMXe40PTKnXrLnZ9+fkDaog==" crossorigin="anonymous" />
9 <style>.py4web-validation-error{margin-top:-16px; font-size:0.8em;color:red}</style>
10 [[block page_head]]<!-- individual pages can customize header here -->[[end]]
11 </head>
12 <body>
13 <header>
14 <!-- Navigation bar -->
15 <nav class="black">
16 <!-- Logo -->
17 <a href="[[=URL('index')]]">
18 <b>py4web <script>document.write(window.location.href.split('/')[3]);</script></b>
19 </a>
20 <!-- Do not touch this -->
21 <label for="hamburger">☰</label>
22 <input type="checkbox" id="hamburger">
23 <!-- Left menu ul/li -->
24 [[block page_left_menu]][[end]]
25 <!-- Right menu ul/li -->
26 <ul>
27 [[if globals().get('user'):]]
28 <li>
29 <a class="navbar-link is-primary">
30 [[=globals().get('user',{}).get('email')]]
31 </a>
32 <ul>
33 <li><a href="[[=URL('auth/profile')]]">Edit Profile</a></li>
34 <li><a href="[[=URL('auth/change_password')]]">Change Password</a></li>
35 <li><a href="[[=URL('auth/logout')]]">Logout</a></li>
36 </ul>
37 </li>
38 [[else:]]
39 <li>
40 Login
41 <ul>
42 <li><a href="[[=URL('auth/register')]]">Sign up</a></li>
43 <li><a href="[[=URL('auth/login')]]">Log in</a></li>
44 </ul>
45 </li>
46 [[pass]]
47 </ul>
48 </nav>
49 </header>
50 <!-- beginning of HTML inserted by extending template -->
51 <center>
52 <div>
53 <!-- Flash alert messages, first optional one in data-alert -->
54 <flash-alerts class="padded" data-alert="[[=globals().get('flash','')]]"></flash-alerts>
55 </div>
56 <main class="padded">
57 <!-- contect injected by extending page -->
58 [[include]]
59 </main>
60 </center>
61 <!-- end of HTML inserted by extending template -->
62 <footer class="black padded">
63 <p>
64 Made with py4web
65 </p>
66 </footer>
67 </body>
68 <!-- You've gotta have utils.js -->
69 <script src="js/utils.js"></script>
70 [[block page_scripts]]<!-- individual pages can add scripts here -->[[end]]
71 </html>
There are a few features of this default layout that make it very easy to use and customize:
it is written in HTML5
on line 7 it’s used the
no.css
stylesheet, see hereon line 58
[[include]]
is replaced by the content of the extending template when the page is renderedit contains the following blocks: page_head, page_left_menu, page_scripts
on line 30 it checks if the user is logged on and changes the menu accordingly
on line 54 it checks for flash alert messages
Of course you can also completely replace the “layout.html” and the stylesheet with your own.
Mobile development
Although the default layout.html is designed to be mobile-friendly, one may sometimes need to use different templates when a page is visited by a mobile device.