. # 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; } } ?>