, 2018
https://github.com/greg-kennedy/php-bbcode
This is public domain software. Please see LICENSE for more details.
******************************************************************************/
class BBCode
{
// Tag aliases. Item on left translates to item on right.
const TAG_ALIAS = [
'url' => 'a',
'code' => 'pre',
'quote' => 'blockquote',
'*' => 'li',
'list' => 'ul'
];
// helper function: normalize a potential "tag"
// convert to lowercase and check against the alias list
// returns a named array with details about the tag
static private function decode_tag($input) : array
{
// first determine if it's opening on closing tag, then substr out the inner portion
if ($input[1] === '/') {
$open = 0;
$inner = substr($input, 2, -1);
} else {
$open = 1;
$inner = substr($input, 1, -1);
}
// oneliner to burst inner by spaces, then burst each of those by equals signs
$params = array_map(
function($a) { return explode('=', $a, 2); },
explode(' ', $inner));
// first "param" is special - it's the tag name and (optionally) the default arg
$first = array_shift($params);
// tag name
$name = strtolower($first[0]);
if (isset(self::TAG_ALIAS[$name])) {
$name = self::TAG_ALIAS[$name];
}
// "default" (unnamed) argument
$args = null;
if (isset ($first[1])) {
$args['default'] = $first[1];
//echo $first[1];
}
// finally, put the rest of the args in the list
//array_walk( $params, function(&$a, $i, &$args) { print_r($args); $args[strtolower($a[1])] = $a[0]; }, $args);
foreach ($params as &$param) {
$k = isset($param[0]) ? strtolower($param[0]) : '';
$v = isset($param[1]) ? $param[1] : '';
$args[$k] = $v;
}
return [ 'name' => $name, 'open' => $open, 'args' => $args ];
}
// helper function: normalize HTML entities, with newline handling
static private function encode($input) : string
{
return $input;
// break substring into individual unicode chars
$characters = preg_split('//u', $input, null, PREG_SPLIT_NO_EMPTY);
// append each one-at-a-time to create output
$lf = 0;
$output = '';
foreach ($characters as &$ch)
{
if ($ch === "\n") {
$lf ++;
} elseif ($ch === "\r") {
continue;
} else {
if ($lf === 1) {
$output .= "\n
";
$lf = 0;
} elseif ($lf > 1) {
$output .= "\n\n
";
$lf = 0;
}
if ($ch === '<') {
$output .= '<';
} elseif ($ch === '>') {
$output .= '>';
} elseif ($ch === '&') {
$output .= '&';
} elseif ($ch === "\u{00A0}") {
$output .= ' ';
} else {
$output .= $ch;
}
}
}
// trailing linefeed handle
if ($lf === 1) {
$output .= "\n
";
} elseif ($lf > 1) {
$output .= "\n\n
"; } return $output; } // Renders a BBCode string to HTML, for inclusion into a document. static public function bbcode_to_html($input) : string { global $MSG_TOTAL; global $MSG_NUMBER_OF_PROBLEMS; // split input string into array using regex, UTF-8 aware // this should give us tokens to work with // The regex is: one or more characters within square brackets, // where the characters are any in this list (allowable URI chars): // ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 -._~:/?#@!$&'()*+,;=% // Square brackets are technically allowed, but excluded here, because they interfere. $match_count = preg_match_all("/\[[A-Za-z0-9 \-._~:\/?#@!$&'()*+,;=%]+\]/u", $input, $matches, PREG_OFFSET_CAPTURE); if ($match_count === FALSE) { throw new RuntimeException('Fatal error in preg_match_all for BBCode tags'); } // begin with the empty string $output = ''; $input_ptr = 0; $plist_color = Array('panel-success','panel-info','panel-warning','panel-danger'); global $colorIndex; $stack = []; for ($match_idx = 0; $match_idx < $match_count; $match_idx ++) { list($match, $offset) = $matches[0][$match_idx]; // pick up chars between tags and HTML-encode them $output .= self::encode(substr($input, $input_ptr, $offset - $input_ptr)); // advance input_ptr to just past the current tag $input_ptr = $offset + strlen($match); // decode the tag 7.0 do not supported (16.04) //list('name' => $name, 'open' => $open, 'args' => $args) = self::decode_tag($match); $decode_data= self::decode_tag($match); $name=$decode_data['name']; $open=$decode_data['open']; $args=$decode_data['args']; if (! $open) { // CLOSING TAG // Search the tag stack and see if the opening tag was pushed into it if (array_search($name, $stack, TRUE) === FALSE) { // Attempted to close a tag that was not on the stack! $output = $output . self::encode($match); } else { //pop repeatedly until we pop the tag, and close everything on the way do { $popped_name = array_pop($stack); $output = $output . '' . $popped_name . '>'; } while ($name !== $popped_name); } } else { // OPENING TAG // Big if / elseif ladder to handle each tag if ($name === 'b' || $name === 'u' || $name === 'sup' || $name === 'sub' || $name === 'blockquote' || $name === 'ol' || $name === 'ul' || $name === 'table') { // Simple tags (no validation or alternate modes) $stack[] = $name; $output = $output . '<' . $name . '>'; } elseif ($name === 'li') { // Disallow [li] outside of [ol] or [ul] if (array_search('ol', $stack, TRUE) !== FALSE || array_search('ul', $stack, TRUE) !== FALSE) { $stack[] = 'li'; $output .= '
' . self::encode(substr($input, $input_ptr, $search_offset - $input_ptr)) . ''; // advance ptr (again) $input_ptr = $search_offset + strlen($search_match); // update search position $match_idx = $i; } else { // Unrecognized type! $output .= self::encode($match); } } elseif ($name === 'md') { // markdown handling. : [md]title[/md] . $buffer = null; $i = $match_idx + 1; if ($i < $match_count) { list($search_match, $search_offset) = $matches[0][$i]; $search_tag = self::decode_tag($search_match); if (! $search_tag['open'] && $search_tag['name'] === 'md') { $buffer = substr($input, $input_ptr, $search_offset - $input_ptr); } } // matched something in the middle if (isset($buffer)) { //var_dump($colorIndex); $output = $output . '