Filtering¶
TDI allows modifying and extending its parser events before they reach the tree builder. Since these filters are applied once before the rendering process, they have no performance impact during the actual rendering.
Understanding The Build Process¶
From Template To Template Tree¶
First let’s take a look at the basic template tree building logic:
It’s more or less standard compiler logic. The steps are as follows:
- The template is fed chunk by chunk to the parser (which contains a lexer and a “tokenizer”).
- The found tokens are just messages or events like “a starttag with this name and these attributes” or “an endtag with this name”. These events are passed to the tree builder.
- The tree builder creates relations between the events by maintaining a stack of them and thus determining the current nesting. This information is used to actually build the tree.
- The template tree is the final structure containing all nodes, prepared for rendering.
Adding Filters¶
Installing event filters into the process flow described above is easy. You just need to intercept the events between step 2 (events emitted by the parser) and step 3 (events received by the tree builder). The following figure visualizes the updated logic:
And here is, how it’s implemented:
The tree builder is an object implementing two interfaces:
→ BuilderInterface
and
→ BuildingListenerInterface
. The former is used by
a user of the builder object (the code expecting the result). The latter
is used by the parser, which feeds the builder with events. Events are
implemented as different methods of the BuildingListenerInterface
,
like handle_starttag()
or handle_endtag()
.
A filter is an object, which “impersonates” a builder in every direction. It has to implement both interfaces and pass along or manipulate the method calls it receives. Sometimes filters may even generate their own. In other words, a filter object simply wraps the builder or the next filter. By stacking them that way you can easily combine as many filters as you want.
Using Filters¶
Filters can be hooked into different stages of the template loading
process. But all of them follow the same principle. You need to specify
a list of filter factories. A second parameter specifies, whether the
default filters should be applied (usually a good idea). A filter
factory is a callable returning the filter object
(→ FilterFactoryInterface
). Often the filter class
itself can be passed as factory.
There’s only one default filter right now - the encoding detection
filter - which generates handle_encoding
events, whenever it sees a
suitable declaration in the event stream.
TDI always adds a filter, which provides the filename of the template
(which may or may be not something useful). But this filter cannot be
turned off. (→ filters.FilterFilename
)
The following sections describe the different places, where you can hook in your filters.
Load Filters¶
Load filters are invoked when initially loading a template into the
memory. They are specified using the eventfilters
and
default_eventfilters
arguments in
→ Factory.__init__
and
→ Factory.replace
.
A typical load filter is the → MinifyFilter
provided by the html tools:
from tdi.tools import html as html_tools
from tdi import html
tpl = html.replace(eventfilters=[
html_tools.MinifyFilter
]).from_string("""
<html>
<head>
<title>Hello World!</title>
<style>/*<![CDATA[*/
Some style.
/*]]>*/</style>
</head>
<body>
<script>//<![CDATA[
Some script.
//]]></script>
<h1>Hello World!</h1>
</body>
""".lstrip())
tpl.render()
Note, how it minifies everything except the <script>
and <style>
elements:
<html><head><title>Hello World!</title><style>/*<![CDATA[*/
Some style.
/*]]>*/</style></head><body><script>//<![CDATA[
Some script.
//]]></script><h1>Hello World!</h1></body>
If you want to squash those, too, just add more filters:
from tdi.tools import html as html_tools
from tdi.tools import css as css_tools
from tdi.tools import javascript as javascript_tools
from tdi import html
tpl = html.replace(eventfilters=[
html_tools.MinifyFilter,
css_tools.MinifyFilter,
javascript_tools.MinifyFilter,
]).from_string("""
<html>
<head>
<title>Hello World!</title>
<style>/*<![CDATA[*/
Some style.
/*]]>*/</style>
</head>
<body>
<script>//<![CDATA[
Some script.
//]]></script>
<h1>Hello World!</h1>
</body>
""".lstrip())
tpl.render()
<html><head><title>Hello World!</title><style>Some style.</style></head><body><script>Some script.</script><h1>Hello World!</h1></body>
Note
Filter order may be important. For example, the
→ javascript.CDATAFilter
adds a CDATA wrapper around
a script block, while the → javascript.MinifyFilter
removes those. So, if you want to create CDATA containers around
your minified script, the CDATA filter needs to run afterwards.
Filters are specified in reverse running order. That is, if you write them vertically (as in the examples above), the builder is “up” and the parser is “down”.
Overlay Filters¶
Overlay filters are applied after all overlaying is done. That’s right
before pre-rendering (or rendering, if the template is not
pre-rendered). The filters are specified using the
overlay_eventfilters
and overlay_default_eventfilters
arguments
in → Factory.__init__
.
Filtering at this stage is mostly useful if you need to look at the final template (constructed by the overlays). For example, you might write a filter, which resolves script or style dependencies of the template.
Prerender Filters¶
Prerender filters are run after pre-rendering, but before actual rendering (well, as pre-rendering invokes the parser, it’s technically during the pre-rendering). The point about these filters is - they are specified per prerender-model and not per per factory. So you can apply different filters for different needs.
TDI looks if the prerender-model provides a prerender_filters()
method. If it exists, it’s called without any arguments. The method is
expected to return a dict, which may contain the well-known
eventfilters
and default_eventfilters
keys. The specified
filters are injected into the parser, which is set up to generate the
final template tree.