Si eres de los que, como yo, no terminas de entender las expresiones regulares y quieres permitir que los usuarios de tu web puedan usar los BBCodes típicos de los foros, también puedes conseguir el mismo efecto creando un intérprete simple que vaya leyendo caracter a caracter y remplazando las etiquetas.
La idea se basa en usar una "pila" para los códigos que vayamos encontrando y un buffer para el texto (realmente sería una cola, pues luego extraemos elementos en el mismo orden que los insertamos). Leeremos caracter a caracter el BBCode, y cuando encontremos una etiqueta apilaremos el texto que habíamos leido antes de encontrarla, para terminar luego de leer la etiqueta. Luego leeremos sus atributos (si tiene), lo guardaremos en un array y lo apilaremos. Más adelante recorreremos la "pila" desde la base hasta el final e iremos convirtiendo los códigos en html.
También comentar que la función realiza varias llamadas recursivas para procesar los códigos que pueden contener a su vez otras etiquetas en su interior (por ejemplo, [b][i]negrita y cursiva[/i][/b]), que permite especificar los colores más comunes por si nombre y no por su código RGB, y que permite habilitar y deshabilitar etiquetas (en el sitio que estoy programando necesito deshabilitar ciertas carácterísticas, por ejemplo insertar videos, en algunas partes donde sí se podrían usar otros códigos). También hay algunos códigos que todavía no están incluidos, pero viendo el código que ya hay programado no es dificil extender la clase.
class bbcode {
const BB_COLOR = 0;
const BB_IMAGES = 1;
const BB_QUOTES = 2;
const BB_VIDEO = 3;
const BB_LINKS = 4;
const BB_LUSERS = 5;
const BB_MUSICRELS = 6;
private $features;
public function __construct() {
$this->features = array(
self::BB_COLOR => false,
self::BB_IMAGES => false,
self::BB_QUOTES => true,
self::BB_VIDEO => false,
self::BB_LINKS => true,
self::BB_LUSERS => true,
self::BB_MUSICRELS => true
);
}
public function disable($feature) {
$this->features[$feature] = false;
}
public function enable($feature) {
$this->features[$feature] = true;
}
public function get_features() {
/* devuelve las características habilitadas */
return $this->features;
}
const BB_PARSER_INIT = 0;
const BB_LABEL_START = 1;
const BB_ATTRIB_NAME = 2;
const BB_ATTRIB_VALUE= 3;
const BB_TYPE_TEXT = 0;
const BB_TYPE_LABEL = 1;
private function real_parse($bbcode) {
/*esta función debe construir una pila con el BBCode a procesar
y pasársela a otra función que realizará los remplazos oportunos
esta función no sabe nada de la sintaxis del bbcode, ni que etiquetas tienen
otra de cierre, ni las que tienen parámetros internos, etc; solo crea una pila
*/
$buffer = $c = $attrib_name = '';
$label = $attrib_value = '';
$attrib_first_char = $attrib_quoted = false;
$attribs = $stack = array();
$state = self::BB_PARSER_INIT;
$textual_tag = '';
for($i=0,$l=strlen($bbcode) ; $i <= $l ; $i++) {
$c = $bbcode{$i};
switch($state) {
case self::BB_PARSER_INIT:
if($c != '[') $buffer .= $c;
else {
/* Cambiamos de estado, apilamos el texto "normal" que teníamos
e inicializamos variables que usaremos para leer la etiqueta */
if($buffer != '') {
$stack[] = array(self::BB_TYPE_TEXT, $buffer, array(), '');
}
$state = self::BB_LABEL_START;
$attribs = array();
$buffer = '';
$label = '';
$textual_tag = '[';
}
break;
case self::BB_LABEL_START:
$textual_tag .= $c;
if($c == ']') {
$state = self::BB_PARSER_INIT; //Apilamos la etiqueta con lista de atributos vacía
//y volvemos al inicio
$stack[] = array(self::BB_TYPE_LABEL, $label, $attribs, $textual_tag);
$textual_tag = '';
} elseif($c == ' ') {
//inicio lista atributos a una lista vacía
$state = self::BB_ATTRIB_NAME;
$attrib_name = '';
$attrib_value = '';
$attribs = array();
} elseif($c == '=') {
//"valor de etiqueta", x ejemplo [color=#FFFFFF]
//guarda un atributo de mismo nombre que la etiqueta
$state = self::BB_ATTRIB_VALUE;
$attrib_first_char = true;
$attrib_name = $label;
$attrib_value = '';
$attribs = array();
} else {
$label .= $c;
}
break;
case self::BB_ATTRIB_NAME:
$textual_tag .= $c;
if($c == ' ') {
//FIXUP: salta al siguiente atributo sin dar valor al
// actual, asi que ignoramos el anterior
$attrib_name = '';
} elseif($c == ']') {
//Cierra la etiqueta así que la guardamos y volvemos al inicio
$stack[] = array(self::BB_TYPE_LABEL, $label, $attribs, $textual_tag);
$state = self::BB_PARSER_INIT;
$textual_tag = '';
} elseif($c != '=') {
$attrib_name .= $c;
} else {
$state = self::BB_ATTRIB_VALUE;
$attrib_first_char = true;
$attrib_value = '';
}
break;
case self::BB_ATTRIB_VALUE:
$textual_tag .= $c;
if($attrib_first_char && ($c == '"')) {
//Si el primer caracter del atributo son unas comillas,
// significa que el valor estará entre comillas, así
// que leeremos hasta las siguientes comillas
$attrib_quoted = true;
$attrib_first_char = false;
$attrib_value = '';
} elseif($attrib_first_char && ($c == ']')) {
//En el primer caracter está el caracter de cierre
//luego el atributo no tendrá valor
$stack[] = array(self::BB_TYPE_LABEL, $label, $attribs, $textual_tag);
$state = self::BB_PARSER_INIT;
} elseif($attrib_first_char && ($c != '"')) {
//Si el primer caracter no son comillas, leeremos hasta
// que encontremos un espacio o un ]
$attrib_quoted = false;
$attrib_first_char = false;
$attrib_value = $c;
} elseif(!$attrib_first_char) {
if($attrib_quoted) {
//atributo entre comillas -> leemos hasta encontrar las de cierre
if($c != '"') {
$attrib_value .= $c;
} else {
$attribs[$attrib_name] = $attrib_value;
$attrib_name = $attrib_value = '';
$state = self::BB_ATTRIB_NAME;
}
} else {
//atributo sin comillas -> leemos hasta encontrar un espacio o un ]
if( ($c != ' ') && ($c != ']') ) {
$attrib_value .= $c;
} else {
$attribs[$attrib_name] = $attrib_value;
$attrib_name = $attrib_value = '';
if($c == ' ') {
$state = self::BB_ATTRIB_NAME;
} else {
//Fin de etiqueta, la guardamos
$stack[] = array(self::BB_TYPE_LABEL, $label, $attribs, $textual_tag);
$state = self::BB_PARSER_INIT;
}
}
}
}
break;
}
}
//$stack contiene una pila con los códigos a procesar, que pueden estar mal cerrados, no existir, etc
//var_export($stack);
//Comprobamos que lo que estaba entre [] eran realmente etiquetas BBCode,
// lo que no lo fuera vuelve a convertirse en texto
$this->check_tags(&$stack);
$last = 0;
echo $this->tags_to_html(&$stack, 0, false, $last);
}
private function tags_to_html(&$stack, $first_element=0, $closing_tag=false, &$last_processed) {
//Esto va leyendo todos los elementos y creando un buffer con todo el texto formateado a html
//En caso de encontrar un tag que no sepa formatear, lo escribe directamente al buffer
//Se trata de una función recursiva, y va leyendo la pila desde $first_element
//Cuando encuentra un tag de apertura, se llama a si misma para obtener todo el texto formateado
// hasta la etiqueta de cierre; eventualmente, si encuentra otras etiquetas de apertura antes de la etiqueta
// de cierre buscada, hará lo propio para formatear los bCodes intermedios
//Debemos intentar por todos los medios crear HTML válido aunque el BBCode sea incorrecto, por tanto, si
//encontramos una etiqueta de cierre distinta de la que buscamos deberemos actuar como si la hubiéramos encontrado,
//devolviendo el formateo correcto hasta la etiqueta (incorrecta) de cierre actual, y forzando que se vuelva a procesar
//la etiqueta incorrecta de cierre encontrada por si era de un elemento anterior. ÉSTO CAUSA QUE, en caso de encontrar una
//etiqueta de cierre existente, pero que no cierra nada que estuviera abierto, SE CIERREN TODAS LAS ETIQUETAS ABIERTAS
//... AL final esto último no es así; si una etiqueta se abre dentro de otra pero se cierra fuera ([b][i] lalala [/b][/i]),
// al convertirlo en html resultará que se cierra la cursiva al encontrar el /i, pero no el b (ya que se encontró cuando buscábamos
// el /i)
$buffer = '';
$last = count($stack);
for($i=$first_element ; $i <= $last ; $i++) {
if($stack[$i][0] == self::BB_TYPE_TEXT) {
$buffer .= $stack[$i][1];
} else { //TYPE_LABEL
if(($closing_tag !== false) && ($closing_tag == $stack[$i][1])) {
//Encontrado tag de cierre, devolvemos el buffer procesado hasta ahora
$last_processed = $i;
return $buffer;
}
switch($stack[$i][1]) {
case 'img':
//Lo de dentro tiene que ser una URL, asi que recreamos todo como texto hasta el tag [/img]
//Si no encontramos un tag [/img], no procesamos el tag
$url = ''; $align = false; $found = false; $title=false;
if(isset($stack[$i][2]['align'])) {
if($stack[$i][2]['align'] == 'left') $align = 'left';
if($stack[$i][2]['align'] == 'right') $align = 'right';
if($stack[$i][2]['align'] == 'center') $align = 'center';
}
if(isset($stack[$i][2]['title'])) {
$title = $stack[$i][2]['title'];
}
for($j=$i+1 ; $j <= $last ; $j++) {
if($stack[$j][0] == self::BB_TYPE_LABEL) {
//hemos encontrado una etiqueta dentro del tag [img]
if ($stack[$j][1] == '/img') {
//si es la de cierre
$url = htmlentities($url,ENT_QUOTES);
if(is_valid_url($url)) {
$found = true;
if(!$title) {
if($align == 'center') {
$buffer .= '< div style="width:100%;clear:both;text-align:center;">';
$buffer .= "< img alt="\" src="\" />";
$buffer .= '< /div>';
} elseif($align != false) {
$buffer .= "< img alt="\" src="\" style="\" />";
} else {
$buffer .= "< img alt="\" src="\" />";
}
} else {
if($align == 'center') {
$buffer .= '< div style="width:100%;clear:both;text-align:center;">';
$buffer .= "< img alt="\" src="\" title="\" />";
$buffer .= '< /div>';
} elseif($align != false) {
$buffer .= "< img alt="\" src="\" title="\" style="\" />";
} else {
$buffer .= "< img alt="\" src="\" title="\" />";
}
}
}
//Avanzamos el bucle original hasta el final de la etiqueta
$i = $j;
break;
} else {
//hemos encontrado una etiqueta dentro de otra, la tratamos
//como parte de la URL...
$url .= $stack[$j][3];
}
} else {
$url .= $stack[$j][1];
}
} // - for
if(!$found) {
//Se hemos llegado aquí significa que no hemos encontrado un tag [/img],
//ponemos el tag [img] en el buffer y seguimos después de él
$buffer .= '['.$stack[$i][1].']';
}
break;
case 'b':
//Enviamos $i como referencia para que nuestra iteración continue donde
//acabe la de la llamada recursiva
$buffer .= '< b>'.$this->tags_to_html(&$stack, $i+1, '/b', &$i).'< /b>';
break;
case 'i':
$buffer .= '< i>'.$this->tags_to_html(&$stack, $i+1, '/i', &$i).'< /i>';
break;
case 'u':
$buffer .= '< span style="text-decoration:underline;">'.
$this->tags_to_html(&$stack, $i+1, '/u', &$i).'< /span>';
break;
case 's':
$buffer .= '< strike>'.$this->tags_to_html(&$stack, $i+1, '/s', &$i).'< /strike>';
break;
case 'color':
if(isset($stack[$i][2]['color'])) {
$color = $this->color_to_hex($stack[$i][2]['color']);
if($color !== false) {
//hemos determinado que el color es válido
$buffer .= '< span style="color:'.$color.';">'.
$this->tags_to_html(&$stack, $i+1, '/color', &$i).'< /span>';
} else {
//color no válido, no hacemos cambios en la salida
$buffer .= $this->tags_to_html(&$stack, $i+1, '/color', &$i);
}
} else {
//no ha especificado qué color quiere usar... así que procesamos el tag sin incluir
//cambios en la salida
$buffer .= $this->tags_to_html(&$stack, $i+1, '/color', &$i);
}
break;
case 'url':
if(isset($stack[$i][2]['url'])) {
//tag tipo [url=URL]texto enlace[/url]
if(is_valid_url($stack[$i][2]['url'])) {
$buffer .= '< a href="'.$stack[$i][2]['url'].'">'.
$this->tags_to_html(&$stack, $i+1, '/url', &$i).'< /a>';
} else {
//URL inválida, la mostramos como texto
$buffer .= $this->tags_to_html(&$stack, $i+1, '/url', &$i).
' ['.$stack[$i][2]['url'].']';
}
} else {
//tag tipo [url]URL[/url]
$url='';
for($j=$i+1 ; $j <= $last ; $j++) {
if($stack[$j][0] == self::BB_TYPE_TEXT) {
$url .= $stack[$j][1];
} elseif($stack[$j][1] != '/url') { //($stack[$i][0] == self::BB_TYPE_LABEL)
//Lo habíamos "confundido" con un tag, leemos la representación textual
$url .= $stack[$j][3];
} else { //$stack[$i][1] == '/url' && $stack[$i][0] == self::BB_TYPE_LABEL
$i = $j;
break;
}
}
if(is_valid_url($url)) {
$buffer .= '< a href="'.$url.'">'.$url.'< /a>';
} else {
//URL inválida, la mostramos como texto
$buffer .= $url. ' ['.$url.']';
}
}
break;
default:
//Recrear etiqueta, ya que no existe
//esto no deberia pasar, pues ya habíamos comprobado que existia
$buffer .= '['.$stack[$i][3].']';
}
}
}
$last_processed = $i;
return $buffer;
}
private static function color_to_hex($name) {
//La lista de nombres está sacada de http://www.w3schools.com/css/css_colornames.asp
static $color_table = array(
'aliceblue' => '#F0F8FF', 'antiquewhite' => '#FAEBD7',
'aqua' => '#00FFFF', 'aquamarine' => '#7FFFD4',
'azure' => '#F0FFFF', 'beige' => '#F5F5DC',
'bisque' => '#FFE4C4', 'black' => '#000000',
'blanchedalmond'=> '#FFEBCD', 'blue' => '#0000FF',
'blueviolet' => '#8A2BE2', 'brown' => '#A52A2A',
'burlywood' => '#DEB887', 'cadetblue' => '#5F9EA0',
'chartreuse' => '#7FFF00', 'chocolate' => '#D2691E',
'coral' => '#FF7F50', 'cornflowerblue'=> '#6495ED',
'cornsilk' => '#FFF8DC', 'crimson' => '#DC143C',
'cyan' => '#00FFFF', 'darkblue' => '#00008B',
'darkcyan' => '#008B8B', 'darkgoldenrod' => '#B8860B',
'darkgray' => '#A9A9A9', 'darkgrey' => '#A9A9A9',
'darkgreen' => '#006400', 'darkkhaki' => '#BDB76B',
'darkmagenta' => '#8B008B', 'darkolivegreen'=> '#556B2F',
'darkorange' => '#FF8C00', 'darkorchid' => '#9932CC',
'darkred' => '#8B0000', 'darksalmon' => '#E9967A',
'darkseagreen' => '#8FBC8F', 'darkslateblue' => '#483D8B',
'darkslategray' => '#2F4F4F', 'darkslategrey' => '#2F4F4F',
'darkturquoise' => '#00CED1', 'darkviolet' => '#9400D3',
'deeppink' => '#FF1493', 'deepskyblue' => '#00BFFF',
'dimgray' => '#696969', 'dimgrey' => '#696969',
'dodgerblue' => '#1E90FF', 'firebrick' => '#B22222',
'floralwhite' => '#FFFAF0', 'forestgreen' => '#228B22',
'fuchsia' => '#FF00FF', 'gainsboro' => '#DCDCDC',
'ghostwhite' => '#F8F8FF', 'gold' => '#FFD700',
'goldenrod' => '#DAA520', 'gray' => '#808080',
'grey' => '#808080', 'green' => '#008000',
'greenyellow' => '#ADFF2F', 'honeydew' => '#F0FFF0',
'hotpink' => '#FF69B4', 'indianred' => '#CD5C5C',
'indigo' => '#4B0082', 'ivory' => '#FFFFF0',
'lhaki' => '#F0E68C', 'lavender' => '#E6E6FA',
'lavenderblush' => '#FFF0F5', 'lawnGreen' => '#7CFC00',
'lemonchiffon' => '#FFFACD', 'lightblue' => '#ADD8E6',
'lightcoral' => '#F08080', 'lightcyan' => '#E0FFFF',
'lightgoldenrodyellow' => '#FAFAD2', 'lightgray' => '#D3D3D3',
'lightgrey' => '#D3D3D3', 'lightgreen' => '#90EE90',
'lightpink' => '#FFB6C1', 'lightsalmon' => '#FFA07A',
'lightseagreen' => '#20B2AA', 'lightskyblue' => '#87CEFA',
'lightslategray'=> '#778899', 'lightslategrey'=> '#778899',
'lightsteelblue'=> '#B0C4DE', 'lightyellow' => '#FFFFE0',
'lime' => '#00FF00', 'limegreen' => '#32CD32',
'linen' => '#FAF0E6', 'magenta' => '#FF00FF',
'maroon' => '#800000', 'mediumaquamarine'=> '#66CDAA',
'mediumblue' => '#0000CD', 'mediumorchid' => '#BA55D3',
'mediumpurple' => '#9370D8', 'mediumseagreen'=> '#3CB371',
'mediumslateblue'=> '#7B68EE', 'mediumspringgreen'=> '#00FA9A',
'mediumturquoise'=> '#48D1CC', 'mediumvioletred'=> '#C71585',
'midnightblue' => '#191970', 'mintcream' => '#F5FFFA',
'mistyrose' => '#FFE4E1', 'moccasin' => '#FFE4B5',
'navajowhite' => '#FFDEAD', 'navy' => '#000080',
'oldlace' => '#FDF5E6', 'olive' => '#808000',
'olivedrab' => '#6B8E23', 'orange' => '#FFA500',
'orangered' => '#FF4500', 'orchid' => '#DA70D6',
'palegoldenrod' => '#EEE8AA', 'palegreen' => '#98FB98',
'paleturquoise' => '#AFEEEE', 'palevioletred' => '#D87093',
'papayawhip' => '#FFEFD5', 'peachpuff' => '#FFDAB9',
'peru' => '#CD853F', 'pink' => '#FFC0CB',
'plum' => '#DDA0DD', 'powderblue' => '#B0E0E6',
'purple' => '#800080', 'red' => '#FF0000',
'rosybrown' => '#BC8F8F', 'royalblue' => '#4169E1',
'saddlebrown' => '#8B4513', 'salmon' => '#FA8072',
'sandybrown' => '#F4A460', 'seagreen' => '#2E8B57',
'seashell' => '#FFF5EE', 'sienna' => '#A0522D',
'silver' => '#C0C0C0', 'skyblue' => '#87CEEB',
'slateblue' => '#6A5ACD', 'slategray' => '#708090',
'slategrey' => '#708090', 'snow' => '#FFFAFA',
'springgreen' => '#00FF7F', 'steelblue' => '#4682B4',
'tan' => '#D2B48C', 'teal' => '#008080',
'thistle' => '#D8BFD8', 'tomato' => '#FF6347',
'turquoise' => '#40E0D0', 'violet' => '#EE82EE',
'wheat' => '#F5DEB3', 'white' => '#FFFFFF',
'whitesmoke' => '#F5F5F5', 'yellow' => '#FFFF00',
'yellowgreen' => '#9ACD32'
);
//$name puede ser un nombre de color o un código rgb
//admitiremos nombres de color, RGB sin # y RGB con #
//En caso de que no sea ninguna de estas cosas retornaremos false
$name = strtolower(trim($name));
//Si es un nombre de color, lo devolvemos usando la tabla
if(isset($color_table[$name])) return $color_table[$name];
//Ajuste previo: ya que no es un nombre de color, solo puede
//ser un código RGB. Si empieza por # lo eliminamos para simplificar
if($name{0} == '#') {
$name = substr($name,1); //omitimos el #
}
//Solo puede ser un codigo RGB sin el # al principio o algo incorrecto
//comprobar si es un RGB correcto sin el #
if(!eregi("^[0-9abcdef]+$", $name)) {
return false; //no esta formado sólo por caracteres hex
} else {
//está formado solo por caracteres hex, comprobar si la longitud es correcta
//los codigos hex pueden tener longitud 2, 3 o 6
$l = strlen($name);
if(($l==6) || ($l==3) || ($l==2))
return '#'.$name;
else
return false;
}
}
private function check_tags(&$stack) {
/*
self::BB_QUOTES => true,
self::BB_VIDEO => false,
self::BB_LUSERS => true,
self::BB_MUSICRELS => true*/
//siempre habilitados....
$labels = array('b','/b','i','/i','u','/u','s','/s');
if($this->features[self::BB_COLOR]) {
$labels[] = 'color';
$labels[] = '/color';
}
if($this->features[self::BB_LINKS]) {
$labels[] = 'url';
$labels[] = '/url';
}
if($this->features[self::BB_IMAGES]) {
$labels[] = 'img';
$labels[] = '/img';
}
//Leemos toda la pila, lo que esté en nuestra lista de etiquetas válidas es
//dejado tal cual, lo que no es reconstruido
$new_stack = array();
foreach($stack as &$s) {
switch($s[0]) {
case self::BB_TYPE_LABEL:
if(array_search($s[1], $labels) === false) {
//La etiqueta no existe, guardamos su representacion textual
// (almacenada en el 3er elem. del label)
$new_stack[] = array(self::BB_TYPE_TEXT, $s[3], '');
} else {
//La etiqueta existe, la guardamos tal cual
$new_stack[] = $s;
}
break;
default: //es decir, BB_TYPE_TEXT, solo texto
$new_stack[] = $s;
break;
}
}
$stack = $new_stack;
}
public function parse($bbcode) {
//Operamos directamente sobre la variable $bbcode
//$bbcode = $this->escape_amp($bbcode);
$bbcode = str_replace('&', '&', $bbcode);
$bbcode = mb_convert_encoding($bbcode, 'HTML-ENTITIES', 'UTF-8');
$bbcode = nl2br($bbcode);
$pattern = array();
$replacement = array();
return $this->real_parse($bbcode);
}
}
$bb = new bbcode();
$bb->enable(bbcode::BB_COLOR);
$bb->enable(bbcode::BB_IMAGES);
//$bb->disable(bbcode::BB_LINKS);
echo $bb->parse('[color=#FF0000][b][i][u]red![/u][/i][/b][/color] & & ¿
[img]http://userserve-ak.last.fm/serve/160/328697.jpg[/img]
[img align="left"]http://userserve-ak.last.fm/serve/160/265995.jpg[/img]
[img align=left]http://userserve-ak.last.fm/serve/160/265995.jpg[/img]
[img align=right]http://userserve-ak.last.fm/serve/160/386255.jpg[/img]
[img align=center]http://userserve-ak.last.fm/serve/160/6371.jpg[/img]
[url]http://www.google.es/a[/url] - [url]http://www.google.es[/url] - [url]ftp://www.google.es/a[/url]
[url]ftp://pepe:1234@www.google.es/a[/url]
[url=http://www.google.es/dsgggggd/hds]Google[/url]
[url=http://www.google.es/search?q=hangar+18&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:official&client=firefox-a]Búsqueda hangar 18[/url]
[url]http://www.google.es/search?q=hangar+18&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:official&client=firefox-a[/url]
---------------
[b][url=http://www.google.es/dsgggggd/hds][color=]Google[/color][/url][/b]
[s]tachado[/s]
[img][b]black!
[color=purple][b]Deep Purple[/b][/color]
');
?>