HalloPHP

Reguläre Ausdrücke für BBCodes

Tutorials | letzte Änderung am 27. Juli '10 um 15:25 Uhr

Motivation

Für Foren oder Gästebücher möchte man es seinen Usern häufig erlauben, bestimmte Formatierungen am Text vornehmen und Bilder oder Weblinks posten zu können.

Grundlagen

BBCode (engl. Bulletin Board Code) wird im Allgemeinen mit eckigen Klammern geschrieben. Über BBCodes lassen sich HTML-Tags wie <a> oder <h1> beliebig nachbilden, wodurch es möglich wird, eine Schnittstelle zwischen Usereingaben und tatsächlicher Darstellung anzubieten. Zu diesem Zweck wurden sie erfunden, denn HTML und ganz besonders Javascript sollte aus Sicherheitsgründen stets maskiert werden.

Die ersten und simpelsten Tags sind natürlich die Tags zur Schriftformatierung. In einem ersten Schritt wollen wir also die HTML-Tags

in BBCodes nachbilden. Dabei können wir unserer Phantasie freien Lauf lassen und zum Beispiel neue Tags erfinden. Doch in diesem Beispiel werden wir ganz stumpf lediglich die spitzen Klammern < und > durch eckige Klammern [ und ] ersetzen. Dabei beachten wir selbstverständlich, dass diese Tags seit langer Zeit als deprecated, also veraltet, gekennzeichnet sind und zur Formatierung nicht mehr benutzt werden sollten. Stattdessen greifen wir zu CSS, um unseren Text entsprechend zu formatieren.

Schön und gut, doch wie ersetzen wir eigentlich unsere BBCodes durch echten HTML-Code? Wir werden in diesem Tutorial zu regulären Ausdrücken greifen, um unseren Text nach BBCodes zu durchsuchen und durch entsprechenden HTML-Code zu ersetzen.

Fett, Unterstrichen und Kursiv

Für diese wirklich simplen Tags brauchen wir uns - wenn wir es nicht so genau nehmen - noch nicht einmal mit regulären Ausdrücken auseinanderzusetzen.

<?php
$bbcodes 
= array(
  
'[b]'   => '<span class="b">'   ,
  
'[/b]'  => '</span>'            ,
  
'[i]'   => '<span class="i">'   ,
  
'[/i]'  => '</span>'            ,
  
'[u]'   => '<span class="u">'   ,
  
'[/u]'  => '</span>'            ,
);

$string str_replace(array_keys($bbcodes), array_values($bbcodes), $string);

Wenn man noch mal kurz darüber nachdenkt fällt auf, dass wir uns dieses Vorgehen nur erlauben dürfen, wenn wir davon ausgehen können, dass die Texteingabe des Users vollständig, d.h. dass jedes öffnende Tag auch ein zugehöriges schließendes Tag besitzt und sämtliche Tags korrekt verschachtelt sind. Da wir dies nicht von jedem User erwarten können, müssen wir nun doch auf reguläre Ausdrücke ausweichen, da wir nicht riskieren möchten, dass fehlerhaftes HTML das Design unserer Website zerstört.
Ein paar Beispiele sollen im Folgenden mögliche Usereingaben demonstrieren.

<?php
$string 
'[b][i][u]string[/u][/i][/b]';
$string str_replace(array_keys($bbcodes), array_values($bbcodes), $string);
echo 
'<p>' $string '</p>';

$string '[i][/u][b]string[/i][/i][/b]';
$string str_replace(array_keys($bbcodes), array_values($bbcodes), $string);
echo 
'<p>' $string '</p>';

$string '[b][/i][/u]string[/b][/b][/b]';
$string str_replace(array_keys($bbcodes), array_values($bbcodes), $string);
echo 
'<p>' $string '</p>';

Der durch unser Script erzeugte HTML-Code sieht so aus:

<p>
  <span class="b">
    <span class="i">
      <span class="u">
        string
      </span>
    </span>
  </span>
</p>

<p>
  <span class="i">
  </span>
  <span class="b">
    string
  </span>
  </span>
  </span>
</p>

<p>
  <span class="b">
  </span>
  </span>
  string</span>
  </span>
  </span>
</p>

