BB-Code Parserklasse für PHP

Wichtiger Hinweis: Dies ist die Dokumentation einer alten Version der Parserklasse. Die neue Version bietet mehr Features und ist weitaus besser dokumentiert.

Was ist BB-Code?

BB-Code ist nichts anderes als eine Sammlung verschiedener Zeichenfolgen, mit denen man bestimmte Forensoftware und inzwischen auch andere Webanwendungen anweisen kann, Text anders zu formatieren sowie Links und Graphiken einzubinden. BB-Code wird auch unter anderen Bezeichnungen geführt, wie zum Beispiel vB-Code oder EasyHTML. Einen BB-Code gibt es nicht. Es existieren verschiedene Varianten mit unterschiedlichen Möglichkeiten.

Warum BB-Code?

Es stellt sich die Frage, warum nicht direkt HTML erlaubt wird. Zum einen stellt dies ein potentielles Sicherheitsrisiko dar, da Angreifer unter Umständen beliebigen HTML-Quelltext einfügen können. Desweiteren muss man dann normal im Text vorkommende Zeichen, die in HTML eine besondere Bedeutung haben, maskieren. Für einen Autor einer Webseite über HTML mag dies akzeptabel sein, jedoch ist dies eine enorme Hürde für den Nutzer eines Forums.

Wie sieht BB-Code aus?

Es hat sich die Konvention eingebürgert, eckige Klammern statt spitzen Klammern wie bei HTML zu verwenden. Normalerweise besteht eine Auszeichnung wie in HTML aus einem Start- und einem Endtag. Häufig werden von dem Betreiber eines Forums folgende häufige Codes zur Verfügung gestellt: [b] und [/b] zum Einschließen von fettgedrucktem Text, [i] und [/i] zum Einschließen von kursivem Text, [url] und [/url] zum Kennzeichnen als Link sowie [img] und [/img] zum Angeben, dass unter der eingeschlossenen Adresse sich ein Bild befindet. Es existieren viele verschiedene weitere Notationen, die ich hier nicht alle aufzählen will.

Die PHP-Klasse

Um nun den Umgang für einen Programmierer mit BB-Code zu vereinfachen habe ich eine Klasse geschrieben. Diese Klasse ermöglicht es dem Programmierer, beliebige BB-Codes festzulegen und daraufhin in Texten die BB-Codes durch zum Beispiel HTML-Code zu ersetzen.

Arten von BB-Code, der interpretiert wird

Zuerst einmal wird nur BB-Code interpretiert, der sich in eckigen Klammern befindet. Es werden folgende Typen interpretiert:

Der Inhalt und der Endtag können auch weggelassen werden, Konstrukte wie [name] oder [name attribut=wert] alleine sind möglich. Wenn der Starttag eines BB-Codes in der Form [name=wert], [name:wert] oder [name: wert] vorliegt, dann ist diese Schreibweile äquivalent zu [name default=wert].

Schnittstelle des Programmierers

Erzeugen eines Objektes

Um die Klasse nutzen zu können, muss die Datei, die sie enthält, zuerst eingebunden werden. Der Konstruktor der Klasse erwartet keine Parameter:

require 'bbcode.inc.php';

$bbcode = new BBCode ();

Parserfunktionen

Jeglicher Text, der kein BB-Code ist, kann von der Klasse beim Verarbeiten durch andere Funktionen verarbeitet werden. Diese Funktionen müssen genau einen Paramter erwarten: den Text. Sie müssen den verarbeiteten Text zurückgeben. Man teilt der Klasse mit, dass sie so eine Funktion verwenden soll, indem man die Methode addParser aufruft. Diese Methode erwartet zwei Parameter: den Namen der Funktion und den Elementtyp, indem diese Parserfunktion aufgerufen wird. Man kann für unterschiedliche Elemente unterschiedliche Parserfunktionen aufrufen lassen. Der Elementtyp ist weiter unten erklärt. Die in PHP eingebaute Funktion htmlspecialchars ist ein gutes Beispiel für so eine Verwendung. Damit werden nämlich alle Zeichen, die in HTML eine besondere Bedeutung haben, maskiert. Dies darf natürlich nur bei normalem Text geschehen; wenn BB-Codes verarbeitet werden, entsteht ja HTML. Es können beliebig viele Parserfunktionen definiert werden, diese werden in der Reihenfolge der Definition aufgerufen. Beispiel:

