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:
W klikalny link. W powyższym tagu "anchor" jest nazwą taga (rk:NAZWA), dalej mamy atrybuty klucz wartość. Tag z zamknięciem np.:
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>
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
<a name="{{ tag_number }}" title="{{ code }}"></a>
<h{{ attributes.id }}><a href="#{{ tag_number }}">{{ code }}</a></h{{ attributes.id }}>
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).
Comment article