BBCode Parser

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
mitsuhiko
User
Beiträge: 1790
Registriert: Donnerstag 28. Oktober 2004, 16:33
Wohnort: Graz, Steiermark - Österreich
Kontaktdaten:

Hier ein kleiner BBCode Parser mit in etwa dem Umfang von phpbb.
Er sollte gültigen XHTML Code erzeugen, bei Fragen einfach hier posten.

Code: Alles auswählen

# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------------- #
#   BBCode Parser                                                              #
#   Author: Armin Ronacher <armin.ronacher@active-4.com>                       #
#       licensed under the GPL                                                 #
# ---------------------------------------------------------------------------- #
#      This program is free software; you can redistribute it and/or           #
#       modify it under the terms of the GNU General Public License            #
#     as published by the Free Software Foundation;  either version 2          #
#         of the License, or (at your option) any later version.               #
# ---------------------------------------------------------------------------- #

import re
from random import random
from md5 import md5
from time import time

LANGUAGE = {
    "SIMPLE_QUOTE":         "Zitat:",
    "AUTHOR_QUOTE":         "%s schrieb:"
}

"""
    this module implements a simple BBCode parser for pythonBB
    Here a small example for using it:     
        text = file("input.txt", "r").read()
        p = parser(text)
        text = p.parse()
        file("output.html", "w").write(text)
"""

class parser:
    def __init__(self, text):
        self.text = text
        self.table = {}
        self.parse()
       
    def parse(self):
        text = self.text
        text = self.htmlspecialchars(text)
        text = self.extract(text)
        text = self.inlineformat(text)
        text = self.paragraphs(text)
        text = self.assamble(text)
        self.table = {}
        return text
       
    def htmlspecialchars(self, text):
        rules = [
            ("&",   "&"),
            ("< ",  "<"),
            (">",   ">")
        ]
        for rule in rules:
            text = text.replace(rule[0], rule[1])
        return text
       
    def extract(self, text):
        areas = [
            ("\[code\](.*?)\[/code\](?uism)", self.do_code),
            ("\[list(=(.*?))?\](.*?)\[/list](?uism)", self.do_list)
        ]
        for area in areas:
            try:
                while re.search(area[0], text):
                    token_id = self.__create_token_id()
                    result = re.search(area[0], text)
                    self.table[token_id] = (area[1], result)
                    text = text.replace(result.group(0), token_id, 1)
            except:
                pass
        return text
           
    def inlineformat(self, text):
        rules = [
            ("\[b\](.*?)\[/b\](?uism)", r"<strong>\1</strong>"),
            ("\[i\](.*?)\[/i\](?uism)", r"<em>\1</em>"),
            ("\[u\](.*?)\[/u\](?uism)", r"<u>\1</u>"),
            ("\[url(=(.*?))?\](.*?)\[/url\](?uism)", self.do_link),
            ("\[img(=\"(.*?)\")?\](.*?)\[/img\](?uism)", self.do_image),
            ("\[quote(=\"(.*?)\")?\](.*?)\[/quote\](?uism)", self.do_quote),
        ]
        for rule in rules:
            text = re.sub(rule[0], rule[1], text)
        return text
         
    def paragraphs(self, text):
        lines = re.split("\n{2,}", text)
        text = ""
        for line in lines:
            line = line.strip().replace("\n", "<br />\n")
            text += "<p>"+line+"</p>\n\n"
        return text
       
    def assamble(self, text):
        for token in self.table:
            handler = self.table[token][0]
            content = self.table[token][1]
            block = handler(content)
            text = text.replace(token, block)
        text = re.sub("<p>([\s]*?)</p>", "", text) #removes empty paragraphs
        return text
           
    def __create_token_id(self):
        while True:
            token = "~~"+md5(str(time() + random())).hexdigest()+"~~"
            if not self.table.has_key(token):
                return token
       
    def do_link(self, matchobj):
        caption = matchobj.group(3)       
        if matchobj.group(1) == None:
            link = matchobj.group(3)
        else:
            link = matchobj.group(2)
        return "<a href=\""+link+"\">"+caption+"</a>"
   
    def do_image(self, matchobj):
        href = matchobj.group(3)
        if matchobj.group(1) == None:
            title = matchobj.group(3)
        else:
            title = matchobj.group(2)
        return "<img src=\""+href+"\" title=\""+title+"\" alt=\""+title+"\" />"
   
    def do_quote(self, matchobj):
        text = matchobj.group(3)       
        if matchobj.group(1) == None:
            title = LANGUAGE["SIMPLE_QUOTE"]
        else:
            title = matchobj.group(2)
            if not title.endswith(":"):
                title = LANGUAGE["AUTHOR_QUOTE"].replace("%s", title)
        return "</p><div class=\"quote\"><h2>"+title+"</h2><p>"+text+"</p></div><p>"
   
    def do_code(self, matchobj):
        content = matchobj.group(1)
        if content[0] == "\n": content = content[1:]
        result = "</p><code>"+content+"</code><p>"
        return result
       
    def do_list(self, matchobj):
        content = self.inlineformat(matchobj.group(3))
        content = re.sub("\[\*\](.*?)\n", r"<li>\1</li>\n", content)   

        if matchobj.group(1) == None:
            list_type = "*"
        else:
            list_type = matchobj.group(2)
        if list_type == "1":
            result == "<ol class=\"numeric\">"+content+"</ol>"
        elif list_type == "a":
            result = "<ol class=\"alpha\">"+content+"</ol>"
        elif list_type == "i":
            result = "<ol class=\"roman\">"+content+"</ol>"
        else:
            result = "<ul>"+content+"</ul>"
        result = "</p>"+result+"<p>"
        return result
