Source for file stringparser_bbcode.class.php
Documentation is available at stringparser_bbcode.class.php
* BB code string parsing class
* @author Christian Seiler <spam@christian-seiler.de>
* @copyright Christian Seiler 2006
* Copyright (c) 2004-2007 Christian Seiler
* 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
require_once dirname(__FILE__
).
'/stringparser.class.php';
define ('BBCODE_CLOSETAG_FORBIDDEN', -
1);
define ('BBCODE_CLOSETAG_OPTIONAL', 0);
define ('BBCODE_CLOSETAG_IMPLICIT', 1);
define ('BBCODE_CLOSETAG_IMPLICIT_ON_CLOSE_ONLY', 2);
define ('BBCODE_CLOSETAG_MUSTEXIST', 3);
define ('BBCODE_NEWLINE_PARSE', 0);
define ('BBCODE_NEWLINE_IGNORE', 1);
define ('BBCODE_NEWLINE_DROP', 2);
define ('BBCODE_PARAGRAPH_ALLOW_BREAKUP', 0);
define ('BBCODE_PARAGRAPH_ALLOW_INSIDE', 1);
define ('BBCODE_PARAGRAPH_BLOCK_ELEMENT', 2);
* BB code string parser class
* The BBCode string parser works in search mode
* @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
* The registered BB codes
* Defined maximum occurrences
* Do not output but return the tree
* Global setting: case sensitive
* Root paragraph handling enabled
* Paragraph handling parameters
'detect_string' =>
"\n\n",
* Allow mixed attribute types (e.g. [code=bla attr=blub])
* Whether to call validation function again (with $action == 'validate_auto') when closetag comes
* @param string $name The name of the code
* @param string $callback_type See documentation
* @param string $callback_func The callback function to call
* @param array $callback_params The callback parameters
* @param string $content_type See documentation
* @param array $allowed_within See documentation
* @param array $not_allowed_within See documentation
function addCode ($name, $callback_type, $callback_func, $callback_params, $content_type, $allowed_within, $not_allowed_within) {
if (isset
($this->_codes[$name])) {
return false; // already exists
if (!preg_match ('/^[a-zA-Z0-9*_!+-]+$/', $name)) {
$this->_codes[$name] =
array (
'callback_type' =>
$callback_type,
'callback_func' =>
$callback_func,
'callback_params' =>
$callback_params,
'content_type' =>
$content_type,
'allowed_within' =>
$allowed_within,
'not_allowed_within' =>
$not_allowed_within,
* @param $name The code to remove
if (isset
($this->_codes[$name])) {
* @param string $name The name of the code
* @param string $flag The name of the flag to set
* @param mixed $value The value of the flag to set
if (!isset
($this->_codes[$name])) {
$this->_codes[$name]['flags'][$flag] =
$value;
* $bbcode->setOccurrenceType ('url', 'link');
* $bbcode->setMaxOccurrences ('link', 4);
* Would create the situation where a link may only occur four
* times in the hole text.
* @param string $code The name of the code
* @param string $type The name of the occurrence type to set
return $this->setCodeFlag ($code, 'occurrence_type', $type);
* Set maximum number of occurrences
* @param string $type The name of the occurrence type
* @param int $count The maximum number of occurrences
if ($count <
0) { // sorry, does not make any sense
* @param string $type The content type for which the parser is to add
* @param mixed $parser The function to call
* @param string $content_type The new root content type
* Set paragraph handling on root element
* @param bool $enabled The new status of paragraph handling on root element
* Set paragraph handling parameters
* @param string $detect_string The string to detect
* @param string $start_tag The replacement for the start tag (e.g. <p>)
* @param string $end_tag The replacement for the start tag (e.g. </p>)
'detect_string' =>
$detect_string,
'start_tag' =>
$start_tag,
* Set global case sensitive flag
* If this is set to true, the class normally is case sensitive, but
* the case_sensitive code flag may override this for a single code.
* If this is set to false, all codes are case insensitive.
* @param bool $caseSensitive
* Get global case sensitive flag
* Set mixed attribute types flag
* If set, [code=val1 attr=val2] will cause 2 attributes to be parsed:
* 'default' will have value 'val1', 'attr' will have value 'val2'.
* If not set, only one attribute 'default' will have the value
* 'val1 attr=val2' (the default and original behaviour)
* @param bool $mixedAttributeTypes
* Get mixed attribute types flag
* Set validate again flag
* If this is set to true, the class calls the validation function
* again with $action == 'validate_again' when closetag comes.
* @param bool $validateAgain
* Get validate again flag
* @param string $name The name of the code
* @param string $flag The name of the flag to get
* @param string $type The type of the return value
* @param mixed $default The default return value
function getCodeFlag ($name, $flag, $type =
'mixed', $default =
null) {
if (!isset
($this->_codes[$name])) {
$return =
$this->_codes[$name]['flags'][$flag];
$this->_charactersSearch =
array (']', ' = "', '="', ' = \'', '=\'', ' = ', '=', ': ', ':', ' ');
if ($this->_quoting !==
null) {
$this->_charactersSearch =
array ('\\\\', '\\'.
$this->_quoting, $this->_quoting.
' ', $this->_quoting.
']', $this->_quoting);
$this->_charactersSearch =
array ('\\\\', '\\'.
$this->_quoting, $this->_quoting.
']', $this->_quoting);
if ($this->_quoting !==
null) {
$this->_charactersSearch =
array ('\\\\', '\\'.
$this->_quoting, $this->_quoting.
' ', $this->_quoting.
']', $this->_quoting);
* Abstract method Append text depending on current status
* @param string $text The text to append
* @return bool On success, the function returns true, else false
return $this->_topNode ('appendToName', $text);
$this->_savedName .=
$text;
return $this->_topNode ('appendToAttribute', 'default', $text);
$this->_savedValue .=
$text;
* Restart parsing after current block
* To achieve this the current top stack object is removed from the
* tree. Then the current item
// this status will *never* call _reparseAfterCurrentBlock itself
// so this is called if the loop ends
// therefore, just add the [/ to the text
// _savedName should be empty but just in case
foreach ($this->_parsers[$type] as $parser) {
* @param int $status The current status
* @param string $needle The needle that was found
if ($needle !=
'[' &&
$needle !=
'[/') {
} else if ($needle ==
'[/') {
} else if (trim ($needle) ==
':' ||
trim ($needle) ==
'=') {
} else if (trim ($needle) ==
'="' ||
trim ($needle) ==
'= "' ||
trim ($needle) ==
'=\'' ||
trim ($needle) ==
'= \'') {
$this->_setStatus (3); // default value parser with quotation
} else if ($needle ==
' ') {
// break not necessary because every if clause contains return
if (!$this->_isCloseable ($this->_savedName, $closecount)) {
// this validates the code(s) to be closed after the content tree of
// that code(s) are built - if the second validation fails, we will have
// to reparse. note that as _reparseAfterCurrentBlock will not work correctly
// if we're in $status == 2, we will have to set our status to 0 manually
for ($i =
0; $i <
$closecount; $i++
) {
if ($i ==
$closecount -
1) {
case 3:
// DEFAULT ATTRIBUTE
if ($this->_quoting !==
null) {
} else if ($needle ==
'\\'.
$this->_quoting) {
} else if ($needle ==
$this->_quoting.
' ') {
} else if ($needle ==
$this->_quoting.
']') {
} else if ($needle ==
$this->_quoting) {
// can't be, only ']' and ' ' allowed after quoting char
} else if ($needle ==
']') {
// break not needed because every if clause contains return!
case 4:
// ATTRIBUTE NAME
if (strlen ($this->_savedName)) {
$this->_topNode ('setAttribute', $this->_savedName, true);
// just ignore and continue in same mode
} else if ($needle ==
']') {
if (strlen ($this->_savedName)) {
$this->_topNode ('setAttribute', $this->_savedName, true);
} else if ($needle ==
'=') {
} else if ($needle ==
'="') {
} else if ($needle ==
'=\'') {
// break not needed because every if clause contains return!
case 5:
// ATTRIBUTE VALUE
if ($this->_quoting !==
null) {
} else if ($needle ==
'\\'.
$this->_quoting) {
} else if ($needle ==
$this->_quoting.
' ') {
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
} else if ($needle ==
$this->_quoting.
']') {
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
} else if ($needle ==
$this->_quoting) {
// can't be, only ']' and ' ' allowed after quoting char
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
} else if ($needle ==
']') {
$this->_topNode ('setAttribute', $this->_savedName, $this->_savedValue);
// break not needed because every if clause contains return!
// this was case insensitive match
$closecount =
$this->_savedCloseCount;
// do we have to close subnodes?
for ($i =
0; $i <=
$closecount; $i++
) {
$occ_type =
$this->getCodeFlag ($name, 'occurrence_type', 'string');
$occs =
$this->_root->getNodeCountByCriterium ('flag:occurrence_type', $occ_type);
if ($occs >=
$max_occs) {
$this->_savedCloseCount =
$closecount;
// do we have to close subnodes?
for ($i =
0; $i <=
$closecount; $i++
) {
if ($this->_codes[$name]['callback_type'] ==
'simple_replace_single' ||
$this->_codes[$name]['callback_type'] ==
'callback_replace_single') {
for ($i =
$scount -
1; $i >
0; $i--
) {
if ($this->_stack[$i]->equals ($node)) {
* Revalidate codes when close tags appear
for ($i =
$scount -
1; $i >=
$scount -
$closecount; $i--
) {
if (!$this->_stack[$i]->validate ('validate_again')) {
if (!isset
($this->_codes[$name])) {
$allowed_within =
$this->_codes[$name]['allowed_within'];
$not_allowed_within =
$this->_codes[$name]['not_allowed_within'];
if ($scount ==
2) { // top level element
if (!in_array ($this->_stack[$scount-
2]->_codeInfo['content_type'], $allowed_within)) {
for ($i =
1; $i <
$scount -
1; $i++
) {
if (in_array ($this->_stack[$i]->_codeInfo['content_type'], $not_allowed_within)) {
* Is a node openable by closing other nodes?
for ($i =
$scount -
2; $i >
0; $i--
) {
if ($this->_stack[$i]->equals ($node)) {
if (!$this->_stack[$i]->validate ('validate_again')) {
* Abstract method: Close remaining blocks
* Find a node with a specific name in stack
for ($i =
$scount -
1; $i >
0; $i--
) {
$cmp_name =
$this->_stack[$i]->name ();
if ($cmp_name ==
$name) {
* Abstract method: Output tree
$ccount =
count ($node->_children);
for ($i =
0; $i <
$ccount; $i++
) {
$suboutput =
$this->_outputNode ($node->_children[$i]);
return $node->getReplacement ($output);
$output =
$node->content;
if ($ol &&
$output{0} ==
"\n") {
if ($ol &&
$output{0} ==
"\n") {
$output =
substr ($output, 1);
if ($ol &&
$output{$ol-
1} ==
"\n") {
if ($ol &&
$output{$ol-
1} ==
"\n") {
$output =
substr ($output, 0, -
1);
if ($node->_parent ===
null) {
return $before.
$output.
$after;
$parent =
& $node->_parent;
// if no parent for this paragraph
if ($node->_parent ===
null) {
return $before.
$output.
$after;
return $before.
$this->_applyParsers ($node->_parent->_codeInfo['content_type'], $output).
$after;
return $before.
$output.
$after;
* Abstract method: Manipulate the tree
// first pass: try to do newline handling
$nodes =
& $this->_root->getNodesByCriterium ('needsTextNodeModification', true);
$nodes_count =
count ($nodes);
for ($i =
0; $i <
$nodes_count; $i++
) {
$n =
& $nodes[$i]->findPrevAdjentTextNode ();
$n->setFlag ('newlinemode.end', $v);
$n =
& $nodes[$i]->firstChildIfText ();
$n->setFlag ('newlinemode.begin', $v);
$n =
& $nodes[$i]->lastChildIfText ();
$n->setFlag ('newlinemode.end', $v);
$n =
& $nodes[$i]->findNextAdjentTextNode ();
$n->setFlag ('newlinemode.begin', $v);
// second pass a: do paragraph handling on root element
// second pass b: do paragraph handling on other elements
$nodes =
& $this->_root->getNodesByCriterium ('flag:paragraphs', true);
$nodes_count =
count ($nodes);
for ($i =
0; $i <
$nodes_count; $i++
) {
// second pass c: search for empty paragraph nodes and remove them
$nodes =
& $this->_root->getNodesByCriterium ('empty', true);
$nodes_count =
count ($nodes);
unset
($parent); $parent =
null;
for ($i =
0; $i <
$nodes_count; $i++
) {
$parent =
& $nodes[$i]->_parent;
$parent->removeChild ($nodes[$i], true);
* @param object $node The node to handle
// if this node is already a subnode of a paragraph node, do NOT
// do paragraph handling on this node!
$last_node_was_paragraph =
false;
while (count ($node->_children)) {
$mynode =
& $node->_children[0];
$node->removeChild ($mynode);
$subprevtype =
$prevtype;
for ($i =
0; $i <
count ($sub_nodes); $i++
) {
$prevtype =
$sub_nodes[$i]->_type;
$paragraph->appendChild ($sub_nodes[$i]);
$dest_nodes[] =
& $paragraph;
$last_node_was_paragraph =
true;
$dest_nodes[] =
& $sub_nodes[$i];
$last_onde_was_paragraph =
false;
$count =
count ($dest_nodes);
for ($i =
0; $i <
$count; $i++
) {
$node->appendChild ($dest_nodes[$i]);
* Search for a paragraph node in tree in upward direction
* @param object $node The node to analyze
if ($node->_parent ===
null) {
$parent =
& $node->_parent;
* @param object $node The node to break up
// text node => no problem
while (($npos =
strpos ($node->content, $detect_string, $cpos)) !==
false) {
foreach ($node->_flags as $flag =>
$value) {
if ($flag ==
'newlinemode.begin') {
$subnode->setFlag ($flag, $value);
} else if ($flag ==
'newlinemode.end') {
$subnode->setFlag ($flag, $value);
$dest_nodes[] =
& $subnode;
$cpos =
$npos +
strlen ($detect_string);
$value =
$node->getFlag ('newlinemode.begin', 'integer', null);
$subnode->setFlag ('newlinemode.begin', $value);
$value =
$node->getFlag ('newlinemode.end', 'integer', null);
$subnode->setFlag ('newlinemode.end', $value);
$dest_nodes[] =
& $subnode;
// not a text node or an element node => no way
$dest_node =
& $node->duplicate ();
$nodecount =
count ($node->_children);
// now this node allows breakup - do it
for ($i =
0; $i <
$nodecount; $i++
) {
$firstnode =
& $node->_children[0];
$node->removeChild ($firstnode);
for ($j =
0; $j <
count ($sub_nodes); $j++
) {
$dest_nodes[] =
& $dest_node;
$dest_node =
& $node->duplicate ();
$dest_node->appendChild ($sub_nodes[$j]);
$dest_nodes[] =
& $dest_node;
* Is this node a usecontent node
* @param object $node The node to check
* @param bool $check_attrs Also check whether 'usecontent?'-attributes exist
// this should NOT happen
if ($this->_codes[$name]['callback_type'] ==
'usecontent') {
if ($this->_codes[$name]['callback_type'] ==
'callback_replace?') {
} else if ($this->_codes[$name]['callback_type'] !=
'usecontent?') {
if ($check_attrs ===
false) {
$p =
@$this->_codes[$name]['callback_params']['usecontent_param'];
* Get canonical name of a code
if (isset
($this->codes[$name])) {
// try to find the code in the code list
* Node type: BBCode Element node
* @see StringParser_BBCode_Node_Element::_type
define ('STRINGPARSER_BBCODE_NODE_ELEMENT', 32);
* Node type: BBCode Paragraph node
* @see StringParser_BBCode_Node_Paragraph::_type
define ('STRINGPARSER_BBCODE_NODE_PARAGRAPH', 33);
* BBCode String parser paragraph node class
* This node is a bbcode paragraph node.
* @see STRINGPARSER_BBCODE_NODE_PARAGRAPH
var $_type =
STRINGPARSER_BBCODE_NODE_PARAGRAPH;
* Determines whether a criterium matches this node
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return bool True if this node matches that criterium
if ($criterium ==
'empty') {
$content =
substr ($content, 1);
$content =
substr ($content, 0, -
1);
* BBCode String parser element node class
* This node is a bbcode element node.
* @see STRINGPARSER_BBCODE_NODE_ELEMENT
var $_type =
STRINGPARSER_BBCODE_NODE_ELEMENT;
* @see StringParser_BBCode_Node_Element::name
* @see StringParser_BBCode_Node_Element::setName
* @see StringParser_BBCode_Node_Element::appendToName
* Was processed by paragraph handling
//////////////////////////////////////////////////
* Duplicate this node (but without children / parents)
$newnode->_name =
$this->_name;
$newnode->_flags =
$this->_flags;
$newnode->_codeInfo =
$this->_codeInfo;
* Retreive name of this element
* Set name of this element
* @param string $name The new name of the element
* Append to name of this element
* @param string $chars The chars to append to the name of the element
* Append to attribute of this element
* @param string $name The name of the attribute
* @param string $chars The chars to append to the attribute of the element
* @param string $name The name of the attribute
* @param string $value The new value of the attribute
* @param array $info The code info array
$this->_codeInfo =
$info;
$this->_flags =
$info['flags'];
* @param string $name The name of the attribute
* Set flag that this element had a close tag
* Set flag that this element was already processed by paragraph handling
* Get flag if this element was already processed by paragraph handling
* Get flag if this element had a close tag
* Determines whether a criterium matches this node
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return bool True if this node matches that criterium
if ($criterium ==
'tagName') {
return ($value ==
$this->_name);
if ($criterium ==
'needsTextNodeModification') {
if (substr ($criterium, 0, 5) ==
'flag:') {
$criterium =
substr ($criterium, 5);
return ($this->getFlag ($criterium) ==
$value);
if (substr ($criterium, 0, 6) ==
'!flag:') {
$criterium =
substr ($criterium, 6);
return ($this->getFlag ($criterium) !=
$value);
if (substr ($criterium, 0, 6) ==
'flag=:') {
$criterium =
substr ($criterium, 6);
return ($this->getFlag ($criterium) ===
$value);
if (substr ($criterium, 0, 7) ==
'!flag=:') {
$criterium =
substr ($criterium, 7);
return ($this->getFlag ($criterium) !==
$value);
* Get first child if it is a text node
// DON'T DO $ret = null WITHOUT unset BEFORE!
// ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
* Get last child if it is a text node AND if this element had a close tag
// DON'T DO $ret = null WITHOUT unset BEFORE!
// ELSE WE WILL ERASE THE NODE ITSELF! EVIL!
$ret2 =
& $ret->_findPrevAdjentTextNodeHelper ();
* Find next adjent text node after close tag
* returns the node or null if none exists
for ($i =
0; $i <
$ccount; $i++
) {
if ($this->_parent->_children[$i]->equals ($this)) {
if ($found <
$ccount -
1) {
return $this->_parent->_children[$found+
1];
$ret =
& $this->_parent->findNextAdjentTextNode ();
* Find previous adjent text node before open tag
* returns the node or null if none exists
for ($i =
0; $i <
$ccount; $i++
) {
if ($this->_parent->_children[$i]->equals ($this)) {
return $this->_parent->_children[$found-
1];
if (!$this->_parent->_children[$found-
1]->hadCloseTag ()) {
$ret =
& $this->_parent->_children[$found-
1]->_findPrevAdjentTextNodeHelper ();
* Helper function for findPrevAdjentTextNode
* Looks at the last child node; if it's a text node, it returns it,
* if the element node did not have an open tag, it calls itself
if (!$lastnode->hadCloseTag ()) {
$ret =
& $lastnode->_findPrevAdjentTextNodeHelper ();
* @param string $flag The requested flag
* @param string $type The requested type of the return value
* @param mixed $default The default return value
function getFlag ($flag, $type =
'mixed', $default =
null) {
if (!isset
($this->_flags[$flag])) {
$return =
$this->_flags[$flag];
* @param string $name The name of the flag
* @param mixed $value The value of the flag
$this->_flags[$name] =
$value;
* @param string $action The action which is to be called ('validate'
* for first validation, 'validate_again' for
* second validation (optional))
function validate ($action =
'validate') {
if ($action !=
'validate' &&
$action !=
'validate_again') {
if ($this->_codeInfo['callback_type'] !=
'simple_replace' &&
$this->_codeInfo['callback_type'] !=
'simple_replace_single') {
if (!is_callable ($this->_codeInfo['callback_func'])) {
if (($this->_codeInfo['callback_type'] ==
'usecontent' ||
$this->_codeInfo['callback_type'] ==
'usecontent?' ||
$this->_codeInfo['callback_type'] ==
'callback_replace?') &&
count ($this->_children) ==
1 &&
$this->_children[0]->_type ==
STRINGPARSER_NODE_TEXT) {
// we have to make sure the object gets passed on as a reference
// if we do call_user_func(..., &$this) this will clash with PHP5
$callArray =
array ($action, $this->_attributes, $this->_children[0]->content, $this->_codeInfo['callback_params']);
// ok, now, if we've got a usecontent type, set a flag that
// this may not be broken up by paragraph handling!
// but PLEASE do NOT change if already set to any other setting
// than BBCODE_PARAGRAPH_ALLOW_BREAKUP because we could
// override e.g. BBCODE_PARAGRAPH_BLOCK_ELEMENT!
// we have to make sure the object gets passed on as a reference
// if we do call_user_func(..., &$this) this will clash with PHP5
$callArray =
array ($action, $this->_attributes, null, $this->_codeInfo['callback_params']);
* Get replacement for this code
* @param string $subcontent The content of all sub-nodes
if ($this->_codeInfo['callback_type'] ==
'simple_replace' ||
$this->_codeInfo['callback_type'] ==
'simple_replace_single') {
if ($this->_codeInfo['callback_type'] ==
'simple_replace_single') {
if (strlen ($subcontent)) { // can't be!
return $this->_codeInfo['callback_params']['start_tag'];
return $this->_codeInfo['callback_params']['start_tag'].
$subcontent.
$this->_codeInfo['callback_params']['end_tag'];
// else usecontent, usecontent? or callback_replace or callback_replace_single
// => call function (the function is callable, determined in validate()!)
// we have to make sure the object gets passed on as a reference
// if we do call_user_func(..., &$this) this will clash with PHP5
$callArray =
array ('output', $this->_attributes, $subcontent, $this->_codeInfo['callback_params']);
* Dump this node to a string
foreach ($attribs as $attrib) {
Documentation generated on Mon, 10 Dec 2007 13:29:48 +0100 by phpDocumentor 1.4.0