tags (etc), or as processTextBlock() to // be printed straight into a
. echo $cmsText->processTextLine($text); echo $cmsText->processTextBlock($text); //-------------------------------------------------- // End of example setup ***************************************************/ class cmsText { var $preservedInlineTags; var $indentLevel; var $config; function __construct($config = NULL) { $this->preservedInlineTags = array(); $this->indentLevel = 1; $this->setConfig($config); } function setConfig($config) { //-------------------------------------------------- // If this class was not initialised with a config // array parameter. if ($config === NULL) { $config = array(); } //-------------------------------------------------- // The config setup is an array $this->config = array(); //-------------------------------------------------- // Boolean (permission) values - default to false $this->config['allowHtmlCode'] = (isset($config['allowHtmlCode']) && $config['allowHtmlCode'] === true); $this->config['allowPopupLinks'] = (isset($config['allowPopupLinks']) && $config['allowPopupLinks'] === true); $this->config['allowMailLinks'] = (isset($config['allowMailLinks']) && $config['allowMailLinks'] === true); $this->config['allowImgTags'] = (isset($config['allowImgTags']) && $config['allowImgTags'] === true); $this->config['allowParaAlign'] = (isset($config['allowParaAlign']) && $config['allowParaAlign'] === true); $this->config['allowListTags'] = (isset($config['allowListTags']) && $config['allowListTags'] === true); $this->config['allowTableTags'] = (isset($config['allowTableTags']) && $config['allowTableTags'] === true); $this->config['allowHeadingTags'] = (isset($config['allowHeadingTags']) && $config['allowHeadingTags'] === true); $this->config['noFollowLinks'] = (isset($config['noFollowLinks']) && $config['noFollowLinks'] === true); $this->config['hideCmsComments'] = (isset($config['hideCmsComments']) && $config['hideCmsComments'] === true); $this->config['plainTextMailLinks'] = (isset($config['plainTextMailLinks']) && $config['plainTextMailLinks'] === true); //-------------------------------------------------- // General config if (isset($config['headingLevel'])) { $this->config['headingLevel'] = $config['headingLevel']; // Valid number checked later } else { $this->config['headingLevel'] = NULL; } } function changeConfig($key, $value) { if ($key == 'headingLevel') { $this->config['headingLevel'] = $value; } else if (isset($this->config[$key])) { $this->config[$key] = ($value === true); } } function processTextLine($string, $preserveOpenTags = false, $childOfTag = NULL) { // Do not set 'childOfTag', its considered PRIVATE //-------------------------------------------------- // Make the string HTML safe and remove whitespace // at the end. // NOTE: If this is a child of a tag, // then the content would have already // been html encoded. if ($childOfTag === NULL) { $string = rtrim(html($string)); } //-------------------------------------------------- // Defaults $inTagText = NULL; $inTagAttribute = NULL; $escapingStack = 0; $tagStack = array(); $outputHtml = ''; $inlineTags = array(); $inlineTags['b']['open'] = ''; $inlineTags['b']['close'] = ''; $inlineTags['i']['open'] = ''; $inlineTags['i']['close'] = ''; //-------------------------------------------------- // If any tags were preserved from the last run, // then re-open them if ($preserveOpenTags === true) { while ($tag = array_pop($this->preservedInlineTags)) { $tagStack[] = $tag; $outputHtml .= $inlineTags[$tag]['open']; } } //-------------------------------------------------- // Loop though each of the characters in the string $stringLength = strlen($string); for ($i = 0; $i < $stringLength; $i++) { $char = $string[$i]; if ($char == '[' && $inTagAttribute === NULL) { //-------------------------------------------------- // If we are already collecting data for a tag, which // has not finished yet, then then its not really a // tag... the user has just used "[" if ($inTagText !== NULL) { $tmp = str_repeat('\\', $escapingStack) . '[' . $inTagText; $result = preg_match('/^(.*?)(\\\\*)$/', $tmp, $matches); if ($result) { $outputHtml .= $matches[1]; $escapingStack = strlen($matches[2]); // Slashes before the new "[" } else { $outputHtml .= $tmp; $escapingStack = 0; } } //-------------------------------------------------- // Remember we are now in tag mode (not NULL) $inTagText = ''; } else if ($inTagText !== NULL) { if ($char == ']' && $inTagAttribute === NULL) { //-------------------------------------------------- // The tag text has been collected, so now extract // the tag parts $result = preg_match('/^(\/?)([^ ]*)( +.*)?$/', $inTagText, $matches); if ($result) { $tagTypeOpener = ($matches[1] == ''); $tagNameLower = strtolower($matches[2]); $tagAttributes = (isset($matches[3]) ? $matches[3] : ''); } else { $tagTypeOpener = false; $tagNameLower = ''; $tagAttributes = ''; } //-------------------------------------------------- // Process the tag (if ness) by setting the // $tagOutput to something other than NULL $tagOutput = NULL; $tagEscaped = (($escapingStack % 2) != 0); $tagValid = isset($inlineTags[$tagNameLower]); if ($tagValid && $tagAttributes != '') { $tagValid = false; // Inline tags arn't allowed attributes } if ($tagValid && $tagEscaped != true) { if ($tagTypeOpener) { //-------------------------------------------------- // This this is an opening tag, if it is not already // open AND this is not at the end of the string, // then open it now... otherwise we cannot open it // again, so silently ignore it. if (!in_array($tagNameLower, $tagStack) && (($i + 1) < $stringLength)) { $tagOutput = $inlineTags[$tagNameLower]['open']; $tagStack[] = $tagNameLower; } else { $tagOutput = ''; } } else if (!in_array($tagNameLower, $tagStack)) { //-------------------------------------------------- // This is a CLOSING tag that is NOT open, so // silently ignore it. $tagOutput = ''; } else { //-------------------------------------------------- // This is a CLOSING tag that is already open $tagOutput = ''; $tempStack = array(); //-------------------------------------------------- // Close all of the tags which are currently open, // until we find the one the user has requested that // we close (XML formatting). $processing = true; while ($processing) { $tag = array_pop($tagStack); $tagOutput .= $inlineTags[$tag]['close']; if ($tag == $tagNameLower) { $processing = false; } else { $tempStack[] = $tag; } } //-------------------------------------------------- // Reopen any of the tags which were closed in order // to meet the users requested tag close. while ($tag = array_pop($tempStack)) { $tagStack[] = $tag; $tagOutput .= $inlineTags[$tag]['open']; } } } else { //-------------------------------------------------- // This might be an inline element, so extract // the attributes $tagAttrib1 = ''; $tagAttrib2 = ''; $tagAttrib3 = ''; $found = preg_match('/^ +("|')(.*?)\1(?: +\1(.*?)\1)?(?: +\1(.*?)\1)?$/', $tagAttributes, $matches); if ($found) { //-------------------------------------------------- // Attribute 1 $tagAttrib1 = $matches[2]; //-------------------------------------------------- // Attribute 2 // NOTE: Due to inTagAttribute, a child tag might // appear within this attribute. However, we cannot // preserveOpenTags, as we would need to re-open // them blindly, potentially creating XML errors. if (isset($matches[3])) { $tagAttrib2 = $matches[3]; if ($tagNameLower == 'link' || $tagNameLower == 'open' || $tagNameLower == 'mail') { $tagAttrib2 = $this->processTextLine($tagAttrib2, false, true); } } //-------------------------------------------------- // Attribute 3 if (isset($matches[4])) { $tagAttrib3 = $matches[4]; } } //-------------------------------------------------- // If this is a recognised inline element, replace // it with the relevant HTML // NOTE: If we are processing some text which is // a child of a tag, then it should be a child // of a link... and as such, we don't to allow // and of the following link tags. if ($tagAttrib1 != '') { if ($tagAttrib3 != '') { $classHtml = ' class="' . $tagAttrib3 . '"'; } else { $classHtml = ''; } if ($tagNameLower == 'link' && $childOfTag === NULL) { $tagValid = true; if ($tagEscaped != true) { if ($tagAttrib2 == '') { $tagOutput = 'config['noFollowLinks'] ? ' rel="nofollow"' : '') . '>' . $tagAttrib1 . ''; } else { $tagOutput = 'config['noFollowLinks'] ? ' rel="nofollow"' : '') . '>' . $tagAttrib2 . ''; } } } else if ($tagNameLower == 'open' && $this->config['allowPopupLinks'] && $childOfTag === NULL) { $tagValid = true; if ($tagEscaped != true) { if ($tagAttrib2 == '') { $tagOutput = 'config['noFollowLinks'] ? ' rel="nofollow"' : '') . '>' . $tagAttrib1 . ''; } else { $tagOutput = 'config['noFollowLinks'] ? ' rel="nofollow"' : '') . '>' . $tagAttrib2 . ''; } } } else if ($tagNameLower == 'mail' && $this->config['allowMailLinks'] && $childOfTag === NULL) { $tagValid = true; if ($tagEscaped != true) { if ($this->config['plainTextMailLinks']) { if ($tagAttrib2 == '') { $tagOutput = '' . $tagAttrib1 . ''; } else { $tagOutput = '' . $tagAttrib2 . ''; } } else { if ($tagAttrib2 == '') { $tagOutput = '' . $tagAttrib1 . ''; } else { $tagOutput = '' . $tagAttrib2 . ''; } } } } else if ($tagNameLower == 'img' && $this->config['allowImgTags']) { $tagValid = true; if ($tagEscaped != true) { if ($tagAttrib2 == '' && $tagAttrib3 == '') { $tagOutput = ''; } else if ($tagAttrib3 == '') { $tagOutput = '' . $tagAttrib2 . ''; } else { $tagOutput = '' . $tagAttrib2 . ''; } } } } else if ($tagNameLower == 'hr') { $tagValid = true; $tagOutput = '
'; } } //-------------------------------------------------- // If the tag is invalid, or was escaped, return // its text value to the $outputHtml if ($tagValid) { $escapingStack = floor($escapingStack / 2); } if ($tagOutput === NULL) { $tagOutput = '[' . $inTagText . ']'; // Invalid tag, or escaped tag } $outputHtml .= str_repeat('\\', $escapingStack) . $tagOutput; //-------------------------------------------------- // Reset the tag tracking variables $inTagText = NULL; $escapingStack = 0; } else { //-------------------------------------------------- // The tag has not finished yet, so keep stacking // its contents up. $inTagText .= $char; //-------------------------------------------------- // Detect if we are in an attribute $marker = substr($inTagText, -6); if ($marker == '"') $marker = '"'; if ($marker == ''') $marker = "'"; if ($inTagAttribute === NULL) { if ($marker == '"' || $marker == "'") { $inTagAttribute = $marker; } } else { if ($inTagAttribute == $marker) { $inTagAttribute = NULL; } } } } else if ($char == '\\') { //-------------------------------------------------- // Add to the escaping string stack... these should // be added to the $outputHtml later $escapingStack++; } else { //-------------------------------------------------- // We are looking at an ordinary character which // is not part of a tag, so add the relevant number // of escaping characters which have been "ignored" // just incase we could hit a tag. if ($escapingStack > 0) { $outputHtml .= str_repeat('\\', $escapingStack); $escapingStack = 0; } //-------------------------------------------------- // Store the character into the output buffer, // although if a new line is being generated, use // a simple
... if its a double newline that // should have been handled by blockLevelTagClose() if ($char == "\n") { if ($i > 0 && ($i + 1) < $stringLength) { // Don't process the first and last "\n" $outputHtml .= "
\n"; } } else { $outputHtml .= $char; } } } //-------------------------------------------------- // If requested, remember the tags which were left // open (about to be closed) so they can be reopened // next time this function is called. $this->preservedInlineTags = $tagStack; //-------------------------------------------------- // If there is any text remaining in the tag buffer, // dump it into $outputHtml if ($escapingStack > 0) { $outputHtml .= str_repeat('\\', $escapingStack); } if ($inTagText !== NULL) { $outputHtml .= '[' . $inTagText; } while ($tag = array_pop($tagStack)) { $outputHtml .= $inlineTags[$tag]['close']; } //-------------------------------------------------- // Post process - remove any tags which have no // effect on the output (surround white-space). Not // ness, but it keeps the output cleaner and stops // upsetting a few code checkers $reRun = 0; $reRunMax = count($inlineTags); while ($reRun++ < $reRunMax) { foreach ($inlineTags as $tagInfo) { $regExpOpen = preg_quote($tagInfo['open'], '/'); $regExpClose = preg_quote($tagInfo['close'], '/'); $outputHtml = preg_replace('/' . $regExpOpen . '(\s*)' . $regExpClose . '/i', '$1', $outputHtml, -1); $outputHtml = preg_replace('/' . $regExpClose . '(\s*)' . $regExpOpen . '/i', '$1', $outputHtml, -1); } } //-------------------------------------------------- // Encode the whitespace characters so browsers // will render them $outputHtml = str_replace(' ', '  ', $outputHtml); // 2 spaces $outputHtml = str_replace(chr(9), '    ', $outputHtml); //-------------------------------------------------- // Set the indent level - again, for code checkers $outputHtml = str_replace("\n", "\n" . str_repeat("\t", $this->indentLevel), $outputHtml); //-------------------------------------------------- // Return the output return $outputHtml; } function processTextBlock($string, $preserveOpenTags = false) { //-------------------------------------------------- // Ensure were using use the proper UNIX "\n" $string = str_replace("\r\n", "\n", $string); //-------------------------------------------------- // A new block, so we should not care about the // previous indent level $this->indentLevel = 1; //-------------------------------------------------- // Defaults $inTagText = NULL; $escapingStack = 0; $currentBlockTag = 'p'; $outputBuffer = ''; $outputHtml = ''; $blockLevelTags = array(); $blockLevelTags[] = 'p'; if ($this->config['allowParaAlign']) { $blockLevelTags[] = 'left'; $blockLevelTags[] = 'center'; $blockLevelTags[] = 'right'; } if ($this->config['allowHeadingTags']) { $blockLevelTags[] = 'h'; $blockLevelTags[] = 'h1'; $blockLevelTags[] = 'h2'; $blockLevelTags[] = 'h3'; $blockLevelTags[] = 'h4'; $blockLevelTags[] = 'h5'; $blockLevelTags[] = 'h6'; } if ($this->config['allowListTags']) { $blockLevelTags[] = 'list'; } if ($this->config['allowTableTags']) { $blockLevelTags[] = 'table'; } if ($this->config['allowHtmlCode']) { $blockLevelTags[] = 'html'; } //-------------------------------------------------- // Loop though each of the characters in the string $stringLength = strlen($string); for ($i = 0; $i < $stringLength; $i++) { $char = $string[$i]; if ($char == '[') { //-------------------------------------------------- // If we are already collecting data for a tag, which // has not finished yet, then then its not really a // tag... the user has just used "[" if ($inTagText !== NULL) { $tmp = str_repeat('\\', $escapingStack) . '[' . $inTagText; $result = preg_match('/^(.*?)(\\\\*)$/', $tmp, $matches); if ($result) { $outputBuffer .= $matches[1]; $escapingStack = strlen($matches[2]); // Slashes before the new "[" } else { $outputBuffer .= $tmp; $escapingStack = 0; } } //-------------------------------------------------- // Remember we are now in tag mode (not NULL) $inTagText = ''; } else if ($inTagText !== NULL) { if ($char == ']') { //-------------------------------------------------- // Extract the tag parts $result = preg_match('/^(\/?)([^ ]*)$/', $inTagText, $matches); if ($result) { $tagTypeOpener = ($matches[1] == ''); $tagNameLower = strtolower($matches[2]); } else { $tagTypeOpener = false; $tagNameLower = ''; } //-------------------------------------------------- // Default processing variables $tagOutput = NULL; $tagIgnored = false; if (in_array($tagNameLower, $blockLevelTags)) { // If this is a valid tag if (($escapingStack % 2) == 0) { // Tag not escaped, with an odd number of slashes if ($tagTypeOpener == true) { if ($tagNameLower != $currentBlockTag) { $tagOutput .= $this->blockLevelTagClose($currentBlockTag, $outputBuffer, true); $currentBlockTag = $tagNameLower; } else { $tagIgnored = true; // Already open } } else { if ($tagNameLower == $currentBlockTag) { $tagOutput = $this->blockLevelTagClose($currentBlockTag, $outputBuffer, true); $currentBlockTag = 'p'; // Return to default } else { $tagIgnored = true; // Already closed } } } $escapingStack = floor($escapingStack / 2); } //-------------------------------------------------- // If tag output was generated add it to outputHtml, // otherwise dump the (invalid) tag back in to the // outputBuffer if ($tagOutput === NULL) { $outputBuffer .= str_repeat('\\', $escapingStack) . ($tagIgnored ? '' : '[' . $inTagText . ']'); // Invalid tag, or escaped tag } else { $outputHtml .= str_repeat('\\', $escapingStack) . $tagOutput; $outputBuffer = ''; } //-------------------------------------------------- // Reset the tag tracking variables $inTagText = NULL; $escapingStack = 0; } else { //-------------------------------------------------- // The tag has not finished yet, so keep stacking // its contents up. $inTagText .= $char; } } else if ($char == '\\') { //-------------------------------------------------- // Add to the escaping string stack... these should // be added to the $outputBuffer later $escapingStack++; } else { //-------------------------------------------------- // We are looking at an ordinary character which // is not part of a tag, so add the relevant number // of escaping characters which have been "ignored" // just incase we could hit a tag. if ($escapingStack > 0) { $outputBuffer .= str_repeat('\\', $escapingStack); $escapingStack = 0; } //-------------------------------------------------- // Store the character into the output buffer. $outputBuffer .= $char; } } //-------------------------------------------------- // If there is any text remaining in the tag buffer, // dump it into $outputHtml if ($escapingStack > 0) { $outputBuffer .= str_repeat('\\', $escapingStack); } if ($inTagText !== NULL) { $outputBuffer .= '[' . $inTagText; } $outputHtml .= $this->blockLevelTagClose($currentBlockTag, $outputBuffer, $preserveOpenTags); // By default, tags are not preserved //-------------------------------------------------- // Return the output if ($this->config['hideCmsComments']) { return $outputHtml; } else { return "\n" . $outputHtml . "\n\n"; } } function blockLevelTagClose($currentBlockTag, $content, $preserveOpenTags = false) { //-------------------------------------------------- // If the tag is empty, there is no point generating // any html output if (trim($content) == '') { return ''; } //-------------------------------------------------- // Process the relevant tags if ($currentBlockTag == 'p' || $currentBlockTag == 'left' || $currentBlockTag == 'center' || $currentBlockTag == 'right') { //-------------------------------------------------- // Determine text alignment (none by default) if ($currentBlockTag == 'p') { $align = NULL; } else { $align = $currentBlockTag; } //-------------------------------------------------- // If double newlines are used, split into // multiple paragraphs $htmlOutput = ''; $subParas = explode("\n\n", $content); $subParasLength = count($subParas); $k = 1; foreach ($subParas as $subPara) { if (trim($subPara) != '') { $htmlOutput .= "\n" . $this->processParagraph($subPara, ($k == 1 ? $preserveOpenTags : true), $align); } $k++; } //-------------------------------------------------- // Return the output return $htmlOutput; } else if (preg_match('/^h([1-6])?$/', $currentBlockTag, $matches)) { $level = intval($this->config['headingLevel']); if (isset($matches[1])) { $level += (intval($matches[1]) - 1); } if ($level < 1) $level = 1; if ($level > 6) $level = 6; return "\n" . $this->processHeading($content, $preserveOpenTags, $level); } else if ($currentBlockTag == 'list') { return "\n" . $this->processList($content, $preserveOpenTags); } else if ($currentBlockTag == 'table') { return "\n" . $this->processTable($content, $preserveOpenTags); } else if ($currentBlockTag == 'html') { if ($this->config['hideCmsComments']) { return $content; } else { return "\n\n" . $content . "\n"; } } } function processParagraph($content, $preserveOpenTags, $align = NULL) { //-------------------------------------------------- // Process the content, but have it correctly // indented if its multi-lined $itemHtml = $this->processTextLine($content, $preserveOpenTags); if (strpos($itemHtml, "\n") !== false) { $itemHtml = "\n\t" . $itemHtml . "\n"; } //-------------------------------------------------- // Return the output in its wrapper return '' . $itemHtml . '

'; } function processHeading($content, $preserveOpenTags, $level) { //-------------------------------------------------- // Process the content, but have it correctly // indented if its multi-lined $itemHtml = $this->processTextLine($content, $preserveOpenTags); if (strpos($itemHtml, "\n") !== false) { $itemHtml = "\n\t" . $itemHtml . "\n"; } //-------------------------------------------------- // Return the output in its wrapper return '' . $itemHtml . ''; } function processList($content, $preserveOpenTags) { //-------------------------------------------------- // Increase the indent level $this->indentLevel++; //-------------------------------------------------- // Process each item in the list $outputHtml = ''; $items = preg_split('/\n(\*|#)/', $content); foreach ($items as $item) { $item = trim($item); if ($item != '') { $itemHtml = $this->processTextLine($item, $preserveOpenTags); if (strpos($itemHtml, "\n") !== false) { $itemHtml = "\n\t\t" . $itemHtml . "\n\t"; } $outputHtml .= "\n\t" . '
  • ' . $itemHtml . '
  • '; } } //-------------------------------------------------- // Restore the indent level $this->indentLevel--; //-------------------------------------------------- // Return the output in its wrapper if (preg_match('/^\s*#/', $content)) { return '
      ' . $outputHtml . "\n" . '
    '; } else { return ''; } } function processTable($content, $preserveOpenTags) { //-------------------------------------------------- // Split the data into a multi-dimensional array $tableColumns = 1; $tableData = array(); $tableRowCells = array(); $k = 0; $rows = explode("\n", $content); foreach ($rows as $row) { if ($row != '') { $cells = explode('|', $row); $tableData[$k] = $cells; $tableRowCells[$k] = count($cells); if ($tableRowCells[$k] > $tableColumns) { $tableColumns = $tableRowCells[$k]; } $k++; } } //-------------------------------------------------- // Build the html output $k = 0; $inHead = true; $outputHeadHtml = ''; $outputBodyHtml = ''; foreach ($tableData as $cells) { $rowHtml = "\n\t\t"; $i = 0; foreach ($cells as $cellData) { $i++; if ($i == 1 && $inHead) { if (substr($cellData, 0, 1) == '#') { $cellData = substr($cellData, 1); } else { $inHead = false; } } $colSpan = (($tableColumns + 1) - $tableRowCells[$k]); $rowHtml .= "\n\t\t\t" . '<' . ($inHead ? 'th' : 'td') . ($colSpan > 1 && $i == $tableRowCells[$k] ? ' colspan="' . intval($colSpan) . '"' : '') . '>' . $this->processTextLine($cellData, $preserveOpenTags) . ($inHead ? '' : ''); } $rowHtml .= "\n\t\t"; if ($inHead) { $outputHeadHtml .= $rowHtml; } else { $outputBodyHtml .= $rowHtml; } $k++; } //-------------------------------------------------- // Return the output in its wrapper return "\n" . ($outputHeadHtml == '' ? '' : "\t" . $outputHeadHtml . "\n\t\n") . ($outputBodyHtml == '' ? '' : "\t" . $outputBodyHtml . "\n\t\n") . "
    "; } } //-------------------------------------------------- // Copyright (c) 2006, Craig Francis All rights // reserved. // // Redistribution and use in source and binary forms, // with or without modification, are permitted provided // that the following conditions are met: // // * Redistributions of source code must retain the // above copyright notice, this list of // conditions and the following disclaimer. // * Redistributions in binary form must reproduce // the above copyright notice, this list of // conditions and the following disclaimer in the // documentation and/or other materials provided // with the distribution. // * Neither the name of the copyright holder nor the // names of its contributors may be used to endorse // or promote products derived from this software // without specific prior written permission. // // This software is provided by the copyright holders // and contributors "as is" and any express or implied // warranties, including, but not limited to, the // implied warranties of merchantability and fitness // for a particular purpose are disclaimed. In no event // shall the copyright holder or contributors be liable // for any direct, indirect, incidental, special, // exemplary, or consequential damages (including, but // not limited to, procurement of substitute goods or // services; loss of use, data, or profits; or business // interruption) however caused and on any theory of // liability, whether in contract, strict liability, or // tort (including negligence or otherwise) arising in // any way out of the use of this software, even if // advised of the possibility of such damage. //-------------------------------------------------- ?>