Parsowanie tagów za pomocą django-content-bbcode w przykładach

Jakiś czas temu wypuściłem django-content-bbcode - parser tagów w stylu BBCode. Dzisiaj zaprezentuję kilka przykładów wykorzystania tego pakietu do tworzenia tagów o różnej złożoności. Od prostych znajdź i zamień po bardziej złożone wykorzystujące dane z bazy danych.

Przykłady

Co nieco o definiowaniu parserów własnych tagów napisałem na githubie. Zacznijmy od czegoś prostego. Powiedzmy że chcemy zmienić to:

[rk:anchor href="http://www.google.pl"]

W klikalny link. W powyższym tagu "anchor" jest nazwą taga (rk:NAZWA), dalej mamy atrybuty klucz wartość. Tag z zamknięciem np.:

[rk:anchor href="http://www.google.pl"]click me![/rk:anchor]

Miałby także treść - zwartość pomiędzy tagami otwierającym i zamykającym. django-content-bbcode parsuje takie tagi i zrzuca poszczególne elementy do słownika. Nasza funkcja parsująca dostaje listę słowników - listę wszystkich wystąpień danego taga w parsowanym tekście. Oto przykład parsera dla pierwszego taga:

def anchor(occurrences, text):
    for occurrence in occurrences:
        href = occurrence['attributes']['href']
        text = text.replace(occurrence['tag'], '<a href="%s">link</a>' % href)
    return text

Kod umieszczamy w pliku tags.py naszej aplikacji. Dodatkowo w pliku tym tworzymy słownik registered_tags gdzie jako klucz podajemy nazwę taga, a jako wartość - nazwę funkcji jaka ma go obsłużyć. Przykład dostępny jest na githubie.

Powyższa funkcja dostaje dwa argumenty - listę wystąpień oraz parsowany tekst. Słownik zawierać będzie atrybut "href". Pod specjalnym kluczem "tag" znajdziemy cały tag, który powinniśmy zastąpić czymś w tekście. Tak więc iterujemy po liście i zastępujemy wszystkie wystąpienia taga w tekście prostym HTMLowym linkiem.

By obsłużyć tag zamykany wystarczy skorzystać z treści taga dostępnej pod kluczem "code".

Linkowalne nagłówki

Powiedzmy że chcemy w artykułach mieć klikalne nagłówki h1,2,3,4 itd. Oprócz samego taga H chcemy mieć generowane etykiety, a same nagłówki podlinkować do nich - tak by dało się dać linka do danego nagłówka artykułu:

<a name="1" title="Linkowalne nagłówki"></a>
<h4><a href="#1">Linkowalne nagłówki</a></h4>
Można zrealizować to tagiem typu:
[ rk:h id="4" ]Linkowalne nagłówki[ /rk:h ]
Najprostszy parser wyglądałby tak:
def h(occurrences, text):
    for number, tag in enumerate(occurrences):
        tag_number = number + 1
        result = ('<a name="' + str(tag_number) + '" title="' + tag['code'] + '"></a>'
                  '<h' + tag['attributes']['id'] + '><a href="#' + str(tag_number) + '">' + tag['code'] + '</a></h' +
                  tag['attributes']['id'] + '>')
        text = text.replace(tag['tag'], result)
    return text

Za tag wstawiamy wygenerowany kod HTML. Numerujemy wszystkie wystąpienia taga by móc generować kolejne etykiety o unikalnych nazwach. Atrybut ID określa rozmiar nagłówka od h1 w dół. Generowanie kodu nie jest fajne więc zróbmy to lepiej:

from django.template.loader import render_to_string

def h(occurrences, text):
    for number, tag in enumerate(occurrences):
        tag['tag_number'] = number + 1
        result = render_to_string('tags/headline.html', tag)
        text = text.replace(tag['tag'], result)
    return text
Który wykorzysta szablon Django:
<a name="{{ tag_number }}" title="{{ code }}"></a>
<h{{ attributes.id }}><a href="#{{ tag_number }}">{{ code }}</a></h{{ attributes.id }}>
Tym sposobem wydzieliliśmy wynikowy kod HTML od kodu Pythona.

