Edgewall Software

Changes between Version 11 and Version 12 of TracDev/Proposals/Jinja


Ignore:
Timestamp:
Feb 9, 2016, 12:01:24 PM (8 years ago)
Author:
Christian Boos
Comment:

start to document how to perform the Genshi to Jinja2 migration, with a #Conversionexample

Legend:

Unmodified
Added
Removed
Modified
  • TracDev/Proposals/Jinja

    v11 v12  
    1414See also [googlegroups:trac-dev:fc8d8c0447140110 this Trac-Dev discussion] from 2010, which is still pertinent. Well, obviously we managed to release Genshi 0.6 since then, but the issue is a recurring one, see this recent (2016-01)
    1515[gmessage:trac-users:PYqQ4UDRnl8/wg8lQzrGDAAJ Genshi question] on Trac-Users.
     16
    1617
    1718
     
    4950 - CD (Content download), idem
    5051 - Rdr (template rendering time), mostly significant for the "blob" method otherwise it also takes the network latency into account
     52All values are given in milliseconds.
    5153
    5254Note that even if the total "blob" time seems better than the total "stream" one, the lower TTFB is nevertheless a major benefit for the streaming variant, as this means the secondary requests can start earlier (and in this case, finish before the main request).
     
    5456In addition, while I didn't measure precisely the memory usage, Genshi made the python.exe process jump from 109MB to 239MB while rendering the request (blob). The memory seems to be freed afterwards (there were no concurrent requests). By contrast, with Jinja2 the memory spike was 106MB to 126MB.
    5557
    56 In summary, this means that for the big problematic pages, we can easily have a 10x speedup and more, by migrating to Jinja2,and this with a much lighter memory footprint.
     58In summary, this means that for the big problematic pages, we can easily have a 10x speedup and more, by migrating to Jinja2, and this with a much lighter memory footprint.
    5759For smaller pages, the speed-up is between 5x to 10x as well.
    5860
     
    9395
    9496The [source:tags/trac-1.0.9/trac/templates/layout.html  layout.html]
    95 page in turn is structure like this:
     97page in turn is structured like this:
    9698{{{#!html+genshi
    9799<html>
     
    159161
    160162
    161 Note that this scheme can be extended to having more intermediate levels,
    162 for example what we have for the admin panels.
     163Note that this scheme can be extended to additional intermediate levels,
     164for example see what we do with the admin panels.
    163165
    164166One such panel is
     
    341343    # block body
    342344  </body>
     345</html>
    343346}}}
    344347 - it defines a ''head'' block inside an otherwise empty `<head>` element;
     
    349352   what we want to achieve here is to provide a ''slot''
    350353   at the place where we want to insert the content
    351    produced by the jlayout.html template:
     354   produced by the jlayout.html template;
    352355   I didn't name that inner block "body", as this could be confusing:
    353356   we're not in control of the `<body>` element there, just of
     
    370373replacement for the `<xi:include href="site.html>` template
    371374(`# include site_head.html` and `# include site_body.html`).
     375
     376
     377== Genshi to Jinja2 Migration
     378
     379=== Conversion example
     380
     381Here we detail the conversion process of a sample Genshi template ([source:tags/trac-1.0.9/trac/wiki/templates/wiki_view.html wiki_view.html]) into the corresponding Jinja2 template ([source:cboos.git/trac/wiki/templates/jwiki_view.html jwiki_view.html]).
     382
     383As will become apparent, we mainly use [http://jinja.pocoo.org/docs/dev/templates/#line-statements line statements] for the Jinja2 control structures.
     384Note that we have reformatted the Genshi template to use shorter line widths, so that both columns of the table below stay visible (a wide screen also helps).
     385
     386{{{#!th style="max-width: 200px"
     387wiki_view.html (Genshi template)
     388}}}
     389{{{#!th style="max-width: 200px"
     390jwiki_view.html (Jinja2 template)
     391}}}
     392|------
     393||= ||= ||
     394{{{#!td
     395{{{#!html+genshi
     396<!--!  Copyright (C) 2006-2014 Edgewall Software
     397
     398  This software is licensed as described in the file COPYING, which
     399  you should have received as part of this distribution. The terms
     400  are also available at http://trac.edgewall.com/license.html.
     401
     402  This software consists of voluntary contributions made by many
     403  individuals. For the exact contribution history, see the revision
     404  history and logs, available at http://trac.edgewall.org/.
     405-->
     406}}}
     407}}}
     408{{{#!td
     409{{{#!html+jinja
     410{# Copyright (C) 2006-2014 Edgewall Software
     411
     412  This software is licensed as described in the file COPYING, which
     413  you should have received as part of this distribution. The terms
     414  are also available at http://trac.edgewall.com/license.html.
     415
     416  This software consists of voluntary contributions made by many
     417  individuals. For the exact contribution history, see the revision
     418  history and logs, available at http://trac.edgewall.org/.
     419#}
     420}}}
     421}}}
     422|-----------------------------------------------------------------------------
     423|||| Block comments ||
     424|-----------------------------------------------------------------------------
     425{{{#!td
     426{{{#!html+genshi
     427}}}
     428}}}
     429{{{#!td
     430{{{#!html+jinja
     431# extends "jlayout.html"
     432}}}
     433}}}
     434|-----------------------------------------------------------------------------
     435|||| This is the equivalent of Genshi's `<py:include href="layout.html" />`. We have to do the extends before outputing any content on our own, otherwise it would show up in the final result. Once we made the `extends`, only the content written within //blocks// will matter. ||
     436|-----------------------------------------------------------------------------
     437{{{#!td
     438{{{#!html+genshi
     439<!DOCTYPE html
     440    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     441    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
     442}}}
     443}}}
     444{{{#!td
     445{{{#!html+jinja
     446<!DOCTYPE html>
     447}}}
     448}}}
     449|-----------------------------------------------------------------------------
     450|||| Let's directly make the jump to HTML5, while we're at it (I'm not sure what is the current state of the HTML5 support in Genshi, but it no longer matters) ||
     451|-----------------------------------------------------------------------------
     452{{{#!td
     453{{{#!html+genshi
     454<html xmlns="http://www.w3.org/1999/xhtml"
     455      xmlns:py="http://genshi.edgewall.org/"
     456      xmlns:i18n="http://genshi.edgewall.org/i18n"
     457      xmlns:xi="http://www.w3.org/2001/XInclude"
     458      py:with="modify_perm = 'WIKI_MODIFY' in perm(page.resource);
     459               create_perm = 'WIKI_CREATE' in perm(page.resource);
     460               admin_perm = 'WIKI_ADMIN' in perm(page.resource);
     461               is_not_latest = page.exists and page.version != latest_version">
     462}}}
     463}}}
     464{{{#!td
     465{{{#!html+jinja
     466<html>
     467
     468}}}
     469}}}
     470|-----------------------------------------------------------------------------
     471{{{#!td
     472No need for exotic namespace declarations.
     473
     474Note that we can't use a `with` declaration that would encompass the ''head'' and ''content'' blocks we'll define in a moment, as this would force the generation of content twice, once by having the with block present in the template, and a second time because of the extends and the call of the blocks.
     475}}}
     476|-----------------------------------------------------------------------------
     477{{{#!td
     478{{{#!html+genshi
     479  <xi:include href="layout.html" />
     480}}}
     481}}}
     482{{{#!td
     483{{{#!html+jinja
     484}}}
     485}}}
     486|-----------------------------------------------------------------------------
     487|||| See above, this will be converted to an initial `extends` statement. ||
     488|-----------------------------------------------------------------------------
     489{{{#!td
     490{{{#!html+genshi
     491  <head>
     492    <title py:if="title">$title</title>
     493}}}
     494}}}
     495{{{#!td
     496{{{#!html+jinja
     497  <head>
     498    <title>
     499      # block title
     500      #   if title:
     501      ${title} ${ super() }
     502      #   endif
     503      # endblock title
     504    </title>
     505
     506    # block head
     507    #  set modify_perm = 'WIKI_MODIFY' in perm(page.resource)
     508    #  set is_not_latest = page.exists and page.version != latest_version
     509
     510    ${ super() }
     511}}}
     512}}}
     513|-----------------------------------------------------------------------------
     514{{{#!td colspan=2
     515The content of the <title> element is placed in the ''title'' block,
     516followed by the content of the parent ''title'' block (`${ super() }`).
     517When closing the block, we reuse the name of the block.
     518
     519Note that while the only thing that really matters in an extending template
     520is the content of the blocks, we try hard to keep a correct HTML structure
     521for the whole template page, so that its HTML content can be validated in a
     522standalone way (cf. [#jinjachecker]). This is why we add the <html>, <head> and
     523<title> tags.
     524
     525We explained earlier why the title block has to be outside of the ''head'' block:
     526to avoid appearing twice, once as part of the head block, and the second time
     527when we call `${ super() }` in order to retrieve the content of the parent ''head''
     528block.
     529
     530Finally, we define some variables at the beginning of the block (corresponding to
     531some which were part of the `py:with` attribute in the `<html>` tag, in the Genshi
     532template).
     533}}}
     534|-----------------------------------------------------------------------------
     535{{{#!td
     536{{{#!html+genshi
     537    <meta py:if="version or page.author == 'trac'"
     538          name="ROBOTS" content="NOINDEX, NOFOLLOW" />
     539    <link py:if="modify_perm" rel="alternate"
     540          type="application/x-wiki"
     541          href="${href.wiki(page.name, action='edit',
     542                  version=page.version if is_not_latest else None)}"
     543          title="${_('Revert page to this version') if
     544                     is_not_latest else _('Edit this page')}"/>
     545}}}
     546}}}
     547{{{#!td
     548{{{#!html+jinja
     549    #   if version or page.author == 'trac':
     550    <meta name="ROBOTS" content="NOINDEX, NOFOLLOW" />
     551    #   endif
     552    #   if modify_perm:
     553    <link rel="alternate" type="application/x-wiki"
     554          href="${href.wiki(page.name, action='edit',
     555                version=page.version if is_not_latest)}"
     556          title="${_("Revert page to this version") if is_not_latest else
     557                                                  _("Edit this page")}"/>
     558    #   endif
     559}}}
     560}}}
     561|-----------------------------------------------------------------------------
     562|||| With Jinja2, the logic is now clearly distinct from the content. ||
     563|-----------------------------------------------------------------------------
     564{{{#!td
     565{{{#!html+genshi
     566    <script type="text/javascript">
     567      jQuery(document).ready(function($) {
     568        $("#content").find("h1,h2,h3,h4,h5,h6")
     569          .addAnchor(_("Link to this section"));
     570        $("#content").find(".wikianchor").each(function() {
     571          $(this).addAnchor(babel.format(_("Link to #%(id)s"),
     572                                         {id: $(this).attr('id')}));
     573        });
     574        $(".foldable").enableFolding(true, true);
     575      });
     576    </script>
     577}}}
     578}}}
     579{{{#!td
     580{{{#!html+jinja
     581    <script type="text/javascript">
     582      jQuery(document).ready(function($) {
     583        $("#content").find("h1,h2,h3,h4,h5,h6")
     584          .addAnchor(_("Link to this section"));
     585        $("#content").find(".wikianchor").each(function() {
     586        $(this).addAnchor(babel.format(_("Link to #%(id)s"), {
     587                                       id: $(this).attr('id')}));
     588        });
     589        $(".foldable").enableFolding(true, true);
     590      });
     591    </script>
     592}}}
     593}}}
     594|-----------------------------------------------------------------------------
     595|||| No changes here, except for some reformatting (we try to stay below 80 chars for nicer future side-by-side diffs). ||
     596|-----------------------------------------------------------------------------
     597{{{#!td
     598{{{#!html+genshi
     599  </head>
     600
     601}}}
     602}}}
     603{{{#!td
     604{{{#!html+jinja
     605    # endblock head
     606  </head>
     607
     608}}}
     609}}}
     610|-----------------------------------------------------------------------------
     611|||| We close the ''head'' block. ||
     612|-----------------------------------------------------------------------------
     613{{{#!td
     614{{{#!html+genshi
     615  <body>
     616    <div id="content" class="${classes('wiki', create=not page.exists)}">
     617
     618}}}
     619}}}
     620{{{#!td
     621{{{#!html+jinja
     622  <body>
     623    # block content
     624    #   set modify_perm = 'WIKI_MODIFY' in perm(page.resource)
     625    #   set create_perm = 'WIKI_CREATE' in perm(page.resource)
     626    #   set admin_perm = 'WIKI_ADMIN' in perm(page.resource)
     627    #   set is_not_latest = page.exists and page.version != latest_version
     628
     629    <div id="content" class="${classes('wiki', create=not page.exists)}">
     630
     631}}}
     632}}}
     633|-----------------------------------------------------------------------------
     634|||| We start the ''content'' block. As before with the ''header'' block, we define some variables at the beginning of the block. ||
     635|-----------------------------------------------------------------------------
     636{{{#!td
     637{{{#!html+genshi
     638      <py:if test="version">
     639        <br />
     640        <table id="info" summary="Revision info">
     641          <tr><th scope="row" i18n:msg="version, author, date">
     642             Version $page.version (modified by ${authorinfo(page.author)},
     643                                            ${pretty_dateinfo(page.time)})
     644             (<a href="${href.wiki(page.name, action='diff',
     645                                   version=page.version)}">diff</a>)
     646          </th></tr>
     647          <tr><td class="message" xml:space="preserve">
     648            ${wiki_to_html(context, page.comment or '--')}
     649          </td></tr>
     650        </table>
     651      </py:if>
     652}}}
     653}}}
     654{{{#!td
     655{{{#!html+jinja
     656      # if version:
     657      <br />
     658      <table id="info" summary="${_("Revision info")}">
     659        <tr><th scope="row">
     660            # with
     661            #   set version = page.version
     662            #   set author = authorinfo(page.author)
     663            #   set date = pretty_dateinfo(page.time)
     664            #   set hef = href.wiki(page.name, action='diff', version=page.version)
     665            #   trans version, author, date, href
     666
     667            Version ${version} (modified by ${author}, ${date})
     668            (<a href="${href}">diff</a>)
     669
     670            #   endtrans
     671            # endwith
     672        </th></tr>
     673        <tr><td class="message">
     674            ${wiki_to_html(context, page.comment or '--')}
     675        </td></tr>
     676      </table>
     677      # endif
     678
     679}}}
     680}}}
     681|-----------------------------------------------------------------------------
     682{{{#!td colspan=2
     683Here we illustrate the i18n changes.
     684
     685First, any sentence that corresponds to visible end-user text that should be
     686translated has to be marked somehow.
     687
     688One way is to use the standard i18n calls,
     689`_`, `ngettext()`, etc.
     690
     691The other way is to use `trans` blocks.
     692There are two big differences with their Genshi i18n equivalent:
     693 - variable substitutions can only be those of direct variables,
     694   no kind of expression is allowed, even as simple as attribute
     695   lookup
     696 - Jinja2, markup neutral as it is, will not do any substitutions
     697   on the markup found in a trans block; what would have ended
     698   in a Genshi bracketed expression `... [1:diff]` in the catalogs
     699   will now remain HTML markup: `... <a href="${href}">diff</a>`.
     700Note that one can use a `with` statement for breaking up the
     701assignments needed on multiple separate lines.
     702Smaller lists of variables can be placed on the `trans` line directly.
     703}}}
     704|-----------------------------------------------------------------------------
     705{{{#!td
     706{{{#!html+genshi
     707      <div class="wikipage searchable" py:choose="" xml:space="preserve">
     708        <py:when test="page.exists">
     709          <div id="wikipage" class="trac-content"
     710               py:content="wiki_to_html(context, text)" />
     711          <?python
     712            last_modification = (page.comment and
     713                 _('Version %(version)s by %(author)s: %(comment)s',
     714                   version=page.version, author=format_author(page.author),
     715                   comment=page.comment) or
     716                 _('Version %(version)s by %(author)s',
     717                   version=page.version, author=format_author(page.author)))
     718          ?>
     719          <div py:if="not version" class="trac-modifiedby">
     720            <span i18n:msg="reldate">
     721              <a href="${href.wiki(page.name, action='diff',
     722                                   version=page.version)}"
     723                 title="$last_modification">Last modified</a>
     724                 ${pretty_dateinfo(page.time)}
     725            </span>
     726            <span class="trac-print" i18n:msg="date">Last modified on
     727              ${format_datetime(page.time)}</span>
     728          </div>
     729        </py:when>
     730        <py:otherwise>
     731          <p i18n:msg="name">The page <strong>${name_of(page.resource)}</strong>
     732          does not exist. You can create it here.</p>
     733        </py:otherwise>
     734      </div>
     735}}}
     736}}}
     737{{{#!td
     738{{{#!html+jinja
     739      <div class="wikipage searchable">
     740        # if page.exists:
     741        <div id="wikipage" class="trac-content">${
     742          wiki_to_html(context, text)
     743          }</div>
     744        #   set last_modification = (page.comment and
     745        _('Version %(version)s by %(author)s: %(comment)s',
     746        version=page.version, author=format_author(page.author),
     747        comment=page.comment) or
     748        _('Version %(version)s by %(author)s',
     749        version=page.version, author=format_author(page.author)))
     750        #   if not version:
     751        <div class="trac-modifiedby">
     752          <span>
     753            # with
     754            #   set href = href.wiki(page.name, action='diff',
     755                                     version=page.version),
     756            #   set date = pretty_dateinfo(page.time)
     757            #   trans href, last_modification, date
     758
     759            <a href="${href}"
     760               title="${last_modification}">Last modified</a> ${date}
     761
     762            #   endtrans
     763            # endwith
     764          </span>
     765          <span class="trac-print">
     766            ${_("Last modified on %(date)s", date=format_datetime(page.time))}
     767          </span>
     768        </div>
     769        #   endif
     770        # else:
     771        <p>
     772          # trans name = name_of(page.resource)
     773
     774          The page <strong>${name}</strong> does not exist.
     775          You can create it here.
     776
     777          # endtrans
     778        </p>
     779        # endif
     780      </div>
     781}}}
     782}}}
     783|-----------------------------------------------------------------------------
     784|||| ||
     785|-----------------------------------------------------------------------------
     786{{{#!td
     787{{{#!html+genshi
     788      <xi:include href="list_of_attachments.html"
     789                  py:with="alist = attachments; compact = True;
     790                           foldable = True"/>
     791}}}
     792}}}
     793{{{#!td
     794{{{#!html+jinja
     795      # with
     796      #   set alist = attachments
     797      #   set compact = True
     798      #   set foldable = True
     799      #   include "jlist_of_attachments.html"
     800      # endwith
     801}}}
     802}}}
     803|-----------------------------------------------------------------------------
     804|||| Jinja2 includes also know about their context, so that make them kind of parametric. ||
     805|-----------------------------------------------------------------------------
     806{{{#!td
     807{{{#!html+genshi
     808      <py:with vars="delete_perm = 'WIKI_DELETE' in perm(page.resource);
     809                     rename_perm = 'WIKI_RENAME' in perm(page.resource)">
     810        <py:if test="modify_perm or create_perm or delete_perm">
     811          <div class="buttons">
     812            <py:if test="modify_perm or create_perm">
     813}}}
     814}}}
     815{{{#!td
     816{{{#!html+jinja
     817      # with
     818      #   set delete_perm = 'WIKI_DELETE' in perm(page.resource)
     819      #   set rename_perm = 'WIKI_RENAME' in perm(page.resource)
     820      #   if modify_perm or create_perm or delete_perm:
     821      <div class="buttons">
     822        #   if modify_perm or create_perm:
     823}}}
     824}}}
     825|-----------------------------------------------------------------------------
     826|||| Again, even when there's a lot of control lines, it's a bit easier to immediately spot the actual content in Jinja2 templates, due to the clearer syntactic difference. ||
     827|-----------------------------------------------------------------------------
     828{{{#!td
     829{{{#!html+genshi
     830              <form method="get" action="${href.wiki(page.name)}" id="modifypage">
     831                <div>
     832                  <input type="hidden" name="action" value="edit" />
     833                  <py:choose>
     834                    <py:when test="is_not_latest and modify_perm">
     835                      <input type="hidden" name="version" value="${page.version}"/>
     836                      <input type="submit" value="${_('Revert to this version')}"/>
     837                    </py:when>
     838                    <py:when test="page.exists and modify_perm">
     839                      <input type="submit" value="${_('Edit this page')}"
     840                             accesskey="e" />
     841                    </py:when>
     842                    <py:when test="not page.exists and create_perm">
     843                      <input type="submit" value="${_('Create this page')}"
     844                             accesskey="e" />
     845                      <div py:if="templates" id="template">
     846                        <label for="template">using the template:</label>
     847                        <select name="template">
     848                          <option selected="${not default_template in templates
     849                                              or None}"
     850                                  value="">(blank page)</option>
     851                          <option py:for="t in sorted(templates)" value="$t"
     852                                  selected="${t == default_template or None}"
     853                                  >$t</option>
     854                        </select>
     855                      </div>
     856                    </py:when>
     857                  </py:choose>
     858                </div>
     859              </form>
     860}}}
     861}}}
     862{{{#!td
     863{{{#!html+jinja
     864        <form method="get" action="${href.wiki(page.name)}" id="modifypage">
     865          <div>
     866            <input type="hidden" name="action" value="edit" />
     867            # if is_not_latest and modify_perm:
     868            <input type="hidden" name="version" value="${page.version}"/>
     869            <input type="submit" value="${_('Revert to this version')}"/>
     870            # elif page.exists and modify_perm:
     871            <input type="submit" value="${_('Edit this page')}"
     872                   accesskey="e" />
     873            # elif not page.exists and create_perm:
     874            <input type="submit" value="${_('Create this page')}"
     875                   accesskey="e" />
     876            #   if templates:
     877            <div id="template">
     878              <label for="template">${_("using the template:")}</label>
     879              <select name="template">
     880                <option ${{'selected': not default_template in templates
     881                        }|htmlattr}
     882                        value="">${_("(blank page)")}</option>
     883                # for t in sorted(templates):
     884                <option value="${t}"
     885                        ${{'selected': t == default_template
     886                        }|htmlattr}>${t}</option>
     887                # endfor
     888              </select>
     889            </div>
     890            #   endif
     891            # endif
     892          </div>
     893        </form>
     894
     895}}}
     896}}}
     897|-----------------------------------------------------------------------------
     898{{{#!td
     899A `<py:choose>` and its series of `<py:when>` translates very smoothly into a sequence of `if / elif` statements.
     900
     901Special care should be taken when producing attributes dynamically, as it's the
     902case here for `selected`. We use the `htmlattr` filter that takes care of
     903producing the right value for the attribute depending on its content: with
     904a truth value, we'll output `selected="selected"` otherwise we'll just omit
     905the parameter.
     906}}}
     907|-----------------------------------------------------------------------------
     908|||| ||
     909|-----------------------------------------------------------------------------
     910{{{#!td
     911{{{#!html+genshi
     912        <py:if test="page.exists">
     913          <xi:include href="attach_file_form.html"
     914                      py:with="alist = attachments"/>
     915        </py:if>
     916      </py:if>
     917}}}
     918}}}
     919{{{#!td
     920{{{#!html+jinja
     921        #   if page.exists:
     922        #     with alist = attachments
     923        #       include "jattach_file_form.html"
     924        #     endwith
     925        #   endif
     926        # endif
     927}}}
     928}}}
     929|-----------------------------------------------------------------------------
     930|||| We pass `attachments` as `alist` to the included template (a `j...` template, obviously). ||
     931|-----------------------------------------------------------------------------
     932{{{#!td
     933{{{#!html+genshi
     934            <form method="get" action="${href.wiki(page.name)}"
     935                  id="rename" py:if="page.exists and rename_perm">
     936              <div>
     937                <input type="hidden" name="action" value="rename" />
     938                <input type="submit" value="${_('Rename page')}" />
     939              </div>
     940            </form>
     941            <form method="get" action="${href.wiki(page.name)}"
     942                  id="delete" py:if="page.exists and delete_perm">
     943              <div>
     944                <input type="hidden" name="action" value="delete" />
     945                <input type="hidden" name="version" value="$page.version" />
     946                <py:if test="page.version == latest_version">
     947                  <input type="submit" name="delete_version"
     948                         value="${_('Delete this version')}" />
     949                </py:if>
     950                <input type="submit" value="${_('Delete page')}" />
     951              </div>
     952            </form>
     953          </div>
     954        </py:if>
     955      </py:with>
     956}}}
     957}}}
     958{{{#!td
     959{{{#!html+jinja
     960        # if page.exists and rename_perm:
     961        <form method="get" action="${href.wiki(page.name)}" id="rename">
     962          <div>
     963            <input type="hidden" name="action" value="rename" />
     964            <input type="submit" value="${_('Rename page')}" />
     965          </div>
     966        </form>
     967        # endif
     968        # if page.exists and delete_perm:
     969        <form method="get" action="${href.wiki(page.name)}" id="delete">
     970          <div>
     971            <input type="hidden" name="action" value="delete" />
     972            <input type="hidden" name="version" value="${page.version}" />
     973            # if page.version == latest_version:
     974            <input type="submit" name="delete_version" value="${_('Delete this version')}" />
     975            # endif
     976            <input type="submit" value="${_('Delete page')}" />
     977          </div>
     978        </form>
     979        # endif
     980      </div>
     981      # endif
     982      # endwith
     983
     984}}}
     985}}}
     986|-----------------------------------------------------------------------------
     987|||| ||
     988|-----------------------------------------------------------------------------
     989{{{#!td
     990{{{#!html+genshi
     991      <div class="wikipage searchable" py:if="not page.exists and higher">
     992        <p>You could also create the same page higher in the hierarchy:</p>
     993        <ul>
     994          <li py:for="markup in higher">${markup}</li>
     995        </ul>
     996      </div>
     997}}}
     998}}}
     999{{{#!td
     1000{{{#!html+jinja
     1001      # if not page.exists and higher:
     1002      <div class="wikipage searchable">
     1003        <p>You could also create the same page higher in the hierarchy:</p>
     1004        <ul>
     1005          # for markup in higher:
     1006          <li>${markup}</li>
     1007          # endfor
     1008        </ul>
     1009      </div>
     1010      # endif
     1011
     1012}}}
     1013}}}
     1014|-----------------------------------------------------------------------------
     1015|||| Here's the first use of the `for` statement. Straightforward. ||
     1016|-----------------------------------------------------------------------------
     1017{{{#!td
     1018{{{#!html+genshi
     1019      <div class="wikipage searchable" py:if="not page.exists and related">
     1020        <p>The following pages have a name similar to this page,
     1021           and may be related:</p>
     1022        <ul>
     1023          <li py:for="markup in related">${markup}</li>
     1024        </ul>
     1025      </div>
     1026}}}
     1027}}}
     1028{{{#!td
     1029{{{#!html+jinja
     1030      # if not page.exists and related:
     1031      <div class="wikipage searchable">
     1032        <p>${_("The following pages have a name similar to this page, and may be related:")}</p>
     1033        <ul>
     1034          # for markup in related:
     1035          <li>${markup}</li>
     1036          # endif
     1037        </ul>
     1038      </div>
     1039      # endif
     1040}}}
     1041}}}
     1042|-----------------------------------------------------------------------------
     1043|||| ||
     1044|-----------------------------------------------------------------------------
     1045{{{#!td
     1046{{{#!html+genshi
     1047    </div>
     1048  </body>
     1049</html>
     1050}}}
     1051}}}
     1052{{{#!td
     1053{{{#!html+jinja
     1054    </div>
     1055
     1056    ${ super() }
     1057
     1058    # endblock content
     1059  </body>
     1060</html>
     1061}}}
     1062}}}
     1063|-----------------------------------------------------------------------------
     1064|||| Once we're done with the `<div id="content">`, we also call `${ super() }` to re-insert the parent content of the ''content'' block, i.e. the altlinks div and the late_links/late_scripts logic. ||
     1065|-----------------------------------------------------------------------------
     1066
     1067=== `jinjachecker`
     1068
     1069Jinja2 is very helpful when it detects any kind of error, as you always end up with a meaningful backtrace. Nevertheless, it can be tedious to get the nesting of control structures right.
     1070
     1071To help with that, there's the [source:cboos.git/contrib/jinjachecker.py contrib/jinjachecker.py] tool.
     1072
     1073{{{
     1074python contrib/jinjachecker.py trac/wiki/templates/jwiki_view.html
     1075}}}
     1076
     1077It first analyzes the Jinja2 control structures, and tries to provide some helpful diagnostics in case of nesting or stylistic errors.
     1078It also adds curly braces to the statements, so if you have an editor which can do matching of brace pairs, you can quickly spot the origin o a nesting problem.
     1079
     1080Finally, while the indentation of the statements is free in Jinja2 templates, being consistent with it also helps to ensure a proper nesting.
     1081{{{
     1082# -- Jinja2 check for 'trac/wiki/templates/jwiki_view.html'
     1083   12    EXTENDS "jlayout.html"
     1084   18          {BLOCK title
     1085   19            {IF title:
     1086   21            }IF
     1087   22          }BLOCK title
     1088   25        {BLOCK head
     1089   26          SET modify_perm = 'WIKI_MODIFY' in perm(page.resource)
     1090   27          SET is_not_latest = page.exists and page.version != latest_version
     1091   31          {IF version or page.author == 'trac':
     1092   33          }IF
     1093   34          {IF modify_perm:
     1094   40          }IF
     1095   52        }BLOCK head
     1096   56        {BLOCK content
     1097   57          SET modify_perm = 'WIKI_MODIFY' in perm(page.resource)
     1098   58          SET create_perm = 'WIKI_CREATE' in perm(page.resource)
     1099   59          SET admin_perm = 'WIKI_ADMIN' in perm(page.resource)
     1100   60          SET is_not_latest = page.exists and page.version != latest_version
     1101   63          {IF version:
     1102   67                {WITH
     1103   68                  SET version = page.version
     1104   69                  SET author = authorinfo(page.author)
     1105   70                  SET date = pretty_dateinfo(page.time)
     1106   71                  SET hef = href.wiki(page.name, action='diff', version=page.version)
     1107   72                  {TRANS version, author, date, href
     1108   77                  }TRANS
     1109   78                }WITH
     1110   84          }IF
     1111   87            {IF page.exists:
     1112   91              SET last_modification = (page.comment and
     1113   97              {IF not version:
     1114  100                {WITH
     1115  101                  SET href = href.wiki(page.name, action='diff',
     1116  103                  SET date = pretty_dateinfo(page.time)
     1117  104                  {TRANS href, last_modification, date
     1118  109                  }TRANS
     1119  110                }WITH
     1120  116              }IF
     1121  117            ELSE:
     1122  119              {TRANS name = name_of(page.resource)
     1123  124              }TRANS
     1124  126            }IF
     1125  129          {WITH
     1126  130            SET alist = attachments
     1127  131            SET compact = True
     1128  132            SET foldable = True
     1129  133            INCLUDE "jlist_of_attachments.html"
     1130  134          }WITH
     1131  136          {WITH
     1132  137            SET delete_perm = 'WIKI_DELETE' in perm(page.resource)
     1133  138            SET rename_perm = 'WIKI_RENAME' in perm(page.resource)
     1134  139            {IF modify_perm or create_perm or delete_perm:
     1135  141              {IF modify_perm or create_perm:
     1136  145                {IF is_not_latest and modify_perm:
     1137  148                ELIF page.exists and modify_perm:
     1138  151                ELIF not page.exists and create_perm:
     1139  154                  {IF templates:
     1140  161                    {FOR t in sorted(templates):
     1141  165                    }FOR
     1142  168                  }IF
     1143  169                }IF
     1144  173              {IF page.exists:
     1145  174                {WITH alist = attachments
     1146  175                  INCLUDE "jattach_file_form.html"
     1147  176                }WITH
     1148  177              }IF
     1149  178            }IF
     1150  180            {IF page.exists and rename_perm:
     1151  187            }IF
     1152  188            {IF page.exists and delete_perm:
     1153  193                {IF page.version == latest_version:
     1154  195                }IF
     1155  199            }IF
     1156  201          }IF
     1157  202          }WITH
     1158  204          {IF not page.exists and higher:
     1159  208              {FOR markup in higher:
     1160  210              }FOR
     1161  213          }IF
     1162  215          {IF not page.exists and related:
     1163  219              {FOR markup in related:
     1164  221              }FOR
     1165  224          }IF
     1166  230        }BLOCK content
     1167# -- Jinja2 OK
     1168}}}
     1169
     1170As a second step, jinjachecker removes the Jinja2 markup and performs a validation of the document, using lxml.
     1171{{{
     1172# -- HTML check for 'trac/wiki/templates/jwiki_view.html'
     1173    1
     1174...
     1175   13
     1176   14 <!DOCTYPE html>
     1177   15 <html>
     1178   16   <head>
     1179   17     <title>
     1180   18
     1181   19
     1182   20       ${title} ${ super() }
     1183   21
     1184   22
     1185   23     </title>
     1186   24
     1187   25
     1188   26
     1189   27
     1190   28
     1191   29     ${ super() }
     1192   30
     1193...
     1194   52
     1195   53   </head>
     1196   54
     1197   55   <body>
     1198   56
     1199   57
     1200   58
     1201   59
     1202   60
     1203   61     <div id="content" class="${classes('wiki', create=not page.exists)}">
     1204   62
     1205...
     1206  213
     1207  214
     1208  215     </div>
     1209  216
     1210  217     ${ super() }
     1211  218
     1212  219
     1213  220   </body>
     1214  221 </html>
     1215# -- HTML OK
     1216}}}