TemplateParser » History » Version 32
« Previous -
Version 32/56
(diff) -
Next » -
Current version
Elmer de Looff, 2012-04-16 11:50
Documented tag indexing
TemplateParser¶
- Table of contents
- TemplateParser
- Template class
- Parser class
- Using TemplateParser inside µWeb
- Templating language syntax
- The Template class, used to parse the templating language
- The Parser class, which provides template loading and caching
- Using TemplateParser inside a µWeb PageMaker
- Template syntax, an overview of the language's constructs and behaviors
First though, to help with understanding the TemplateParser, a minimal size template document:
Hello [title] [name]
The above document contains two simple template tags. These tags are delimited by square brackets, and they will be replaced by the named argument provided during parsing. If this name is not present, then the literal presentation of the tag will remain in the output.
Template class¶
The Template
class provides the interface for pre-parsing templates, loading them from files and parsing single templates to completion. During pre-parsing, constructs such as loops and conditional statements are converted to TemplateLoop
and TemplateConditional
objects, and their scopes nested appropriately in the Template
. Tags are replaced by TemplateTag
instances, and text is captured in TemplateText
. All of these provide Parse
methods, which together result in the combined parsed template output.
Creating a template¶
A template is created simple by providing a string input to the Template
's constructor. This will return a valid Template instance (or raise an error if there is a problem with the syntax:
>>> import templateparser
>>> template = templateparser.Template('Hello [title] [name]')
>>> template
Template([TemplateText('Hello '), TemplateTag('[title]'), TemplateText(' '), TemplateTag('[name]')])
Above can be seen the various parts of the template, which will be combined to output once parsed.
Loading a template from file¶
The Template
class provides a classmethod
called FromFile
, which loads the template at the path.
Loading a template named example.utp
from the current working directory:
>>> import templateparser
>>> template = templateparser.Template.FromFile('example.utp')
>>> template
Template([TemplateText('Hello '), TemplateTag('[title]'), TemplateText(' '), TemplateTag('[name]')])
Parsing a template¶
Parsing a template can be done by calling the Template
's Parse
method. The keyword arguments provided to this call will from the replacement mapping for the template. In the following example, we will provide one such keyword, and leave the other undefined to show the (basic) behavior of the Template.Parse
method.
>>> import templateparser
>>> template = templateparser.Template('Hello [title] [name]')
>>> template.Parse(title='sir')
'Hello sir [name]'
Parser class¶
The Parser
class provides simple management of multiple Template
objects. It is mainly used to load templates from disk. When initiating a Parser
, the first argument provides the search path from where templates should be loaded (the default is the current working directory). An optional second argument can be provided to preload the template cache: a mapping of names and Template
objects.
Loading templates¶
Creating a parser object, and loading the 'example.utp' template from the 'templates' directory works like this:
>>> import templateparser
>>> # This sets the 'templates' directory as the search path for AddTemplate
>>> parser = templateparser.Parser('templates')
>>> # Loads the 'templates/example.utp' and stores it as 'example.utp':
>>> parser.AddTemplate('example.utp')
>>> parser.Parse('example.utp', title='mister', name='Bob Dobalina')
'Hello mister Bob Dobalina'
The AddTemplate
method takes a second optional argument, which allows us to give the template a different name in the cache:
>>> parser = templateparser.Parser('templates')
>>> parser.AddTemplate('example.utp', name='greeting')
>>> parser.Parse('greeting', title='mister', name='Bob Dobalina')
'Hello mister Bob Dobalina'
As you can see, the name of the template in the cache is not necessarily the same as the one on disk. Often though, this is not necessary to change, so AddTemplate
need only be called with one argument. Or not at all, as the following section will show.
Template cache and auto-loading¶
The Parser
object behaves like a slightly modified dictionary to achieve this. Retrieving keys yields the associated template. Keys that are not present in the cache are automatically retrieved from the filesystem:
>>> import templateparser
>>> parser = templateparser.Parser('templates')
>>> 'example.utp' in parser
False # Since we haven't loaded it, the template it not in the parser
>>> parser
Parser({}) # The parser is empty (has no cached templates)
Attempting to parse a template that doesn't exist in the parser cache triggers an automatic load:
>>> parser['example.utp'].Parse(title='mister', name='Bob Dobalina')
'Hello mister Bob Dobalina'
>>> 'example.utp' in parser
True
>>> parser
Parser({'example.utp': Template([TemplateText('Hello '), TemplateTag('[title]'),
TemplateText(' '), TemplateTag('[name]')])})
If these cannot be found, TemplateReadError
is raised:
>>> import templateparser
>>> parser = templateparser.Parser('templates')
>>> parser['bad_template.utp'].Parse(failure='imminent')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/var/lib/underdark/libs/uweb/templateparser.py", line 147, in __getitem__
self.AddTemplate(template)
File "/var/lib/underdark/libs/uweb/templateparser.py", line 171, in AddTemplate
raise TemplateReadError('Could not load template %r' % template_path)
underdark.libs.uweb.templateparser.TemplateReadError: Could not load template 'templates/bad_template.utp'
Parse
and ParseString
methods¶
For convencience and consistency, the Parser
comes with two handy methods to provide parsing of Template
objects, one from its cache, one from raw template strings. It is recommended to use these over the previously shown direct key-based access:
>>> import templateparser
>>> parser = templateparser.Parser('templates')
>>> parser.Parse('example.utp', title='mister', name='Bob Dobalina')
'Hello mister Bob Dobalina'
>>> parser.ParseString('Hello [title] [name]', title='mister', name='Bob Dobalina')
'Hello mister Bob Dobalina'
Using TemplateParser inside µWeb¶
Within the default µWeb PageMaker
, there is a parser
property, which provides a Parser object. The class constant TEMPLATE_DIR
provides the template search directory. The default template directory is 'templates'
. N.B. This path is relative to the file that contains the PageMaker class.
An example of TemplateParser to create a complete response:
import uweb
import time
class PageMaker(uweb.PageMaker):
def VersionPage(self):
return self.parser.Parse(
'version.utp', year=time.strftime('%Y'), version=uweb.__version__)
The example template for the above file could look something like this:
<!DOCTYPE html>
<html>
<head>
<title>µWeb version info</title>
</head>
<body>
<p>µWeb version [version] - Copyright 2010-[year] Underdark</p>
</body>
</html>
Templating language syntax¶
The templating syntax is relatively limited, but with the limited syntax it provides a flexible and rich system to create templates. Covered in these examples are:- Simple tags (used in various examples above)
- Tag indexing
- Tag functions
- Template language constructs
- The example template
- The python invocation string (the template will be named 'example.utp')
- The resulting output (as source, not as parsed HTML)
Simple tags¶
This is an example for the most basic form of template tags. The tag is enclosed by square brackets as such: [tag]
. Tags that match a provided argument to the Parse call get replaced. If there is no argument that matches the tag name, it is returned in the output verbatim. This is also demonstrated in the below example
The example below is a repeat of the example how to use TemplateParser inside µWeb, and shows the template result:
<!DOCTYPE html>
<html>
<head>
<title>µWeb version info</title>
</head>
<body>
<p>µWeb version [version] - Copyright 2010-[year] Underdark</p>
<p>
This [paragraph] is not replaced because there is no
paragraph argument provided to the parser.
</p>
</body>
</html>
>>> parser.Parse('version.utp', year=time.strftime('%Y'), version=uweb.__version__)
<!DOCTYPE html>
<html>
<head>
<title>µWeb version info</title>
</head>
<body>
<p>µWeb version 0.11 - Copyright 2010-212 Underdark</p>
<p>
This [paragraph] is not replaced because there is no
paragraph argument provided to the parser.
</p>
</body>
</html>
Tag indexing¶
In addition to simple (re)placement of strings using the TemplateParser
, you can also provide it with a list
, dictionary
, or other indexable object, and from it, fetch various indices
, keys
or attributes
. The separation character between the tagname and the index is the colon (":"):
List/tuple index addressing¶
This is [var:0] [var:1].
>>> parser.Parse('message.utp', var=('delicious', 'spam'))
This is delicious spam.
Dictionary key addressing¶
This is [var:adjective] [var:noun].
>>> parser.Parse('message.utp', var={'adjective': 'delicious', 'noun': 'spam'})
This is delicious spam.
Attribute name addressing¶
This is [var:adjective] [var:noun].
>>> class Struct(object):
... pass
...
>>> var = Struct()
>>> var.adjective = 'delicious'
>>> var.noun = 'spam'
>>> parser.Parse('message.utp', var=var)
This is delicious spam.
Lookup order¶
For objects and constructs that provide multiple ways of looking up information, the lookup order can be very important. For any of the first three steps, if they are successful, the retrieved value is returned, and no further attempts are made:
- If the
needle
is parseable as integer, it will first be used as an index. This will also work for mappings with numeric keys; - If the above fails, the
needle
is assumed to be a string-like mapping key, and this is attempted - If the above fails, the
needle
is used as an attribute name; - If all of the above fail,
TemplateKeyError
is raised, as theneedle
could not be found on the object.
Tag functions¶
Tag functions
Default html escaping¶
Adding custom functions¶
TemplateLoop¶
As a language construct, TemplateParser has an understanding of iteration. The TemplateLoop
can be compared to the Python for
-loop, or the foreach
construct in other languages (lazy iteration over the values of an iterable).
Syntax and properties¶
Syntax:{{ for local_var in [collection] }}
- The double accolades (curly braces) indicate the beginning and end of the construct;
- The
for
keyword indicates the structure to execute; local_var
is the name which references the loop variable;[collection]
is the tag that provides the iteratable.
- The local name is stated without brackets (as it's no tag itself)
- When it needs to be placed in the output, the local name should have brackets (like any other tag)
- N.B. The local variable does not bleed into the outer scope after the loop has completed.
It is therefore possible (though not recommended) to name the loop variable after the iterable:{{ for collection in [collection] }}
.
Example of a TemplateLoop
¶
<html>
<body>
<ul>
{{ for name in [presidents] }}
<li>President [name]</li>
{{ endfor }}
</ul>
</body>
</html>
>>> parser.Parse('rushmore.utp', presidents=['Washington', 'Jefferson', 'Roosevelt', 'Lincoln'])
<html>
<body>
<ul>
<li>President Washington</li>
<li>President Jefferson</li>
<li>President Roosevelt</li>
<li>President Lincoln</li>
</ul>
</body>
</html>
Inlining templates¶
Often, there will be snippets of a template that will see a lot of reuse. Page headers and footers are often the same on many pages, and having several redundant copies means that changes will have to be replicated to each of these occurrances. To reduce the need for this, TemplateParser has an inline
statement. Using this you can specify a template that is available in the [[TemplateParser#Parser]]
instance and the statement will be replaced by the template.
Of course, if the inlined template is not already in the Parser
instance, the autoloading mechanism will trigger, and the named template will be search for in the Parser
's template directory.
First, we will define our inline template, 'inline_hello.utp'
:
<p>Hello [name]</p>
Secondly, our main template, 'hello.utp'
:
<h1>Greetings</h1>
{{ inline inline_hello.utp }}
Then we parse the template:
>>> parser.Parse('hello.utp', name='Dr John')
<h1>Greetings</h1>
<p>Hello Dr John</p>
Conditional statements¶
Often, you'll want the output of your template to be dependent on the value, presence, or boolean value of another tag. For instance, we may want a print a list of attendees to a party. We start the if
conditional by checking the boolean value of the attendees
tag. If this list if not-empty, we will print the attendee names, but if it's empty (or contains only a single entry), we'll tell the user in more intelligent ways than giving them a list with zero entries:
<h1>Party attendees</h1>
{{ if len([attendees]) > 1 }}
<ol>
{{ for attendee in [attendees] }}
<li>[attendee:name]</li>
{{ endfor }}
</ol>
{{ elif [attendees] }}
<p>only [attendees:0:name] is attending.</p>
{{ else }}
<p>There are no registered attendees yet.</p>
{{ endif }}
For the case where there are several attendees:
>>> parser.Parse('party.utp', attendees=[
... {'name': 'Livingstone'},
... {'name': 'Cook'},
... {'name': 'Drake'}])
<h1>Party attendees</h1>
<ol>
<li>Livingstone</li>
<li>Cook</li>
<li>Drake</li>
</ol>
For the case where there is one attendee:
>>> parser.Parse('party.utp', attendees=[{'name': 'Johnny'}])
<h1>Party attendees</h1>
<p>Only Johnny is attending.</p>
And in the case where there are no attendees:
>>> parser.Parse('party.utp', attendees=[])
<h1>Party attendees</h1>
<p>There are no registered attendees yet.</p>
Properties of conditional statements¶
- All template keys must be referenced as proper tag
This is to prevent mixing of the template variables with the functions and reserved names of Python itself. Conditional expressions are evaluated usingeval()
, and proper tags are replaced by temporary names, the values of which are stored in a retrieve-on-demand dictionary. This makes them perfectly safe with regard to the value of template replacements, but some care should be taken with the writing of the conditional expressions. - It is possible to index tags in conditional statements
This allows for decisions based on the values in those indexes/keys. For instance,Person
objects can be checked for gender, so that the correct gender-based icon can be displayed next to them. - Referencing a tag or index that doesn't exist raises @TemplateNameError
Unlike in regular template text, there is no suitable fallback value for a tag or index that cannot be retrieved. However, in most cases this can be prevented by making use of the following property: - Statement evaluation is lazy
Template conditions are processed left to right, and short-circuited where possible. If the first member of anor
group succeeds, the return value is already known. Similarly, if the first member of anand
group fails, the second part need not be evaluated. This wayTemplateNameErrors
can often be prevented, as in most cases, presence of indexes can be confirmed before accessing.
Template unicode handling¶
Any unicode
object found while parsing, will automatically be encoded to UTF-8:
>>> template = 'Underdark [love] [app]'
>>> output = parser.ParseString(template, love=u'\u2665', app=u'\N{micro sign}Web')
>>> output
'Underdark \xe2\x99\xa5 \xc2\xb5Web' # The output in its raw UTF-8 representation
>>> output.decode('UTF8')
u'Underdark \u2665 \xb5Web' # The output converted to a Unicode object
>>> print output
Underdark ♥ µWeb # And the printed UTF-8 as we desired it.