$bbcode->addParser ('htmlspecialchars', 'block');

Wenn man eine Funktion für verschiedene Elementtypen definieren will, kann man folgenden Code verwenden:

$bbcode->addParser ('htmlspecialchars', array ('block, inline'));

Wenn man das Objekt der Klasse weiterverwenden will, aber eine bestimmte Parserfunktion nicht mehr gebrauchen kann, dann kann man die Liste der Parserfunktionen in dem Objekt löschen; dies geschieht mit der Methode clearParserList:

$bbcode->clearParserList ();

BB-Codes

Die Klasse selbst enthält keine vordefinierten BB-Codes, der Programmierer muss diese alle selbst festlegen. Die Klasse unterscheidet zwischen verschiedenen Typen von BB-Codes, die der Programmierer festlegen kann:

BB-Codes lassen sich verschachteln, Konstrukte wie [b]bla bla bla [i]bla[/i] bla[/b] bla sind möglich. Diese Klasse überprüft auch die Verschachtelung von BB-Codes. Zum einen muss die Verschachtelung korrekt sein. Wenn ein Konstrukt wie [b]bla bla [i]bla[/b] bla[/i] auftritt, dann wird bei dem [/b] das [i] automatisch mit geschlossen. Das [/i] wird ignoriert, da kein [i] offen ist. Außerdem wird am Ende der Ausführung jeglicher offener Code automatisch geschlossen. So kann sichergestellt werden, dass nur valides HTML entsteht, sofern der Programmierer bei seinen Funktionen und Ersetzungen auch dafür sorgt.

Man kann der Klasse desweiteren mitteilen, welche BB-Codes in welchen anderen vorkommen dürfen und welche nicht. Dazu kann man jedem BB-Code einen Inhaltstyp zuweisen. Man der Klasse dann mitteilen, in welchen Inhaltstypen dieser BB-Code direkt vorkommen darf. Außerdem besteht die Möglichkeit, zu sagen, welche Inhaltstypen überhaupt nicht oberhalb eines BB-Codes vorhanden sein dürfen. So kann man zum Beispiel für Links den Inhaltstyp link festlegen und einem Link sagen, dass er nicht in anderen Links vorkommen darf. Wird ein BB-Code an einer falschen Stelle gefunden, wird er als ungültig erachtet. Wenn noch gar kein BB-Code vorgekommen ist, dann heißt der aktuelle Inhaltstyp block.

Wenn die Klasse einen BB-Code als ungültig erachtet, dann wird dieser BB-Code so wie er ist wieder ausgegeben.

Ein BB-Code kann der Klasse über die Methode addCode bekannt gemacht werden:

$bbcode->addCode (
  'b',                         // Name
  'simple_replace',            // Typ
  null,                        // Funktion zum Aufrufen
  array ('<b>', '</b>'),       // Funktionsparameter
  'inline',                    // Inhaltstyp
  array ('block', 'inline'),   // Erlaubt innerhalb von
  array ()                     // Nicht erlaubt innerhalb von
);

Der erste Parameter der Methode ist der Name des Codes; das, was der Benutzer in den eckigen Klammern eingibt. Der zweite Parameter ist der Typ des BB-Code, einer aus simple_replace, simple_replace_single, callback_replace, callback_replace_simple, usecontent und usecontent?. Diese wurden oben erläutert. Falls der Typ eine Funktion zur Verarbeitung benötigt, dann muss der Name dieser Funktion als dritter Parameter angegeben werden, ansonsten sollte null übergeben werden. Als vierter Parameter wird ein Parameter für die Funktion mitgegeben. Die angegebene Funktion bekommt diesen Parameter beim Aufruf übergeben. Bei einigen Typen wird dieser Parameter auch direkt verwendet; dies wird weiter unten erläutert. Der fünfte Parameter ist der Inhaltstyp des BB-Codes. Der sechste Parameter gibt die Inhaltstypen der BB-Codes an, in denen sich der BB-Code direkt befinden darf, der siebte Parameter gibt die Inhaltstypen der BB-Codes an, in denen sich der BB-Code nicht befinden darf. Der sechste und siebte Parameter sind Arrays.

