Linguagem de template YATL
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.
Aviso
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.
Aqui está um exemplo:
[[
if i == 0:
response.write('i is 0')
else:
response.write('i is not 0')
pass
]]
Note que `` pass`` é uma palavra-chave Python, não uma palavra-chave py4web. Alguns editores Python, como Emacs, use a palavra-chave `` pass`` para significar a divisão de blocos e usá-lo para o código re-indent automaticamente.
O linguagem de template py4web faz exatamente a mesma. Quando encontra algo como:
<html><body>
[[for x in range(10):]][[=x]] hello <br />[[pass]]
</body></html>
que traduz em um programa:
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).
Observe também que:
[[=x]]
gera
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).
Aqui está um exemplo que introduz o `` H1`` helper:
[[=H1(i)]]
que é traduzido para:
response.write(H1(i))
mediante avaliação, o objeto `` H1`` e seus componentes são recursivamente serializados, escapou e escrita para o corpo da resposta. As tags gerados pelo `` H1`` e HTML interior não escapamos. Este mecanismo garante que todo o texto - e somente texto - exibido na página web é sempre escaparam, evitando assim vulnerabilidades XSS. Ao mesmo tempo, o código é simples e fácil de depurar.
O método `` response.write (obj, escapar = True) `` recebe dois argumentos, o objeto a ser escrito e se ele tem que ser escapado (definido como `` True`` por padrão). Se `` obj`` tem um `` .xml () `` método, ele é chamado e o resultado escrito para o corpo da resposta (o argumento `` escape`` é ignorado). Caso contrário, ele usa `` __str__`` o método do objeto para serializar-lo e, se o argumento fuga é `` True``, lhe escapa. Todos os built-in helper objetos ( `` H1`` no exemplo) são objetos que sabem como serializar-se através do `` .xml () `` método.
This is all done transparently.
Nota
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.
Sintaxe básica
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
.
`` Para … in``
Em templates você pode fazer um loop sobre qualquer objeto iterável:
[[items = ['a', 'b', 'c'] ]]
<ul>
[[for item in items:]]<li>[[=item]]</li>[[pass]]
</ul>
que produz:
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
Aqui `` items`` é qualquer objeto iterável como uma lista Python, Python tupla, ou linhas objeto, ou qualquer objeto que é implementado como um iterador. Os elementos apresentados são primeiro serializado e escapou.
`` While``
Você pode criar um loop usando a palavra-chave, enquanto:
[[k = 3]]
<ul>
[[while k > 0:]]<li>[[=k]][[k = k - 1]]</li>[[pass]]
</ul>
que produz:
<ul>
<li>3</li>
<li>2</li>
<li>1</li>
</ul>
`` If … elif … else``
Você pode usar cláusulas condicionais:
[[
import random
k = random.randint(0, 100)
]]
<h2>
[[=k]]
[[if k % 2:]]is odd[[else:]]is even[[pass]]
</h2>
que produz:
<h2>
45 is odd
</h2>
Uma vez que é óbvio que `` else`` encerra a primeira `` if`` bloco, não há necessidade de um `` declaração pass``, e usando um seria incorreto. No entanto, você deve fechar explicitamente a opção `` bloco else`` com um `` pass``.
Lembre-se que, em Python “else if” está escrito `` elif`` como no exemplo a seguir:
[[
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>
Produz:
<h2>
64 is divisible by 4
</h2>
`` Tentar … exceto … 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]]
Ela irá produzir o seguinte resultado:
Hello division by zero
<br />
Este exemplo ilustra que todas as saídas gerado antes de ocorrer uma exceção é processado (incluindo a saída que precedeu a excepção) no interior do bloco de teste. “Olá” é escrito porque precede a exceção.
`` Def … return``
O linguagem de template py4web permite ao desenvolvedor definir e implementar funções que podem retornar qualquer objeto Python ou uma cadeia de texto / html. Aqui, consideramos dois exemplos:
[[def itemize1(link): return LI(A(link, _href="http://" + link))]]
<ul>
[[=itemize1('www.google.com')]]
</ul>
produz o seguinte resultado:
<ul>
<li><a href="http://www.google.com">www.google.com</a></li>
</ul>
A função `` itemize1`` devolve um objecto auxiliar que é inserido no local em que a função é chamada.
Considere agora o seguinte código:
[[def itemize2(link):]]
<li><a href="http://[[=link]]">[[=link]]</a></li>
[[return]]
<ul>
[[itemize2('www.google.com')]]
</ul>
Ela produz exactamente o mesmo resultado como acima. Neste caso, a função `` itemize2`` representa um pedaço de HTML que vai substituir a tag py4web onde a função é chamada. Observe que não existe ‘=’ na frente da chamada para `` itemize2``, já que a função não retornar o texto, mas escreve-lo diretamente para a resposta.
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.