Der zweite und dritte Absatz ist alles andere als HTML-Konform und kann im schlimmsten Fall das Design unserer Website zerstören.

Ansatz BBCode - Parser

Es wird nun, um auch bei falscher BBCode-Eingabe valides HTML zu gewährleisten, eine Art Parser benötigt, der für uns die BB-Tags stückweise übersetzt! Im Grunde ist es egal, ob die BB-Tags von innen nach außen oder von außen nach innen ersetzt werden, da uns aufgrund einer falschen Usereingabe die korrekte - vom User vorgesehene - Formatierung sowieso unbekannt ist. Uns geht es hauptsächlich um validen Code.

<?php
function parseBBCode($string) {
  
$regex '/\[([biu])\](.*?)\[\/\1\]/is';
  while (
preg_match($regex$string)) {
    
$string preg_replace($regex'<span class="\1">\2</span>'$string);
  }

  return 
'<p>' $string '</p>';
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Ausdrücke für BBCodes</title>

    <style type="text/css">
      span.b {
        font-weight:bold;
      }

      span.u {
        text-decoration:underline;
      }

      span.i {
        font-style:italic;
      }
    </style>
  </head>
  <body>
    <?php
    $string 
'[b][i][u]string[/u][/i][/b]';
    echo 
parseBBCode($string);

    
$string '[i][/u][b]string[/i][/i][/b]';
    echo 
parseBBCode($string);

    
$string '[b][/i][/u]string[/b][/b][/b]';
    echo 
parseBBCode($string); 
    
?>
  </body>
</html>

Der Nachteil dieser Lösung ist offensichlich: Wir benötigen eine Schleife, um validen Code gewährleisten zu können. Dennoch ist semantisch korrektes HTML dem (geringen) Performanceverlust vorzuziehen.
Als Ausgabe erhalten wir für diese 3 Beispiele folgenden HTML-Code.

<p>
  <span class="b">
    <span class="i">
      <span class="u">string</span>
    </span>
  </span>
</p>

<p>
  <span class="i">
    [/u]<span class="b">string</span>[/i]
  </span>
</p>

<p>
  <span class="b">[/i][/u]string</span>[/b][/b]
</p>

Diesmal erzeugen wir auch im zweiten und dritten Fall, trotz falscher Verschachtelung und fehlender Tags, dank unseres Parsers, valides HTML. Die falschen BB-Tags, die noch übrig geblieben sind, sind zwar unschön, können aber das Design unserer Website nicht zerstören.

Achtung Falle

Augenscheinlich sieht der HTML-Code korrekt aus, doch erneut können wir mit etwas Überlegung eine Schwäche unseres Parsers ausmachen. Dazu tauschen wir einfach mal die <span> - Tags, die wir über CSS formatieren, durch die veralteten Tags <b>, <i> und <u> aus und schon wird das Problem offensichtlich.

<p>
  <b>
    <i>
      <u>string</u>
    </i>
  </b>
</p>

<p>
  <i>[/u]<b>string</i>[/i]</b>
</p>

<p>
  <b>[/i][/u]string</b>
  [/b][/b]
</p>

Im zweiten Beispiel sind zwei Tags falsch verschachtelt. Dies haben wir zuvor leicht übersehen, da die schließenden Tags (</span>) für die 3 Formatierungen identisch sind. Da dies nicht für alle BB-Tags gilt, die wir in diesem Tutorial noch besprechen werden, müssen wir uns überlegen, wie wir dieses Problem beheben können.

Dazu müssen wir noch mal an unseren regulären Ausdruck ran!

$regex = '/\[([biu])\](.*?)\[\/\1\]/is';

Offenbar liegt der Fehler darin, dass wir innerhalb einer Formatierung das Öffnen eines weiteren Tags zulassen. Dieser soll also im folgenden nicht mehr Bestandteil des Inhalts sein, den wir mit

(.*?)

gruppieren. Das Stichwort lautet in dem Fall: negative assertions. Der angepasste Ausdruck samt Parser sieht wie folgt aus:

<?php
function parseBBCode($string) {
  
$regex '/\[([biu])\](?!.*?\[[biu]\])(.*?)(?<!\[\/[biu]\])\[\/\1\]/is';
  while (
preg_match($regex$string)) {
    
$string preg_replace($regex'<span class="\1">\2</span>'$string);
  }

  return 
'<p>' $string '</p>';
}

Lassen wir unseren Parser nun mit diesem Ausdruck arbeiten, erhalten wir diesmal folgenden HTML-Code:

<p>
  <span class="b">
    <span class="i">
      <span class="u">
        string
      </span>
    </span>
  </span>
</p>

<p>
  [i][/u][b]string[/i][/i][/b]
</p>

<p>
  <span class="b">[/i][/u]string</span>[/b][/b]
</p>

Selbst ohne Probe des Codes mit den veralteten Tags sieht man, dass im zweiten Beispiel keine Tags mehr ersetzt werden und das Problem mit der falschen Verschachtelung der Tags behoben ist.
Da es in diesem Fall allerdings nicht unbedingt notwendig ist, die Tags derart zu behandeln (die schließenden Tags sind identisch), kann auch weiterhin der erste Ausdruck verwendet werden.

Außerdem werden wir auch im Folgenden falsche Verschachtelungen nicht weiter berücksichtigen, da der Parser dadurch einfach zu umfangreich werden und den Rahmen dieses Artikels sprengen würde.

BBCode für Überschriften

Unseren kleinen Parser können wir nun um beliebige weitere BB-Tags erweitern. Eine weitere interessante Möglichkeit für die Verwendung von BB-Code ist die Realisierung von Tags für Überschriften. Das besondere an HTML-Überschriften, im Gegensatz zu den Formatierungen fett, kursiv und unterstrichen ist die Tatsache, dass Überschriften nicht Teil eines Absatzes (<p></p>) sein dürfen. Dies ist aber kein allzu großes Problem, wenn wir voraussetzen, dass der gesamte Text immer als Absatz ausgezeichnet wird. Unsere Parserfunktion gibt den Text bereits als Absatz ausgezeichnet zurück!

return '<p>' . $string . '</p>';

Eine Überschrift trennt dann den Absatz, in dem sie steht, in zwei Absätze auf. Ein Beispiel verdeutlicht dies vermutlich etwas besser.

<p>
  Zwischen diesen beiden Tags soll der gesamte Text stehen. 
  Im Text können sich auch Überschriften befinden.
</p>

Befindet sich nun eine Überschrift innerhalb des Textes, müssen wir vor der Überschrift den Absatz beenden und nach der Überschrift einen neuen Absatz beginnen.

<p>
  Zwischen diesen beiden Tags soll der gesamte Text stehen.
</p>
<h3>Testüberschrift</h3>
<p>
  Im Text können sich auch Überschriften befinden.
</p>

Ein passender Ausdruck ist in Anlehnung an unseren bisherigen Ausdruck rasch formuliert:

<?php
$regex 
'/\[h([1-6])\](.*?)\[\/h\1\]/is';
while (
preg_match($regex$string)) {
  
$string preg_replace($regex'</p><h\1>\2</h\1><p>'$string);
}

Allerdings gilt noch zu beachten, dass wenn vor der Überschrift kein Text steht, bei der Rückgabe unseres Strings

<?php
// ... (beispielhaft) ...

$string '[h3]Testüberschrift[/h3]
Im Text können sich auch Überschriften befinden.'
;

// $string parsen ergibt
$string '</p>
<h3>Testüberschrift</h3>
<p>Im Text können sich auch Überschriften befinden.'
;

return 
'<p>' $string '</p>'

// ...

ein leerer Absatz entstünde, da die Überschrift ja immer noch versucht, den geöffneten Absatz zu schließen.

<p></p>
<h3>Testüberschrift</h3>
<p>
  Im Text können sich auch Überschriften befinden.
</p>

Wir müssen also alle leeren Absätze, die beim Parsen entstanden sind, vor der Rückgabe des Textes durch eine leere Zeichenkette ersetzen. Dies kann z.B. mit str_replace() geschehen.

return str_replace('<p></p>', '', '<p>' . $string . '</p>'); 

Durch dieses Vorgehen erzeugen wir wieder valides HTML. Unseren Parser können wir nach diesen Vorüberlegungen also entsprechend erweitern.

<?php
function parseBBCode($string) {
  
// bold, italic, underline

  // headlines
  
$regex '/\[h([1-6])\](.*?)\[\/h\1\]/is';
  while (
preg_match($regex$string)) {
    
$string preg_replace($regex'</p><h\1>\2</h\1><p>'$string);
  }

  return 
str_replace('<p></p>''''<p>' $string '</p>');
}

Der Parser interpretiert nun die BB-Tags [h1][/h1] bis [h6][/h6] für Überschriften erster bis sechster Ebene.

BBCode für HTML-Listen

Genau wie Überschriften, dürfen auch Listen nicht Teil eines Absatzes sein. Vor dem öffnenden <ul> - Tag und nach dem schließenden </ul> - Tag muss also der Absatz beendet beziehungsweise geöffnet werden.

Ein neues Problem wird schnell klar, wenn man sich eine HTML-Liste mal genauer anschaut.

<ul>
  <li>Listenpunkt 1</li>
  <li>Listenpunkt 2</li>
  <li>...</li>
</ul>

Wir müssen also diesmal nicht nur ein Tag (<ul> bzw. </ul>) verarbeiten, sondern auch noch die Tags für die einzelnen Listenpunkte beim Parsen identifizieren und korrekt ersetzen. Zu Beginn steht natürlich wieder die Wahl des Musters, nach dem wir unsere HTML-Listen in BBCodes darstellen möchten. Für dieses Tutorial habe ich mir das gebräuchliche Muster

[list]
  [*]Listenpunkt 1
  [*]Listenpunkt 2
  [*]...
[/list]

ausgesucht. Zuerst schnappen wir uns mal den Inhalt, der zwischen den Listentags steht. Dieser enthält dann nur noch unsere Listenpunkte, die wir ebenfalls parsen müssen. Dazu bedienen wir uns der Funktion preg_replace_callback().

<?php
$regex 
'/\[list\](.*?)\[\/list\]/is';
while (
preg_match($regex$string)) {
  
$string preg_replace_callback($regex'parseHTMLList'$string);
}

Es wird also eine Callback-Funktion parseHTMLList aufgerufen, die unsere Treffer aus dem obigen Ausdruck übergeben bekommt und die wir dort weiterverarbeiten können. Die genannte Funktion definieren wir wie folgt:

<?php
function parseHTMLList($match) {
  
$regex '/\s*\[\*\](.*?)(?=\[\*\]|(?:\r?\n|\r|$))/s';
  
$string preg_replace($regex'<li>\1</li>'trim($match[1]));

  return 
'</p><ul>' $string '</ul><p>';
}

Diese Funktion gibt die fertige Liste zurück und beachtet auch, dass die Liste - wie die Überschriften - nicht innerhalb eines Absatzes steht.

Unsere erweiterte Parserfunktion sieht nun so aus:

<?php
function parseHTMLList($match) {
  
$regex '/\s*\[\*\](.*?)(?=\[\*\]|(?:\r?\n|\r|$))/s';
  
$string preg_replace($regex'<li>\1</li>'trim($match[1]));

  return 
'</p><ul>' $string '</ul><p>';
}

function 
parseBBCode($string) {
  
// bold, italic, underline

  // headlines

  // lists
  
$regex '/\[list\](.*?)\[\/list\]/is';
  while (
preg_match($regex$string)) {
    
$string preg_replace_callback($regex'parseHTMLList'$string);
  }

  return 
str_replace('<p></p>''''<p>' $string '</p>');
}

BBCode für Hyperlinks

Für Hyperlinks gibt es in den meisten Boards 2 verschiedene Möglichkeiten der Notation eines Hyperlinks in BBCode.

[url]http://example.com[/url]

[url=http://example.com]Besuchen Sie example.com[/url]

Im ersten Beispiel wird die Adresse direkt zwischen die Tags [url] und [/url] geschrieben. Eine Angabe des Linktextes ist dabei nicht möglich. Möchte man den Linktext ändern, kann man auf die zweite Variante zurückgreifen und die URL als Attribut im [url] - Tag angeben. Wir beschränken uns in diesem Tutorial auf die zweite Variante.

<?php
function parseBBCode($string) {
  
// bold, italic, underline

  // headlines

  // lists

  // replace [url=] - Tags
  
$regex '/\[url\=([^\]]+)\]([^\[]+)\[\/url\]/i';
  
$string preg_replace($regex'<a href="\1">\2</a>'$string);

  return 
str_replace('<p></p>''''<p>' $string '</p>');
}

BBCode für Bilder

Um Bilder einbinden zu können, möchten wir unseren Usern ein weiteres BB-Tag anbieten. Dazu orientieren wir uns an der ersten Möglichkeit zur Einbindung eines Hyperlinks aus dem vorigen Abschnitt. Zwischen die [img] - Tags soll die Adresse zum Bild angegeben werden.

[img]http://example.com/images/picture.png[/img] <?php
// replace [img] - Tags
$regex '/\[img\]([^\[]+)\[\/img\]/i';
$string preg_replace($regex'<img src="\1" alt="" />'$string);

Doch wollen wir uns diesmal mit dieser einen Möglichkeit nicht zufrieden geben. Der User soll die Möglichkeit bekommen, einen alternativen Text für sein Bild anzugeben. Dazu kommt uns die zweite Variante wieder aus dem Abschnitt zu Hyperlinks gelegen.
Wir ändern lediglich den Tagnamen und verwenden den Inhalt zwischen den Tags für das alt="" - Attribut des <img> - Tags und die Adresse zum Bild wird wie bei den Hyperlinks angegeben.

[img=http://example.com/images/picture.png]Alternativer Text[/img] <?php
function parseBBCode($string) {
  
// bold, italic, underline

  // headlines

  // lists

  // replace [url=] - Tags

  // replace [img] - Tags
  
$regex '/\[img\]([^\[]+)\[\/img\]/i';
  
$string preg_replace($regex'<img src="\1" alt="" />'$string);

  
// replace [img=] - Tags
  
$regex '/\[img\=([^\]]+)\]([^\[]+)\[\/img\]/i';
  
$string preg_replace($regex'<img src="\1" alt="\2" />'$string);

  return 
str_replace('<p></p>''''<p>' $string '</p>');

Antworten

Steffen | verfasst am 07. Februar '11 um 00:37 Uhr

#1

Hallo,

schönes Tutorial hast du da gemacht, gefällt mir. Im Moment komm ich bei mir leider nicht weiter und möchte daher Fragen ob hier in den Kommentaren auch CODE-Tags zur Verfügung stehen. Vielleicht kannst du mir ja helfen, dann poste ich den Code.

Es geht um Listen, ich hab da eine eigene Funktion. Diese funktioniert soweit einwandfrei, nur komm ich mit den P-Tags nicht klar.

Steffen | verfasst am 07. Februar '11 um 14:07 Uhr

#2

Hallo,

ich hab das ganze mal hochgeladen, ist denke ich auch besser so. Mein Problem ist, wenn ich Listen verschachtel, dann hab ich immer den P-Tag um die verschachtelte Liste drum rum. Und genau da komm ich nicht weiter, ich weiß nicht wie ich den P-Tag da entfernen soll.

Vielleicht hast du ja eine Idee?

Testseite: http://www.codz.de/bb/bb.php
PHP-Code: http://www.codz.de/bb/bb.php?code

Asipak | verfasst am 08. Februar '11 um 15:57 Uhr

#3

Hi,

ja, Codetags sind erlaubt (glaube ich :D).

<?php
echo 'test';

[code ]<?php
echo 'test';[/code ]

Ich schau mir das mal an. Melde mich noch mal.

Asipak | verfasst am 08. Februar '11 um 17:03 Uhr

#4

return '</p>' . preg_replace('/^<\/p>(.*)<p>$/', '\1', $text . $text_end) . '<p>';

So dürfte es gehen, oder?

Übrigens ein guter Punkt, den ich oben offensichtlich nicht berücksichtigt habe, danke.

Steffen | verfasst am 08. Februar '11 um 20:18 Uhr

#5

Ja, so geht es perfekt. Auf die Idee mit dem preg_replace wäre ich nicht gekommen.

Nochmal vielen Dank für deine Hilfe.

mk202 | verfasst am 06. September '11 um 17:08 Uhr

#6

Ich will bei mir Tabellen machen und mit regulären Ausdrücken bin ich noch sehr unerfahren.

Könnt ihr mir da helfen?

Danke