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

Kilka przykładów wykorzystania parsera tagów do obsługi tagów o różnorakiej logice

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).

blog comments powered by Disqus

Kategorie

Strony