.
#
class BBCodeParser {
const TEXT = 0;
const WHSP = 1;
const CTAG = 2;
const OTAG = 3;
const DONE = 4;
function parse($in, $uid) {
# decode HTML entities before parsing
$in = html_entity_decode($in, ENT_QUOTES, 'UTF-8');
# convert smilies, which aren't in BBCode (ack!)
$in = preg_replace('/(.*?)<\/a>/', "[url:$uid=\\2]\\3[/url:$uid]", $in);
$in = preg_replace('/(.*?)<\/a>/', "[url:$uid=\\1]\\2[/url:$uid]", $in);
$in = preg_replace('/(.*?)<\/a>/', "[email:$uid=\\1]\\2[/email:$uid]", $in);
$text_stack = array();
$arg_stack = array();
$fn_number = 1;
$fn = array();
$i = 0;
$len = strlen($in);
$list_counter_stack = array();
$out = '';
$state = self::TEXT;
while ($state != self::DONE) {
switch ($state) {
case self::TEXT:
# find next occurance of uid
$ustart = strpos($in, ":$uid", $i);
if ($ustart === false) {
# no more tags, all done
$out .= substr($in, $i);
$state = self::DONE;
}
else {
$ulen = strlen($uid) + 1;
# locate the start and end of next tag
$tstart = strrpos($in, '[', $ustart-$len);
$tend = strpos($in, ']', $ustart+$ulen);
# slice out the uid
$tag = substr($in, $tstart+1, $ustart-$tstart-1) .
substr($in, $ustart+$ulen, $tend-$ustart-$ulen);
# determine whether it's an open or close tag
if ($tag[0] == '/') {
$state = self::CTAG;
$tag = substr($tag, 1);
}
else {
$state = self::OTAG;
}
# copy leading text to output
$out .= substr($in, $i, $tstart-$i);
# advance past the closing ']' to next unparsed character
$i = $tend + 1;
}
break;
case self::WHSP:
while ($in[$i] == "\n" || $in[$i] == "\t" || $in[$i] == ' ') ++$i;
$state = self::TEXT;
break;
case self::OTAG:
# split tag into tag name and argument, if any
if (strpos($tag, '=') !== false) {
list($tag, $arg) = explode('=', $tag, 2);
}
else {
$arg = false;
}
$arg_stack[] = $arg;
switch ($tag) {
case 'b':
$out .= '__';
$state = self::TEXT;
break;
case 'u':
case 'i':
$out .= '_';
$state = self::TEXT;
break;
case 'url':
case 'email':
# nothing to do on opening
$state = self::TEXT;
break;
case 'quote':
if ($arg !== false) {
$text_stack[] = $out . "\n$arg wrote:\n";
}
else {
$text_stack[] = $out . "\n";
}
$out = '';
$state = self::TEXT;
break;
case 'code':
$out .= "\nCode:\n";
$state = self::TEXT;
break;
case 'list':
# if ($out[strlen($out)-1] != "\n") $out .= "\n";
switch ($arg) {
case '1': $list_counter_stack[] = 1; break;
case 'a': $list_counter_stack[] = 'a'; break;
default: $list_counter_stack[] = '*'; break;
}
$state = self::TEXT;
# $state = self::WHSP;
break;
case '*':
# if ($out[strlen($out)-1] != "\n") $out .= "\n";
$out .= str_repeat(' ', 2*count($list_counter_stack));
$c = array_pop($list_counter_stack);
if (is_int($c)) {
$out .= $c . '. ';
$list_counter_stack[] = $c + 1;
}
else if ($c == '*') {
$out .= $c . ' ';
$list_counter_stack[] = '*';
}
else {
$out .= $c . '. ';
$list_counter_stack[] = chr(ord($c)+1);
}
$state = self::TEXT;
# $state = self::WHSP;
break;
case 'img':
$text_stack[] = $out;
$out = '';
$state = self::TEXT;
break;
case 'attachment':
# TODO: unimplemented
$state = self::TEXT;
break;
case 'color':
case 'size':
# ignored
$state = self::TEXT;
break;
default:
throw new Exception('Unrecognized open tag: ' . $tag);
}
break;
case self::CTAG:
$arg = array_pop($arg_stack);
switch ($tag) {
case 'b':
$out .= '__';
$state = self::TEXT;
break;
case 'u':
case 'i':
$out .= '_';
$state = self::TEXT;
break;
case 'url':
case 'email':
# TODO: untested
if ($arg !== false) {
# built footnotes for links with text
$out .= '[' . $fn_number++ .']';
$fn[] = $arg;
}
$state = self::TEXT;
break;
case 'quote':
$level = count($text_stack);
$out = wordwrap($out, 72 - 2*$level);
$out = str_replace("\n", "\n> ", $out);
$out = '> ' . $out;
$out = array_pop($text_stack) . $out . "\n";
$state = self::TEXT;
break;
case 'code':
# TODO: untested
# FIXME: don't wordwrap code!
$out .= "\n\n";
$state = self::TEXT;
break;
case 'list':
case 'list:o':
case 'list:u':
array_pop($list_counter_stack);
$out .= "\n\n";
# $state = self::WHSP;
$state = self::TEXT;
break;
case '*':
case '*:m':
# if ($out[strlen($out)-1] == "\n") $out = substr($out, 0, -1);
# $state = self::WHSP;
$state = self::TEXT;
break;
case 'img':
# TODO: untested
$fn[] = $out;
$out = array_pop($text_stack) . '[' . $fn_number++ . ']';
$state = self::TEXT;
break;
case 'attachment':
$state = self::TEXT;
break;
case 'color':
case 'size':
# ignored
$state = self::TEXT;
break;
default:
throw new Exception('Unrecognized close tag: ' . $tag);
}
break;
}
}
$out = wordwrap($out, 72);
if (!empty($fn)) {
# build footnotes
$out .= "\n";
for ($i = 0; $i < count($fn); ++$i) {
$out .= "\n[" . ($i+1) . '] ' . $fn[$i];
}
$out .= "\n";
}
return $out;
}
}
?>