array('type' => TT_TRIVIAL, 'args' => 1), 'i' => array('type' => TT_TRIVIAL, 'args' => 1), 'u' => array('type' => TT_TRIVIAL, 'args' => 1), 'hr' => array('type' => TT_TRIVIAL, 'args' => 0), 'img' => array( 'type' => TT_INTERLEAVE, 'args' => 1, 'html' => array('User image') ), 'url' => array( 'type' => TT_CALLBACK, 'args' => 2, 'func' => function($txt, $url) { return '' . $txt . ''; } ), 'bubble' => array( 'type' => TT_CALLBACK, 'args' => 4, 'func' => function($txt, $args) { $br = isset($args['radius'])? $args['radius'] : 6; $col = isset($args['color'])? $args['color'] : "000"; return '' . $txt . ''; } ), 'grad' => array( 'type' => TT_CALLBACK, 'args' => 3, 'func' => function($txt, $arg0, $arg1) { if (empty($arg0)) $arg0="pink"; if (empty($arg1)) $arg1="green"; return "" . $txt . ''; } ) ); function notice($s) { echo "\n"; } // This is a recursive function; call it with i = 0, and it will // call itself recursively until all tags are parsed. It returns the // parsed string, with $i set to the position of the first-read closing tag. function parse_sub($str, &$i) { global $bbtags; $outstr = ""; $bookmark = $i; for ($i = strpos($str, '[', $i); $i !== FALSE; $i = strpos($str, '[', $i)) { $close = strpos($str, ']', $i + 1); if ($close == FALSE) { $i = FALSE; return $outstr . substr($str, $bookmark, $i - $bookmark); } // Look inside our tag, now $stag = substr($str, $i+1, $close - $i - 1); // If it's a closing tag, return and let the parent handle that if ($stag[0] == '/') return $outstr . substr($str, $bookmark, $i - $bookmark); // Doplegänger if ($stag[0] == '[') { ++$i; continue; } // Make sure we're safe if args=1; we don't know, yet $tagc = preg_split('/\s*[\s=]\s*/', $stag, 2); if (count($tagc) == 2) $arg1 = $tagc[1]; // This technically allows [tag x]y[/tag] else $arg1 = NULL; // Don't reuse old arg values $tname = strtolower($tagc[0]); // Deliberately not trimmed // Look up tag $tstart = $i; $i = $close + 1; if (!array_key_exists($tname, $bbtags)) { notice("No bbtag called [$tname] ($stag)"); continue; } // Tag found $bbtag = $bbtags[$tname]; if ($bbtag['args'] > 0) { $tlen = strlen($tname) + 2; // Handle associative tags if ($bbtag['args'] > 3) { $i = $tstart + $tlen; if (ctype_space($str[$i-1])) { $args = read_attr_list($str, $i); if ($args === NULL) { // Bail on failure $i = FALSE; return $outstr . substr($str, $bookmark); } ++$i; } else if ($str[$i-1] == ']') $args = array(); else continue; } else { $args = NULL; if ($bbtag['args'] == 1 && $str[$tstart + $tlen - 1] != ']') { notice("$str [$tstart+$tlen-1] != ']'"); continue; } } $arg0 = ''; for (;;) { // This is where shit gets interesting $arg0 .= parse_sub($str, $i); // Make sure we arrived at our own closing tag if ($i == FALSE) return $outstr . substr($str, $bookmark); $close = strpos($str, ']', $i); $ctag = substr($str, $i, $close - $i); if (strncasecmp($ctag, '[/'.$tname, $tlen) != 0) { notice("strncasecmp('$ctag', '[/'.'$tname', '$tlen') != 0"); $arg0 .= $str[$i++]; continue; // If not, just keep looking } break; } // Now we have a little more parsing to do for ternary tags if ($bbtag['args'] == 3) { $arg2 = trim(substr($str, $i + $tlen, $close - $i - $tlen)); if (strlen($arg2) > 0 && $arg2[0] == '=') $arg2 = trim(substr($arg2, 1)); } else $arg2 = NULL; $i = $close + 1; } else $arg0 = $arg1 = $arg2 = $args = NULL; $outstr .= substr($str, $bookmark, $tstart - $bookmark); $outstr .= evaluate_tag($tname, $bbtag, $arg0, $arg1, $arg2, $args); $bookmark = $i; } $outstr .= substr($str, $bookmark); return $outstr; } // This function might as well be in that last block of code, // but it was getting frighteningly big, so I moved it here. function evaluate_tag($tname, $bbtag, $arg0, $arg1, $arg2, $args) { switch ($bbtag['type']) { case TT_TRIVIAL: switch ($bbtag['args']) { case 0: return "<$tname />"; case 1: return "<$tname>$arg0"; case 2: return ""; case 3: return ""; default: return ""; } case TT_INTERLEAVE: $h = $bbtag['html']; switch ($bbtag['args']) { case 0: return $h[0]; case 1: return $h[0] . $arg0 . $h[1]; case 2: return $h[0] . $arg1 . $h[1] . $arg0 . $h[2]; case 3: return $h[0] . $arg1 . $h[1] . $arg0 . $h[2] . $arg2 . $h[3]; default: return ""; } case TT_CALLBACK: $f = $bbtag['func']; switch ($bbtag['args']) { case 0: return $f(); case 1: return $f($arg0); case 2: return $f($arg0, $arg1); case 3: return $f($arg0, $arg1, $arg2); default: return $f($arg0, $args); } } } // And then all this nonsense is to parse associative tags. function read_attr_list($str, &$i) { $len = strlen($str); $attrs = array(); while ($i < $len && $str[$i] != ']') { if (ctype_space($str[$i])) continue; // Read an attribute name $attr_start = $i; while (++$i < $len && $str[$i] != '=' && !ctype_space($str[$i-1])); $attr_name = substr($str, $attr_start, $i-$attr_start); // Read past the attribute name while ($i < $len && ctype_space($str[$i])) ++$i; if ($i >= $len) // Bail if out of bounds { notice("OOB1"); return NULL; } if ($str[$i] == '=') { while (++$i < $len && ctype_space($str[$i])); if ($str[$i] == '"' || $str[$i] == '\'') { $val_start = $i + 1; $ochar = $str[$i]; while (++$i < $len && $str[$i] != $ochar) if ($str[$i] == '\\') ++$i; if ($i >= $len) { notice("OOB2"); return NULL; } $val = str_replace( array("\\\\", "\\\'", "\\\"", "\\r", "\\n", "\\t"), array("\\", "\'", "\"", "\r", "\n", "\t"), substr($str, $val_start, $i - $val_start) ); } else { $val_start = $i; while (++$i < $len && $str[$i] != ']' && !ctype_space($str[$i])); if ($i >= $len) { notice("OOB3"); return NULL; } $val = substr($str, $val_start, $i - $val_start); } } else $val = NULL; $attrs[$attr_name] = $val; } return $attrs; } function parse_bbcode($str) { $i = 0; $res = ""; while ($i !== false) { $res .= parse_sub($str, $i); if ($i !== false) $res .= $str[$i++]; } return $res; } echo "Result:
\n"; echo parse_bbcode( "This is some [b]cool shit[/b], I'm sure you'll agree. Italics: [i]check[/i]. Bold italics: [b][i]check[/i] and [i]double check[/i][/b] Now, let's try [url=asses]some urls[/url]. And now, let's try [url = asses in thongs]urls with gaps[/url]. For good measure, [grad=red]graded spans[/grad=blue]. To fuck shit up, [grad=red]graded spans[/grad]. To really fuck shit up, [grad]graded spans[/grad]. Okay, [bubble]complicated tag time[/bubble]! Now, [bubble radius='5' color=255]more complicated tag time[/bubble]! Finally, [bubble radius='5[b]ha[/b][/bubble]' color=255]extremely complicated tag time[/bubble]! This is an unmatched closing italic tag: [/i] Here's one in a bold tag: [b]wat[/[/i][/[[/b] [b]This is an unmatched bold tag. This concludes the BBCode portion of your exam. "); ?>