TUFKAB – the user formerly known as blackbird
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Was dagegen, das ich ihn (irgendwann) in PyLucid als Markup mit aufnehme???

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
mitsuhiko
User
Beiträge: 1790
Registriert: Donnerstag 28. Oktober 2004, 16:33
Wohnort: Graz, Steiermark - Österreich
Kontaktdaten:

jens hat geschrieben:Was dagegen, das ich ihn (irgendwann) in PyLucid als Markup mit aufnehme???
Natürlich nicht :D
TUFKAB – the user formerly known as blackbird
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Dank der GPL konnte ich den Code ändern :D Bei LodgeIt ist die überareitete Version zu finden. Hier kurz meine Änderungen:
  • viele kleine Änderungen (PEP8, New-Style-Klassen, String-Formatting, ...)
  • Code, der in Code-Tags ist, wird jetzt mit Pygments gehighlighted.
  • neue Methoden: do_size und do_color
  • Links werden überprüft: Wenn kein Protokoll angegeben wurde, dann wird http benutzt.
  • LANGUAGE ist jetzt auf Englisch
  • bei do_quote wird das semantisch korrekte blockquote-Tag eingesetzt
  • do_list benutzt jetzt CSS-Anweisungen und keine veralteten HTML-Elemente
  • inline_format wurde überarbeitet: Attribute werden jetzt nicht mehr in Anführungszeichen gesetzt.
Weil ich nicht alles vom Code verstanden hab, hier noch ein paar Fragen:
  • Wozu ist die Methode __create_token_id da?
  • Was macht die while-Schleife in der Methode extract?
PS: Ja, ich weiß, dass der Thread uralt ist! Los, steinigt mich!
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

__create_token_id erzeugt eine einzigartige ID, wenn auch recht umständlich. Ich würde eher zu uuid.uuid4 greifen.

Bei extract solltest du mal die letzte Zeile im Block der while-Schleife beachten, text wird jedesmal verändert. Das rechtfertigt eine Schleife.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

DasIch hat geschrieben:Bei extract solltest du mal die letzte Zeile im Block der while-Schleife beachten, text wird jedesmal verändert. Das rechtfertigt eine Schleife.
Dass da text verändert wird, hab ich verstanden. Was ich nicht verstehe, ist warum da ein Dictionary (table) und eine einzigartige ID benötigt wird (was ja miteinander in Verbindung steht).
Benutzeravatar
str1442
User
Beiträge: 520
Registriert: Samstag 31. Mai 2008, 21:13

Ohne mir den Code genauer angeschaut zu haben (ausser __create_token_id):

