Seite 1 von 1

BBCode Parser

Verfasst: Samstag 28. Mai 2005, 15:57
von mitsuhiko
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

Verfasst: Donnerstag 8. September 2005, 16:11
von jens
Was dagegen, das ich ihn (irgendwann) in PyLucid als Markup mit aufnehme???

Verfasst: Donnerstag 8. September 2005, 16:53
von mitsuhiko
jens hat geschrieben:Was dagegen, das ich ihn (irgendwann) in PyLucid als Markup mit aufnehme???
Natürlich nicht :D

Verfasst: Samstag 15. November 2008, 19:05
von derdon
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!

Verfasst: Samstag 15. November 2008, 19:45
von DasIch
__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.

Verfasst: Samstag 15. November 2008, 20:02
von derdon
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).

Verfasst: Samstag 15. November 2008, 20:09
von str1442
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.

Verfasst: Samstag 15. November 2008, 20:10
von lunar
Warum nutzt du nicht einfach "pygments.lexers.get_lexer_by_name"?

Verfasst: Samstag 15. November 2008, 20:39
von derdon
@lunar: Ist erledigt. Code ist hier zu finden: http://paste.pocoo.org/show/91262/

Verfasst: Samstag 15. November 2008, 20:49
von lunar
Die solltest noch die Ausnahme abfangen, wenn jemand einen nicht existierenden Lexer angibt, und den TextLexer als Fallback nutzen.

Verfasst: Sonntag 16. November 2008, 10:21
von sma
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

Verfasst: Sonntag 16. November 2008, 10:56
von helduel
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

Verfasst: Montag 24. November 2008, 15:52
von derdon
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?

Verfasst: Dienstag 25. November 2008, 10:08
von sma
[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