Proste wyciąganie danych z bazy danych

W takie tagi możemy zaszyć znacznie bardziej złożoną logikę. Możemy pobierać coś z bazy danych albo z innego źródła i wykorzystać to w odpowiedzi. Np. lista ostatnio zarejestrowanych użytkowników:

def noobs(occurrences, text):
    noob_users = User.objects.all().order_by('-date_joined')[:5]
    response = render_to_string('tags/noobs.html', {'users': noob_users})
    for occurrence in occurrences:
        text = text.replace(occurrence['tag'], response)
    return text
Dla szablonu:
<ul>
{% for user in users %}
    <li>{{ user.username }}</li>
{% endfor %}
</ul>

Projektując takie i bardziej złożone tagi warto pomyśleć o keszowaniu. Czy to danych na poziomie taga, widoku, czy szablonu, w którym pojawi się wolno wykonujący się tag.

Dane takie jak lista ostatnio zarejestrowanych użytkowników mogłyby po prostu pojawić się w określonym szablonie Django i być obsługiwanym przez widok albo funkcję z TEMPLATE_CONTEXT_PROCESSORS. Wersja z tagami pozwala nam samemu rozmieszczać elementy na stronach bez konieczności zmiany kodu projektu. Nie zawsze może jest to potrzebne, ale na bardziej luźnych stronach w stylu wiki własne generowanie zawartości strony może być bardzo przydatne.

Co jeśli mamy wiele tagów na stronie i każdy powoduje zapytanie do bazy danych? Np. mamy tag wstawiający link i opis artykułu na podstawie podanego sluga. Każde wystąpienie taga byłoby jednym zapytaniem. Można zrobić to np tak:

def art(occurrences, text):
    from articles.models import Article
    slugs = []
    for i in occurrences:
        slugs.append(i['attributes']['slug'])
    pages = Article.objects.filter(slug__in=slugs).select_related('site')
    for i in pages:
        text = text.replace('[ rk:art slug="' + i.slug + '" ]',
                            '<li><a href="%s">%s</a> - %s</li>' % (i.get_absolute_url(), i.title, i.short_description))

Zbieramy slugi i robimy jedno zapytanie "IN". Zamiast wielu zapytań mamy jedno. Warto też sprawdzić czy nie wymaga np. select_related. Przy zapytania "IN" trzeba też uważać by nie dawać querysetów, co może wygenerować zapytanie-potwora z podzapytaniami. Warto też ograniczyć generowanie kodu (podmiana taga).

Kolorowanie składni

Można wykorzystać też różne pakiety, np pygments do kolorowania składni, czy Pillow do generowania i wstawiania miniatury do podanego zdjęcia. W przypadku pygments parser mógłby wyglądać tak:

from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter


def syntax(occurrences, text):
    pygments_formatter = HtmlFormatter()
    langs = {}
    for i in occurrences:
        language = i['attributes'].get('lang', 'text')
        lexer = get_lexer_by_name(language)
        parsed = highlight(i['code'], lexer, pygments_formatter)
        text = text.replace(i['tag'],  parsed)
        # css styles for given lang
        langs['<style>%s</style>' % pygments_formatter.get_style_defs()] = True

    #add CSS in to the text
    styles = ''
    for style in langs.keys():
        styles = styles + style
    text = '%s%s' % (text, styles)
    return text

Nie licząc hakerskiego wstawiania CSSów podświetlających dany język (zawsze można je na sztywno dodać do stylów strony) funkcja wygląda podobnie do innych. Atrybut "lang" określa język, a treść taga ("code") to kod, jaki ma zostać pokolorowany.

Takie tagi dają nam dodatkową zaletę - możemy dowolnie zmieniać backend dla danego taga, bez konieczności dokonywania zmian wszędzie tam, gdzie został użyty (np. zmieniając bibliotekę kolorującą składnie).

RkBlog

Django, 13 April 2014, Piotr Maliński

Comment article
RkBlog main page Search RSS Contact