Man könnte für jedes Token doch einfach id(token) nutzen anstatt sowas kompliziertes zu machen. Ist garantiert einzigartig, da die Speicher Addresse des Tokens zurückgegeben wird. Man darf das Token natürlich nicht wegschmeißen.
lunar

Warum nutzt du nicht einfach "pygments.lexers.get_lexer_by_name"?
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

@lunar: Ist erledigt. Code ist hier zu finden: http://paste.pocoo.org/show/91262/
lunar

Die solltest noch die Ausnahme abfangen, wenn jemand einen nicht existierenden Lexer angibt, und den TextLexer als Fallback nutzen.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Warum ist in Paste #91262 denn das `htmlspecialchars` auskommentiert? Das > muss AFAIK übrigens nicht ersetzt werden. Merkwürdig finde ich auch den Gebrauch des Attributes `text` bzw. dessen Nichtgebrauch. Das Attribut `table` müsste dann auch keins sein, da es in `extract` gesetzt und nur in `assemble` benötigt wird. Das Zurücksetzen in `parse` scheint mir auch unnötig (jedenfalls wenn man `text`nicht auch löscht). In diesem Forum wird bei `quote` der Name in Anführungszeichen eingeschlossen. Damit rechnet diese bbcode-Implementierung wenn ich's richtig sehe aber nicht.

Stefan
Benutzeravatar
helduel
User
Beiträge: 300
Registriert: Montag 23. Juli 2007, 14:05
Wohnort: Laupheim

Moin,

ich versteht sowieso nicht, warum eine htmlspecialchars-Methode implementiert wird. Die macht ja nichts anderes als die escape-Funktion in den saxutils.

Die do_list-Methode würde ich so schreiben, um das IMO hässliche if/elif/else-Konstrukt zu beseitigen (ungetestet):

Code: Alles auswählen

LIST_STYLES = {
    '1': '<ol style="list-style-type:decimal">%s</ol>',
    'a': '<ol style="list-style-type:lower-alpha">%s</ol>',
    'i': '<ol style="list-style-type:upper-roman">%s</ol>',
}
DEFAULT_LIST_STYLE = "<ul>%s</ul>" 

def do_list(self, matchobj): 
    content = self.inlineformat(matchobj.group(3)) 
    content = re.sub("\[\*\](.*?)\n", r"<li>\1</li>\n", content)    
    list_type = (matchobj.group(2) if matchobj.group(0) else None)
    result = LIST_STYLES.get(list_type, DEFAULT_LIST_STYLE)
    return result % content
Frage: Wäre es nicht besser, die ganzen Regexes auf Modul-Ebene zu definieren? Wenn ich viele verschiedene Texte zu parsen habe, dann stört doch das ständige neukompilieren der Regexes (auch wenn hier die ersten hundert regexes oder so gecached werden).

Gruß,
Manuel
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Hab gerade einen Bug gefunden: verschachtelte Zitate werden nicht unterstützt. Hier ein Beispiel: http://paste.pocoo.org/show/92393/ Wie kann man das verhindern?
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

[quote=derdon]Wie kann man das verhindern?[/quote]
Da reguläre Ausdrücke keine Verschachtelungen zählen können, wird der jetzige Ansatz nicht funktionieren. Man braucht einen rekursiv absteigenden Parser.

Code: Alles auswählen

def parse():
    if current_token.name == "quote":
        parse_quote()
    ...

def parse_quote():
    emit("<h4>% schrieb:</h4><blockquote>" % current_token.match[1])
    next_token()
    parse()
    if current_token.name == "/quote": next_token()
    emit("</blockquote>")
Einen Strom von Token bestehend aus in eckigen Klammern geschriebenen BBCode-Tags und normalem Text kann man nach wie vor per regulärem Ausdruck aus dem Eingabetext herausholen und dann in Form von Token-Objekten dem Parser zur Verfügung stellen. Die Sprache ist so einfach, dass ein LL(1)-Parser reicht und daher habe ich mal `current_token` als look-ahead-Token gewählt. Alternativ könnte man auch einfach eine Liste von Token berechnen und dann auf dieser Liste arbeiten. Kostet mehr Hauptspeicher, ist aber einfacher bei komplizierteren Grammatiken, weil man dann LL(*) hat.

Stefan
Antworten