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.)
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.
First of all, I started to think of what my filter need to be able to.
Table of Contents Optionally shown So it can be switched off, but the anchors are created anyway. Tim Berners-Lee says:
‘Everything of importance deserves a URI.’
So I guess it is clear, why I want chapters to be accessible, although I don’t refer to it via a table of contents.
Flexible Title So the table of contents can be titled with Entry Contents, Table of Contents or Flatpage Index
Valid XHTML 1.1 So I can use it without being embarrassed of having errors in my HTML.
Usable on any HTML As not everybody uses Markdown, the filter must process usual HTML.
Nice-looking Anchors
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.
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)
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)
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.
I’ve used two variables, which you can easily customize:
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><link rel="archives" /></code>-HTML-Tag</a>
<ul>
<li><a href="#how-the-link-tag-works">How the <code><link></code>-tag works</a></li>
</ul>
</li>
</ul>
</div>
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.
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.
No Comments
There are no comments for this entry, be the first to leave one.