* @copyright Christian Seiler 2004-2008
* @package stringparser
*
* The MIT License
*
* Copyright (c) 2004-2009 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
* THE SOFTWARE.
*/
/**
* String parser mode: Search for the next character
* @see StringParser::_parserMode
*/
define ('STRINGPARSER_MODE_SEARCH', 1);
/**
* String parser mode: Look at each character of the string
* @see StringParser::_parserMode
*/
define ('STRINGPARSER_MODE_LOOP', 2);
/**
* Filter type: Prefilter
* @see StringParser::addFilter, StringParser::_prefilters
*/
define ('STRINGPARSER_FILTER_PRE', 1);
/**
* Filter type: Postfilter
* @see StringParser::addFilter, StringParser::_postfilters
*/
define ('STRINGPARSER_FILTER_POST', 2);
/**
* Generic string parser class
*
* This is an abstract class for any type of string parser.
*
* @package stringparser
*/
class StringParser {
/**
* String parser mode
*
* There are two possible modes: searchmode and loop mode. In loop mode
* every single character is looked at in a loop and it is then decided
* what action to take. This is the most straight-forward approach to
* string parsing but due to the nature of PHP as a scripting language,
* it can also cost performance. In search mode the class posseses a
* list of relevant characters for parsing and uses the
* {@link PHP_MANUAL#strpos strpos} function to search for the next
* relevant character. The search mode will be faster than the loop mode
* in most circumstances but it is also more difficult to implement.
* The subclass that does the string parsing itself will define which
* mode it will implement.
*
* @access protected
* @var int
* @see STRINGPARSER_MODE_SEARCH, STRINGPARSER_MODE_LOOP
*/
var $_parserMode = STRINGPARSER_MODE_SEARCH;
/**
* Raw text
* @access protected
* @var string
*/
var $_text = '';
/**
* Parse stack
* @access protected
* @var array
*/
var $_stack = array ();
/**
* Current position in raw text
* @access protected
* @var integer
*/
var $_cpos = -1;
/**
* Root node
* @access protected
* @var mixed
*/
var $_root = null;
/**
* Length of the text
* @access protected
* @var integer
*/
var $_length = -1;
/**
* Flag if this object is already parsing a text
*
* This flag is to prevent recursive calls to the parse() function that
* would cause very nasty things.
*
* @access protected
* @var boolean
*/
var $_parsing = false;
/**
* Strict mode
*
* Whether to stop parsing if a parse error occurs.
*
* @access public
* @var boolean
*/
var $strict = false;
/**
* Characters or strings to look for
* @access protected
* @var array
*/
var $_charactersSearch = array ();
/**
* Characters currently allowed
*
* Note that this will only be evaluated in loop mode; in search mode
* this would ruin every performance increase. Note that only single
* characters are permitted here, no strings. Please also note that in
* loop mode, {@link StringParser::_charactersSearch _charactersSearch}
* is evaluated before this variable.
*
* If in strict mode, parsing is stopped if a character that is not
* allowed is encountered. If not in strict mode, the character is
* simply ignored.
*
* @access protected
* @var array
*/
var $_charactersAllowed = array ();
/**
* Current parser status
* @access protected
* @var int
*/
var $_status = 0;
/**
* Prefilters
* @access protected
* @var array
*/
var $_prefilters = array ();
/**
* Postfilters
* @access protected
* @var array
*/
var $_postfilters = array ();
/**
* Recently reparsed?
* @access protected
* @var bool
*/
var $_recentlyReparsed = false;
/**
* Constructor
*
* @access public
*/
function StringParser () {
}
/**
* Add a filter
*
* @access public
* @param int $type The type of the filter
* @param mixed $callback The callback to call
* @return bool
* @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
*/
function addFilter ($type, $callback) {
// make sure the function is callable
if (!is_callable ($callback)) {
return false;
}
switch ($type) {
case STRINGPARSER_FILTER_PRE:
$this->_prefilters[] = $callback;
break;
case STRINGPARSER_FILTER_POST:
$this->_postfilters[] = $callback;
break;
default:
return false;
}
return true;
}
/**
* Remove all filters
*
* @access public
* @param int $type The type of the filter or 0 for all
* @return bool
* @see STRINGPARSER_FILTER_PRE, STRINGPARSER_FILTER_POST
*/
function clearFilters ($type = 0) {
switch ($type) {
case 0:
$this->_prefilters = array ();
$this->_postfilters = array ();
break;
case STRINGPARSER_FILTER_PRE:
$this->_prefilters = array ();
break;
case STRINGPARSER_FILTER_POST:
$this->_postfilters = array ();
break;
default:
return false;
}
return true;
}
/**
* This function parses the text
*
* @access public
* @param string $text The text to parse
* @return mixed Either the root object of the tree if no output method
* is defined, the tree reoutput to e.g. a string or false
* if an internal error occured, such as a parse error if
* in strict mode or the object is already parsing a text.
*/
function parse ($text) {
if ($this->_parsing) {
return false;
}
$this->_parsing = true;
$this->_text = $this->_applyPrefilters ($text);
$this->_output = null;
$this->_length = strlen ($this->_text);
$this->_cpos = 0;
unset ($this->_stack);
$this->_stack = array ();
if (is_object ($this->_root)) {
StringParser_Node::destroyNode ($this->_root);
}
unset ($this->_root);
$this->_root =& new StringParser_Node_Root ();
$this->_stack[0] =& $this->_root;
$this->_parserInit ();
$finished = false;
while (!$finished) {
switch ($this->_parserMode) {
case STRINGPARSER_MODE_SEARCH:
$res = $this->_searchLoop ();
if (!$res) {
$this->_parsing = false;
return false;
}
break;
case STRINGPARSER_MODE_LOOP:
$res = $this->_loop ();
if (!$res) {
$this->_parsing = false;
return false;
}
break;
default:
$this->_parsing = false;
return false;
}
$res = $this->_closeRemainingBlocks ();
if (!$res) {
if ($this->strict) {
$this->_parsing = false;
return false;
} else {
$res = $this->_reparseAfterCurrentBlock ();
if (!$res) {
$this->_parsing = false;
return false;
}
continue;
}
}
$finished = true;
}
$res = $this->_modifyTree ();
if (!$res) {
$this->_parsing = false;
return false;
}
$res = $this->_outputTree ();
if (!$res) {
$this->_parsing = false;
return false;
}
if (is_null ($this->_output)) {
$root =& $this->_root;
unset ($this->_root);
$this->_root = null;
while (count ($this->_stack)) {
unset ($this->_stack[count($this->_stack)-1]);
}
$this->_stack = array ();
$this->_parsing = false;
return $root;
}
$res = StringParser_Node::destroyNode ($this->_root);
if (!$res) {
$this->_parsing = false;
return false;
}
unset ($this->_root);
$this->_root = null;
while (count ($this->_stack)) {
unset ($this->_stack[count($this->_stack)-1]);
}
$this->_stack = array ();
$this->_parsing = false;
return $this->_output;
}
/**
* Apply prefilters
*
* It is possible to specify prefilters for the parser to do some
* manipulating of the string beforehand.
*/
function _applyPrefilters ($text) {
foreach ($this->_prefilters as $filter) {
if (is_callable ($filter)) {
$ntext = call_user_func ($filter, $text);
if (is_string ($ntext)) {
$text = $ntext;
}
}
}
return $text;
}
/**
* Apply postfilters
*
* It is possible to specify postfilters for the parser to do some
* manipulating of the string afterwards.
*/
function _applyPostfilters ($text) {
foreach ($this->_postfilters as $filter) {
if (is_callable ($filter)) {
$ntext = call_user_func ($filter, $text);
if (is_string ($ntext)) {
$text = $ntext;
}
}
}
return $text;
}
/**
* Abstract method: Manipulate the tree
* @access protected
* @return bool
*/
function _modifyTree () {
return true;
}
/**
* Abstract method: Output tree
* @access protected
* @return bool
*/
function _outputTree () {
// this could e.g. call _applyPostfilters
return true;
}
/**
* Restart parsing after current block
*
* To achieve this the current top stack object is removed from the
* tree. Then the current item
*
* @access protected
* @return bool
*/
function _reparseAfterCurrentBlock () {
// this should definitely not happen!
if (($stack_count = count ($this->_stack)) < 2) {
return false;
}
$topelem =& $this->_stack[$stack_count-1];
$node_parent =& $topelem->_parent;
// remove the child from the tree
$res = $node_parent->removeChild ($topelem, false);
if (!$res) {
return false;
}
$res = $this->_popNode ();
if (!$res) {
return false;
}
// now try to get the position of the object
if ($topelem->occurredAt < 0) {
return false;
}
// HACK: could it be necessary to set a different status
// if yes, how should this be achieved? Another member of
// StringParser_Node?
$this->_setStatus (0);
$res = $this->_appendText ($this->_text{$topelem->occurredAt});
if (!$res) {
return false;
}
$this->_cpos = $topelem->occurredAt + 1;
$this->_recentlyReparsed = true;
return true;
}
/**
* Abstract method: Close remaining blocks
* @access protected
*/
function _closeRemainingBlocks () {
// everything closed
if (count ($this->_stack) == 1) {
return true;
}
// not everything closed
if ($this->strict) {
return false;
}
while (count ($this->_stack) > 1) {
$res = $this->_popNode ();
if (!$res) {
return false;
}
}
return true;
}
/**
* Abstract method: Initialize the parser
* @access protected
*/
function _parserInit () {
$this->_setStatus (0);
}
/**
* Abstract method: Set a specific status
* @access protected
*/
function _setStatus ($status) {
if ($status != 0) {
return false;
}
$this->_charactersSearch = array ();
$this->_charactersAllowed = array ();
$this->_status = $status;
return true;
}
/**
* Abstract method: Handle status
* @access protected
* @param int $status The current status
* @param string $needle The needle that was found
* @return bool
*/
function _handleStatus ($status, $needle) {
$this->_appendText ($needle);
$this->_cpos += strlen ($needle);
return true;
}
/**
* Search mode loop
* @access protected
* @return bool
*/
function _searchLoop () {
$i = 0;
while (1) {
// make sure this is false!
$this->_recentlyReparsed = false;
list ($needle, $offset) = $this->_strpos ($this->_charactersSearch, $this->_cpos);
// parser ends here
if ($needle === false) {
// original status 0 => no problem
if (!$this->_status) {
break;
}
// not in original status? strict mode?
if ($this->strict) {
return false;
}
// break up parsing operation of current node
$res = $this->_reparseAfterCurrentBlock ();
if (!$res) {
return false;
}
continue;
}
// get subtext
$subtext = substr ($this->_text, $this->_cpos, $offset - $this->_cpos);
$res = $this->_appendText ($subtext);
if (!$res) {
return false;
}
$this->_cpos = $offset;
$res = $this->_handleStatus ($this->_status, $needle);
if (!$res && $this->strict) {
return false;
}
if (!$res) {
$res = $this->_appendText ($this->_text{$this->_cpos});
if (!$res) {
return false;
}
$this->_cpos++;
continue;
}
if ($this->_recentlyReparsed) {
$this->_recentlyReparsed = false;
continue;
}
$this->_cpos += strlen ($needle);
}
// get subtext
if ($this->_cpos < strlen ($this->_text)) {
$subtext = substr ($this->_text, $this->_cpos);
$res = $this->_appendText ($subtext);
if (!$res) {
return false;
}
}
return true;
}
/**
* Loop mode loop
*
* @access protected
* @return bool
*/
function _loop () {
// HACK: This method ist not yet implemented correctly, the code below
// DOES NOT WORK! Do not use!
return false;
/*
while ($this->_cpos < $this->_length) {
$needle = $this->_strDetect ($this->_charactersSearch, $this->_cpos);
if ($needle === false) {
// not found => see if character is allowed
if (!in_array ($this->_text{$this->_cpos}, $this->_charactersAllowed)) {
if ($strict) {
return false;
}
// ignore
continue;
}
// lot's of FIXMES
$res = $this->_appendText ($this->_text{$this->_cpos});
if (!$res) {
return false;
}
}
// get subtext
$subtext = substr ($this->_text, $offset, $offset - $this->_cpos);
$res = $this->_appendText ($subtext);
if (!$res) {
return false;
}
$this->_cpos = $subtext;
$res = $this->_handleStatus ($this->_status, $needle);
if (!$res && $strict) {
return false;
}
}
// original status 0 => no problem
if (!$this->_status) {
return true;
}
// not in original status? strict mode?
if ($this->strict) {
return false;
}
// break up parsing operation of current node
$res = $this->_reparseAfterCurrentBlock ();
if (!$res) {
return false;
}
// this will not cause an infinite loop because
// _reparseAfterCurrentBlock will increase _cpos by one!
return $this->_loop ();
*/
}
/**
* Abstract method Append text depending on current status
* @access protected
* @param string $text The text to append
* @return bool On success, the function returns true, else false
*/
function _appendText ($text) {
if (!strlen ($text)) {
return true;
}
// default: call _appendToLastTextChild
return $this->_appendToLastTextChild ($text);
}
/**
* Append text to last text child of current top parser stack node
* @access protected
* @param string $text The text to append
* @return bool On success, the function returns true, else false
*/
function _appendToLastTextChild ($text) {
$scount = count ($this->_stack);
if ($scount == 0) {
return false;
}
return $this->_stack[$scount-1]->appendToLastTextChild ($text);
}
/**
* Searches {@link StringParser::_text _text} for every needle that is
* specified by using the {@link PHP_MANUAL#strpos strpos} function. It
* returns an associative array with the key 'needle'
* pointing at the string that was found first and the key
* 'offset'
pointing at the offset at which the string was
* found first. If no needle was found, the 'needle'
* element is false
and the 'offset'
element
* is -1
.
*
* @access protected
* @param array $needles
* @param int $offset
* @return array
* @see StringParser::_text
*/
function _strpos ($needles, $offset) {
$cur_needle = false;
$cur_offset = -1;
if ($offset < strlen ($this->_text)) {
foreach ($needles as $needle) {
$n_offset = strpos ($this->_text, $needle, $offset);
if ($n_offset !== false && ($n_offset < $cur_offset || $cur_offset < 0)) {
$cur_needle = $needle;
$cur_offset = $n_offset;
}
}
}
return array ($cur_needle, $cur_offset, 'needle' => $cur_needle, 'offset' => $cur_offset);
}
/**
* Detects a string at the current position
*
* @access protected
* @param array $needles The strings that are to be detected
* @param int $offset The current offset
* @return mixed The string that was detected or the needle
*/
function _strDetect ($needles, $offset) {
foreach ($needles as $needle) {
$l = strlen ($needle);
if (substr ($this->_text, $offset, $l) == $needle) {
return $needle;
}
}
return false;
}
/**
* Adds a node to the current parse stack
*
* @access protected
* @param object $node The node that is to be added
* @return bool True on success, else false.
* @see StringParser_Node, StringParser::_stack
*/
function _pushNode (&$node) {
$stack_count = count ($this->_stack);
$max_node =& $this->_stack[$stack_count-1];
if (!$max_node->appendChild ($node)) {
return false;
}
$this->_stack[$stack_count] =& $node;
return true;
}
/**
* Removes a node from the current parse stack
*
* @access protected
* @return bool True on success, else false.
* @see StringParser_Node, StringParser::_stack
*/
function _popNode () {
$stack_count = count ($this->_stack);
unset ($this->_stack[$stack_count-1]);
return true;
}
/**
* Execute a method on the top element
*
* @access protected
* @return mixed
*/
function _topNode () {
$args = func_get_args ();
if (!count ($args)) {
return; // oops?
}
$method = array_shift ($args);
$stack_count = count ($this->_stack);
$method = array (&$this->_stack[$stack_count-1], $method);
if (!is_callable ($method)) {
return; // oops?
}
return call_user_func_array ($method, $args);
}
/**
* Get a variable of the top element
*
* @access protected
* @return mixed
*/
function _topNodeVar ($var) {
$stack_count = count ($this->_stack);
return $this->_stack[$stack_count-1]->$var;
}
}
/**
* Node type: Unknown node
* @see StringParser_Node::_type
*/
define ('STRINGPARSER_NODE_UNKNOWN', 0);
/**
* Node type: Root node
* @see StringParser_Node::_type
*/
define ('STRINGPARSER_NODE_ROOT', 1);
/**
* Node type: Text node
* @see StringParser_Node::_type
*/
define ('STRINGPARSER_NODE_TEXT', 2);
/**
* Global value that is a counter of string parser node ids. Compare it to a
* sequence in databases.
* @var int
*/
$GLOBALS['__STRINGPARSER_NODE_ID'] = 0;
/**
* Generic string parser node class
*
* This is an abstract class for any type of node that is used within the
* string parser. General warning: This class contains code regarding references
* that is very tricky. Please do not touch this code unless you exactly know
* what you are doing. Incorrect handling of references may cause PHP to crash
* with a segmentation fault! You have been warned.
*
* @package stringparser
*/
class StringParser_Node {
/**
* The type of this node.
*
* There are three standard node types: root node, text node and unknown
* node. All node types are integer constants. Any node type of a
* subclass must be at least 32 to allow future developements.
*
* @access protected
* @var int
* @see STRINGPARSER_NODE_ROOT, STRINGPARSER_NODE_TEXT
* @see STRINGPARSER_NODE_UNKNOWN
*/
var $_type = STRINGPARSER_NODE_UNKNOWN;
/**
* The node ID
*
* This ID uniquely identifies this node. This is needed when searching
* for a specific node in the children array. Please note that this is
* only an internal variable and should never be used - not even in
* subclasses and especially not in external data structures. This ID
* has nothing to do with any type of ID in HTML oder XML.
*
* @access protected
* @var int
* @see StringParser_Node::_children
*/
var $_id = -1;
/**
* The parent of this node.
*
* It is either null (root node) or a reference to the parent object.
*
* @access protected
* @var mixed
* @see StringParser_Node::_children
*/
var $_parent = null;
/**
* The children of this node.
*
* It contains an array of references to all the children nodes of this
* node.
*
* @access protected
* @var array
* @see StringParser_Node::_parent
*/
var $_children = array ();
/**
* Occured at
*
* This defines the position in the parsed text where this node occurred
* at. If -1, this value was not possible to be determined.
*
* @access public
* @var int
*/
var $occurredAt = -1;
/**
* Constructor
*
* Currently, the constructor only allocates a new ID for the node and
* assigns it.
*
* @access public
* @param int $occurredAt The position in the text where this node
* occurred at. If not determinable, it is -1.
* @global __STRINGPARSER_NODE_ID
*/
function StringParser_Node ($occurredAt = -1) {
$this->_id = $GLOBALS['__STRINGPARSER_NODE_ID']++;
$this->occurredAt = $occurredAt;
}
/**
* Type of the node
*
* This function returns the type of the node
*
* @access public
* @return int
*/
function type () {
return $this->_type;
}
/**
* Prepend a node
*
* @access public
* @param object $node The node to be prepended.
* @return bool On success, the function returns true, else false.
*/
function prependChild (&$node) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// if node already has a parent
if ($node->_parent !== false) {
// remove node from there
$parent =& $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
$index = count ($this->_children) - 1;
// move all nodes to a new index
while ($index >= 0) {
// save object
$object =& $this->_children[$index];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$index]);
// put object to new position
$this->_children[$index+1] =& $object;
$index--;
}
$this->_children[0] =& $node;
return true;
}
/**
* Append text to last text child
* @access public
* @param string $text The text to append
* @return bool On success, the function returns true, else false
*/
function appendToLastTextChild ($text) {
$ccount = count ($this->_children);
if ($ccount == 0 || $this->_children[$ccount-1]->_type != STRINGPARSER_NODE_TEXT) {
$ntextnode =& new StringParser_Node_Text ($text);
return $this->appendChild ($ntextnode);
} else {
$this->_children[$ccount-1]->appendText ($text);
return true;
}
}
/**
* Append a node to the children
*
* This function appends a node to the children array(). It
* automatically sets the {@link StrinParser_Node::_parent _parent}
* property of the node that is to be appended.
*
* @access public
* @param object $node The node that is to be appended.
* @return bool On success, the function returns true, else false.
*/
function appendChild (&$node) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// if node already has a parent
if ($node->_parent !== null) {
// remove node from there
$parent =& $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
// append it to current node
$new_index = count ($this->_children);
$this->_children[$new_index] =& $node;
$node->_parent =& $this;
return true;
}
/**
* Insert a node before another node
*
* @access public
* @param object $node The node to be inserted.
* @param object $reference The reference node where the new node is
* to be inserted before.
* @return bool On success, the function returns true, else false.
*/
function insertChildBefore (&$node, &$reference) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// is the reference node a child?
$child = $this->_findChild ($reference);
if ($child === false) {
return false;
}
// if node already has a parent
if ($node->_parent !== null) {
// remove node from there
$parent =& $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
$index = count ($this->_children) - 1;
// move all nodes to a new index
while ($index >= $child) {
// save object
$object =& $this->_children[$index];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$index]);
// put object to new position
$this->_children[$index+1] =& $object;
$index--;
}
$this->_children[$child] =& $node;
return true;
}
/**
* Insert a node after another node
*
* @access public
* @param object $node The node to be inserted.
* @param object $reference The reference node where the new node is
* to be inserted after.
* @return bool On success, the function returns true, else false.
*/
function insertChildAfter (&$node, &$reference) {
if (!is_object ($node)) {
return false;
}
// root nodes may not be children of other nodes!
if ($node->_type == STRINGPARSER_NODE_ROOT) {
return false;
}
// is the reference node a child?
$child = $this->_findChild ($reference);
if ($child === false) {
return false;
}
// if node already has a parent
if ($node->_parent !== false) {
// remove node from there
$parent =& $node->_parent;
if (!$parent->removeChild ($node, false)) {
return false;
}
unset ($parent);
}
$index = count ($this->_children) - 1;
// move all nodes to a new index
while ($index >= $child + 1) {
// save object
$object =& $this->_children[$index];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$index]);
// put object to new position
$this->_children[$index+1] =& $object;
$index--;
}
$this->_children[$child + 1] =& $node;
return true;
}
/**
* Remove a child node
*
* This function removes a child from the children array. A parameter
* tells the function whether to destroy the child afterwards or not.
* If the specified node is not a child of this node, the function will
* return false.
*
* @access public
* @param mixed $child The child to destroy; either an integer
* specifying the index of the child or a reference
* to the child itself.
* @param bool $destroy Destroy the child afterwards.
* @return bool On success, the function returns true, else false.
*/
function removeChild (&$child, $destroy = false) {
if (is_object ($child)) {
// if object: get index
$object =& $child;
unset ($child);
$child = $this->_findChild ($object);
if ($child === false) {
return false;
}
} else {
// remove reference on $child
$save = $child;
unset($child);
$child = $save;
// else: get object
if (!isset($this->_children[$child])) {
return false;
}
$object =& $this->_children[$child];
}
// store count for later use
$ccount = count ($this->_children);
// index out of bounds
if (!is_int ($child) || $child < 0 || $child >= $ccount) {
return false;
}
// inkonsistency
if ($this->_children[$child]->_parent === null ||
$this->_children[$child]->_parent->_id != $this->_id) {
return false;
}
// $object->_parent = null would equal to $this = null
// as $object->_parent is a reference to $this!
// because of this, we have to unset the variable to remove
// the reference and then redeclare the variable
unset ($object->_parent); $object->_parent = null;
// we have to unset it because else it will be overridden in
// in the loop
unset ($this->_children[$child]);
// move all remaining objects one index higher
while ($child < $ccount - 1) {
// save object
$obj =& $this->_children[$child+1];
// we have to unset it because else it will be
// overridden in in the loop
unset ($this->_children[$child+1]);
// put object to new position
$this->_children[$child] =& $obj;
// UNSET THE OBJECT!
unset ($obj);
$child++;
}
if ($destroy) {
return StringParser_Node::destroyNode ($object);
unset ($object);
}
return true;
}
/**
* Get the first child of this node
*
* @access public
* @return mixed
*/
function &firstChild () {
$ret = null;
if (!count ($this->_children)) {
return $ret;
}
return $this->_children[0];
}
/**
* Get the last child of this node
*
* @access public
* @return mixed
*/
function &lastChild () {
$ret = null;
$c = count ($this->_children);
if (!$c) {
return $ret;
}
return $this->_children[$c-1];
}
/**
* Destroy a node
*
* @access public
* @static
* @param object $node The node to destroy
* @return bool True on success, else false.
*/
function destroyNode (&$node) {
if ($node === null) {
return false;
}
// if parent exists: remove node from tree!
if ($node->_parent !== null) {
$parent =& $node->_parent;
// directly return that result because the removeChild
// method will call destroyNode again
return $parent->removeChild ($node, true);
}
// node has children
while (count ($node->_children)) {
$child = 0;
// remove first child until no more children remain
if (!$node->removeChild ($child, true)) {
return false;
}
unset($child);
}
// now call the nodes destructor
if (!$node->_destroy ()) {
return false;
}
// now just unset it and prey that there are no more references
// to this node
unset ($node);
return true;
}
/**
* Destroy this node
*
*
* @access protected
* @return bool True on success, else false.
*/
function _destroy () {
return true;
}
/**
* Find a child node
*
* This function searches for a node in the own children and returns
* the index of the node or false if the node is not a child of this
* node.
*
* @access protected
* @param mixed $child The node to look for.
* @return mixed The index of the child node on success, else false.
*/
function _findChild (&$child) {
if (!is_object ($child)) {
return false;
}
$ccount = count ($this->_children);
for ($i = 0; $i < $ccount; $i++) {
if ($this->_children[$i]->_id == $child->_id) {
return $i;
}
}
return false;
}
/**
* Checks equality of this node and another node
*
* @access public
* @param mixed $node The node to be compared with
* @return bool True if the other node equals to this node, else false.
*/
function equals (&$node) {
return ($this->_id == $node->_id);
}
/**
* Determines whether a criterium matches this node
*
* @access public
* @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
*/
function matchesCriterium ($criterium, $value) {
return false;
}
/**
* Search for nodes with a certain criterium
*
* This may be used to implement getElementsByTagName etc.
*
* @access public
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return array All subnodes that match this criterium
*/
function &getNodesByCriterium ($criterium, $value) {
$nodes = array ();
$node_ctr = 0;
for ($i = 0; $i < count ($this->_children); $i++) {
if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
$nodes[$node_ctr++] =& $this->_children[$i];
}
$subnodes = $this->_children[$i]->getNodesByCriterium ($criterium, $value);
if (count ($subnodes)) {
$subnodes_count = count ($subnodes);
for ($j = 0; $j < $subnodes_count; $j++) {
$nodes[$node_ctr++] =& $subnodes[$j];
unset ($subnodes[$j]);
}
}
unset ($subnodes);
}
return $nodes;
}
/**
* Search for nodes with a certain criterium and return the count
*
* Similar to getNodesByCriterium
*
* @access public
* @param string $criterium The criterium that is to be checked
* @param mixed $value The value that is to be compared
* @return int The number of subnodes that match this criterium
*/
function getNodeCountByCriterium ($criterium, $value) {
$node_ctr = 0;
for ($i = 0; $i < count ($this->_children); $i++) {
if ($this->_children[$i]->matchesCriterium ($criterium, $value)) {
$node_ctr++;
}
$subnodes = $this->_children[$i]->getNodeCountByCriterium ($criterium, $value);
$node_ctr += $subnodes;
}
return $node_ctr;
}
/**
* Dump nodes
*
* This dumps a tree of nodes
*
* @access public
* @param string $prefix The prefix that is to be used for indentation
* @param string $linesep The line separator
* @param int $level The initial level of indentation
* @return string
*/
function dump ($prefix = " ", $linesep = "\n", $level = 0) {
$str = str_repeat ($prefix, $level) . $this->_id . ": " . $this->_dumpToString () . $linesep;
for ($i = 0; $i < count ($this->_children); $i++) {
$str .= $this->_children[$i]->dump ($prefix, $linesep, $level + 1);
}
return $str;
}
/**
* Dump this node to a string
*
* @access protected
* @return string
*/
function _dumpToString () {
if ($this->_type == STRINGPARSER_NODE_ROOT) {
return "root";
}
return (string)$this->_type;
}
}
/**
* String parser root node class
*
* @package stringparser
*/
class StringParser_Node_Root extends StringParser_Node {
/**
* The type of this node.
*
* This node is a root node.
*
* @access protected
* @var int
* @see STRINGPARSER_NODE_ROOT
*/
var $_type = STRINGPARSER_NODE_ROOT;
}
/**
* String parser text node class
*
* @package stringparser
*/
class StringParser_Node_Text extends StringParser_Node {
/**
* The type of this node.
*
* This node is a text node.
*
* @access protected
* @var int
* @see STRINGPARSER_NODE_TEXT
*/
var $_type = STRINGPARSER_NODE_TEXT;
/**
* Node flags
*
* @access protected
* @var array
*/
var $_flags = array ();
/**
* The content of this node
* @access public
* @var string
*/
var $content = '';
/**
* Constructor
*
* @access public
* @param string $content The initial content of this element
* @param int $occurredAt The position in the text where this node
* occurred at. If not determinable, it is -1.
* @see StringParser_Node_Text::content
*/
function StringParser_Node_Text ($content, $occurredAt = -1) {
parent::StringParser_Node ($occurredAt);
$this->content = $content;
}
/**
* Append text to content
*
* @access public
* @param string $text The text to append
* @see StringParser_Node_Text::content
*/
function appendText ($text) {
$this->content .= $text;
}
/**
* Set a flag
*
* @access public
* @param string $name The name of the flag
* @param mixed $value The value of the flag
*/
function setFlag ($name, $value) {
$this->_flags[$name] = $value;
return true;
}
/**
* Get Flag
*
* @access public
* @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 $default;
}
$return = $this->_flags[$flag];
if ($type != 'mixed') {
settype ($return, $type);
}
return $return;
}
/**
* Dump this node to a string
*/
function _dumpToString () {
return "text \"".substr (preg_replace ('/\s+/', ' ', $this->content), 0, 40)."\" [f:".preg_replace ('/\s+/', ' ', join(':', array_keys ($this->_flags)))."]";
}
}
?>