Edgewall Software

Architecture of the HTML templates

Here we explain the current template page architecture in Trac which is used since the adoption of the Genshi template engine, i.e. since Trac 0.11.

We also describe the future template page architecture which will be used when we switch to the Jinja2 template engine (see Proposals/Jinja). For now this architecture closely follows the Genshi one.

Genshi architecture

The Genshi template page architecture in Trac follows two key ideas:

  • all pages focus on providing the content which is unique to them, and the common parts and look and feel is obtained by including a "base" template, layout.html
  • the layout.html includes a dynamic template which takes care of the common look and feel and can be substituted through the configuration; this is how "themes" are handled, and the default dynamic inclusion is the theme.html template

I've never really tried the TH:ThemeEnginePlugin plugin, or alternatives, so I can't be sure if I got it right, but from what I can see in Trac's code base itself, the idea with Genshi-based themeing (and page architecture in general) was to have a dynamically loaded "theme" template page that would primarily be in charge of the main structure of all HTML pages.

The search.html template

Let's take the example of a simple "end user" page, the search.html page. What happens is that:

  • search.html includes
    • layout.html, which includes
      • $chrome.theme (typically "theme.html", the default theme page that ships with Trac)

In more details, the search.html is structured like this:

<html>
  <xi:include href="layout.html" />
  <head>
    <title>Search</title>
    ...
  </head>
  <body>
    <div id="content" class="search">
      <h1>Search</h1>
      ...
    </div>
  </body>
</html>
  • it starts by including the layout.html page (<xi:include href="layout.html" />)
  • it then provides the <head> and <body> elements specific to that search page; these elements will be processed by the <py:match>es filters defined so far, in the order in which they have been included

The layout.html page in turn is structured like this:

<html>
  <py:match path="head"><head>
    <title py:with="title = list(select('title/text()'))">
      ${title}${project.name or 'Trac'}  <!-- e.g.  "Search - Trac" -->
    </title>
    ...
    ${select("*[local-name() != 'title']|text()|comment()")}
  </head></py:match>

  <py:match path="body"><body>

    ${select('*|text()|comment()')}         <!-- e.g. "<h1>Search</h1>"
                                                       ... -->
    ...
    <div id="altlinks" py:if="'alternate' in chrome.links">
      ...
    </div>
  </body></py:match>

  <xi:include href="$chrome.theme"><xi:fallback /></xi:include>
</html>      
  • it defines <py:match> filters for transforming the content provided in <head> and <body> elements in the including template (search.html); the head filter adds some content to the <title> and prepends some content in the <head>, the body filter appends some content in the <body>
  • it then dynamically includes some "theme" page, $chrome.theme

By default, this theme page will be our theme.html template:

<html>
  <body>
    <div id="banner">
      ...
    </div>
    <div id="main">
      ... 
      ${select('*|text()|comment()')}       <!-- e.g. "<h1>Search</h1>"
                                                       ... 
                                                       <div id="altlinks" ... </div> -->
    </div>
    <div id="footer">
      ...
    </div>
  </body>
</html>
  • it defines a <py:match> filter on the <body> tag (the one that will be produced by the previously applied filters, i.e. the output of the <py:match> from the layout.html) and it will embed that element into some predefined HTML structure (inside a div element), and it will prepend and append other divs around it:

So this dynamic theme template has the last say, and can theoretically re-order the content generated by the previous filters any way it likes, although in practice it simply inserts the body content produced by previous steps inside a predefined structure (the <div id="main">).

An example of another theme page: trac-bootstrap-theme's theme.html.

The admin.html template as container for other templates

Note that this scheme can be extended to additional intermediate levels, for example see what we do with the admin panels.

One such panel is admin_basics.html:

<html>
  <xi:include href="admin.html" />
  <head>
    <title>Basics</title>
  </head>

  <body>
    ...
  </body>
</html>
  • it starts by including the admin.html page
  • then it provides the <head> and <body> elements specific to that panel

