Parce que le printemps est haïssable...

Coloration syntaxique

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(
        '&lt;', '<').replace(
        '&gt;', '>').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).

24 Avril 2008 - 07:52, par NiKo

Tu voudrais pas mettre les blocs de code dans une balise plutôt ?

24 Avril 2008 - 07:54, par NiKo

Dans mon précédent commentaire, je parlais de la balise <pre/> bien sûr.

document.writeln('Hmm, tu devrais échapper le html dans les commentaires, aussi')

24 Avril 2008 - 07:55, par NiKo

Désolé de polluer, mais il semble que tu fasses l'équivalent d'un strip_tags sur le contenu du commentaire, il vaudrait mieux échapper tout ça, amha.

24 Avril 2008 - 08:15, par No'

Certes, on peut rajouter la balise "pre"... Pour l'échappement, je suis confusé : je pensais que strip_tags suffisait... Après tout, il vire les balises HTML.

24 Avril 2008 - 08:38, par NaWer

Pendant une minute, j'ai pensé à une coloration dans pyroom !! puis je me suis rappeler que ton blog mutait... J'ai quand même eu le temps de bzr avant que mon cerveau tilt.

24 Avril 2008 - 11:51, par Gilles

Bien joué, No`. Il me reste justement à intégrer un système de coloration syntaxique à mon projet django. Je vais tester des petits trucs.

Concernant render_lead, tu rends le texte markupé en HTML directement ou tu stockes la convertion HTML dans la base de données pour un rendu direct HTML ? Parce que j'y pantaille depuis quelques temps.

Actuellement, pour mon projet django, je convertis le markup en HTML que je stocke dans la base de données histoire d'alléger considérablement le rendu. C'est-à-dire, en gros, le markup est enregistré dans "body_wiki" et la conversion HTML dans "body_html". Ainsi, je n'ai plus qu'à appeller un petit "entry.body_html|safe" est le tour est joué. Le problème, c'est ce que je ne trouve pas ça sexy. Ca alourdie la base de données (chaque contenu est dupliqué) et si jamais la syntaxe markup change, ça risque d'être folklo ou lourdingue à mettre à jour. Des avis ?

24 Avril 2008 - 12:03, par No'

je stocke mes champs avec leur markup, et je fais le rendu "en live". C'est vrai que c'est sûrement moins rapide... c'est selon. J'ai jamais été très fan de la conversion à chaque modification d'un champ, je sais pas pourquoi...

Si jamais la syntaxe markup change, en effet, ça risque d'être folklo, mais après tout, ce n'est "que" une boucle sur tes enregistrements avec une sauvegarde à la clé.

Chacun son idée...

25 Avril 2008 - 05:55, par Naji

Petites notes au passage :
1) On dirait que Pygments a du mal avec le CSS : dans le bout de design que tu as posté, "#eeeedd" devrait être considéré comme une couleur et non comme un commentaire Bash / Python.
2) Lexique :
"Par exemple, dans mon modèle qui affiche les articles du blog, j'ai une boucle qui récupère le texte l'accroche de l'article et l'affiche"
Je viens de débuter avec Django, mais ne serait-il pas plus correct de parler de "ma vue / mon template qui affiche les articles du blog" ?

Naji, briseur d'ambiance.

25 Avril 2008 - 09:08, par No'

1) oui, normal. C'est de la détection "automatique", donc pas infaillible... Y'a des bouts de code qui permettent d'aider un peu pygments ; je mettrai peut-être ça en route dès que possible...

2) tu as raison... Je voulais dire "modèle de page" ou "gabarit", en fait.

Je corrige asap.


Toutes les balises HTML seront supprimées.
Tu peux ajouter des liens comme suit :
J'ajoute [a http://exemple.com "un joli lien"]
Tu peux aussi mettre *en gras* ou {en italique}.