djangoproject.com | python.org | nginx.org
version seven.
  http://demongin.org
demongin.org - Django: Use Template Tags to Swap HTML Tags

Django: Use Template Tags to Swap HTML Tags

A short how-to that describes writing a custom template filter in Django to replace one HTML tag with another.


Wednesday, 2010-02-03 | Django, On the Internet, Programming

[I]n the world of global capital, distressed assets are still more valued than distressed people.

Naomi Klein

So, the recent addition of demongin.org to the roster of contributors to the RSS aggregator planetdjango.org brought my attention to a bug that was bollixing up code blocks in my RSS feeds.

Basically, the "code" tag I use to make my syntax high-lighting work wasn't being recognized by RSS clients (and for good reason) and code blocks were collapsing into an ugly, illegible mess:

Screen capture of broken RSS code blocks

Naturally, I couldn't just just throw off some quick postgres-fu to search and replace those tags with pre tags because doing so would break my syntax highlighting. So I decided to write a short Template Filter (a kind of Template Tag to sub code tags for pre tags when generating the RSS XML.

demongin/blog/templatetags/rss_filters.py

The first file I generated was the file in which the Template Filter was going to live. I named it demongin/blog/templatetags/rss_filters.py and wrote it out like this:
from django import template                                                                                                                                  
from django.template.defaultfilters import stringfilter

import re

register = template.Library()

#
#   Helper functions used for match/replace actions
#

def code_repl(match_obj, tag, close=False):
    if close:
        return "<%s>;" % "/" + tag
    else:
        return "<%s>"  % tag


@stringfilter
@register.filter(name='tag_to_other_tag')
def tag_to_other_tag(s, tags):
    """
    Takes a string, the search tag and the replacement tag. Returns a string.
    """
    try:
        # split_contents() knows not to split quoted strings.
        in_tag, out_tag = tags.split(",")
        in_tag = in_tag.strip()
        out_tag = out_tag.strip()
    except ValueError:
        return s

    p_tag_open = re.compile("<%s\s?.*?>|<%s>)" % (in_tag, in_tag))
    p_tag_close = re.compile("<%s>" % "/" + in_tag)
    s = re.sub(p_tag_open, code_repl(s, out_tag), s)
    s = re.sub(p_tag_close, code_repl(s, out_tag, close=True), s)
    return str(s)
The syntax of the filter comes (more-or-less) straight from the official Django documentation. There are, however, two features worth noting:
  1. I opted not to use BeautifulSoup even though I'm very fond of that particular module. The reason I did this is because I realized that I could do what I needed to do with regular expressions in fewer lines of code. And even though I know that HTML tags are not regular expressions and using a parser is technically a "better" solution, I decided to go with the solution that was easier to write/understand.
  2. I also made the function extensible/scalable: the way I set it up, you can pass the conversion function an "in" tag and an "out" tag (or, if you prefer, a "find" tag and a "replace" tag). I did this so that I could swap out other HTML tags in the future, if necessary.

demongin/templates/feeds/django_description.html

The next file to edit was my existing template for the "description" portions of the RSS feed. When I was done making change, the file looked like this:
{% load rss_filters %}                                                                                                                                       

{% autoescape off %}
<i>{{ obj.subtitle }}</i><br />
{{ obj.body|tag_to_other_tag:"code, pre" }}
{% endautoescape %}
Easy-peasy: I load my new rss_filters module at the top of the code and then add a line that runs the body of my posts through my new custom filter. I could, of course, run anything I wanted through my filter (so long as it was a string or could be converted to a string), but all I need (for the time-being) is to filter the code tags out of the body, so that's all I added.

Simple.

Once I was done there, I did the same thing for my primary feed and you can see the results in either feed by loading it into your favorite feed reader. http://demongin.org/feeds/latest or http://demongin.org/feeds/django now come out like this:

Fixed RSS feed, thanks to the new custom Template Filter

And that's word.