Edgewall Software
Home
Trac
Trac Hacks
Genshi
Babel
Bitten
Home
Download
Documentation
Mailing Lists
License
FAQ
Search:
Login
Preferences
Help/Guide
About Trac
Wiki
Timeline
Roadmap
Browse Source
View Tickets
New Ticket
Search
Context Navigation
+0
Start Page
Index
History
Editing TracDev/PortingFromGenshiToJinja
Adjust edit area height:
8
12
16
20
24
28
32
36
40
Edit side-by-side
{{{#!div class="important" style="border-radius: .3em; text-align: center; padding: 0em 2em; margin: 2em 0" ** Warning ** The following documentation corresponds to the [Proposals/Jinja Jinja] development proposal; it will be in Trac [milestone:1.3.2] if all goes well \\ (Git branch: [log:cboos.git@jinja2] (original) - [log:cboos.git@jinja2-trunk-r15379] (latest) - [https://github.com/cboos/trac.git github mirror]). Track the integration progress in #12639. }}} [[PageOutline(2-4)]] = Porting Templates from Genshi to Jinja2 The following documentation is primarily targeted at plugin developers who wish to adapt their Genshi templates to the Jinja2 template engine that will be used in Trac [milestone:1.4]. == Overview We start we some examples, showing both the legacy Genshi templates and the new Jinja2 templates, highlighting their main differences. The second part of the document describes the Python code changes, from what you need to change to trigger the use of the Jinja2 renderer instead of the legacy Genshi renderer which still kicks in if nothing changes, to the new ways of generating content. Finally we go to great length to explain the most difficult part of the migration, how to replace the deprecated `ITemplateStreamFilter` interface which has no direct equivalent with Jinja2. In the last part of this document, we try to cover all the Genshi features used by Trac and present their Jinja2 equivalent. Whenever possible, we tried to minimize these differences by customizing the Jinja2 syntax. For example, we use `${...}` for variable expansion, like Genshi does, instead of `{{...}}`. Another aspect of our usage convention is that we favor [#ifthenelse line statements] over `{% ... %}`. So even someone familiar with the "default" Jinja2 syntax should glance through this document to see how "we" use Jinja2, as summarized in the table below. Note that Genshi will be supported concurrently with Jinja2 only for a short while, for the 1.3.x development period and for the 1.4-stable period. This support will be removed in Trac [milestone:1.5.1]. If for some reason you're stuck to having to support Genshi templates, you'll have to stick to Trac 1.2.x or 1.3.1. But you really should make the transition effort as Jinja2 templates are 5-10x faster than their Genshi equivalent, for only a 1/5th of their cost in memory usage. == Examples! Before going into the details of the code changes involved and the precise differences in the template syntax between the two systems, let's see at a glance how the templates look like. === Standalone template Let's first take a simple full-contained example from the Trac source, the simple index.html / jindex.html templates. - Genshi [source:sandbox/genshi/templates/index.html@3728 index.html]: {{{#!html+genshi <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" xmlns:xi="http://www.w3.org/2001/XInclude"> <head><title>Available Projects</title></head> <body> <h1>Available Projects</h1> <ul> <li py:for="project in projects" py:choose=""> <a py:when="project.href" href="$project.href" title="$project.description"> $project.name</a> <py:otherwise> <small>$project.name: <em>Error</em> <br /> ($project.description)</small> </py:otherwise> </li> </ul> </body> </html> }}} - Jinja [source:cboos.git/trac/templates/index.html@jinja2-trunk-r15379 index.html]: {{{#!html+jinja <!DOCTYPE html> <html> <head> <title>${_("Available Projects")}</title> </head> <body> <h1>${_("Available Projects")}</h1> <ul> # for project in projects: <li> # if 'href' in project: <a href="${project.href}" title="${project.description}">${project.name}</a> # else: <small>${project.name}: <em>${_("Error")}</em> <br /> (${project.description})</small> # endif </li> # endfor </ul> </body> </html> }}} In this small example, there's no common Trac layout used (as the index is a bit special). For how a "normal" template looks like, see for example [source:cboos.git/trac/templates/diff_view.html@jinja2-trunk-r15379 diff_view.html], another small template. The generic templates in [source:cboos.git/trac/templates@jinja2-trunk-r15379 trac/templates] are also interesting because we still have their Genshi equivalent close by, in [source:cboos.git/trac/templates/genshi@jinja2-trunk-r15379 trac/templates/genshi], so you can easily compare them if you're stuck during the migration of your own templates. Note that a Jinja2 .html template can usually be rendered directly in the browser, to have a rough idea about how it will look like: {{{#!div style="border: 2px solid #ccc; margin: 1em; padding: 0 2em" {{{#!html (html mode on purpose here!) <h1>${_("Available Projects")}</h1> <ul> # for project in projects: <li> # if 'href' in project: <a href="${project.href}" title="${project.description}">${project.name}</a> # else: <small>${project.name}: <em>${_("Error")}</em> <br /> (${project.description})</small> # endif </li> # endfor </ul> }}} }}} Though there's absolutely no constraints on what text a Jinja2 template may contain, for templates that will produce HTML (or XML), it will be useful if the template is itself already '''a well-formed XML document'''. **Never** go back to the bad old habits from the ClearSilver time, were sometimes the logic in those templates took advantage of the lack of well-formedness constraints, e.g. by conditionally inserting end/start pairs of tags to split sequences. Such templates were hard to maintain, and you always have cleaner alternatives. The [./Checker jinjachecker] tool should also help you maintain well-formed templates by stripping off Jinja2 expressions and line statements before attempting to XML validate the document ([pypi:lxml] should be installed for this feature). === "Standard" templates By "standard", we mean templates that follow the standard Trac layout, and even adapt to the currently selected theme. Instead of the Genshi way of including a template containing filters, the Jinja2 way follows an "object oriented" approach, with inheritance and overriders. Consider that some named sections (or "blocks") of the base template are similar to "methods", imagine that you only have to "subclass" this base template and "reimplement" the overridable methods with your specific content, and there you have it. More specifically, you'll have to "extend" the [source:cboos.git/trac/templates/layout.html@jinja2-trunk-r15379 layout.html] template, and redefine the "head", and "content" blocks if needed. All the details are available in HtmlTemplates#Jinja2architecture, including a walkthrough for the specific example of the [source:cboos.git/trac/search/templates/search.html@jinja2-trunk-r15379 search.html] template. For the search.html example we focus on the //structure// of the templates, the //include// relationship and the decomposition in //blocks//. But we also have a complete conversion [./Example example], which displays the Genshi wiki_view.html template and the Jinja2 wiki_view.html template side-by-side, along with comments explaining the conversion choices. == Changes in the controllers == === Implementing the `IRequestHandler` interface #IRequestHandler With Genshi, the data for the template is basically a `dict`, which has to be returned by `process_request` at the same time as the template name. This hasn't changed with Jinja2. The `IRequestHandler.process_request` method has seen one, important, change: instead of returning a triple of the template, data, and content type, a simple pair of template and data must be returned. If the legacy return convention is used, this means that `'template.html'` is supposed to be a Genshi template: {{{#!py return 'template.html', data, None }}} (`None` here is interpreted to mean the default content type, i.e. `'text/html'`) The new return convention is simpler, and means that `'template.html'` is now supposed to be a Jinja2 template: {{{#!py return 'template.html', data }}} If a special content-type must be used, or if other variation on the generation of the content must be specified, this can now be done by passing a `dict`: {{{#!py return 'template.html', data, {'content_type': 'application/rss+xml'} }}} This has the advantage of supporting a few more keywords (see the API doc), and to be extensible with more metadata at little cost. Note that as long as we have to support the legacy Genshi templates, a `None` value passed as third argument won't be interpreted as an empty `dict`, but rather as an empty `content_type`. === Generating content ==== Rendering template "fragments" When one wants to directly render a template, the Chrome `render_template` can still be used, as before: {{{#!py return Chrome(self.env).render_template( req, 'query_results.html', data, None, fragment=True) }}} However, `render_template` prepares all the data needed to render a page in the full default layout. It also now consistently returns an output that is prepared to be sent back to the client. So if you need to embed the generated content in other generated content, this method is the best choice. `render_fragment` can be used instead. It returns a `Markup` string when generating output for the web (`text=False`) or an `unicode` string when generating plain text output (`text=True`). //See [source:cboos.git/trac/ticket/query.py@jinja2-trunk-r15379#L1423 Ticket Query] macro (''table'' mode)// When the fragment needs to be sent to the client, there's still a better choice than `render_template`, it's `generate_fragment`, as it won't impose as much overhead on the data dictionary as `render_template`. It's best suited for responding to XHRs: {{{#!python if req.is_xhr: # render and return the content only stream = Chrome(self.env).generate_fragment( req, 'changeset_content.html', data) req.send(stream) }}} This automatically retrieves the `use_chunked_encoding` TracIni setting and uses it to return an iterable. In any case, the returned value can be sent directly from the `Request` object. There's even a lower-level public API in `Chrome` for generating content using Jinja2 templates, which provides even greater control. //See the [source:cboos.git/trac/ticket/notification.py@jinja2-trunk-r15379#L311 rendering of the ticket change notification e-mail]// See the API documentation for further details. ==== The `tag` builder #tag Genshi provided a nice Python API for programmatically building (X)HTML elements. This consisted of the `Fragment`, `Element` and the `tag` builder, all from the `genshi.builder` module: {{{#!python from genshi import Fragment, Element, tag }}} This has now been replaced by an equivalent API which lives in `trac.util.html`, so the above import should be replaced with: {{{#!python from trac.util.html import Fragment, Element, tag }}} Note that the `html` symbol from `trac.util.html` which used to be a alias to `genshi.builder.tag` is now naturally an alias to `trac.util.html.tag`. One way to write "portable" code would be: {{{#!python from trac.util.html import html as tag }}} You can then use the `tag` builder API regardless of its origin. The behavior of the new `tag` builder is nearly the same as the old one, except that it has even more "knowledge" about the HTML format. For example, for the `class` attribute (or rather, `class_` as `class` is a reserved Python keyword), and for the `style` attribute, dicts can be given as parameters instead of plain strings. Other attributes, like `checked`, will be omitted when given a `False` value. As this special behavior could be unwanted when arbitrary XML must be generated instead of XHTML, another builder, `xml`, is now available. The `xml` builder can be used the same way as the `tag` builder, but when serialized, its only special behavior is to omit attributes which have the value `None`. ==== The `Markup` class Likewise, if you want to use the `Markup` class, you should write: {{{#!python from trac.util.html import Markup }}} In "old" versions of Trac, you'll get `genshi.core.Markup`, whereas now you'll get `markupsafe.Markup`: as we're using Jinja2, we're also making use of its direct dependency [PyPI:MarkupSafe]. ==== The `escape` function {{{#!python from trac.util.html import escape }}} Note that in a similar way to `Markup`, `escape` now also comes from `markupsafe`, with some slight adaptations, as `markupsafe.escape` always escapes the quotes, which is something we don't do by default. Hence always import `escape` from `trac.util.html`, never directly from `markupsafe` or Jinja2, unless you really know what you're doing. === Modifying the content without the `ITemplateStreamFilter` interface #ReplacingITemplateStreamFilter One of the strengths of Genshi was its ability to transform the normal HTML content and, for example, to inject arbitrary content at any point in the HTML, thanks to the use of the `Transform` stream filter and its API. However, as elegant as it was, this feature was the main performance killer of Genshi, and Jinja2 doesn't propose an equivalent, for good reasons. With Jinja2, the content is produced in one step, with no kind of post-processing. Hence the content should either be produced right away, or if it really has to be produced as an extra step, it should be produced dynamically on client-side using JavaScript. ==== Produce the correct content directly instead of relying on post-processing There were two post-processing steps from which plugin writers did benefit, possibly unknowingly: 1. the addition of the `__FORM_TOKEN` hidden parameter to <form> elements, necessary for successful POST operations 2. accessibility key enabling/disabling As this no longer happen, it's now the responsibility of plugin writers to add this <input> in their content. This is simple enough: {{{#!html+jinja <form action="#" method="post"> <input type="hidden" name="__FORM_TOKEN" value="${form_token}" /> ... </form> }}} This gets even simpler thanks to a default macro: {{{#!html+jinja <form action="#" method="post"> ${jmacros.form_token_input()} ... </form> }}} The `jmacros` in the above corresponds to the [source:cboos.git/trac/templates/macros.html@jinja2-trunk-r15371 trac/templates/macros.html] default macros, and this file is included by default (in `layout.html`), so you don't have to bother to include it yourself, as long as your template extends `layout.html`. For the accessibility key, it's also quite simple: instead of hard-coding the key as an `accesskey="e"` attribute, simply use the `accesskey('e')` function call, it will know if it has to produce the attribute or not depending on the current user preferences. ==== Modify the content in the client using JavaScript On this day, 127/898 plugins (14.1%) on trac-hacks.org make use of `filter_stream()` from the `ITemplateStreamFilter` interface. So this means this specific step of the migration, perhaps the less straightforward, will be of interest for most plugin developers. Note that though we guarantee some level of support for the `ITemplateStreamFilter` during the transition period, the new suggested way also works great with earlier versions of Trac (1.0 and 1.2, perhaps even 0.12), so there's really no reason to maintain both versions once you did the switch. One strong incentive for dropping the `ITemplateStreamFilter` usage in your code is that by not doing so you basically **kill all the performance benefits** of the switch to Jinja2. The support of `ITemplateStreamFilter` implies that we first render the page to HTML using Jinja2, then parse it back as an HTML stream and feed this stream to the Genshi filter, so that it can be transformed, and then finally rendered again(!). The steps for replacing `filter_stream()` are the following: 1. implement `ITemplateProvider` if you haven't done so already, as you'll need to provide a JavaScript file 2. implement `IRequestFilter` if you haven't done so already, as you'll need to add the <script> tag for that JavaScript file, under the same circumstances in which you'd have injected your HTML code in `filter_stream()` 3. translate the Genshi Transform filter manipulations into JavaScript: - instead of building content with the `tag` builder, use jQuery's facilities for creating HTML elements - instead of using the Transform filter API to append/prepend the content to some place in the input stream identified by XPath expressions, use jQuery DOM manipulation API We'll discuss the specific example of the ticket [source:cboos.git/tracopt/ticket/deleter.py deleter]. 1. this component already implemented `ITemplateProvider` (for providing the `ticket_delete.html` template), but the `get_htdocs_dir` didn't yet return a location. We now have to return the local `htdocs` directory, as we'll put our JavaScript file there: {{{#!diff diff --git a/tracopt/ticket/deleter.py b/tracopt/ticket/deleter.py index bd38c77..9a537bd 100644 --- a/tracopt/ticket/deleter.py +++ b/tracopt/ticket/deleter.py @@ -47,10 +47,11 @@ # ITemplateProvider methods def get_htdocs_dirs(self): - return [] + from pkg_resources import resource_filename + yield 'ticketopt', resource_filename(__name__, 'htdocs') def get_templates_dirs(self): from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] }}} Adding an `ITemplateProvider` implementation from scratch is not more complicated (if there are no templates provided by the plugin, `get_template_dirs()` can simply `return []`). 2. we need to transfer the logic at the beginning of `filter_stream()` into `post_process_request()`, i.e. the condition for which we decided to either let the content pass through unmodified or to modify it, now becomes the condition for which we decide to either add or not add our extra bit of JavaScript code. So we had: {{{#!python def filter_stream(self, req, method, filename, stream, data): if filename not in ('ticket.html', 'ticket_preview.html'): return stream ticket = data.get('ticket') if not (ticket and ticket.exists and 'TICKET_ADMIN' in req.perm(ticket.resource)): return stream # modify the stream! ... }}} which becomes now: {{{#!python def post_process_request(self, req, template, data, content_type): if template in ('ticket.html', 'ticket_preview.html'): ticket = data.get('ticket') if (ticket and ticket.exists and 'TICKET_ADMIN' in req.perm(ticket.resource)): add_script(req, 'ticketopt/ticketdeleter.js') add_script_data(req, ui={'use_symbols': req.session.get('ui.use_symbols')}) return template, data, content_type }}} i.e. the condition remains the same: //the `filename`(/`template`) is either "ticket.html" or "ticket_preview.html", and we have a ticket in the `data`, that ticket exists and we have admin perm on that ticket//; if true, we would have altered the stream in `filter_stream()`, now in `post_process_request()` we'll call `add_script`. Note that we also call `add_script_data`. Here we do it for some piece of session information which is not yet available in the default JavaScript data, but you'll probably have to do that for any piece of the template `data` you'll need to use in the JavaScript code. Don't pass the whole `data` dictionary though, that would be overkill and it's quite likely some bits won't convert readily to JSON. Pass only the information you'll need. 3. now the "juicy" part: do in JavaScript what the Transform filter did in Python. Well, actually the //browser// needs JavaScript, but you can use whatever you want in order to produce that JavaScript code. I personally recommend using CoffeeScript as it's well suited for producing the HTML snippets we'll need. a. **producing the content** \\ \\ The first Python helper method: {{{#!python # Insert "Delete" buttons for ticket description and each comment def delete_ticket(): return tag.form( tag.div( tag.input(type='hidden', name='action', value='delete'), tag.input(type='submit', value=captioned_button(req, u'–', # 'EN DASH' _("Delete")), title=_('Delete ticket'), class_="trac-delete"), class_="inlinebuttons"), action='#', method='get') }}} becomes: {{{#!coffeescript captionedButton = (symbol, text) -> if ui.use_symbols then symbol else "#{symbol} #{text}" deleteTicket = () -> $ """ <form action="#" method="get"> <div class="inlinebuttons"> <input type="hidden" name="action" value="delete"> <input type="submit" value="#{captionedButton '–', _("Delete")}" title="#{_("Delete ticket")}" class="trac-delete"> <input type="hidden" name="__FORM_TOKEN" value="#{form_token}"> </div> </form> """ }}} (`captioned_button(req, symbol, text)` is a small Python utility function, which is trivial to adapt in JavaScript; note however that it's for this part of the logic that we needed to pass `req.session.get('ui.use_symbols')` from Python to JavaScript's `ui.use_symbols` via the call to `add_script_data` in 2.) \\ \\ The second Python helper method: {{{#!python def delete_comment(): for event in buffer: cnum, cdate = event[1][1].get('id')[12:].split('-', 1) return tag.form( tag.div( tag.input(type='hidden', name='action', value='delete-comment'), tag.input(type='hidden', name='cnum', value=cnum), tag.input(type='hidden', name='cdate', value=cdate), tag.input(type='submit', value=captioned_button(req, u'–', # 'EN DASH' _("Delete")), title=_('Delete comment %(num)s', num=cnum), class_="trac-delete"), class_="inlinebuttons"), action='#', method='get') }}} becomes: {{{#!coffeescript deleteComment = (c) -> # c.id == "trac-change-3-1347886395121000" # 0123456789012 [cnum, cdate] = c.id.substr(12).split('-') $ """ <form action="#" method="get"> <div class="inlinebuttons"> <input type="hidden" name="action" value="delete-comment"> <input type="hidden" name="cnum", value="#{cnum}"> <input type="hidden" name="cdate" value="#{cdate}"> <input type="submit" value="#{captionedButton '–', _("Delete")}" title="#{_("Delete comment %(num)s", num: cnum)}" class="trac-delete"> <input type="hidden" name="__FORM_TOKEN" value="#{form_token}"> </div> </form> """ }}} Not really more complex, quite the opposite. And don't ask me what `event[1][1]` was ;-) \\ \\ b. **inserting the content at the right place** \\ \\ Now this bit of Python magic: {{{#!python buffer = StreamBuffer() return stream | Transformer('//div[@class="description"]' '/h3[@id="comment:description"]') \ .after(delete_ticket).end() \ .select('//div[starts-with(@class, "change")]/@id') \ .copy(buffer).end() \ .select('//div[starts-with(@class, "change") and @id]' '//div[@class="trac-ticket-buttons"]') \ .append(delete_comment) }}} becomes a more straightforward sequence of jQuery calls: {{{#!coffeescript $(document).ready () -> # Insert "Delete" buttons for ticket description and each comment $('#ticket .description h3').after(deleteTicket()) $('#changelog div.change').each () -> $('.trac-ticket-buttons', this).prepend deleteComment this }}} For those of you not so familiar with CoffeeScript, here is the corresponding plain JavaScript (assuming we're already in the document ready() callback): {{{#!javascript $('#ticket .description h3').after(deleteTicket()); $('#changelog div.change').each(function() { $('.trac-ticket-buttons', this).prepend(deleteComment(this)); }); }}} See the full changeset, [bf49f871/cboos.git] (and [be0ff94a/cboos.git] for a look to the generated .js). === i18n support for plugins #i18n The basics remain the same, we use Babel for extracting the source strings from Jinja2 templates and for performing the translations at runtime. One notable differences though is how the //domain// used by the plugin specific catalogs is specified. With Genshi, the templates themselves contained a directive which specified the name of the domain, while Jinja2 doesn't provide this facility. It is therefore up to the controller to do this job, and we use for that the //metadata// `dict` returned by [#IRequestHandler IRequestHandler.process_request]. The specification for the extraction is slightly more verbose than it use to be with Genshi. As we use a syntax for the Jinja2 templates which is different than the default ([#TheJinja2syntaxusedbyTrac see below]), it has to be specified for the extractor as well. In what follows, we'll take the example of the SpamFilter plugin. In the `setup.cfg`, we added the `mapping_file` parameter to the `[extract_messages]` section. {{{#!ini [extract_messages] add_comments = TRANSLATOR: msgid_bugs_address = trac@dstoecker.de output_file = tracspamfilter/locale/messages.pot keywords = _ ngettext:1,2 N_ tag_ cleandoc_ Option:4 BoolOption:4 IntOption:4 ChoiceOption:4 ListOption:6 ExtensionOption:5 ConfigSection:2 width = 72 mapping_file = messages.cfg }}} The other i18n sections are unchanged. This makes it possible to specify all the extraction details in a specific configuration file, `messages.cfg`: {{{#!ini # mapping file for extracting messages from Jinja2 templates into # trac/locale/messages.pot (see setup.cfg) [extractors] python = trac.dist:extract_python html = trac.dist:extract_html text = trac.dist:extract_text [python: **.py] [html: **/templates/**.html] extensions = jinja2.ext.with_ variable_start_string = ${ variable_end_string = } line_statement_prefix = # line_comment_prefix = ## trim_blocks = yes lstrip_blocks = yes newstyle_gettext = yes [text: **/templates/**.txt] extensions = jinja2.ext.with_ variable_start_string = ${ variable_end_string = } line_statement_prefix = # line_comment_prefix = ## trim_blocks = yes lstrip_blocks = yes newstyle_gettext = yes }}} The `html` extractor (i.e. the `trac.dist.extract_html` function) will be applied to all HTML files `**/templates/**.html`, while the `text` extractor (the `trac.dist.extract_text` function). wil be applied on the "text" files `**/templates/**.txt`. The HTML extractor "auto-detects" in a simple but effective way the presence of Genshi i18n directives, and will use the legacy Genshi extractor in that case. It is therefore safe to use this new way even before the integrality of the templates has been migrated to Jinja2. Note that as we have a dedicated mapping file anyway, we specify the extractors directly there, so we no longer needs to do that in the setup.py file: {{{#!diff Index: setup.py =================================================================== --- setup.py (revision 15351) +++ setup.py (revision 15352) @@ -23,13 +23,6 @@ cmdclass = get_l10n_cmdclass() if cmdclass: extra['cmdclass'] = cmdclass - extractors = [ - ('**.py', 'trac.dist:extract_python', None), - ('**/templates/**.html', 'genshi', None) - ] - extra['message_extractors'] = { - 'tracspamfilter': extractors, - } except ImportError: pass }}} See r15352 for the full changeset. Finally, as mentioned earlier, we need to specify the domain from the Python code: {{{#!diff Index: tracspamfilter/admin.py =================================================================== --- tracspamfilter/admin.py (revision 15352) +++ tracspamfilter/admin.py (revision 15353) @@ -113,6 +113,8 @@ add_stylesheet(req, 'spamfilter/admin.css') data['accmgr'] = 'ACCTMGR_USER_ADMIN' in req.perm + if page == 'config': + return 'admin_spamconfig.html', data, {'domain': 'tracspamfilter'} return 'admin_spam%s.html' % page, data, None # ITemplateProvider methods }}} In that case, only the `'admin_spamconfig.html'` template has been converted to Jinja2, the other templates remain Genshi templates, hence the value of `None` as third member of the return tuple for them. See r15353 for the full changeset. == Migrating to Jinja2 templates === The Jinja2 syntax used by Trac The Jinja2 template engine is quite flexible and its syntax can be customized to some extent. We took this opportunity to make it as close as possible to the Genshi template syntax, in particular for the variable expansion. We use the following configuration: || key || value || example / explanation || extensions || jinja2.ext.with_ jinja2.ext.i18n || `with` directive can be used ([#with more]) || block_start_string || {% || \ {{{#!td rowspan=2 `{% if cond %} value {% endif %}` possible (but discouraged) }}} |-- || block_end_string || %} || || variable_start_string || ${ || \ {{{#!td rowspan=2 `${the_variable}` (but not `$the_variable`) ([#expandavariable more]) }}} |-- || variable_end_string || } || || line_statement_prefix || # || \ {{{#!td {{{ # if cond: value # endif }}} (preferred form) ([#if more]) }}} |-- || line_comment_prefix || ## || `## comments are good` || trim_blocks || yes || whitespace removal after a block || lstrip_blocks || yes || whitespace removal before a block || newstyle_gettext || yes || i.e. like the Trac `gettext` === HTML differences between Jinja2 and Genshi templates We took the opportunity to **switch to HTML5** while performing this template overhaul. You should probably do the same. This usually won't change anything for your templates, except for a few details. The doctype and the <html> element should be changed from Genshi's: {{{#!html+genshi <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://genshi.edgewall.org/" xmlns:i18n="http://genshi.edgewall.org/i18n" xmlns:xi="http://www.w3.org/2001/XInclude"> }}} to the somewhat simpler: {{{#!xml <!DOCTYPE html> <html> }}} Special care should be taken when dealing with //void elements//. In XHTML, it's fine to write: {{{#!xml <div id="description"/> }}} However, when going to HTML5, you should use instead: {{{#!xml <div id="description"></div> }}} The valid [https://www.w3.org/TR/html-markup/syntax.html#void-elements void elements] in HTML5 are limited to the following list: - area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr === Detailed guide of differences between Jinja2 and Genshi Most of the time, the porting is a straightforward operation. ==== expand a variable - Genshi [[html+genshi(<b>$the_variable</b>)]] - Jinja2 [[html+jinja(<b>${the_variable}</b>)]] Note that Jinja2 doesn't support the `$the_variable` style, the curly braces are mandatory. Tip: {{{#!shell sed -i -e 's,\$\([a-z_][a-z._]*\),${\1},g' template.html }}} ==== expand a simple computation - Genshi [[html+genshi(<b>${the_variable + 1}</b>)]] - Jinja2 [[html+jinja(<b>${the_variable + 1}</b>)]] However, Jinja2 expressions are only //similar// to Python expressions, there are a few differences and limitations, see [http://jinja.pocoo.org/docs/dev/templates/#expressions expressions] doc. See also [#set set complex variables] below for more involved examples. Another customization we made to Jinja2 is to avoid having a Python `None` value be expanded to the `"None"` string. Instead, we make it produce an empty string, like Genshi did. ==== include another template #include - Genshi [[xml(<xi:include href="the_file.html"><xi:fallback/></xi:include>)]] - Jinja2 [[xml(# include "the_file.html" ignore missing)]] See [http://jinja.pocoo.org/docs/dev/templates/#include include] doc. Note that the `ignore missing` part is mandatory no templates are given to the `include` directive. This includes the following situation: {{{#!html+jinja # include "sometemplate.html" if false }}} This alone would raise an error. The correct thing to write is: {{{#!html+jinja # include "sometemplate.html" if false ignore missing }}} It is also possible to pass "parameters" to included templates. Those are not actually declared parameters, simply variables expected to be available in the template that can be set for the scope of one specific include. This works exactly like in Genshi, with a `with` statement: - Genshi {{{#!html+genshi <xi:include href="ticket_box.html" py:with="can_append = False; preview_mode = False"/> }}} - Jinja {{{#!html+jinja # with # set can_append = false # set preview_mode = false # include "ticket_box.html" # endwith }}} See [http://jinja.pocoo.org/docs/dev/extensions/#with-statement with] doc. In passing, note how the boolean constants slightly differ from Python, `False` becomes `false`, `True` becomes `true` and `None` is `none`. Caveat: contrary to the Genshi way, one shouldn't include the `"layout.html"` template in order to inherit the default page layout. There's a big difference in the way template "inheritance" works between Genshi and Jinja2, see HtmlTemplates#Jinjaarchitecture for all the details. Despite these differences, we kept the same spirit and there's actually also a `"layout.html"` template that you can "inherit" from (as well as a `"theme.html"` template that the layout inherits in turn). But instead of "including" it in your template, you "extend" it: {{{#!html+jinja # extends "layout.html" }}} But this is not the place to go further in the details about how extending works, refer to the [http://jinja.pocoo.org/docs/dev/templates/#extends extends] documentation and to the link above for how this applies to Trac. ==== simple if...then (no else) #if - Genshi [[xml(<py:if test="flag"><b>OK</b></py:if>)]] or simply: [[xml(<b py:if="flag">OK</b>)]] - Jinja2 {{{#!html+jinja # if flag: <b>OK</b> # endif }}} See [http://jinja.pocoo.org/docs/dev/templates/#if if] doc. ==== if...then...else - Genshi {{{#!html+genshi <py:choose test="flag"> <py:when test="True"> <b>OK</b> </py:when> <py:otherwise> <i>!!!</i> </py:otherwise> </py:choose> }}} or simply: {{{#!html+genshi <py:choose> <b py:when="flag">OK</b> <i py:otherwise="">!!!</i> </py:choose> }}} - Jinja2 {{{#!html+jinja # if flag: <b>OK</b> # else: <i>!!!</i> # endif }}} If you really have to, you can also use the block style: {{{#!html+jinja {{ if flag }}<b>OK</b>{{ else }}<i>!!!</i>{{ endif }} }}} However this goes against readability and processing via the [./Checker jinjachecker] tool, so we really advise that you stick to the use of //[http://jinja.pocoo.org/docs/dev/templates/#line-statements line statements]//. ==== iterate over a collection #for - Genshi {{{#!html+genshi <ul> <py:for each="element in list"> <li>$element</li> </py:for> </ul> }}} or simply: {{{#!html+genshi <ul> <li py:for="element in list">$element</li> </ul> }}} - Jinja2 {{{#!html+jinja <ul> # for element in list: <li>${element}</li> # endfor </ul> }}} See [http://jinja.pocoo.org/docs/dev/templates/#for for] doc. ===== No need for `enumerate` - Genshi: {{{#!html+genshi <tr py:for="idx,option in enumerate(section.options)" class="${'modified' if option.modified else None}"> <th py:if="idx == 0" class="section" rowspan="${len(section.options)}">${section.name}</th> }}} - Jinja2: {{{#!html+jinja # for option in section.options: <tr ${{'class': 'modified' if option.modified}|htmlattr}> # if loop.first: <th class="section" rowspan="${len(section.options)}">${section.name}</th> }}} All common uses (and more) for such an `idx` variable are addressed by the special `loop` variable. See [http://jinja.pocoo.org/docs/dev/templates/#for loop] doc. ==== define a macro #macro - Genshi {{{#!html+genshi <py:def function="entry(key, val='--')"> <dt>$key</dt><dd>$val</dd> </py:def> }}} - Jinja2 {{{#!html+jinja # macro entry(key, val='--') <dt>${key}</dt><dd>${val}</dd> # endmacro }}} See [http://jinja.pocoo.org/docs/dev/templates/#macros macros] doc. ==== set a variable - Genshi {{{#!html+genshi <py:with vars="count = len(collection)"> We have ${count > 10 and 'too much' or count} elements. </py:with> }}} Note that we had to use `>` in Genshi, we can't use `>` directly. - Jinja2 {{{#!html+jinja # set count = len(collection) We have ${'too much' if count is greaterthan(10) else count} elements. }}} Note that we avoid using `>` in Jinja2 expressions as well, but simply to avoid that XML/HTML text editors get confused. We added a few custom tests for that (`greaterthan`, `greaterthanorequal`, `lessthan`, `lessthanorequal`). See [http://jinja.pocoo.org/docs/dev/templates/#tests tests] doc. ==== set several variables in a scope #with - Genshi {{{#!html+genshi <html py:with="is_query = report.sql.startswith('query:'); new_report = action == 'new' and not is_query; new_query = action == 'new' and is_query"> ... </html> }}} The variables are set for the scope of the element in which they are set (here <html>, so the whole document) - Jinja2 {{{#!html+jinja # with # set is_query = report.sql.startswith('query:') # set new_report = action == 'new' and not is_query ... # endwith }}} But actually you will only use `with` in specific situations, like for wrapping an include directive (see [#include]). If you're already within a `for`, a `block` or a `macro`, the scope of the assignment is already limited to that of this directive. See [http://jinja.pocoo.org/docs/dev/templates/#assignments set] and [http://jinja.pocoo.org/docs/dev/templates/#with-statement with] docs. In addition, `with` can also be used to better control how the successive `set` on a given variable are being applied (see for example [1e25c852/cboos.git] and [cc1b959e/cboos.git]). Finally be careful when using `with`: don't wrap a `block` directive within a `with`. If you want to set a global scope for the document (like in our <html> example above), it's tempting to use a single `with` statement, for clarity. But that would wrap all blocks defined in the template and strange results would ensue. ==== set HTML attributes #htmlattr In Genshi, an attribute with a `None` value wouldn't be output. However, Jinja2 doesn't know what an attribute is (or anything else about HTML, XML for that matter), so we have to use a special filter, `htmlattr`, to reproduce this behavior: - Genshi: {{{#!html+genshi <tr class="${'disabled' if all(not component.enabled for module in plugin.modules.itervalues() for component in module.components.itervalues()) else None}"> }}} - Jinja2: {{{#!html+jinja # set components = plugin.modules|map(attribute='components')|flatten <tr${{'class': 'disabled' if not components|selectattr('enabled') }|htmlattr}> }}} (the `htmlattr` filter will add a space if needed; that way, if the condition is true, you end up with `<tr class="disabled">`, otherwise with just `<tr>`) If you wonder why the `if all(...)` expression morphed into `if not components|...`, it's because Jinja2 expressions are similar to Python expressions, but not quite the same. ==== set complex variables #set Note that Jinja2 expressions are a subset of Python expressions, and for the sake of simplicity the generator expressions are not part of that subset. This limitation often requires one to make creative use of [http://jinja.pocoo.org/docs/dev/templates/#filters filters], [http://jinja.pocoo.org/docs/dev/templates/#builtin-filters built-in] or custom (`min`, `max`, `trim`, `flatten`). A few more examples: ||= Genshi ||= Jinja2 || || `${', '.join([p.name for p in faulty_plugins])}` || \ || `${faulty_plugins|map('name')|join(', ')}` || || `sum(1 for change in changes if 'cnum' in change)` || \ || `changes|selectattr('cnum')|list|count` || || `sum(1 for change in changes if 'cnum' not in change)` || \ || `changes|rejectattr('cnum')|list|count` || || ... || ... || ==== conditional wrappers There's no direct equivalent to the `py:strip`, but it can often be emulated with an `if/else/endif`. - Genshi {{{#!html+genshi <a py:strip="not url" href="$url">$plugin.name</a> }}} - Jinja2 {{{#!html+jinja # if url: <a href="${url}">${plugin.name}</a> # else: ${plugin.name} # endif }}} If the repeated content is complex, one can use a //block assignment// (see below). ==== i18n #trans Genshi had a pretty good notion of what was a piece of translatable text within an HTML template, but Jinja2 doesn't, so there's no "guessing" and no i18n will happen unless explicitly asked. This can be done in two ways. First, with translation expressions, using the familiar `_()` gettext function (`gettext` and `ngettext` also supported). - Genshi: [[xml(<strong>Trac detected an internal error:</strong>)]] - Jinja2: [[xml(<strong>${_("Trac detected an internal error:")}</strong>)]] Second, using `trans` directives. - Genshi: {{{#!html+genshi <p i18n:msg="create">Otherwise, please ${create_ticket(tracker)} a new bug report describing the problem and explain how to reproduce it.</p> }}} - Jinja2: {{{#!html+jinja <p> # trans create = create_ticket(tracker) Otherwise, please ${create} a new bug report describing the problem and explain how to reproduce it. # endtrans </p> }}} another example, without assignment: {{{#!html+jinja # trans formatted In the default time zone, this would be displayed as <strong>${formatted}</strong>. # endtrans }}} last example, two expanded variables, with and without assignment: {{{#!html+jinja # trans tz = nowtz.tzname(), formatted In your time zone ${tz}, this would be displayed as <strong>${formatted}</strong>. # endtrans }}} See [http://jinja.pocoo.org/docs/dev/templates/#i18n i18n] doc. Note that only direct variable expansions are supported in `trans` blocks, nothing more complex. So one way to deal with complex translatable content is to factor out the complex parts in variable blocks. See [http://jinja.pocoo.org/docs/dev/templates/#block-assignments block assignments] doc. - Genshi: {{{#!html+genshi <p i18n:msg="">There was an internal error in Trac. It is recommended that you notify your local <a py:strip="not project.admin" href="mailto:${project.admin}"> Trac administrator</a> with the information needed to reproduce the issue. </p> }}} - Jinja2: {{{#!html+jinja <p> # set trac_admin = _("Trac administrator") # set project_admin # if project.admin: <a href="mailto:${project.admin}">${trac_admin}</a> # else: ${trac_admin} # endif # endset # trans project_admin There was an internal error in Trac. It is recommended that you notify your local ${project_admin} with the information needed to reproduce the issue. # endtrans </p> }}} Note that another tricky case is when you want to use `gettext` and one of the variables is Markup. Using the `|safe` filter is needed, but that's not enough, as currently `gettext()` doesn't support Markup, you need to use `tgettext()` which is available with the `tag_` shortcut: {{{#!html+jinja <em> # set preferences_link <a href="${href.prefs()}" class="trac-target-new">${ _("Preferences")}</a> # endset ${tag_("Set your email in %(preferences_link)s", preferences_link=preferences_link|safe)} </em> }}} ==== Tips and tricks - when you need to output a plain "#" character at the beginning of a line, this will be parsed as a line statement; the trick here is to use an empty inline comment as a prefix: `{##}# ...` (see [61e8d9ec/cboos.git]) - when you need to output indented text, this can be made difficult due to our `lstrip_block` configuration setting; you can work around this by embedding Python strings with the exact whitespace content you need in variable expressions: ` ${"\t "}` (see [0fdef480/cboos.git])
Note:
See
WikiFormatting
and
TracWiki
for help on editing wiki content.
Change information
Your email or username:
E-mail address and name can be saved in the
Preferences
Comment about this change (optional):
Note:
See
TracWiki
for help on using the wiki.