Table Of Contents: Django Template Filter

Written by Martin Geber on November 4, 2007 at 2:03 p.m. Filed unter: AccessibilityDjangoUsabilityNo Comments yet. Trackback URL.

Contents

I always loved being able to link back a certain chapter of a webpage. Sometimes it is great to just link to the source code of a weblog entry or any other part of it. To enable the visitors of my weblog to do that I wanted to set up some kind of Table of Contents.

I wanted it to be flexible, so that I can adjust whether the table of contents is shown or not. This makes sense, because some articles have just two headlines and a table of contents with two points looks pathetic. So the users would be able to click on the headline and jump to it directly. (Now they can copy and paste the URL given in the address line.)

Markdown already has a TOC extension

Like discussed in Markdown With Syntax Highlighting In Django I don't use an HTML-WYSIWYG Editor any more.

Markdown enables me to write much faster and is much slimmer (but needs longer to load, though). As Markdown already has an extension for creating a table of contents I just used it.

So I installed it, similar to CodeHilite, and changed my Django-template to this:

{{ object.entry|markdown:"codehilite,toc" }}

The result was incredibly horrible:

So I started rewriting the extension. After hours and hours I just quit on it. I choose to write my own filter.

Creating the Table of Contents Template Filter

First of all, I started to think of what my filter need to be able to.

Requirements for my Table of Contents Filter

In the next step I needed to set the filter up. In case you know how to set a template filter up, skip the following paragraph.

Setting up the Filter

According to the Django Documentation, we need to set up an folder into an existing application. I use utils/templatetags, where utils is my application (included in your INSTALLED_APPS-setting-variable) and templatetags is predefined by Django.

In my entry about how to set up weblog archives, I created a file called taglib.py, therefore I recommend to create a file called filterlib.py now.

(I'm not sure if this is the best way, if you have another idea, let me know)

Writing the Django Template Filter

Now, we have to write the filter:

from django import template
from django.shortcuts import render_to_response
from django.template.defaultfilters import stringfilter

from django.conf import settings

import re

register = template.Library()

@register.filter(name='toc')
@stringfilter
def table_of_contents(html, show_toc=None):

    LINK_CLASS = 'anchor'
    TOC_ID = 'toc'

    re_tags = re.compile('<.*?>', re.DOTALL)
    re_entities = re.compile('&.*?;', re.DOTALL)

    def slug(text):
        to_minus = [' ', '-', '_']
        text = re.sub(re_tags,'', text.lower())
        text = re.sub(re_entities,'', text)
        cleaned = []
        for l in text:
            if l in to_minus:
                l = '-'
            elif not l.isalnum():
                continue
            cleaned.append(l)
        cleaned = re.sub('-{2,}', '-', ''.join(cleaned))
        cleaned = cleaned.startswith('-') and cleaned[1:] or cleaned
        cleaned = cleaned.endswith('-') and cleaned[:-1] or cleaned
        return cleaned

    headlines = re.findall('<h(?P<type>\d)>(.*?)<\/h(?P=type)>', html)
    if len(headlines) is 0:
        return html
    last_weight, toc = min([int(x[0]) for x in headlines]), ''
    for h in headlines:
        current_weight = int(h[0])
        s = slug(h[1])
        html = html.replace('<h%s>%s</h%s>' % (h[0], h[1], h[0]),
                            '<h%s><a href="#%s" id="%s" class="%s">%s</a></h%s>' % (current_weight,
                                             s, s, LINK_CLASS, h[1], current_weight))
        if show_toc is not None:
            if (current_weight-last_weight) >= 1:
                toc = '%s\n\t<ul>\n' % toc[:-6] * (current_weight-last_weight)
            elif (last_weight-current_weight) >= 1:
                toc += '\t</ul>\n\t\t</li>\n'*(last_weight-current_weight)
            toc += '\t\t<li><a href="#%s">%s</a></li>\n' % (s, h[1])
            last_weight = current_weight
    if show_toc is not None:
        toc += '\t</ul>\n'*(toc.count('<ul>') - toc.count('</ul>'))
        toc += '\t\t</li>\n'*(toc.count('<li>') - toc.count('</li>'))
        toc = '<div id="%s">%s\n\t<ul>\n%s\t</ul>\n</div>' % (TOC_ID, show_toc, toc)
    return '%s %s' % (toc, html)

Notes on the source code

The Table of Contents will be added above the given HTML. This is a difference compared to the Markdown extension. But this is better for accessibility reason. In case someone browses your website by a screen reader, s/he will be thankful to get an overview of the contents before reading all the contents and hear the contents later on.

I've written my own slug function, but you are welcome to use the slugify-function by Django. Do this:

from django.template.defaultfilters import slugify as slug

def table_of_contents(html, show_toc=None):
    LINK_CLASS = 'anchor'
    TOC_ID = 'toc'
    re_tags = re.compile('<.*?>', re.DOTALL)
    re_entities = re.compile('&.*?;', re.DOTALL)
    # Remove ``def slug()``
    headlines = re.findall('<h(?P<type>\d)>(.*?)<\/h(?P=type)>', html)

Let me know, when you change anything else, so I can add it here, maybe others are interested in your idea as well.

Configurations on the source code

I've used two variables, which you can easily customize:

Using this filter in a Django Template

After you've added the source code to your templatetags folder, e.g. in the file filterlib.py, you can create your template.

In case you don't want a table of contents above the text, use the filter like this:

{% load filterlib %}

<!-- Don't supply any argument to the filter, this enables the
     headlines to be linked back, and click able (referring to themselves),
     but there is no table of contents shown above -->
{{ object.html_text|toc }}

<!-- Sample result: -->
<h1><a href="#testing-first-headline" id="testing-first-headline">Testing first headline</a></h1>
<p>Test of first headline</p>
<h2><a href="#just-testing" id="just testing">Just Testing</a></h2>
<p>Test of second headline</p>

In case you'd like to have a table of contents above the text, use the filter like this:

{% load filterlib %}

<!-- Create a Template Tag, which will add a table of contents
     above the text titled with "Contents" as headline 2

     As I don't know which headline dimension, you prefer, I 
     choose, to let the HTML around the TOC-Heading be flexible
     as well. -->
{{ object.html_text|toc:"<h2>Contents</h2>" }}

<!-- Sample result:
     (Note the original HTML-Tags within the headlines are still there.) -->
<div id="toc"><h2>Contents</h2>
    <ul>
        <li><a href="#link-relarchives-html-tag"><code>&lt;link rel=&quot;archives&quot; /&gt;</code>-HTML-Tag</a>
    <ul>
        <li><a href="#how-the-link-tag-works">How the <code>&lt;link&gt;</code>-tag works</a></li>
</ul>
        </li>
</ul>
</div>

One Problem of this filter

The only problem, I have with this filter is, that it isn't translation able. This means you have to give the filter one or more words in one language without being able to use i18n.

I hope someone finds a solution for that, and tells me, how to do it.

Having an Overvalue

Congratulations! You just added a real overvalue to your website. Users are now able to link to certain parts of the page and handycaped persons have the ability (in case you use the table of contents) to jump directly to parts of their interest.

And you have the ability to refer to parts of old entries (in case you have a weblog), what provides a better browsing experience to readers of you page.

Comments

The comments also include all Trackbacks.

Post a Comment


Or

Your Way: Home » Thoughts » 2007 » November » Sunday, 4 » Table Of Contents: Django Template Filter