Der Parameter, den man an die Funktion zusätzlich übergeben kann, sollte immer ein Array sein. Folgende direkten Verwendungszwecke existieren: Beim Typ simple_replace wird der Wert des Elements mit dem Index 0 dieses Arrays als Ersetzung für den Starttag und der Wert des Elements mit dem Index 1 dieses Arrays als Ersetzung für den Endtag verwendet. Beim Typ simple_replace_single wird der Wert des Elements mit dem Index 0 für die Ersetzung des Tags verwendet. Beim Typ usecontent? wird der Wert des Elements mit dem Index 0 als Name des Attributs, dessen Existenz geprüft wird, verwendet.

Jede Funktion, die bei der Methode addCode angegeben wird, wird immer mit fünf Parametern aufgerufen, eine typische Funktion sieht wie folgt aus:

function name_der_funktion ($tag_name, $attrs, $elem_contents, $func_param, $openclose) {
  // ...
}

Im ersten Parameter, tag_name, steht der Name des Tags, das den Aufruf der Funktion hervorgerufen hat. Im zweiten Parameter, attrs, stehen alle Attribute, die dieser Tag enthalten hat, als assoziatives Array. Beim Endtag ist dieser Parameter null. Im dritten Parameter, elem_contents, steht der Inhalt zwischen Start- und Endtag, sofern diese Funktion im Rahmen eines usecontent-BB-Codes aufgerufen wurde. Ansonsten ist der Paramter null. Der vierte Parameter, func_param enthält den Parameter, den man bei der Methode addCode übergeben konnte. Der letzte Parameter, openclose ist entweder open, wenn es sich um den Starttag handelt, close, wenn es sich um den Endtag handelt oder all, wenn es sich um einen Aufruf im Rahmen eines usecontent-BB-Codes handelt.

Beschränkungen für BB-Codes definieren

Man kann die maximale Anzahl an Vorkommnissen eines BB-Codes definieren. Dazu gibt es die Methoden setOccurrenceType und setMaxOccurrences. Mit setOccurrenceType legt man für einen BB-Codes einen Typ fest. Mit setMaxOccurrences legt man fest, wie oft BB-Codes eines solchen Typs vorkommen dürfen. Mit folgenden Anweisungen legt man zum Beispiel fest, dass die Codes [img] und [flash] insgesamt nur 5 Mal vorkommen dürfen.

$bbcode->setOccurrenceType ('img', 'multimedia');
$bbcode->setOccurrenceType ('flash', 'multimedia');
$bbcode->setMaxOccurrences ('multimedia', 5);

Der Typ ist aussschließlich zur Gruppierung gedacht, wenn man einen BB-Code alleine beschränken will, dann muss man für ihn einen eigenen Typ anlegen. Will man zum Beispiel nur 4 Links erlauben, dann ist das hier möglich:

$bbcode->setOccurrenceType ('url', 'link');
$bbcode->setMaxOccurrences ('link', 4);

Wenn das Limit überschritten wird, dann werden diejenigen BB-Codes, die zu häufig vorhanden sind, ignoriert und einfach im Text ausgegeben.

Komplexere Beschränkungen sind nicht möglich.

Parsen eines Textes

Das parsen eines Textes geschieht über die Methode parse:

$text = $bbcode->parse ($text);

Spezielle Eigenschaften von BB-Codes definieren

Man kann für BB-Codes bestimmte Eigenschaften definieren:

$bbcode->setCodeFlag ('*', 'no_close_tag', true);
$bbcode->setCodeFlag ('*', 'do_autoclose_children', false);

Die Methode setCodeFlag erwartet drei Parameter: Den Namen des Codes (hier: [*]), die Eigenschaft und den Wert der Eigenschaft. Der Code muss schon vorher definiert worden sein. Folgende Eigenschaften gibt es:

no_close_tag
Dieser BB-Code besitzt keinen Tag zum schließen. Dies ist Beispielswiese bei Listen sinnvoll:
[list]
  [*]Punkt1
  [*]Punkt2
  [*]Punkt3
