15 | | For migrating your own templates, a good way to start is to learn from examples. |
16 | | Compare the [source:cboos.git/trac/templates@jinja2-trunk-r15379 Jinja2 trac/templates] with their [source:trunk/trac/templates Genshi counterpart]. |
17 | | |
18 | | In the first 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. |
19 | | |
20 | | The last part of the document describes the Python code changes, focusing notably on how to replace the deprecated `ITemplateStreamFilter` interface. |
21 | | |
22 | | 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. 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 the cost in memory usage. |
23 | | |
24 | | |
25 | | == The Jinja2 syntax |
26 | | |
27 | | 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. |
28 | | |
29 | | We use the following configuration: |
30 | | || key || value || example / explanation |
31 | | || extensions || jinja2.ext.with_ jinja2.ext.i18n || `with` directive can be used ([#with more]) |
32 | | || block_start_string || {% || \ |
33 | | {{{#!td rowspan=2 |
34 | | `{% if cond %} value {% endif %}` possible (but discouraged) |
35 | | }}} |
36 | | |-- |
37 | | || block_end_string || %} || |
38 | | || variable_start_string || ${ || \ |
39 | | {{{#!td rowspan=2 |
40 | | `${the_variable}` (but not `$the_variable`) |
41 | | ([#expandavariable more]) |
42 | | }}} |
43 | | |-- |
44 | | || variable_end_string || } || |
45 | | || line_statement_prefix || # || \ |
46 | | {{{#!td |
47 | | {{{ |
48 | | # if cond: |
49 | | value |
50 | | # endif |
51 | | }}} |
52 | | (preferred form) ([#if more]) |
53 | | }}} |
54 | | |-- |
55 | | || line_comment_prefix || ## || `## comments are good` |
56 | | || trim_blocks || yes || whitespace removal after a block |
57 | | || lstrip_blocks || yes || whitespace removal before a block |
58 | | || newstyle_gettext || yes || i.e. like the Trac `gettext` |
59 | | |
60 | | |
61 | | == Changes in the HTML |
62 | | |
63 | | We took the opportunity to switch to HTML5 while performing this template overhaul. |
64 | | You should probably do the same. |
65 | | This usually won't change anything for your templates, except for a few details. |
66 | | |
67 | | The doctype and the <html> element should be changed from Genshi's: |
68 | | {{{#!html+genshi |
69 | | <!DOCTYPE html |
70 | | PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" |
71 | | "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> |
72 | | <html xmlns="http://www.w3.org/1999/xhtml" |
73 | | xmlns:py="http://genshi.edgewall.org/" |
74 | | xmlns:i18n="http://genshi.edgewall.org/i18n" |
75 | | xmlns:xi="http://www.w3.org/2001/XInclude"> |
76 | | }}} |
77 | | |
78 | | to the somewhat simpler: |
79 | | {{{#!xml |
80 | | <!DOCTYPE html> |
81 | | <html> |
82 | | }}} |
83 | | |
84 | | Special care should be taken when dealing with //void elements//. |
85 | | In XHTML, it's fine to write: |
86 | | {{{#!xml |
87 | | <div id="description"/> |
88 | | }}} |
89 | | However, when going to HTML5, you should use instead: |
90 | | {{{#!xml |
91 | | <div id="description"></div> |
92 | | }}} |
93 | | The valid [https://www.w3.org/TR/html-markup/syntax.html#void-elements void elements] in HTML5 are limited to the following list: |
94 | | - area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr |
95 | | |
96 | | |
97 | | == Changes in the template syntax |
98 | | |
99 | | Most of the time, the porting is a straightforward operation. |
100 | | |
101 | | === expand a variable |
102 | | - Genshi [[html+genshi(<b>$the_variable</b>)]] |
103 | | - Jinja2 [[html+jinja(<b>${the_variable}</b>)]] |
104 | | |
105 | | Note that Jinja2 doesn't support the `$the_variable` style, the curly braces are mandatory. |
106 | | |
107 | | Tip: |
108 | | {{{#!shell |
109 | | sed -i -e 's,\$\([a-z_][a-z._]*\),${\1},g' template.html |
110 | | }}} |
111 | | |
112 | | === expand a simple computation |
113 | | - Genshi [[html+genshi(<b>${the_variable + 1}</b>)]] |
114 | | - Jinja2 [[html+jinja(<b>${the_variable + 1}</b>)]] |
115 | | |
116 | | 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. |
117 | | |
118 | | See also [#set set complex variables] below for more involved examples. |
119 | | |
120 | | 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. |
121 | | |
122 | | === include another template #include |
123 | | - Genshi [[xml(<xi:include href="the_file.html"><xi:fallback/></xi:include>)]] |
124 | | - Jinja2 [[xml(# include "the_file.html" ignore missing)]] |
125 | | |
126 | | 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: |
127 | | {{{#!html+jinja |
128 | | # include "sometemplate.html" if false |
129 | | }}} |
130 | | This alone would raise an error. The correct thing to write is: |
131 | | {{{#!html+jinja |
132 | | # include "sometemplate.html" if false ignore missing |
133 | | }}} |
134 | | |
135 | | It is also possible to pass "parameters" to included templates. |
136 | | 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: |
137 | | |
138 | | - Genshi |
139 | | {{{#!html+genshi |
140 | | <xi:include href="ticket_box.html" py:with="can_append = False; preview_mode = False"/> |
141 | | }}} |
142 | | - Jinja |
143 | | {{{#!html+jinja |
144 | | # with |
145 | | # set can_append = false |
146 | | # set preview_mode = false |
147 | | # include "ticket_box.html" |
148 | | # endwith |
149 | | }}} |
150 | | |
151 | | See [http://jinja.pocoo.org/docs/dev/extensions/#with-statement with] doc. |
152 | | |
153 | | In passing, note how the boolean constants slightly differ from Python, `False` becomes `false`, `True` becomes `true` and `None` is `none`. |
154 | | |
155 | | 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. |
156 | | |
157 | | 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: |
158 | | {{{#!html+jinja |
159 | | # extends "layout.html" |
160 | | }}} |
161 | | 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. |
162 | | |
163 | | |
164 | | === simple if...then (no else) #if |
165 | | - Genshi [[xml(<py:if test="flag"><b>OK</b></py:if>)]] or simply: [[xml(<b py:if="flag">OK</b>)]] |
166 | | - Jinja2 |
167 | | {{{#!html+jinja |
168 | | # if flag: |
169 | | <b>OK</b> |
170 | | # endif |
171 | | }}} |
172 | | |
173 | | See [http://jinja.pocoo.org/docs/dev/templates/#if if] doc. |
174 | | |
175 | | === if...then...else |
176 | | - Genshi |
177 | | {{{#!html+genshi |
178 | | <py:choose test="flag"> |
179 | | <py:when test="True"> |
180 | | <b>OK</b> |
181 | | </py:when> |
182 | | <py:otherwise> |
183 | | <i>!!!</i> |
184 | | </py:otherwise> |
185 | | </py:choose> |
186 | | }}} |
187 | | or simply: |
188 | | {{{#!html+genshi |
189 | | <py:choose> |
190 | | <b py:when="flag">OK</b> |
191 | | <i py:otherwise="">!!!</i> |
192 | | </py:choose> |
193 | | }}} |
194 | | |
195 | | - Jinja2 |
196 | | {{{#!html+jinja |
197 | | # if flag: |
198 | | <b>OK</b> |
199 | | # else: |
200 | | <i>!!!</i> |
201 | | # endif |
202 | | }}} |
203 | | |
204 | | If you really have to, you can also use the block style: |
205 | | {{{#!html+jinja |
206 | | {{ if flag }}<b>OK</b>{{ else }}<i>!!!</i>{{ endif }} |
207 | | }}} |
208 | | 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]//. |
209 | | |
210 | | === iterate over a collection #for |
211 | | - Genshi |
212 | | {{{#!html+genshi |
213 | | <ul> |
214 | | <py:for each="element in list"> |
215 | | <li>$element</li> |
216 | | </py:for> |
217 | | </ul> |
218 | | }}} |
219 | | or simply: |
220 | | {{{#!html+genshi |
221 | | <ul> |
222 | | <li py:for="element in list">$element</li> |
223 | | </ul> |
224 | | }}} |
225 | | - Jinja2 |
226 | | {{{#!html+jinja |
227 | | <ul> |
228 | | # for element in list: |
229 | | <li>${element}</li> |
230 | | # endfor |
231 | | </ul> |
232 | | }}} |
233 | | See [http://jinja.pocoo.org/docs/dev/templates/#for for] doc. |
234 | | |
235 | | ==== No need for `enumerate` |
236 | | |
237 | | - Genshi: |
238 | | {{{#!html+genshi |
239 | | <tr py:for="idx,option in enumerate(section.options)" |
240 | | class="${'modified' if option.modified else None}"> |
241 | | <th py:if="idx == 0" class="section" |
242 | | rowspan="${len(section.options)}">${section.name}</th> |
243 | | |
244 | | }}} |
245 | | - Jinja2: |
246 | | {{{#!html+jinja |
247 | | # for option in section.options: |
248 | | <tr ${{'class': 'modified' if option.modified}|htmlattr}> |
249 | | # if loop.first: |
250 | | <th class="section" |
251 | | rowspan="${len(section.options)}">${section.name}</th> |
252 | | |
253 | | }}} |
254 | | |
255 | | All common uses (and more) for such an `idx` variable are addressed by the special `loop` variable. |
256 | | See [http://jinja.pocoo.org/docs/dev/templates/#for loop] doc. |
257 | | |
258 | | === define a macro #macro |
259 | | - Genshi |
260 | | {{{#!html+genshi |
261 | | <py:def function="entry(key, val='--')"> |
262 | | <dt>$key</dt><dd>$val</dd> |
263 | | </py:def> |
264 | | }}} |
265 | | - Jinja2 |
266 | | {{{#!html+jinja |
267 | | # macro entry(key, val='--') |
268 | | <dt>${key}</dt><dd>${val}</dd> |
269 | | # endmacro |
270 | | }}} |
271 | | See [http://jinja.pocoo.org/docs/dev/templates/#macros macros] doc. |
272 | | |
273 | | === set a variable |
274 | | - Genshi |
275 | | {{{#!html+genshi |
276 | | <py:with vars="count = len(collection)"> |
277 | | We have ${count > 10 and 'too much' or count} elements. |
278 | | </py:with> |
279 | | }}} |
280 | | Note that we had to use `>` in Genshi, we can't use `>` directly. |
281 | | - Jinja2 |
282 | | {{{#!html+jinja |
283 | | # set count = len(collection) |
284 | | We have ${'too much' if count is greaterthan(10) else count} elements. |
285 | | }}} |
286 | | 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`). |
287 | | See [http://jinja.pocoo.org/docs/dev/templates/#tests tests] doc. |
288 | | |
289 | | === set several variables in a scope #with |
290 | | |
291 | | - Genshi |
292 | | {{{#!html+genshi |
293 | | <html py:with="is_query = report.sql.startswith('query:'); |
294 | | new_report = action == 'new' and not is_query; |
295 | | new_query = action == 'new' and is_query"> |
296 | | ... |
297 | | </html> |
298 | | }}} |
299 | | The variables are set for the scope of the element in which they are set (here <html>, |
300 | | so the whole document) |
301 | | - Jinja2 |
302 | | {{{#!html+jinja |
303 | | # with |
304 | | # set is_query = report.sql.startswith('query:') |
305 | | # set new_report = action == 'new' and not is_query |
306 | | ... |
307 | | # endwith |
308 | | }}} |
309 | | |
310 | | 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. |
311 | | |
312 | | See [http://jinja.pocoo.org/docs/dev/templates/#assignments set] and [http://jinja.pocoo.org/docs/dev/templates/#with-statement with] docs. |
313 | | |
314 | | 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]). |
315 | | |
316 | | 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. |
317 | | |
318 | | |
319 | | === set HTML attributes #htmlattr |
320 | | 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: |
321 | | |
322 | | - Genshi: |
323 | | {{{#!html+genshi |
324 | | <tr class="${'disabled' if all(not component.enabled for module in plugin.modules.itervalues() |
325 | | for component in module.components.itervalues()) else None}"> |
326 | | }}} |
327 | | - Jinja2: |
328 | | {{{#!html+jinja |
329 | | # set components = plugin.modules|map(attribute='components')|flatten |
330 | | <tr${{'class': 'disabled' if not components|selectattr('enabled') |
331 | | }|htmlattr}> |
332 | | |
333 | | }}} |
334 | | (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>`) |
335 | | |
336 | | 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. |
337 | | |
338 | | === set complex variables #set |
339 | | 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`). |
340 | | |
341 | | A few more examples: |
342 | | |
343 | | ||= Genshi ||= Jinja2 || |
344 | | || `${', '.join([p.name for p in faulty_plugins])}` || \ |
345 | | || `${faulty_plugins|map('name')|join(', ')}` || |
346 | | || `sum(1 for change in changes if 'cnum' in change)` || \ |
347 | | || `changes|selectattr('cnum')|list|count` || |
348 | | || `sum(1 for change in changes if 'cnum' not in change)` || \ |
349 | | || `changes|rejectattr('cnum')|list|count` || |
350 | | || ... || ... || |
351 | | |
352 | | |
353 | | === conditional wrappers |
354 | | There's no direct equivalent to the `py:strip`, but it can often be emulated with an `if/else/endif`. |
355 | | - Genshi |
356 | | {{{#!html+genshi |
357 | | <a py:strip="not url" href="$url">$plugin.name</a> |
358 | | }}} |
359 | | - Jinja2 |
360 | | {{{#!html+jinja |
361 | | # if url: |
362 | | <a href="${url}">${plugin.name}</a> |
363 | | # else: |
364 | | ${plugin.name} |
365 | | # endif |
366 | | }}} |
367 | | If the repeated content is complex, one can use a //block assignment// (see below). |
368 | | |
369 | | === i18n #trans |
370 | | |
371 | | Genshi had a pretty good notion of what was a piece of translatable text within an HTML template, |
372 | | but Jinja2 doesn't, so there's no "guessing" and no i18n will happen unless explicitly asked. |
373 | | |
374 | | This can be done in two ways. First, with translation expressions, using the familiar `_()` gettext function (`gettext` and `ngettext` also supported). |
375 | | |
376 | | - Genshi: [[xml(<strong>Trac detected an internal error:</strong>)]] |
377 | | - Jinja2: [[xml(<strong>${_("Trac detected an internal error:")}</strong>)]] |
378 | | |
379 | | Second, using `trans` directives. |
380 | | |
381 | | - Genshi: |
382 | | {{{#!html+genshi |
383 | | <p i18n:msg="create">Otherwise, please ${create_ticket(tracker)} a new bug report |
384 | | describing the problem and explain how to reproduce it.</p> |
385 | | }}} |
386 | | - Jinja2: |
387 | | {{{#!html+jinja |
388 | | <p> |
389 | | # trans create = create_ticket(tracker) |
390 | | |
391 | | Otherwise, please ${create} a new bug report |
392 | | describing the problem and explain how to reproduce it. |
393 | | |
394 | | # endtrans |
395 | | </p> |
396 | | }}} |
397 | | another example, without assignment: |
398 | | {{{#!html+jinja |
399 | | # trans formatted |
400 | | |
401 | | In the default time zone, this would be displayed |
402 | | as <strong>${formatted}</strong>. |
403 | | |
404 | | # endtrans |
405 | | }}} |
406 | | last example, two expanded variables, with and without assignment: |
407 | | {{{#!html+jinja |
408 | | # trans tz = nowtz.tzname(), formatted |
409 | | |
410 | | In your time zone ${tz}, this would be displayed as |
411 | | <strong>${formatted}</strong>. |
412 | | |
413 | | # endtrans |
414 | | }}} |
415 | | |
416 | | See [http://jinja.pocoo.org/docs/dev/templates/#i18n i18n] doc. |
417 | | |
418 | | Note that only direct variable expansions are supported in `trans` blocks, nothing more complex. |
419 | | 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. |
420 | | - Genshi: |
421 | | {{{#!html+genshi |
422 | | <p i18n:msg="">There was an internal error in Trac. |
423 | | It is recommended that you notify your local |
424 | | <a py:strip="not project.admin" href="mailto:${project.admin}"> |
425 | | Trac administrator</a> with the information needed to |
426 | | reproduce the issue. |
427 | | </p> |
428 | | |
429 | | }}} |
430 | | - Jinja2: |
431 | | {{{#!html+jinja |
432 | | <p> |
433 | | # set trac_admin = _("Trac administrator") |
434 | | # set project_admin |
435 | | # if project.admin: |
436 | | <a href="mailto:${project.admin}">${trac_admin}</a> |
437 | | # else: |
438 | | ${trac_admin} |
439 | | # endif |
440 | | # endset |
441 | | # trans project_admin |
442 | | |
443 | | There was an internal error in Trac. |
444 | | It is recommended that you notify your local |
445 | | ${project_admin} with the information needed to |
446 | | reproduce the issue. |
447 | | |
448 | | # endtrans |
449 | | </p> |
450 | | }}} |
451 | | |
452 | | 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: |
453 | | {{{#!html+jinja |
454 | | <em> |
455 | | # set preferences_link |
456 | | <a href="${href.prefs()}" class="trac-target-new">${ |
457 | | _("Preferences")}</a> |
458 | | # endset |
459 | | ${tag_("Set your email in %(preferences_link)s", |
460 | | preferences_link=preferences_link|safe)} |
461 | | </em> |
462 | | }}} |
463 | | |
464 | | |
465 | | == Examples |
| 15 | == Overview |
| 16 | |
| 17 | We start we some examples, showing both the legacy Genshi templates and the new Jinja2 templates, highlighting their main differences. |
| 18 | |
| 19 | 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. |
| 20 | |
| 21 | 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. |
| 22 | |
| 23 | 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. |
| 24 | |
| 25 | == Examples! |
| 26 | |
| 27 | 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. |
| 455 | == Migrating to Jinja2 templates |
| 456 | |
| 457 | === The Jinja2 syntax used by Trac |
| 458 | |
| 459 | 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. |
| 460 | |
| 461 | We use the following configuration: |
| 462 | || key || value || example / explanation |
| 463 | || extensions || jinja2.ext.with_ jinja2.ext.i18n || `with` directive can be used ([#with more]) |
| 464 | || block_start_string || {% || \ |
| 465 | {{{#!td rowspan=2 |
| 466 | `{% if cond %} value {% endif %}` possible (but discouraged) |
| 467 | }}} |
| 468 | |-- |
| 469 | || block_end_string || %} || |
| 470 | || variable_start_string || ${ || \ |
| 471 | {{{#!td rowspan=2 |
| 472 | `${the_variable}` (but not `$the_variable`) |
| 473 | ([#expandavariable more]) |
| 474 | }}} |
| 475 | |-- |
| 476 | || variable_end_string || } || |
| 477 | || line_statement_prefix || # || \ |
| 478 | {{{#!td |
| 479 | {{{ |
| 480 | # if cond: |
| 481 | value |
| 482 | # endif |
| 483 | }}} |
| 484 | (preferred form) ([#if more]) |
| 485 | }}} |
| 486 | |-- |
| 487 | || line_comment_prefix || ## || `## comments are good` |
| 488 | || trim_blocks || yes || whitespace removal after a block |
| 489 | || lstrip_blocks || yes || whitespace removal before a block |
| 490 | || newstyle_gettext || yes || i.e. like the Trac `gettext` |
| 491 | |
| 492 | |
| 493 | === HTML differences between Jinja2 and Genshi templates |
| 494 | |
| 495 | We took the opportunity to **switch to HTML5** while performing this template overhaul. |
| 496 | You should probably do the same. |
| 497 | This usually won't change anything for your templates, except for a few details. |
| 498 | |
| 499 | The doctype and the <html> element should be changed from Genshi's: |
| 500 | {{{#!html+genshi |
| 501 | <!DOCTYPE html |
| 502 | PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" |
| 503 | "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> |
| 504 | <html xmlns="http://www.w3.org/1999/xhtml" |
| 505 | xmlns:py="http://genshi.edgewall.org/" |
| 506 | xmlns:i18n="http://genshi.edgewall.org/i18n" |
| 507 | xmlns:xi="http://www.w3.org/2001/XInclude"> |
| 508 | }}} |
| 509 | |
| 510 | to the somewhat simpler: |
| 511 | {{{#!xml |
| 512 | <!DOCTYPE html> |
| 513 | <html> |
| 514 | }}} |
| 515 | |
| 516 | Special care should be taken when dealing with //void elements//. |
| 517 | In XHTML, it's fine to write: |
| 518 | {{{#!xml |
| 519 | <div id="description"/> |
| 520 | }}} |
| 521 | However, when going to HTML5, you should use instead: |
| 522 | {{{#!xml |
| 523 | <div id="description"></div> |
| 524 | }}} |
| 525 | The valid [https://www.w3.org/TR/html-markup/syntax.html#void-elements void elements] in HTML5 are limited to the following list: |
| 526 | - area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr |
| 527 | |
| 528 | |
| 529 | === Detailed guide of differences between Jinja2 and Genshi |
| 530 | |
| 531 | Most of the time, the porting is a straightforward operation. |
| 532 | |
| 533 | ==== expand a variable |
| 534 | - Genshi [[html+genshi(<b>$the_variable</b>)]] |
| 535 | - Jinja2 [[html+jinja(<b>${the_variable}</b>)]] |
| 536 | |
| 537 | Note that Jinja2 doesn't support the `$the_variable` style, the curly braces are mandatory. |
| 538 | |
| 539 | Tip: |
| 540 | {{{#!shell |
| 541 | sed -i -e 's,\$\([a-z_][a-z._]*\),${\1},g' template.html |
| 542 | }}} |
| 543 | |
| 544 | ==== expand a simple computation |
| 545 | - Genshi [[html+genshi(<b>${the_variable + 1}</b>)]] |
| 546 | - Jinja2 [[html+jinja(<b>${the_variable + 1}</b>)]] |
| 547 | |
| 548 | 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. |
| 549 | |
| 550 | See also [#set set complex variables] below for more involved examples. |
| 551 | |
| 552 | 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. |
| 553 | |
| 554 | ==== include another template #include |
| 555 | - Genshi [[xml(<xi:include href="the_file.html"><xi:fallback/></xi:include>)]] |
| 556 | - Jinja2 [[xml(# include "the_file.html" ignore missing)]] |
| 557 | |
| 558 | 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: |
| 559 | {{{#!html+jinja |
| 560 | # include "sometemplate.html" if false |
| 561 | }}} |
| 562 | This alone would raise an error. The correct thing to write is: |
| 563 | {{{#!html+jinja |
| 564 | # include "sometemplate.html" if false ignore missing |
| 565 | }}} |
| 566 | |
| 567 | It is also possible to pass "parameters" to included templates. |
| 568 | 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: |
| 569 | |
| 570 | - Genshi |
| 571 | {{{#!html+genshi |
| 572 | <xi:include href="ticket_box.html" py:with="can_append = False; preview_mode = False"/> |
| 573 | }}} |
| 574 | - Jinja |
| 575 | {{{#!html+jinja |
| 576 | # with |
| 577 | # set can_append = false |
| 578 | # set preview_mode = false |
| 579 | # include "ticket_box.html" |
| 580 | # endwith |
| 581 | }}} |
| 582 | |
| 583 | See [http://jinja.pocoo.org/docs/dev/extensions/#with-statement with] doc. |
| 584 | |
| 585 | In passing, note how the boolean constants slightly differ from Python, `False` becomes `false`, `True` becomes `true` and `None` is `none`. |
| 586 | |
| 587 | 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. |
| 588 | |
| 589 | 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: |
| 590 | {{{#!html+jinja |
| 591 | # extends "layout.html" |
| 592 | }}} |
| 593 | 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. |
| 594 | |
| 595 | |
| 596 | ==== simple if...then (no else) #if |
| 597 | - Genshi [[xml(<py:if test="flag"><b>OK</b></py:if>)]] or simply: [[xml(<b py:if="flag">OK</b>)]] |
| 598 | - Jinja2 |
| 599 | {{{#!html+jinja |
| 600 | # if flag: |
| 601 | <b>OK</b> |
| 602 | # endif |
| 603 | }}} |
| 604 | |
| 605 | See [http://jinja.pocoo.org/docs/dev/templates/#if if] doc. |
| 606 | |
| 607 | ==== if...then...else |
| 608 | - Genshi |
| 609 | {{{#!html+genshi |
| 610 | <py:choose test="flag"> |
| 611 | <py:when test="True"> |
| 612 | <b>OK</b> |
| 613 | </py:when> |
| 614 | <py:otherwise> |
| 615 | <i>!!!</i> |
| 616 | </py:otherwise> |
| 617 | </py:choose> |
| 618 | }}} |
| 619 | or simply: |
| 620 | {{{#!html+genshi |
| 621 | <py:choose> |
| 622 | <b py:when="flag">OK</b> |
| 623 | <i py:otherwise="">!!!</i> |
| 624 | </py:choose> |
| 625 | }}} |
| 626 | |
| 627 | - Jinja2 |
| 628 | {{{#!html+jinja |
| 629 | # if flag: |
| 630 | <b>OK</b> |
| 631 | # else: |
| 632 | <i>!!!</i> |
| 633 | # endif |
| 634 | }}} |
| 635 | |
| 636 | If you really have to, you can also use the block style: |
| 637 | {{{#!html+jinja |
| 638 | {{ if flag }}<b>OK</b>{{ else }}<i>!!!</i>{{ endif }} |
| 639 | }}} |
| 640 | 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]//. |
| 641 | |
| 642 | ==== iterate over a collection #for |
| 643 | - Genshi |
| 644 | {{{#!html+genshi |
| 645 | <ul> |
| 646 | <py:for each="element in list"> |
| 647 | <li>$element</li> |
| 648 | </py:for> |
| 649 | </ul> |
| 650 | }}} |
| 651 | or simply: |
| 652 | {{{#!html+genshi |
| 653 | <ul> |
| 654 | <li py:for="element in list">$element</li> |
| 655 | </ul> |
| 656 | }}} |
| 657 | - Jinja2 |
| 658 | {{{#!html+jinja |
| 659 | <ul> |
| 660 | # for element in list: |
| 661 | <li>${element}</li> |
| 662 | # endfor |
| 663 | </ul> |
| 664 | }}} |
| 665 | See [http://jinja.pocoo.org/docs/dev/templates/#for for] doc. |
| 666 | |
| 667 | ===== No need for `enumerate` |
| 668 | |
| 669 | - Genshi: |
| 670 | {{{#!html+genshi |
| 671 | <tr py:for="idx,option in enumerate(section.options)" |
| 672 | class="${'modified' if option.modified else None}"> |
| 673 | <th py:if="idx == 0" class="section" |
| 674 | rowspan="${len(section.options)}">${section.name}</th> |
| 675 | |
| 676 | }}} |
| 677 | - Jinja2: |
| 678 | {{{#!html+jinja |
| 679 | # for option in section.options: |
| 680 | <tr ${{'class': 'modified' if option.modified}|htmlattr}> |
| 681 | # if loop.first: |
| 682 | <th class="section" |
| 683 | rowspan="${len(section.options)}">${section.name}</th> |
| 684 | |
| 685 | }}} |
| 686 | |
| 687 | All common uses (and more) for such an `idx` variable are addressed by the special `loop` variable. |
| 688 | See [http://jinja.pocoo.org/docs/dev/templates/#for loop] doc. |
| 689 | |
| 690 | ==== define a macro #macro |
| 691 | - Genshi |
| 692 | {{{#!html+genshi |
| 693 | <py:def function="entry(key, val='--')"> |
| 694 | <dt>$key</dt><dd>$val</dd> |
| 695 | </py:def> |
| 696 | }}} |
| 697 | - Jinja2 |
| 698 | {{{#!html+jinja |
| 699 | # macro entry(key, val='--') |
| 700 | <dt>${key}</dt><dd>${val}</dd> |
| 701 | # endmacro |
| 702 | }}} |
| 703 | See [http://jinja.pocoo.org/docs/dev/templates/#macros macros] doc. |
| 704 | |
| 705 | ==== set a variable |
| 706 | - Genshi |
| 707 | {{{#!html+genshi |
| 708 | <py:with vars="count = len(collection)"> |
| 709 | We have ${count > 10 and 'too much' or count} elements. |
| 710 | </py:with> |
| 711 | }}} |
| 712 | Note that we had to use `>` in Genshi, we can't use `>` directly. |
| 713 | - Jinja2 |
| 714 | {{{#!html+jinja |
| 715 | # set count = len(collection) |
| 716 | We have ${'too much' if count is greaterthan(10) else count} elements. |
| 717 | }}} |
| 718 | 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`). |
| 719 | See [http://jinja.pocoo.org/docs/dev/templates/#tests tests] doc. |
| 720 | |
| 721 | ==== set several variables in a scope #with |
| 722 | |
| 723 | - Genshi |
| 724 | {{{#!html+genshi |
| 725 | <html py:with="is_query = report.sql.startswith('query:'); |
| 726 | new_report = action == 'new' and not is_query; |
| 727 | new_query = action == 'new' and is_query"> |
| 728 | ... |
| 729 | </html> |
| 730 | }}} |
| 731 | The variables are set for the scope of the element in which they are set (here <html>, |
| 732 | so the whole document) |
| 733 | - Jinja2 |
| 734 | {{{#!html+jinja |
| 735 | # with |
| 736 | # set is_query = report.sql.startswith('query:') |
| 737 | # set new_report = action == 'new' and not is_query |
| 738 | ... |
| 739 | # endwith |
| 740 | }}} |
| 741 | |
| 742 | 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. |
| 743 | |
| 744 | See [http://jinja.pocoo.org/docs/dev/templates/#assignments set] and [http://jinja.pocoo.org/docs/dev/templates/#with-statement with] docs. |
| 745 | |
| 746 | 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]). |
| 747 | |
| 748 | 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. |
| 749 | |
| 750 | |
| 751 | ==== set HTML attributes #htmlattr |
| 752 | 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: |
| 753 | |
| 754 | - Genshi: |
| 755 | {{{#!html+genshi |
| 756 | <tr class="${'disabled' if all(not component.enabled for module in plugin.modules.itervalues() |
| 757 | for component in module.components.itervalues()) else None}"> |
| 758 | }}} |
| 759 | - Jinja2: |
| 760 | {{{#!html+jinja |
| 761 | # set components = plugin.modules|map(attribute='components')|flatten |
| 762 | <tr${{'class': 'disabled' if not components|selectattr('enabled') |
| 763 | }|htmlattr}> |
| 764 | |
| 765 | }}} |
| 766 | (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>`) |
| 767 | |
| 768 | 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. |
| 769 | |
| 770 | === set complex variables #set |
| 771 | 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`). |
| 772 | |
| 773 | A few more examples: |
| 774 | |
| 775 | ||= Genshi ||= Jinja2 || |
| 776 | || `${', '.join([p.name for p in faulty_plugins])}` || \ |
| 777 | || `${faulty_plugins|map('name')|join(', ')}` || |
| 778 | || `sum(1 for change in changes if 'cnum' in change)` || \ |
| 779 | || `changes|selectattr('cnum')|list|count` || |
| 780 | || `sum(1 for change in changes if 'cnum' not in change)` || \ |
| 781 | || `changes|rejectattr('cnum')|list|count` || |
| 782 | || ... || ... || |
| 783 | |
| 784 | |
| 785 | ==== conditional wrappers |
| 786 | There's no direct equivalent to the `py:strip`, but it can often be emulated with an `if/else/endif`. |
| 787 | - Genshi |
| 788 | {{{#!html+genshi |
| 789 | <a py:strip="not url" href="$url">$plugin.name</a> |
| 790 | }}} |
| 791 | - Jinja2 |
| 792 | {{{#!html+jinja |
| 793 | # if url: |
| 794 | <a href="${url}">${plugin.name}</a> |
| 795 | # else: |
| 796 | ${plugin.name} |
| 797 | # endif |
| 798 | }}} |
| 799 | If the repeated content is complex, one can use a //block assignment// (see below). |
| 800 | |
| 801 | ==== i18n #trans |
| 802 | |
| 803 | Genshi had a pretty good notion of what was a piece of translatable text within an HTML template, |
| 804 | but Jinja2 doesn't, so there's no "guessing" and no i18n will happen unless explicitly asked. |
| 805 | |
| 806 | This can be done in two ways. First, with translation expressions, using the familiar `_()` gettext function (`gettext` and `ngettext` also supported). |
| 807 | |
| 808 | - Genshi: [[xml(<strong>Trac detected an internal error:</strong>)]] |
| 809 | - Jinja2: [[xml(<strong>${_("Trac detected an internal error:")}</strong>)]] |
| 810 | |
| 811 | Second, using `trans` directives. |
| 812 | |
| 813 | - Genshi: |
| 814 | {{{#!html+genshi |
| 815 | <p i18n:msg="create">Otherwise, please ${create_ticket(tracker)} a new bug report |
| 816 | describing the problem and explain how to reproduce it.</p> |
| 817 | }}} |
| 818 | - Jinja2: |
| 819 | {{{#!html+jinja |
| 820 | <p> |
| 821 | # trans create = create_ticket(tracker) |
| 822 | |
| 823 | Otherwise, please ${create} a new bug report |
| 824 | describing the problem and explain how to reproduce it. |
| 825 | |
| 826 | # endtrans |
| 827 | </p> |
| 828 | }}} |
| 829 | another example, without assignment: |
| 830 | {{{#!html+jinja |
| 831 | # trans formatted |
| 832 | |
| 833 | In the default time zone, this would be displayed |
| 834 | as <strong>${formatted}</strong>. |
| 835 | |
| 836 | # endtrans |
| 837 | }}} |
| 838 | last example, two expanded variables, with and without assignment: |
| 839 | {{{#!html+jinja |
| 840 | # trans tz = nowtz.tzname(), formatted |
| 841 | |
| 842 | In your time zone ${tz}, this would be displayed as |
| 843 | <strong>${formatted}</strong>. |
| 844 | |
| 845 | # endtrans |
| 846 | }}} |
| 847 | |
| 848 | See [http://jinja.pocoo.org/docs/dev/templates/#i18n i18n] doc. |
| 849 | |
| 850 | Note that only direct variable expansions are supported in `trans` blocks, nothing more complex. |
| 851 | 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. |
| 852 | - Genshi: |
| 853 | {{{#!html+genshi |
| 854 | <p i18n:msg="">There was an internal error in Trac. |
| 855 | It is recommended that you notify your local |
| 856 | <a py:strip="not project.admin" href="mailto:${project.admin}"> |
| 857 | Trac administrator</a> with the information needed to |
| 858 | reproduce the issue. |
| 859 | </p> |
| 860 | |
| 861 | }}} |
| 862 | - Jinja2: |
| 863 | {{{#!html+jinja |
| 864 | <p> |
| 865 | # set trac_admin = _("Trac administrator") |
| 866 | # set project_admin |
| 867 | # if project.admin: |
| 868 | <a href="mailto:${project.admin}">${trac_admin}</a> |
| 869 | # else: |
| 870 | ${trac_admin} |
| 871 | # endif |
| 872 | # endset |
| 873 | # trans project_admin |
| 874 | |
| 875 | There was an internal error in Trac. |
| 876 | It is recommended that you notify your local |
| 877 | ${project_admin} with the information needed to |
| 878 | reproduce the issue. |
| 879 | |
| 880 | # endtrans |
| 881 | </p> |
| 882 | }}} |
| 883 | |
| 884 | 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: |
| 885 | {{{#!html+jinja |
| 886 | <em> |
| 887 | # set preferences_link |
| 888 | <a href="${href.prefs()}" class="trac-target-new">${ |
| 889 | _("Preferences")}</a> |
| 890 | # endset |
| 891 | ${tag_("Set your email in %(preferences_link)s", |
| 892 | preferences_link=preferences_link|safe)} |
| 893 | </em> |
| 894 | }}} |
| 895 | |
| 896 | |
| 897 | ==== Tips and tricks |
| 898 | |
| 899 | - 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]) |
| 900 | - 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]) |
| 901 | |
| 902 | |