Edgewall Software

Theme Plugins

Do not edit this page until this notice is removed

This proposal describes a way trac could get a working theme support. The main idea is that every project which uses trac should have it's own theme. Or at least each major trac installation.

The current way is overriding htdocs_location in the trac config to bypass the shipped css and js files and inject new ones. Using the site.html file (talking of trac 0.11) you can then override the way the template is rendered. Because of the great Genshi API with XPATH support nearly everything is overrideable. But trac upgrades are a pain in the ass and distributing themes does not work.

Solution

  • generic css classes (see below)
  • prefixing every css class with #x-trac
  • shipping two themes trac and custom, first one is the default trac theme, the latter a instance based theme, also see below
  • map theme providers to names using entrypoints
  • specify the used theme in the config with [trac]\ntheme=TracTheme

State Of Implementation

I am currently working on implementing this. See the most recent patch for some more information. Available themes in that patch:

  • TracTheme - a theme that looks like a normal trac installation
  • JinjaTheme - a theme that integrates trac into the jinja webpage as proof of concept
  • CustomTheme - a theme that loads the site.html and instance htdocs

Generic CSS Classes

One of the first things which should be done is cleaning up the shipped css files. They should use more generic classes and documented classes. Each plugin the developer should be able to use them too in his or her plugins.

Also, the trac core would only ship layout css files without color definitions. The default theme would then add the color definitions for the elements (links, browser etc). Currently the only way to change the trac colors is copying the default css files and replace all the color hex values with own ones.

To create a list of generic classes an analysis of the current css files and genshi templates would be required.

Also *all* css rules must be prefixed with #x-trac in order to avoid name collisions with existing templates of project webpages.

Example Theme Provider

This is an example theme provider for a distributable theme.

class MyTheme(Component):
    implements(IThemeProvider, ITemplateProvider)

    # IThemeProvider
    def get_theme_htdocs_id(self):
        return 'mytheme'

    def get_theme_site_template(self):
        return 'mytheme/site.html'

    # ITemplateProvider methods
    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        from pkg_resources import resource_filename
        return [('mytheme', resource_filename(__name__, 'htdocs'))]

Custom Theme Provider

This is included the trac distribution in order to load templates from the instance folder.

class CustomTheme(Component):
    implements(IThemeProvider)

    # IThemeProvider
    def get_theme_htdocs_id(self):
        return 'site'

    def get_theme_site_template(self):
        return 'site.html'

There will also be a UserTheme component class which uses the site.html from the trac instance folder and the template/htdocs folder in the instance. It's one of the both default themes:

  • trac - the trac default theme
  • custom - a special theme that forwards the htdocs/template lookups to the instance folders and uses the normal site.html as template.

Templates

