* * This file is a BBCode parser. Permission is hereby granted, * free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, * sublicense, and/or sell copies of the Software, and to permit * persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE * OR OTHER DEALINGS IN THE SOFTWARE. * * Amen. */ const TT_TRIVIAL = 0; ///< This tag is a direct HTML wrapper tag const TT_INTERLEAVE = 1; ///< This tag becomes "html"[0] . arg0 . "html"[1] . arg1 ... const TT_CALLBACK = 2; ///< This tag gets evaluated by a callback function const CT_MARKDOWN = 0; ///< The inside of the tag is Markdown-format text as non-BB. This is the default. const CT_RAW = 1; ///< The inside of the tag is raw data and should not be formatted. include('tags.php'); 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. * @param $str The string to parse. * @param $i [in/out] The position from which to start parsing. * Set at the end of the function to denote the first * unparsed character, or FALSE if all characters have * been consumed. * @return Returns the HTML parsed substring of the given input. */ function parse_sub($str, &$i, $contenttype, $opentags) { 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; break; } // 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 . parse_nonbb(substr($str, $bookmark, $i - $bookmark), $contenttype); if ($contenttype == CT_RAW) { ++$i; continue; } // Dopplegänger $doppl = strpos($stag, '['); if ($doppl !== FALSE) { $i += $doppl + 1; 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 . parse_nonbb(substr($str, $bookmark), $contenttype); } ++$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 = ''; $stend = $i; for (;;) { // This is where shit gets interesting array_push($opentags, $tname); $arg0 .= parse_sub($str, $i, isset($bbtag['content'])? $bbtag['content'] : CT_MARKDOWN, $opentags); array_pop($opentags); // Make sure we arrived at a closing tag if ($i == FALSE) return $outstr . parse_nonbb(substr($str, $bookmark, $stend - $bookmark), $contenttype) . $arg0; $close = strpos($str, ']', $i); $ctag = substr($str, $i, $close - $i); // Make sure this is *our* closing tag if (strncasecmp($ctag, '[/'.$tname, $tlen) != 0) { // If someone else's closing tag, bail; tags should be closed in order foreach ($opentags as $k => $otname) if (strncasecmp($ctag, '[/'.$otname, 2 + strlen($otname)) == 0) return $outstr . parse_nonbb(substr($str, $bookmark, $stend - $bookmark), $contenttype) . $arg0; // If not anyone's tag, just keep looking $arg0 .= $str[$i++]; continue; } 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 .= parse_nonbb(substr($str, $bookmark, $tstart - $bookmark), $contenttype); $outstr .= evaluate_tag($tname, $bbtag, $arg0, $arg1, $arg2, $args); $bookmark = $i; } $outstr .= parse_nonbb(substr($str, $bookmark), $contenttype); 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. * @note Unused tag arguments should be NULL; \p $arg1 and \p $arg2 should * never be non-null at the same time as \p $args. * @param $tname The name of the tag. * @param $bbtag The tag array from the \c bbtags array. * @param $arg0 The first argument, the text between the tags, if applicable. * @param $arg1 The second argument, the = value in the opening tag, if applicable. * @param $arg2 The third argument, the = value in the closing tag, if applicable. * @param $args An associative array of arguments, if applicable. * @return Returns the result of evaluating the tag; * a string with which to replace the tag. */ 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); } } } /*! This nonsense is to parse associative tags. * @param $str The string from which to read attributes. * @param $i [in/out] The position from which to start reading. * Set at end of function call to denote the end of the list. * @return Returns the associative array read in from the string. */ function read_attr_list($str, &$i) { $len = strlen($str); $attrs = array(); while ($i < $len && $str[$i] != ']') { if (ctype_space($str[$i])) { ++$i; continue; } // Read an attribute name $attr_start = $i; while (++$i < $len && $str[$i] != '=' && !ctype_space($str[$i])); $attr_name = substr($str, $attr_start, $i-$attr_start); if ($i >= $len) // Bail if out of bounds { notice("OOB0: '$attr_name'"); return NULL; } // Read past the attribute name while ($i < $len && ctype_space($str[$i])) { notice("white: '" . $str[$i] . "'"); ++$i; } if ($i >= $len) // Bail if out of bounds { notice("OOB1: '$attr_name'"); 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) ); ++$i; } 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; ++$i; } $attrs[$attr_name] = $val; } return $attrs; } /*! * Parse for non-BBCode elements, such as URLs or (God forbid) Markdown. * @param $str HTML-escaped plain text input. * @return Returns HTML-formatted parsed text. */ function parse_nonbb($str, $contenttype) { if ($contenttype != CT_MARKDOWN) return $str; $urlexp = "/(?i)\b(" . "(?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)" . "(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+" . "(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’])" .")/"; return preg_replace($urlexp, "$1", $str); return $str; } /*! Main BBCode parser call. * @param $str The BBCode string to parse. * @return Returns the HTML parsed version of the input. */ function parse_bbcode($str) { $i = 0; $res = ""; while ($i !== false) { $res .= parse_sub($str, $i, CT_MARKDOWN, array()); if ($i !== false) $res .= $str[$i++]; } return $res; } $test = " [b]Bold[/b] [assoc one=\"two\" two=\"four\" three=eight four=sixteen]yes[/assoc] [/code] www.google.com [b]this should be[i] bold[/b] [b]this should not be, but this should be: [b] bold[/b] [hr] [ [b]bold[/b] ] [ [b]bold[/b] ] http://google.com/ www.google.com [ "; echo "=================================\nTest:\n=================================\n" . $test . "\n"; echo "=================================\nResult:\n=================================\n"; echo parse_bbcode($test); ?>