BBCode and Rich Text
Valhalla's BBCode function. Automatically parses lists into HTML lists, wraps paragraphs in p tags, and replaces BBCode with HTML.
<?php /* * This class automatically inserts HTML tags to make text clean and pretty. * With this class you can choose to execute any of the functions independently, for example, * if you only want lists, or just BBCode. Note: doParagraphs should be run after doLists. * * Usage: * * $richText = new RichText(); * //Note: important to sanitize *before* adding rich text. * $string = htmlentities($string, ENT_QUOTE, 'utf-8'); * $string = $richText->doBBCode( * $richText->doParagraphs( * $richText->doLists($string) * ), * array('halls-of-valhalla.org') * ); * */ namespace Valhalla\BaseBundle\Classes; class RichText { CONST UL_PATTERN = '[*+-]'; CONST OL_PATTERN = '[0-9]+[.]'; /** * Block-level HTML elements which should not be placed in <p> tags * * @var array */ public static $blockHTMLElements = array( 'div', 'section', 'article', 'table', 'iframe', 'code', 'pre', 'p', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', ); /** * Block-level BBCode elements which should not be placed in <p> tags * * @var array */ public static $blockBBCodeElements = array( 'code', 'quote', 'video', 'h', 'center', ); /** * Replaces BBCode with HTML. * * @param string $string * @param array $domainWhitelist URLs to not mark as nofollow * @return string */ public function doBBCode($string, $domainWhitelist = array()) { $search = array( '@\[b\](.*?)\[/b\]@si', '@\[i\](.*?)\[/i\]@si', '@\[u\](.*?)\[/u\]@si', '@\[h\](.*?)\[/h\]@si', '@\[img\](?:http(s)?://)?(.*?)\[/img\]@si', '@\[url=(?:http(s)?://)?(.*?)\](.*?)\[/url\]@si', '@\[code\](.*?)\[/code\]@si', '@\[video\]http://www\.youtube\.com/watch\?v=(.*?)(&.*)*\[/video\]@si', '@\[highlight\](.*?)\[/highlight]@si', '@\[center\](.*?)\[/center]@si', '@\[quote\](.*?)\[/quote]@si', '@\[sup\](.*?)\[/sup]@si', ); $replace = array( '<b>\\1</b>', '<i>\\1</i>', '<u>\\1</u>', '<h4>\\1</h4>', '<img src="http\\1://\\2" alt="\\2" class="imgTag"/>', '<a href="http\\1://\\2">\\3</a>', '<pre class="codeTag prettyprint">\\1</pre>', '<iframe width="560" height="315" src="http://www.youtube.com/embed/\\1" frameborder="0" allowfullscreen></iframe>', '<font color="red">\\1</font>', '<div align="center">\\1</div>', '<pre class="quote">\\1</pre>', '<sup>\\1</sup>', ); $currentHTML = preg_replace($search , $replace, $string); $currentHTML = preg_replace( '%(<a\s*(?!.*\brel=)[^>]*)(href="https?://)((?!(?:(?:www\.)?' . implode('|(?:www\.)?', $domainWhitelist) . '))[^"]+)"((?!.*\brel=)[^>]*)(?:[^>]*)>%', '$1$2$3"$4 rel="nofollow">', $currentHTML); return $currentHTML; } /** * Automatically turns lists beginning with hyphens, asterisks, or numbers into ul/ol tags. * * @param string $text * @return string */ public function doLists($text){ $finalLines = array(); $text = $this->prepareText($text); $lines = explode("\n", $text); $listLines = array(); $openBlockElements = array(); foreach($lines as $line){ list($listType, $pattern) = (empty($line) or $line[0] <= '-') ? array('ul', self::UL_PATTERN) : array('ol', self::OL_PATTERN); $openBlockElements = array_filter($this->checkForOpenBlockElements($line, $openBlockElements)); $blockElementsContained = !empty($openBlockElements); if($blockElementsContained){ $finalLines = $this->writeListLines($listLines, $finalLines, true); $finalLines[] = $line; $listLines = array(); } elseif (preg_match('/^('.$pattern.'[ ]+)(.*)/', $line, $matches)) { if(empty($listLines) or $listLines[0]['listType'] == $listType){ $listLines[] = array( 'listType' => $listType, 'content' => $matches[2], 'rawContent' => $line, ); } else { //Not a valid list; abort $finalLines = $this->writeListLines($listLines, $finalLines, true); $finalLines[] = $line; $listLines = array(); } } else { //Normal line $finalLines = $this->writeListLines($listLines, $finalLines); $listLines = array(); $finalLines[] = $line; } $openBlockElements = $this->checkForClosingBlockElements($line, $openBlockElements); } $finalLines = $this->writeListLines($listLines, $finalLines); return implode("\n", $finalLines); } /** * Adds the list to the fine array of lines. If $raw=true then just add the plain text to the array. * * @param array $listLines * @param array $finalLines * @param boolean $raw * @return array */ protected function writeListLines(array $listLines, array $finalLines, $raw = false){ if(!empty($listLines)){ if($raw or count($listLines) == 1){ foreach($listLines as $listLine){ $finalLines[] = $listLine['rawContent']; } } else { $finalLines[] = "<{$listLines[0]['listType']} class='bbcodeList'>"; foreach($listLines as $listLine){ $finalLines[] = "<li>{$listLine['content']}</li>"; } $finalLines[] = "</{$listLines[0]['listType']}>"; } } return $finalLines; } /** * Automatically places p tags around paragraphs. * * @param string $text * @return string */ public function doParagraphs($text){ $finalText = array(); $text = $this->prepareBlockElements($text); $paragraphs = explode("\n\n", $text); $openBlockElements = array(); foreach($paragraphs as $paragraph){ $openBlockElements = array_filter($this->checkForOpenBlockElements($paragraph, $openBlockElements)); $blockElementsContained = !empty($openBlockElements); //Don't put any p tags around block elements. if($blockElementsContained) { $finalText[] = $paragraph; } else { $finalText[] = '<p>' . nl2br($paragraph) . '</p>'; } $openBlockElements = $this->checkForClosingBlockElements($paragraph, $openBlockElements); } return implode("\n", $finalText); } /** * Checks if the given text contains any block elements. And returns a tally of open elements. * * @param string $snippet * @param array $openBlockElements * @return array */ public function checkForOpenBlockElements($snippet, $openBlockElements = array()){ foreach(self::$blockHTMLElements as $element){ if(preg_match("/\\<{$element}(\\s.*)?\\>/", $snippet)){ $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] + 1 : 1; } } foreach(self::$blockBBCodeElements as $element){ if(preg_match("/\\[{$element}]/", $snippet)){ $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] + 1 : 1; } } return $openBlockElements; } /** * Checks if the given text contains any closing blcok elements and returns a tally of open elements * * @param string $snippet * @param array $openBlockElements * @return array */ public function checkForClosingBlockElements($snippet, $openBlockElements = array()){ foreach(self::$blockHTMLElements as $element){ if(preg_match("/\\<\\/{$element}>/", $snippet)){ $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] - 1 : 0; } } foreach(self::$blockBBCodeElements as $element){ if(preg_match("/\\[\\/{$element}]/", $snippet)){ $openBlockElements[$element] = (isset($openBlockElements[$element])) ? $openBlockElements[$element] - 1 : 0; } } return $openBlockElements; } /** * Inserts line breaks before and after all block elements. * * @param string $text * @return string */ public function prepareBlockElements($text){ foreach(self::$blockHTMLElements as $element){ $text = preg_replace("/\\<{$element}(\\s.*)?\\>/", "\n\n\\0", $text); $text = preg_replace("/\\<\\/{$element}>/", "\\0\n\n", $text); } foreach(self::$blockBBCodeElements as $element){ $text = preg_replace("/\\[{$element}]/", "\n\n\\0", $text); $text = preg_replace("/\\[\\/{$element}]/", "\\0\n\n", $text); } $text = preg_replace("/\n{2,}/", "\n\n", $text); //no more than two in a row return $text; } /** * Prepares and formats text for modification. * * @param string $text * @return string */ public function prepareText($text){ $text = str_replace("\r\n", "\n", $text); $text = str_replace("\r", "\n", $text); # replace tabs with spaces $text = str_replace("\t", ' ', $text); # remove surrounding line breaks $text = trim($text, "\n"); return $text; } }

This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Download this code in plain text format here