Changes in the templates are quite small. The layout.html should look like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:xi="http://www.w3.org/2001/XInclude"
      xmlns:py="http://genshi.edgewall.org/"
      py:strip="">

  <head py:match="head">
    <title py:with="title = list(select('title/text()'))">
      <py:if test="title">${title} –</py:if>
      ${' – '.join(filter(None, [project.name, 'Trac']))}
    </title>
    <py:if test="chrome.links">
      <py:for each="rel, links in chrome.links.items()">
        <link rel="${rel}" py:for="link in links" py:attrs="link" />
      </py:for>
    </py:if>
    <py:if test="'SEARCH_VIEW' in perm" id="search">
      <link type="application/opensearchdescription+xml" rel="search"
            href="${href.search('opensearch')}" title="Search $project.name"/>
    </py:if>
    <script py:for="script in chrome.scripts"
            type="${script.type}" src="${script.href}"></script>
    ${Markup('&lt;!--[if lt IE 7]&gt;')}
    <script type="text/javascript" src="${href.chrome('common/js/ie_pre7_hacks.js')}"></script>
    ${Markup('&lt;![endif]--&gt;')}
    ${select("*[local-name() != 'title']")}
  </head>

  <!-- navigation helper -->
  <div py:def="navigation(category)" id="${category}" class="nav">
    <ul py:if="chrome.nav[category]">
      <li py:for="idx, item in  enumerate(chrome.nav[category])"
          class="${classes(first_last(idx, chrome.nav[category]), active=item.active)}">${item.label}</li>
    </ul>
  </div>

  <body py:match="body">

    <!-- search form -->
    <form py:if="'SEARCH_VIEW' in perm" id="search"
          action="${href.search()}" method="get"><div>
      <label for="proj-search">Search:</label>
      <input type="text" id="proj-search" name="q" size="18" accesskey="f" value="" />
      <input type="submit" value="Search" />
      <input type="hidden" name="wiki" value="on" />
      <input type="hidden" name="changeset" value="on" />
      <input type="hidden" name="ticket" value="on" />
    </div></form>

    <!-- meta navigation -->
    ${navigation('metanav')}

    <!-- main navigation -->
    ${navigation('mainnav')}

    <!-- trac body -->
    <div id="body">
      ${select('*|text()')}

      <script type="text/javascript" py:if="chrome.late_links">
        <py:for each="link in chrome.late_links.get('stylesheet')">
          $.loadStyleSheet("${link.href}", "${link.title}");
        </py:for>
      </script>
      <script py:for="script in chrome.late_scripts"
              type="${script.type}" src="${script.href}"></script>
    </div>

    <!-- alternative links -->
    <div id="altlinks" py:if="'alternate' in chrome.links">
      <h3>Download in other formats:</h3>
      <ul>
        <li py:for="idx, link in enumerate(chrome.links.alternate)"
            class="${first_last(idx, chrome.links.alternate)}">
          <a rel="nofollow" href="${link.href}" class="${link['class']}"
             py:content="link.title" />
        </li>
      </ul>
    </div>

    <!-- footer -->
    <div id="footer"><hr/>
      <a id="tracpowered" href="http://trac.edgewall.org/"><img
        src="${href.chrome('common/trac_logo_mini.png')}" height="30"
        width="107" alt="Trac Powered"/></a>
      <p class="left">
        Powered by <a href="${href.about()}"><strong>Trac ${trac.version}</strong></a><br />
        By <a href="http://www.edgewall.org/">Edgewall Software</a>.
      </p>
      <p class="right">${chrome.footer}</p>
    </div>
  </body>

  <xi:include href="${theme.get_theme_site_template()}"><xi:fallback /></xi:include>
</html>

All the data elements moved into little div/ul boxes which are quite flat. The included filename is retrieved from the theme provider.

Default Theme

Here's the default theme for the new structure.

class TracTheme(Component):
    implements(IThemeProvider)

    # IThemeProvider
    def get_theme_htdocs_id(self):
        return 'common'

    def get_theme_site_template(self):
        return 'trac_site.html'

And here's the trac_site.html. It's called this way so that we can load site.html from the instance folder. All other themes have to create a folder for their themes with an unique name.

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude" py:strip="">
  <head py:match="head">
    ${select('*')}
    <link rel="stylesheet" href="${theme.get_chrome_url('trac_style/css/style.css')}" />
    <link rel="stylesheet" href="${theme.get_chrome_url('trac_style/css/print.css')}" media="print" />
  </head>
  <body py:match="body">
    <div id="x-trac">
      <div id="banner">
        <div id="header" py:choose="">
          <a py:when="chrome.logo.src" id="logo" href="${chrome.logo.link}"><img
            src="${chrome.logo.src}" alt="${chrome.logo.alt}" /></a>
          <h1 py:otherwise=""><a href="${chrome.logo.link}">${project.name}</a></h1>
        </div>
        ${select('ul[@id="metanav"]')}
      </div>
      ${select('ul[@id="mainnav"]')}

      <div id="main">
        ${select('div[@id="body"]/*')}
        ${select('div[@id="altlinks"]')}
      </div>

      ${select('div[@id="footer"]')}
    </div>
  </body>
</html>

As you can see the idea is that the default layout.html does not include *any* style elements. It just wraps the content elements in divs and uls. (navigation bars, content etc). The default css files have all their rules prefixed with #x-trac in order to avoid clashes with included css files from project webpages. The idea is that you just have to add a div with the idea #x-trac where the trac should appear, select everything there and there you go. Additionally you can of course as shown above, move the navigation bars around thanks to the ass-kicking genshi xpath support.

theme.get_chrome_url() creates an url to the chrome folder of the current theme.

Last modified 9 years ago Last modified on Feb 5, 2009, 8:38:30 PM

Attachments (3)

Download all attachments as: .zip

Note: See TracWiki for help on using the wiki.