Coloration syntaxique

Mer 23 avril 2008

J'ai réussi, en quelques minutes, à intégrer une coloration syntaxique pour le code source. Quand on a des briques toutes faites, c'est encore plus drôle qu'une blague de sinkrou. Euh, pardon, mauvais exemple...

Les templatetags

Dans Django, les "templatetags" sont des ensembles de fonctions permettant de faire des "petits traitements" sur les contenus affichés dans une page...

Par exemple, "capfirst", permet de transformer la chaîne "ceci est un test" en "Ceci est un test".

Ils sont définis au niveau du coeur du framework, mais comme Django se veut extensible, il est bien sûr possible de définir ses propres tags. La documentation explique très bien comment organiser les templatetags dans l'application, je ne reviens pas là-dessus.

Pygmentize

Pygments est une bibliothèque Python permettant de préparer la coloration syntaxique d'un bout de code à l'intérieur d'une page HTML. Pygments récupère une chaîne en entrée, essaie de deviner quel type de langage c'est, et s'il y arrive, "encadre" les mots-clés du langage par des petits <span />, auxquels sont affectés des classes CSS en fonction du type. Si Pygments n'arrive pas à découvrir ce qu'est le langage utilisé, il laisse tomber.

Si on prend le cas de Python, l'élément du langage "def" (qui introduit la définition d'une fonction) sera considéré comme "mot-clé du langage" et sera transformé en <span class="k">def</span>. Et ainsi de suite pour le reste du code.

Mettre tout ça en route

Par exemple, dans mon modèlesgabarit qui affiche les articles du blog, j'ai une boucle qui récupère le texte l'accroche de l'article et l'affiche :

{% for article in latest %}
/* je coupe... */
    <div class="content">
        {{ article.render_lead }}
    </div>
/* je coupe encore... */    
{% endfor %}

La méthode "render_lead" est une méthode perso pour renvoyer le contenu en html, en fonction du markup choisi... je reviendrai là-dessus une autre fois.

Dans mes templatetags, j'ai une fonction que j'appelle pygmentize, et qui sera déclarée comme un filtre auprès de Django. Le code qui suit est tiré du snippet #25. Il n'est sûrement pas optimal, mais il a le mérite de fonctionner sans BeautifulSoup.

from django import template
import re
import pygments
from pygments.lexers import guess_lexer, PythonLexer
from pygments.formatters import HtmlFormatter

register = template.Library()

regex = re.compile(r'<code>(.*?)</code>', re.DOTALL)

@register.filter(name='pygmentize')
def pygmentize(value):
    last_end = 0
    to_return = ''
    found = 0
    for match_obj in regex.finditer(value):
        code_string = match_obj.group(1)
        try:
            lexer = guess_lexer(code_string)
        except Exception, e:
            lexer = PythonLexer()
        # Adding it on Apr 23rd 2008
        code_string = unescape(code_string)
        #
        pygmented_string = pygments.highlight(code_string, lexer,
            HtmlFormatter(nowrap=True))
        to_return = to_return + value[last_end:match_obj.start(1)] +
            pygmented_string.encode('utf-8')
        last_end = match_obj.end(1)
        found = found + 1
    to_return = to_return + value[last_end:]
    return to_return

register.filter(name='unescape')
def unescape(value):
    return value.replace(
        '&quot;', '"').replace(
        '<', '<').replace(
        '>', '>').replace(
        '&amp;', '&')

Le parsing de tout ce qui ressemble de près ou de loin à du code est très simple, c'est une expression régulière. À l'avenir, on peut très bien imaginer qu'on utilise BeautifulSoup pour être encore plus sûr du résultat. J'ai fait divers tests avec BeautifulSoup, mais j'ai eu un méli-mélo de charsets qui m'ont emmerdé, et BeautifulSoup supprime des choses dont je n'ai pas envie de me défaire, en sus... Bref.

Pour chaque "code" détecté, la fonction guess_lexer essaie de deviner le type de code source utilisé. Puis la méthod highlight transforme le code en le balisant avec les petits "spans".

J'utilise une petite fonction pour supprimer les doubles-quotes et autres entités HTML, parce que markdown me les échappait un peu brutalement.

Je reviens à mon template...

Il me suffit de rajouter

{% load pygmentize %}

et de changer l'affichage de mon accroche :

{{ article.render_lead|pygmentize }}

Test, commit, upload, reload, terminé.

[Ajout du 2008-04-24 - 08:44] Oups, j'ai oublié hier soir... C'est pas tout d'avoir son code spannisé dans tous les sens... Encore faut-il avoir la feuille de style CSS qui va bien. Là encore, pygments vient à la rescousse :

$ pygmentize -f html -S <theme-name> -a code

Cette commande génère une feuille CSS pour pygmentize, du genre :

code  { background: #eeeedd; }
code .c { color: #228B22 } /* Comment */
code .err { color: #a61717; background-color: #e3d2d2 } /* Error */
code .k { color: #8B008B; font-weight: bold } /* Keyword */
/* etc, etc, etc... */

Pour ma part, la feuille de style que j'utilise est basée sur le thème "perldoc" (pas parce que je suis fan de Perl, mais parce que c'est celle qui reste la plus lisible en fonction de mon nouveau design).