The admin.html page is similar to the search.html page in that it includes the layout.html, but it also first contains its own <py:match> templates to organize the content of the admin panel which included it:

<html>
  <py:match path="head" once="true"><head>
    <title>Administration: ${select('title/text()')}</title>
    ${select("*[local-name() != 'title']")}
  </head></py:match>

  <py:match path="body" once="true" buffer="false"><body>
    <div id="content" class="admin">
      <h1>Administration</h1>
      <div id="tabs">
      <div id="tabcontent">
        ${select("*|text()")}
        <br style="clear: right" />
      </div>
    </div>

  </body></py:match>

  <xi:include href="layout.html" />
</html>
  • defines <py:match> filters for <head> and <body> (of the including panel page), which in turn will produce modified <head> and <body> elements
  • it then includes the layout.html page (see above)

Feel free to brush up your Genshi craft by reading (GenshiTutorial#AddingaLayoutTemplate), as I just did ;-)

Jinja2 architecture

Jinja2 can do dynamic includes as well, or more precisely, dynamic extends. Therefore the Genshi approach can be transposed to Jinja: have the end user page extend the layout page, then have the layout page extend whatever has been defined to be the theme page.

The differences with Genshi are subtle: while in both case the control of the output is delegated to the more generic page, with Jinja2 the parent only controls what it puts around blocks. It can put some default content in these blocks, but the end user page has the final say about what to do with this default content, as it can reuse it inside its block (by calling super()) or not.

The search.html template

Let's transpose the previous example of the search.html template.

So we're now discussing:

A template which extends another can redefine the content of the named blocks defined in the extended template. This redefinition may or may not include the original content of the block in the extended template (${ super() }).

We first give an overview of the three templates and how their content will be combined in the generated output.

theme.html layout.html
# extends theme.html
search.html
# extends layout.html
<html>
 <head>
# block head
# endblock head
 </head>
 <body>
# block body
… banner + metanav + mainnav + contextnav + warnings + notices …
# block content
# endblock content
… footer …
# endblock body
 </body>
</html>


(head and content blocks
have no default content)

<html>
 <head>
# block head
 <title>
# block title
- Trac
# endblock title
 </title>
… meta + link + script …
# endblock head
 </head>
 <body>
# block content
… alternate formats …
# endblock content
 </body>
</html>


(head and content blocks
have default content)

<html>
 <head>
  <title>
# block title
Search
${ super() }
# endblock title
  </title>
# block head
${ super() }
… meta …
# endblock head
 </head>
 <body>
# block content
<div class="content">
… Search Results …
</div>
${ super() }
# endblock content
 </body>
</html>

generated output:

<html>
 <head>
  <title>
Search
- Trac
  </title>
… meta + link + script …
… meta …
 </head>
 <body>
… banner + metanav + mainnav + contextnav + warnings + notices …
<div class="content">
… Search Results …
</div>
… alternate formats …
… footer …
 </body>
</html>

Let's have a closer look at the content of each template, starting with the most specific template, in our example the search.html page:

# extends 'layout.html'

<!DOCTYPE html>
<html>
  <head>
    <title>
    #   block title
    ${_("Search")}    ${ super() }
    #   endblock title
    </title>

    # block head
    ${ super() }
    ...
    # endblock head
  </head>

  <body>
    # block content
    <div id="content" class="search">
      <h1>${_("Search")}</h1>
      ...
    </div>
  </body>
</html>
  • it starts by extending the layout.html page (`# extends 'layout.html')
  • then it redefines the title, head and content blocks, and has to place a ${ super() } expression in order to insert the default content proposed by the extended template at the right place; note that the presence of the <html>, <head> and <body> tags here is strictly "decorative", they will be ignored in the final output. As we're in a template extending another, only what's in the redefined blocks matters. This is especially important to remember, as this can be a source of error: for example, no matter that you added your <script> tags to the <head> of your template, they will only make it in the final output if you took care of placing them within the # block head… (e.g. this fix)

These blocks are first defined in the extended template, layout.html.

The layout.html page looks like this:

# extends chrome.theme

<!DOCTYPE html>
<html>
  <head>
    # block head

    <title>
    #  block title
    – ${project.name or 'Trac'}
    #  endblock title
    </title>

    ...
    # endblock head
  </head>

  <body>
    # block content
    # endblock content

    ...
    <div id="altlinks">
      ...
    </div>
  </body>
</html>
  • first dynamically extends in turn some "theme" page (# extends chrome.theme)
  • then the layout.html template defines a few blocks:
    • the head block and its title sub-block; here we understand why we've put the title block outside of the head block in the search.html template: the layout.html's head block contains among other things a <title> element, and we're reusing that default content in the inheriting head block; if in search.html we had defined the <title> element in the head block as well, we would have had two of these <title> elements in that block
    • the content block which is filled with some predefined, generic content, mostly the same stuff that could be found in the corresponding layout.html, in <py:match> filters

By default, this theme template will be our theme.html page:

<!DOCTYPE html>
<html>
  <head>
    # block head
    # endblock head
  </head>

  <body>
    # block body
    <div id="banner">
      ...
    </div>
    <div id="main">
      ... 
      # block content
      (here goes the content of the content block produced by layout.html)
      # endblock content
    </div>
    <div id="footer">
      ...
    </div>
    # block body
  </body>
</html>
  • it defines a head block inside an otherwise empty <head> element; this means this is simply a "slot" that will be filled by the content of the head block in the extending templates (in this case, layout.html)
  • it contains a <body> element; as we want to replicate what the original theme.html did, what we want to achieve here is to provide a slot at the place where we want to insert the content produced by the layout.html template; I didn't name that inner block "body", as this could be confusing: we're not in control of the <body> element there, just of a fraction of it, the bottom part of the main div.

Note that if we wanted to be in full control of the body in the extending template, we could (as opposed to what you can do in Genshi): we would simply have to redefine the body block which contains all of the default structure (possibly reusing the content of that block by a call to ${ super() }). In our case, neither search.html nor layout.html redefine the body block, as they're happy with what theme does with it.

Depending how one looks at it, it seems this approach is even more flexible than what we had in Genshi, as the end user template can decide which bits of the parent template it wants or not ("bottom-up" control), something that was not readily doable with Genshi ("top-down" control).

The admin.html template as container for other templates

Like what we did with Genshi, this scheme can be extended to support additional intermediate levels. We did that for the admin and the preference panels.

Let's take for example the Logging admin panel, admin_logging.html:

# extends "jadmin.html"
<html>
  <head>
    <title>
      # block admintitle
      Logging
      # endblock admintitle
    </title>
  </head>

  <body>
    # block adminpanel
    ...
    # block adminpanel
  </body>
</html>
  • it starts by extending the admin.html page
  • then it provides the <head> and <body> elements specific to that panel, more precisely the admintitle and adminpanel blocks (if some JavaScript or other resources are needed, the head could be redefined as well)

The admin.html template is similar to the search.html page in that it extends the layout.html:

# extends "jlayout.html"
<html>
  <head>
    <title>
      # block title
      Administration:
      #   block admintitle
      #   endblock admintitle 
      ${ super() }
      # endblock
    </title>
  </head>

  <body>
    # block content
    <div id="content" class="admin">
      <h1>Administration</h1>
      <div id="tabs">
      <div id="tabcontent">
        # block adminpanel
        # endblock adminpanel
        <br style="clear: right" />
      </div>
    </div>
    # endblock content
  </body>
</html>

See [1df4e05c/cboos.git] for the initial full conversion of admin.html → jadmin.html and admin_logging.html → jadmin_logging.html)

I omitted the discussion of the replacement for the <xi:include href="site.html> template (# include site_head.html and # include site_body.html).

Last modified 8 years ago Last modified on Jan 15, 2017, 1:10:36 AM
Note: See TracWiki for help on using the wiki.