| 1 | = Architecture of the HTML templates |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | We also describe the future template page architecture which will be used when we switch to the Jinja2 template engine (see Proposals/Jinja). |
| 6 | For now this architecture closely follows the Genshi one. |
| 7 | |
| 8 | == Genshi architecture |
| 9 | |
| 10 | The Genshi template page architecture in Trac follows two key ideas: |
| 11 | - 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// |
| 12 | - 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 |
| 13 | |
| 14 | |
| 15 | 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. |
| 16 | |
| 17 | Let's take the example of a simple "end user" page, the search.html page. |
| 18 | What happens is that: |
| 19 | - search.html includes |
| 20 | - layout.html, which includes |
| 21 | - $chrome.theme (typically "theme.html", the default theme page that ships with Trac) |
| 22 | |
| 23 | In more details, the |
| 24 | [source:tags/trac-1.0.9/trac/search/templates/search.html search.html] is structured like this: |
| 25 | {{{#!html+genshi |
| 26 | <html> |
| 27 | <xi:include href="layout.html" /> |
| 28 | <head> |
| 29 | <title>Search</title> |
| 30 | ... |
| 31 | </head> |
| 32 | <body> |
| 33 | <div id="content" class="search"> |
| 34 | <h1>Search</h1> |
| 35 | ... |
| 36 | </div> |
| 37 | </body> |
| 38 | </html> |
| 39 | }}} |
| 40 | - it starts by including the layout.html page (`<xi:include href="layout.html" />`) |
| 41 | - it then provides the `<head>` and `<body>` elements specific to that search page; |
| 42 | these elements will be processed by the `<py:match>`es filters defined so far, |
| 43 | in the order in which they have been included |
| 44 | |
| 45 | The [source:tags/trac-1.0.9/trac/templates/layout.html layout.html] |
| 46 | page in turn is structured like this: |
| 47 | {{{#!html+genshi |
| 48 | <html> |
| 49 | <py:match path="head"><head> |
| 50 | <title py:with="title = list(select('title/text()'))"> |
| 51 | ${title} – ${project.name or 'Trac'} <!-- e.g. "Search - Trac" --> |
| 52 | </title> |
| 53 | ... |
| 54 | ${select("*[local-name() != 'title']|text()|comment()")} |
| 55 | </head></py:match> |
| 56 | |
| 57 | <py:match path="body"><body> |
| 58 | |
| 59 | ${select('*|text()|comment()')} <!-- e.g. "<h1>Search</h1>" |
| 60 | ... --> |
| 61 | ... |
| 62 | <div id="altlinks" py:if="'alternate' in chrome.links"> |
| 63 | ... |
| 64 | </div> |
| 65 | </body></py:match> |
| 66 | |
| 67 | <xi:include href="$chrome.theme"><xi:fallback /></xi:include> |
| 68 | </html> |
| 69 | }}} |
| 70 | - it defines `<py:match>` filters for transforming the content provided |
| 71 | in `<head>` and `<body>` elements in the including template (search.html); |
| 72 | the head filter adds some content to the `<title>` and prepends some content |
| 73 | in the <head>, the body filter appends some content in the <body> |
| 74 | - it then **dynamically** includes some "theme" page, `$chrome.theme` |
| 75 | |
| 76 | By default, this theme page will be our |
| 77 | [source:tags/trac-1.0.9/trac/templates/theme.html theme.html] template: |
| 78 | {{{#!xml |
| 79 | <html> |
| 80 | <body> |
| 81 | <div id="banner"> |
| 82 | ... |
| 83 | </div> |
| 84 | <div id="main"> |
| 85 | ... |
| 86 | ${select('*|text()|comment()')} <!-- e.g. "<h1>Search</h1>" |
| 87 | ... |
| 88 | <div id="altlinks" ... </div> --> |
| 89 | </div> |
| 90 | <div id="footer"> |
| 91 | ... |
| 92 | </div> |
| 93 | </body> |
| 94 | </html> |
| 95 | }}} |
| 96 | - it defines a `<py:match>` filter on the `<body>` tag |
| 97 | (the one that will be produced by the previously applied filters, |
| 98 | i.e. the output of the `<py:match>` from the layout.html) |
| 99 | and it will **embed** that element into some predefined HTML |
| 100 | structure (inside a div element), and |
| 101 | it will prepend and append other divs around it: |
| 102 | |
| 103 | So this dynamic theme template has the last say, and can theoretically re-order |
| 104 | the content generated by the previous filters any way it likes, although in practice |
| 105 | it simply inserts the body content produced by previous steps inside a predefined |
| 106 | structure (the `<div id="main">`). |
| 107 | |
| 108 | An example of another theme page: [https://github.com/chevah/trac-bootstrap-theme/blob/master/templates/theme.html trac-bootstrap-theme's theme.html]. |
| 109 | |
| 110 | |
| 111 | |
| 112 | Note that this scheme can be extended to additional intermediate levels, |
| 113 | for example see what we do with the admin panels. |
| 114 | |
| 115 | One such panel is |
| 116 | [source:tags/trac-1.0.9/trac/admin/templates/admin_basics.html admin_basics.html]: |
| 117 | {{{#!html+genshi |
| 118 | <html> |
| 119 | <xi:include href="admin.html" /> |
| 120 | <head> |
| 121 | <title>Basics</title> |
| 122 | </head> |
| 123 | |
| 124 | <body> |
| 125 | ... |
| 126 | </body> |
| 127 | </html> |
| 128 | }}} |
| 129 | - it starts by including the admin.html page |
| 130 | - then it provides the `<head>` and `<body>` elements specific to that panel |
| 131 | |
| 132 | The [source:tags/trac-1.0.9/trac/admin/templates/admin.html admin.html] page |
| 133 | is similar to the search.html page in that it includes the layout.html, but |
| 134 | it also first contains its own `<py:match>` templates to organize the content of |
| 135 | the admin panel which included it: |
| 136 | {{{#!html+genshi |
| 137 | <html> |
| 138 | <py:match path="head" once="true"><head> |
| 139 | <title>Administration: ${select('title/text()')}</title> |
| 140 | ${select("*[local-name() != 'title']")} |
| 141 | </head></py:match> |
| 142 | |
| 143 | <py:match path="body" once="true" buffer="false"><body> |
| 144 | <div id="content" class="admin"> |
| 145 | <h1>Administration</h1> |
| 146 | <div id="tabs"> |
| 147 | <div id="tabcontent"> |
| 148 | ${select("*|text()")} |
| 149 | <br style="clear: right" /> |
| 150 | </div> |
| 151 | </div> |
| 152 | |
| 153 | </body></py:match> |
| 154 | |
| 155 | <xi:include href="layout.html" /> |
| 156 | </html> |
| 157 | }}} |
| 158 | - defines `<py:match>` filters for `<head>` and `<body>` (of the including panel page), |
| 159 | which in turn will produce modified `<head>` and `<body>` elements |
| 160 | - it then includes the layout.html page (see above) |
| 161 | |
| 162 | Feel free to brush up your Genshi craft by reading ([G:GenshiTutorial#AddingaLayoutTemplate]), |
| 163 | as I just did ;-) |
| 164 | |
| 165 | |
| 166 | == Jinja2 architecture |
| 167 | |
| 168 | Jinja2 can do dynamic includes as well, or more precisely, dynamic ''extends''. |
| 169 | Therefore the Genshi approach can be transposed to Jinja: have the end user page extend the layout page, |
| 170 | then have the layout page extend whatever has been defined to be the theme page. |
| 171 | |
| 172 | The differences with Genshi are subtle: while in both case the control of the |
| 173 | output is delegated to the more generic page, with Jinja2 the parent only controls |
| 174 | what it puts around //blocks//. It can put some default content in these blocks, |
| 175 | but the end user page has the final say about what to do with this default content, |
| 176 | as it can reuse it inside its block (by calling `super()`) or not. |
| 177 | |
| 178 | Let's transpose the previous example of the search.html template. |
| 179 | For as long as we'll have both template engines coexisting, |
| 180 | we'll prefix the new Jinja2 templates with a `j`. |
| 181 | So we're now discussing: |
| 182 | - jsearch.html, which extends |
| 183 | - jlayout.html, which extends |
| 184 | - jtheme.html (as this is our default theme page) |
| 185 | |
| 186 | In more details, we describe what happens with the |
| 187 | [source:cboos.git/trac/search/templates/jsearch.html jsearch.html] page: |
| 188 | {{{#!html+jinja |
| 189 | # extends 'jlayout.html' |
| 190 | |
| 191 | <!DOCTYPE html> |
| 192 | <html> |
| 193 | <head> |
| 194 | <title> |
| 195 | # block title |
| 196 | ${_("Search")} ${ super() } |
| 197 | # endblock title |
| 198 | </title> |
| 199 | |
| 200 | # block head |
| 201 | ${ super() } |
| 202 | ... |
| 203 | # endblock head |
| 204 | </head> |
| 205 | |
| 206 | <body> |
| 207 | # block content |
| 208 | <div id="content" class="search"> |
| 209 | <h1>${_("Search")}</h1> |
| 210 | ... |
| 211 | </div> |
| 212 | </body> |
| 213 | </html> |
| 214 | }}} |
| 215 | - it starts by //extending// the jlayout.html page (`# extends 'jlayout.html') |
| 216 | - then it redefines the ''title'', ''head'' and ''content'' blocks, |
| 217 | and has to place a `${ super() }` expression in order to insert the default |
| 218 | content proposed by the extended template at the right place; |
| 219 | note that the presence of the `<html>`, `<head>` and `<body>` tags here |
| 220 | is strictly "decorative", it will be ignored in the final output. As we're |
| 221 | in a template extending another, only what's in the redefined blocks matters. |
| 222 | |
| 223 | These blocks are first defined in the extended template, jlayout.html. |
| 224 | |
| 225 | The [source:cboos.git/trac/templates/jlayout.html jlayout.html] page looks like this: |
| 226 | {{{#!html+jinja |
| 227 | # extends ('j' + chrome.theme) |
| 228 | |
| 229 | <!DOCTYPE html> |
| 230 | <html> |
| 231 | <head> |
| 232 | # block head |
| 233 | |
| 234 | <title> |
| 235 | # block title |
| 236 | – ${project.name or 'Trac'} |
| 237 | # endblock title |
| 238 | </title> |
| 239 | |
| 240 | ... |
| 241 | # endblock head |
| 242 | </head> |
| 243 | |
| 244 | <body> |
| 245 | # block content |
| 246 | # endblock content |
| 247 | |
| 248 | ... |
| 249 | <div id="altlinks"> |
| 250 | ... |
| 251 | </div> |
| 252 | </body> |
| 253 | </html> |
| 254 | }}} |
| 255 | - first **dynamically** //extends// in turn some "theme" page (`# extends ('j' + chrome.theme)`) |
| 256 | - then the jlayout.html template defines a few blocks: |
| 257 | - the ''head'' block and its ''title'' sub-block; here we understand why we've put |
| 258 | the ''title'' block **outside** of the ''head'' block in the jsearch.html template: |
| 259 | the jlayout.html's ''head'' block contains among other things a `<title>` element, |
| 260 | and we're reusing that default content in the inheriting ''head'' block; if in |
| 261 | jsearch.html we had defined the <title> element in the ''head'' block as well, |
| 262 | we would have had two of these <title> elements in that block |
| 263 | - the ''content'' block which is filled with some predefined, generic content, |
| 264 | mostly the same stuff that could be found in the corresponding layout.html, |
| 265 | in `<py:match>` filters |
| 266 | |
| 267 | By default, this theme template will be our |
| 268 | [source:cboos.git/trac/templates/jtheme.html jtheme.html] page: |
| 269 | {{{#!html+jinja |
| 270 | |
| 271 | <!DOCTYPE html> |
| 272 | <html> |
| 273 | <head> |
| 274 | # block head |
| 275 | # endblock head |
| 276 | </head> |
| 277 | |
| 278 | <body> |
| 279 | # block body |
| 280 | <div id="banner"> |
| 281 | ... |
| 282 | </div> |
| 283 | <div id="main"> |
| 284 | ... |
| 285 | # block content |
| 286 | (here goes the content of the content block produced by layout.html) |
| 287 | # endblock content |
| 288 | </div> |
| 289 | <div id="footer"> |
| 290 | ... |
| 291 | </div> |
| 292 | # block body |
| 293 | </body> |
| 294 | </html> |
| 295 | }}} |
| 296 | - it defines a ''head'' block inside an otherwise empty `<head>` element; |
| 297 | this means this is simply a "slot" that will be filled by the content |
| 298 | of the `head` block in the extending templates (in this case, jlayout.html) |
| 299 | - it contains a `<body>` element; |
| 300 | as we want to replicate what the original theme.html did, |
| 301 | what we want to achieve here is to provide a ''slot'' |
| 302 | at the place where we want to insert the content |
| 303 | produced by the jlayout.html template; |
| 304 | I didn't name that inner block "body", as this could be confusing: |
| 305 | we're not in control of the `<body>` element there, just of |
| 306 | a fraction of it, the bottom part of the main div. |
| 307 | |
| 308 | Note that //if// we wanted to be in full control of the body in the extending |
| 309 | template, we could (as opposed to what you can do in Genshi): we would simply |
| 310 | have to redefine the ''body'' block which contains all of the default structure |
| 311 | (possibly reusing the content of that block by a call to `${ super() }`). |
| 312 | In our case, neither jsearch.html nor jlayout.html redefine the `body` block, |
| 313 | as they're happy with what jtheme does with it. |
| 314 | |
| 315 | |
| 316 | Depending how one looks at it, it seems this approach is even more flexible than |
| 317 | what we had in Genshi, as the end user template can decide which bits of the |
| 318 | parent template it wants or not ("bottom-up" control), something that was not |
| 319 | readily doable with Genshi ("top-down" control). |
| 320 | |
| 321 | |
| 322 | 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. |
| 323 | |
| 324 | Let's take for example the Logging admin panel, [source:cboos.git/trac/admin/templates/jadmin_logging.html jadmin_logging.html]: |
| 325 | {{{#!html+jinja |
| 326 | # extends "jadmin.html" |
| 327 | <html> |
| 328 | <head> |
| 329 | <title> |
| 330 | # block admintitle |
| 331 | Logging |
| 332 | # endblock admintitle |
| 333 | </title> |
| 334 | </head> |
| 335 | |
| 336 | <body> |
| 337 | # block adminpanel |
| 338 | ... |
| 339 | # block adminpanel |
| 340 | </body> |
| 341 | </html> |
| 342 | }}} |
| 343 | - it starts by extending the jadmin.html page |
| 344 | - then it provides the `<head>` and `<body>` elements specific to that panel, |
| 345 | more precisely the //admintitle// and //adminpanel// blocks (if some JavaScript |
| 346 | or other resources are needed, the //head// could be redefined as well) |
| 347 | |
| 348 | The [source:cboos.git/trac/admin/templates/jadmin.html@jinja2 jadmin.html] page |
| 349 | is similar to the jsearch.html page in that it extends the jlayout.html: |
| 350 | {{{#!html+jinja |
| 351 | # extends "jlayout.html" |
| 352 | <html> |
| 353 | <head> |
| 354 | <title> |
| 355 | # block title |
| 356 | Administration: |
| 357 | # block admintitle |
| 358 | # endblock admintitle |
| 359 | ${ super() } |
| 360 | # endblock |
| 361 | </title> |
| 362 | </head> |
| 363 | |
| 364 | <body> |
| 365 | # block content |
| 366 | <div id="content" class="admin"> |
| 367 | <h1>Administration</h1> |
| 368 | <div id="tabs"> |
| 369 | <div id="tabcontent"> |
| 370 | # block adminpanel |
| 371 | # endblock adminpanel |
| 372 | <br style="clear: right" /> |
| 373 | </div> |
| 374 | </div> |
| 375 | # endblock content |
| 376 | </body> |
| 377 | </html> |
| 378 | }}} |
| 379 | |
| 380 | //See [1df4e05c/cboos.git] for the full conversion of admin.html -> jadmin.html and admin_logging.html -> jadmin_logging.html.// |
| 381 | |
| 382 | |
| 383 | I omitted the discussion of the |
| 384 | replacement for the `<xi:include href="site.html>` template |
| 385 | (`# include site_head.html` and `# include site_body.html`). |