[/list]
Der Code [*] enthält hier unr den öffnenden Tag, der schließende Tag ([/*]) fehlt. Der erzeugte HTML-Code sollte aber so aussehen:
<ul>
  <li>Punkt1</li>
  <li>Punkt2</li>
  <li>Punkt3</li>
</ul>
Die schließenden </li> werden auf zwei Weisen erreicht: Zum einen wird bei jedem neuen [*] das vorhergehende geschlossen, wenn no_close_tag angegeben wird. Zum anderen wird am Ende der Liste der letzte Punkt durch die Fehlerkorrektur automatisch geschlossen.
do_autoclose_children
Diese Eigenschaft wird nur bei gesetztem no_close_tag interpretiert. Dies tritt nur dann in Kraft, wenn man Verschachtelungsfehler macht:
[list]
  [*]Punkt1
  [*][b]Punkt2
  [*]Punkt3[/b]
[/list]
Bei normalen Verschachtelungsfehlern, wie zum Beispiel [b]hallo[i]test[/b]test[/i] wird beim schließenden [b] das [i] automatisch mit geschlossen, der HTML-Code sieht dann so aus: <b>hallo<i>test</i></b>test[/i]. Bei Codes ohne schließenden Tag ist dies jedoch unter Umständen problematisch. Wenn die Einstellung do_autoclose_children auf true steht, dann wird das gleiche Verfahren angewendet, wie bei normalen Verschachtelungsfehlern:
<ul>
  <li>Punkt1</li>
  <li><b>Punkt2</b></li>
  <li>Punkt3</li>
</ul>
Dies ist auch die Standardeinstellung. Dies kann jedoch in einigen Fällen nicht so gewünscht sein. Daher kann man bei dieser Art von Code das Verhalten ändern, im obigen Fall würde folgendes ausgegeben:
<ul>
  <li>Punkt1</li>
  <li><b>Punkt2</b><br>
  [*]Punkt3</b></li>
</ul>

Beispiele für die Verwendung

Folgendes Beispiel teilt der Klasse mit, dass sie folgende BB-Codes akzeptieren soll: [b]fett[/b], [i]kursiv[/i], [url]URL[/url] und [url=URL]linktext[/url]. Desweiteren werden im kompletten Text alle HTML-Sonderzeichen maskiert (mittels htmlspecialchars) und alle Zeilenumbrüche in <br> verwandelt. (mittels nl2br) Dies ist möglich, da von der Klasse sichergestellt ist, dass htmlspecialchars zuerst aufgerufen wird, da es zuerst hinzugefügt wurde.

require 'bbcode.inc.php';

function do_bbcode_url ($tag_name, $attrs, $elem_contents, $func_param, $openclose) {
    // Tag hatte nicht das default-Attribut
    if ($openclose == 'all') {
        return '<a href="'.htmlspecialchars($elem_contents).'">'.
            htmlspecialchars($elem_contents).'</a>';
        // Tag hatte das default-Attribut und das hier ist der öffnende Tag
    } else if ($openclose == 'open') {
        return '<a href="'.htmlspecialchars($attrs['default']).'">';
    // Tag hatte das default-Attribut und das hier ist der schließende Tag
    } else if ($openclose == 'close') {
        return '</a>';
    // Irgendwas seltsames geht vor sich
    } else {
        // Fehler
        return false;
    }
}

$bbcode = new BBCode ();

$bbcode->addParser ('htmlspecialchars', array ('block', 'inline', 'link'));
$bbcode->addParser ('nl2br', array ('block', 'inline', 'link'));

$bbcode->addCode ('b', 'simple_replace', null, array ('<b>', '</b>'),
                  'inline', array ('block', 'inline', 'link'), array ());
$bbcode->addCode ('i', 'simple_replace', null, array ('<i>', '</i>'),
                  'inline', array ('block', 'inline', 'link'), array ());
$bbcode->addCode ('url', 'usecontent?', 'do_bbcode_url', array ('default'),
                  'link', array ('block', 'inline'), array ('link'));

$text = $bbcode->parse ($text);

Hinweis: Die Funktion zum Verarbeiten dieser Links prüft diese nicht auf ihre Gültigkeit. Daher würde [url]Hallo wie geht es Dir?[/url] in <a href="Hallo wie geh es Dir?">Hallo wie geh es Dir?</a> verwandelt werden. Daher sollte im Anwendungsfalls auf noch eine Prüfung auf Gültigkeit mit hineinkommen. Falls der Link nicht gültig ist, kann die Funktion einfach false zurückgeben, dann wird der BB-Code einfach ignoriert und so, wie er ist, ins Dokument geschrieben.

Häufig gestellte Fragen

Hier sammele ich Fragen zu dieser Klasse, die sicherlich häufiger auftauchen werden.

Wird der Text innerhalb von zwei Tags eines BB-Codes auch an die Parserfunktionen übergeben?

Ja, es sei denn, dieser BB-Code ist vom Typ usecontent. Ansonsten ist sichergestellt, dass immer der größtmögliche zusammenhängende Abschnitt der Nachricht an die Parserfunktionen auf einmal weitergegeben wird.

Wie realisiere ich Smilies mit dieser Klasse?

In vielen Boards werden Smilies automatisch durch entsprechende Bilder ersetzt. Mit dieser Klasse lässt sich dies sehr Komfortabel lösen. Man braucht eine Funktion, die die Smily-Ersetzugen innerhalb eines beliebigen Textes durchführt. Diese Funktion muss man danach nur noch als Parserfunktion registrieren:

function ersetze_smilies ($text) {
    // hier sollten die Ersetzungen durchgeführt werden, z.B. so:
    $text = str_replace (':-)', '<img src="freu.gif" alt=":-)">', $text);
    return $text;
}

Wie mache ich URLs innerhalb eines Textes automatisch anklickbar?

Unter http://www.dclp-faq.de/q/q-regexp-uri-klickbar.html findet sich eine Funktion, mit der man URIs innerhalb eines Textes anklickbar machen kann. Diese Funktion kann man als Parserfunktion bei dieser Klasse angeben, sie wird dann nämlich auf alles, was kein BB-Code ist, angewendet. So werden Links, die bereits in BB-Code eingeschlossen sind, nicht ersetzt.

Allerdings halte ich persönlich es für eine schlechte Idee, URLs innerhalb eines Textes anklickbar zu machen, denn wenn jemand eine URL nicht mit BB-Code gekennzeichnet hat, dann hat sie oder er vielleicht einen guten Grund dafür.

Wie erreiche ich, dass innerhalb von [code] und [/code] kein weiterer BB-Code interpretiert wird?

Wenn [code] einfach durch <code>, <tt> oder ähnliches ersetzt wird, dann funktioniert das nicht. Man kann den BB-Code [code] aber als Typ usecontent registrieren. Dann wird der komplette Inhalt nicht an die Parserfunktionen übergeben, desweiteren wird der Inhalt auch nicht weiter selbst verarbeitet, sondern direkt an die Funktion übergeben, die diesen Code verarbeiten soll. Hinweis: Da nun aber keine Parserfunktionien auf den Inhalt angewendet werden, muss die Funktion, die diesen Code verarbeitet, selbst dafür sorgen, dass der Inhalt für HTML korrekt maskiert wird.

Das Nichtanwenden von Parserfunktionen hat aber auch vorteile, denn so können Parserfunktionen für Dinge wie Smilies oder automatisches Erkennen von Links definiert werden, ohne dass sie sich auf das, was zwischen [code] und [/code] steht, auswirken.

Wie realisiere ich Listen ([list]) mit dieser Klasse?

Dazu müssen zwei Codes definiert werden: der Code für die Liste und der Code für das Listenelement:

// Allgemeinen Parser hinzufügen
$bbcode->addParser ('htmlspecialchars', array ('block', ..., 'listitem'));

// Letzten Zeilenumbruch bei Listen entfernen
$bbcode->addParser ('bbcode_striplastlinebreak', 'listitem');

// Zeilenumbrüche ersetzen, in der Liste der Elementtypen darf kein list auftauchen
$bbcode->addParser ('nl2br', array ('block', ..., 'listitem'));

// Inhalte von Liste entfernen
$bbcode->addParser ('bbcode_stripcontents', array ('list'));

// Codes hinzufügen
$bbcode->addCode ('list', 'simple_replace', null, array ('<ul>', '</ul>'),
                  'list', array ('block', 'listitem'), array ());
$bbcode->addCode ('*', 'simple_replace', null, array ('<li>', "</li>\n"),
                  'listitem', array ('list'), array ());

// Dieser Code wird nicht geschlossen
$bbcode->setCodeFlag ('*', 'no_close_tag', true);

Als erstes ist es wichtig, den Teil mit den Codes zu verstehen. Es wird als erstes ein Code für die Begrenzung der Liste ([list]) definiert. Diseer darf in Elementen vom Typ block und listitem vorkommen. Hier wird der Einfachheit halber einach nur eine einfache Ersetzung durchgeführt; der Anfangstag soll durch <ul> und der Endtag durch </ul> ersetzt werden. Natürlich kann man auch komplexere Ersetzungen über Funktionen durchführen lassen, um zum Beispiel zwischen geordneter und ungeordneter Liste unterscheiden zu können. Der zweite Code, der definiert wird, ist [*]. Dieser Code stellt die eigentlichen Listenelemente dar. Der Anfangstag soll durch <li> und der Endtag durch </li>\n ersetzt werden. Der Grund für das \n folgt gleich. Diesem Code wird daraufhin die Eigenschaft, dass er keinen Endtag besitzt, verliehen.

An sich würde es ausreichen, dass man einfach die Parser htmlspecialchars und nl2br verwendet. Allerdings würde dann folgende Eingabe:

[list]
[*]Eintrag 1
[*]Eintrag 2
[/list]

folgende Ausgabe produzieren:

<ul><br>
  <li>Eintrag 1<br>
  </li><li>Eintrag 2<br>
</li></ul>

Dies ist aus zwei Gründen problematisch: Zum einen sind in <ul> nur Listenelementer erlaubt, <br> jedoch nicht. Zum anderen wird die Lesbarkeit des produzierten Quelltextes deutlich reduziert.

Daher werden diese speziellen Parserkombinationen verwendet: Für alle Elemente außer der Liste selbst wird htmlspecialchars als Parser angegeben. (Wichtig hierbei ist, dass nur die Liste selbst nicht mit einbezogen wird, die Listeneinträge sind durchaus abgedeckt) Daraufhin wird eine Parserfunktion bbcode_striplastlinebreak speziell für Listeneinträge hinzugefügt, die den letzten Zeilenumbruch innerhalb eines Listenelements verhindern soll. Diese Funktion ist weiter unten aufgelistet. Daraufhin wird die Funktion nl2br eingebunden, damit alle noch vorhandenen Zeilenumbrüche korrekt ersetzt werden. Diese wird wiederum nur für alle Elemente außer der Liste angegeben. Nun wird eine Parserfunktion bbcode_stripcontents speziell für Listen verwendet. Diese soll bewirken, dass der komplette Inhalt innerhalb von Listen entfernt wird, damit das HTML gültig bleibt.

Hier sind noch die beiden Funktionen bbcode_striplastlinebreak und bbcode_stripcontents, die hier für die Listen verwendet wurden:

function bbcode_stripcontents ($text) {
    return preg_replace ("/[^\n]/", '', $text);
}

function bbcode_striplastlinebreak ($text) {
    $text = preg_replace ("/\n( +)?$/", '$1', $text);
    return $text;
}

Die Funktion bbcode_stripcontents entfernt alles außer den Neuezeile-Zeichen. Nachdem dieses nicht mehr durch nl2br verarbeitet wird (nl2br ist für alles außer Listen definiert) sind diese Zeichen kein Problem. Außerdem dienen Zeilenumbrüche der Lesbarkeit des Quelltextes. Wenn man dagegen Rechenzeit sparen will, kann man natürlich auch einfach eine leere Zeichenkette zurückgeben. Die Funktion bbcode_striplastlinebreak entfernt das letzte \n im Text, das höchstens noch durch Leerzeichen vom Ende entfernt sein darf. Die Leerzeichen werden in dem Fall beibehalten.


Autor dieser Seite ist Christian Seiler. Inhalte dieser Seite dürfen mit Quellenangabe beliebig zitiert werden. Diese Seite darf beliebig verlinkt werden, solange ersichtlich bleibt, wer der Autor ist. Über Anregungen zu dieser Klasse würde ich mich freuen, Fragen dürfen natürlich gerne gestellt werden.

Letzte Aktualisierung: 17.05.2003.