Location: PHPKode > projects > FiForms Framework > FiForms/FiForms-includes/XPath.class.php
<?php
/**
 * Php.XPath
 *
 * +======================================================================================================+
 * | A php class for searching an XML document using XPath, and making modifications using a DOM 
 * | style API. Does not require the DOM XML PHP library. 
 * |
 * +======================================================================================================+
 * | What Is XPath:
 * | --------------
 * | - "What SQL is for a relational database, XPath is for an XML document." -- Sam Blum
 * | - "The primary purpose of XPath is to address parts of an XML document. In support of this 
 * |    primary purpose, it also provides basic facilities for manipulting it." -- W3C
 * | 
 * | XPath in action and a very nice intro is under:
 * |    http://www.zvon.org/xxl/XPathTutorial/General/examples.html
 * | Specs Can be found under:
 * |    http://www.w3.org/TR/xpath     W3C XPath Recommendation 
 * |    http://www.w3.org/TR/xpath20   W3C XPath Recommendation 
 * |
 * | NOTE: Most of the XPath-spec has been realized, but not all. Usually this should not be
 * |       problem as the missing part is either rarely used or it's simpler to do with PHP itself.
 * +------------------------------------------------------------------------------------------------------+
 * | Requires PHP version  4.0.5 and up
 * +------------------------------------------------------------------------------------------------------+
 * | Main Active Authors:
 * | --------------------
 * | Nigel Swinson <hide@address.com>
 * |   Started around 2001-07, saved phpxml from near death and renamed to Php.XPath
 * |   Restructured XPath code to stay in line with XPath spec.
 * | Sam Blum <hide@address.com>
 * |   Started around 2001-09 1st major restruct (V2.0) and testbench initiator.   
 * |   2nd (V3.0) major rewrite in 2002-02
 * | Daniel Allen <hide@address.com>
 * |   Started around 2001-10 working to make Php.XPath adhere to specs 
 * | Main Former Author: Michael P. Mehl <hide@address.com>
 * |   Inital creator of V 1.0. Stoped activities around 2001-03        
 * +------------------------------------------------------------------------------------------------------+
 * | Code Structure:
 * | --------------_
 * | The class is split into 3 main objects. To keep usability easy all 3 
 * | objects are in this file (but may be split in 3 file in future).
 * |   +-------------+ 
 * |   |  XPathBase  | XPathBase holds general and debugging functions. 
 * |   +------+------+
 * |          v      
 * |   +-------------+ XPathEngine is the implementation of the W3C XPath spec. It contains the 
 * |   | XPathEngine | XML-import (parser), -export  and can handle xPathQueries. It's a fully 
 * |   +------+------+ functional class but has no functions to modify the XML-document (see following).
 * |          v      
 * |   +-------------+ 
 * |   |    XPath    | XPath extends the functionality with actions to modify the XML-document.
 * |   +-------------+ We tryed to implement a DOM - like interface.
 * +------------------------------------------------------------------------------------------------------+
 * | Usage:
 * | ------
 * | Scroll to the end of this php file and you will find a short sample code to get you started
 * +------------------------------------------------------------------------------------------------------+
 * | Glossary:
 * | ---------
 * | To understand how to use the functions and to pass the right parameters, read following:
 * |     
 * | Document: (full node tree, XML-tree)
 * |     After a XML-source has been imported and parsed, it's stored as a tree of nodes sometimes 
 * |     refered to as 'document'.
 * |     
 * | AbsoluteXPath: (xPath, xPathSet)
 * |     A absolute XPath is a string. It 'points' to *one* node in the XML-document. We use the
 * |     term 'absolute' to emphasise that it is not an xPath-query (see xPathQuery). A valid xPath 
 * |     has the form like '/AAA[1]/BBB[2]/CCC[1]'. Usually functions that require a node (see Node) 
 * |     will also accept an abs. XPath.
 * |     
 * | Node: (node, nodeSet, node-tree)
 * |     Some funtions require or return a node (or a whole node-tree). Nodes are only used with the 
 * |     XPath-interface and have an internal structure. Every node in a XML document has a unique 
 * |     corresponding abs. xPath. That's why public functions that accept a node, will usually also 
 * |     accept a abs. xPath (a string) 'pointing' to an existing node (see absolutXPath).
 * |     
 * | XPathQuery: (xquery, query)
 * |     A xPath-query is a string that is matched against the XML-document. The result of the match 
 * |     is a xPathSet (vector of xPath's). It's always possible to pass a single absoluteXPath 
 * |     instead of a xPath-query. A valid xPathQuery could look like this:
 * |     '//XXX/*[contains(., "foo")]/..' (See the link in 'What Is XPath' to learn more).
 * |     
 * |     
 * +------------------------------------------------------------------------------------------------------+
 * | Internals:
 * | ----------
 * | - The Node Tree
 * |   -------------
 * | A central role of the package is how the XML-data is stored. The whole data is in a node-tree.
 * | A node can be seen as the equvalent to a tag in the XML soure with some extra info.
 * | For instance the following XML 
 * |                        <AAA foo="x">***<BBB/><CCC/>**<BBB/>*</AAA>
 * | Would produce folowing node-tree:
 * |                              'super-root'      <-- $nodeRoot (Very handy)  
 * |                                    |                                           
 * |             'depth' 0            AAA[1]        <-- top node. The 'textParts' of this node would be
 * |                                /   |   \                     'textParts' => array('***','','**','*')
 * |             'depth' 1     BBB[1] CCC[1] BBB[2]               (NOTE: Is always size of child nodes+1)
 * | - The Node
 * |   --------
 * | The node itself is an structure desiged mainly to be used in connection with the interface of PHP.XPath.
 * | That means it's possible for functions to return a sub-node-tree that can be used as input of an other 
 * | PHP.XPath function.
 * | 
 * | The main structure of a node is:
 * |   $node = array(
 * |     'name'        => '',      # The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
 * |     'attributes'  => array(), # The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
 * |     'textParts'   => array(), # Array of text parts surrounding the children E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd')
 * |     'childNodes'  => array(), # Array of refences (pointers) to child nodes.
 * |     
 * | For optimisation reasions some additional data is stored in the node too:
 * |     'parentNode'  => NULL     # Reference (pointer) to the parent node (or NULL if it's 'super root')
 * |     'depth'       => 0,       # The tag depth (or tree level) starting with the root tag at 0.
 * |     'pos'         => 0,       # Is the zero-based position this node has in the parent's 'childNodes'-list.
 * |     'contextPos'  => 1,       # Is the one-based position this node has by counting the siblings tags (tags with same name)
 * |     'xpath'       => ''       # Is the abs. XPath to this node.
 * |     'generated_id'=> ''       # The id returned for this node by generate-id() (attribute and text nodes not supported)
 * | 
 * | - The NodeIndex
 * |   -------------
 * | Every node in the tree has an absolute XPath. E.g '/AAA[1]/BBB[2]' the $nodeIndex is a hash array
 * | to all the nodes in the node-tree. The key used is the absolute XPath (a string).
 * |    
 * +------------------------------------------------------------------------------------------------------+
 * | License:
 * | --------
 * | The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); 
 * | you may not use this file except in compliance with the License. You may obtain a copy of the 
 * | License at http://www.mozilla.org/MPL/ 
 * | 
 * | Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY
 * | OF ANY KIND, either express or implied. See the License for the specific language governing 
 * | rights and limitations under the License. 
 * |
 * | The Original Code is <phpXML/>. 
 * | 
 * | The Initial Developer of the Original Code is Michael P. Mehl. Portions created by Michael 
 * | P. Mehl are Copyright (C) 2001 Michael P. Mehl. All Rights Reserved.
 * |
 * | Contributor(s): N.Swinson / S.Blum / D.Allen
 * | 
 * | Alternatively, the contents of this file may be used under the terms of either of the GNU 
 * | General Public License Version 2 or later (the "GPL"), or the GNU General Public 
 * | License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the 
 * | LGPL License are applicable instead of those above.  If you wish to allow use of your version 
 * | of this file only under the terms of the GPL or the LGPL License and not to allow others to 
 * | use your version of this file under the MPL, indicate your decision by deleting the 
 * | provisions above and replace them with the notice and other provisions required by the 
 * | GPL or the LGPL License.  If you do not delete the provisions above, a recipient may use 
 * | your version of this file under either the MPL, the GPL or the LGPL License. 
 * | 
 * +======================================================================================================+
 *
 * @author  S.Blum / N.Swinson / D.Allen / (P.Mehl)
 * @link    http://sourceforge.net/projects/phpxpath/
 * @version 3.5
 * @CVS $Id: XPath.class.php,v 1.148 2004/08/13 11:47:36 nigelswinson Exp $
 */

// Include guard, protects file being included twice
$ConstantName = 'INCLUDED_'.strtoupper(__FILE__);
if (defined($ConstantName)) return;
define($ConstantName,1, TRUE);

/************************************************************************************************
* ===============================================================================================
*                               X P a t h B a s e  -  Class                                      
* ===============================================================================================
************************************************************************************************/
class XPathBase {
  var $_lastError;
  
  // As debugging of the xml parse is spread across several functions, we need to make this a member.
  var $bDebugXmlParse = FALSE;

  // Used to help navigate through the begin/end debug calls
  var $iDebugNextLinkNumber = 1;
  var $aDebugOpenLinks = array();
  var $aDebugFunctions = array(
		//'_evaluateStep',
          //'_evaluatePrimaryExpr',
          //'_evaluateExpr',
          //'_evaluateStep',
          //'_checkPredicates',
          //'_evaluateFunction',
          //'_evaluateOperator',
          //'_evaluatePathExpr',
               );

  /**
   * Constructor
   */
  function XPathBase() {
    # $this->bDebugXmlParse = TRUE;
    $this->properties['verboseLevel'] = 1;  // 0=silent, 1 and above produce verbose output (an echo to screen). 
    
    if (!isSet($_ENV)) {  // Note: $_ENV introduced in 4.1.0. In earlier versions, use $HTTP_ENV_VARS.
      $_ENV = $GLOBALS['HTTP_ENV_VARS'];
    }
    
    // Windows 95/98 do not support file locking. Detecting OS (Operation System) and setting the 
    // properties['OS_supports_flock'] to FALSE if win 95/98 is detected. 
    // This will surpress the file locking error reported from win 98 users when exportToFile() is called.
    // May have to add more OS's to the list in future (Macs?).
    // ### Note that it's only the FAT and NFS file systems that are really a problem.  NTFS and
    // the latest php libs do support flock()
    $_ENV['OS'] = isSet($_ENV['OS']) ? $_ENV['OS'] : 'Unknown OS';
    switch ($_ENV['OS']) { 
      case 'Windows_95':
      case 'Windows_98':
      case 'Unknown OS':
        // should catch Mac OS X compatible environment 
        if (!empty($_SERVER['SERVER_SOFTWARE']) 
            && preg_match('/Darwin/',$_SERVER['SERVER_SOFTWARE'])) { 
           // fall-through 
        } else { 
           $this->properties['OS_supports_flock'] = FALSE; 
           break; 
        }
      default:
        $this->properties['OS_supports_flock'] = TRUE;
    }
  }
  
  
  /**
   * Resets the object so it's able to take a new xml sting/file
   *
   * Constructing objects is slow.  If you can, reuse ones that you have used already
   * by using this reset() function.
   */
  function reset() {
    $this->_lastError   = '';
  }
  
  //-----------------------------------------------------------------------------------------
  // XPathBase                    ------  Helpers  ------                                    
  //-----------------------------------------------------------------------------------------
  
  /**
   * This method checks the right amount and match of brackets
   *
   * @param     $term (string) String in which is checked.
   * @return          (bool)   TRUE: OK / FALSE: KO  
   */
  function _bracketsCheck($term) {
    $leng = strlen($term);
    $brackets = 0;
    $bracketMisscount = $bracketMissmatsh = FALSE;
    $stack = array();
    for ($i=0; $i<$leng; $i++) {
      switch ($term[$i]) {
        case '(' : 
        case '[' : 
          $stack[$brackets] = $term[$i]; 
          $brackets++; 
          break;
        case ')': 
          $brackets--;
          if ($brackets<0) {
            $bracketMisscount = TRUE;
            break 2;
          }
          if ($stack[$brackets] != '(') {
            $bracketMissmatsh = TRUE;
            break 2;
          }
          break;
        case ']' : 
          $brackets--;
          if ($brackets<0) {
            $bracketMisscount = TRUE;
            break 2;
          }
          if ($stack[$brackets] != '[') {
            $bracketMissmatsh = TRUE;
            break 2;
          }
          break;
      }
    }
    // Check whether we had a valid number of brackets.
    if ($brackets != 0) $bracketMisscount = TRUE;
    if ($bracketMisscount || $bracketMissmatsh) {
      return FALSE;
    }
    return TRUE;
  }
  
  /**
   * Looks for a string within another string -- BUT the search-string must be located *outside* of any brackets.
   *
   * This method looks for a string within another string. Brackets in the
   * string the method is looking through will be respected, which means that
   * only if the string the method is looking for is located outside of
   * brackets, the search will be successful.
   *
   * @param     $term       (string) String in which the search shall take place.
   * @param     $expression (string) String that should be searched.
   * @return                (int)    This method returns -1 if no string was found, 
   *                                 otherwise the offset at which the string was found.
   */
  function _searchString($term, $expression) {
    $bracketCounter = 0; // Record where we are in the brackets. 
    $leng = strlen($term);
    $exprLeng = strlen($expression);
    for ($i=0; $i<$leng; $i++) {
      $char = $term[$i];
      if ($char=='(' || $char=='[') {
        $bracketCounter++;
        continue;
      }
      elseif ($char==')' || $char==']') {
        $bracketCounter--;
      }
      if ($bracketCounter == 0) {
        // Check whether we can find the expression at this index.
        if (substr($term, $i, $exprLeng) == $expression) return $i;
      }
    }
    // Nothing was found.
    return (-1);
  }
  
  /**
   * Split a string by a searator-string -- BUT the separator-string must be located *outside* of any brackets.
   * 
   * Returns an array of strings, each of which is a substring of string formed 
   * by splitting it on boundaries formed by the string separator. 
   *
   * @param     $separator  (string) String that should be searched.
   * @param     $term       (string) String in which the search shall take place.
   * @return                (array)  see above
   */
  function _bracketExplode($separator, $term) {
    // Note that it doesn't make sense for $separator to itself contain (,),[ or ],
    // but as this is a private function we should be ok.
    $resultArr   = array();
    $bracketCounter = 0;  // Record where we are in the brackets. 
    do { // BEGIN try block
      // Check if any separator is in the term
      $sepLeng =  strlen($separator);
      if (strpos($term, $separator)===FALSE) { // no separator found so end now
        $resultArr[] = $term;
        break; // try-block
      }
      
      // Make a substitute separator out of 'unused chars'.
      $substituteSep = str_repeat(chr(2), $sepLeng);
      
      // Now determine the first bracket '(' or '['.
      $tmp1 = strpos($term, '(');
      $tmp2 = strpos($term, '[');
      if ($tmp1===FALSE) {
        $startAt = (int)$tmp2;
      } elseif ($tmp2===FALSE) {
        $startAt = (int)$tmp1;
      } else {
        $startAt = min($tmp1, $tmp2);
      }
      
      // Get prefix string part before the first bracket.
      $preStr = substr($term, 0, $startAt);
      // Substitute separator in prefix string.
      $preStr = str_replace($separator, $substituteSep, $preStr);
      
      // Now get the rest-string (postfix string)
      $postStr = substr($term, $startAt);
      // Go all the way through the rest-string.
      $strLeng = strlen($postStr);
      for ($i=0; $i < $strLeng; $i++) {
        $char = $postStr[$i];
        // Spot (,),[,] and modify our bracket counter.  Note there is an
        // assumption here that you don't have a string(with[mis)matched]brackets.
        // This should be ok as the dodgy string will be detected elsewhere.
        if ($char=='(' || $char=='[') {
          $bracketCounter++;
          continue;
        } 
        elseif ($char==')' || $char==']') {
          $bracketCounter--;
        }
        // If no brackets surround us check for separator
        if ($bracketCounter == 0) {
          // Check whether we can find the expression starting at this index.
          if ((substr($postStr, $i, $sepLeng) == $separator)) {
            // Substitute the found separator 
            for ($j=0; $j<$sepLeng; $j++) {
              $postStr[$i+$j] = $substituteSep[$j];
            }
          }
        }
      }
      // Now explod using the substitute separator as key.
      $resultArr = explode($substituteSep, $preStr . $postStr);
    } while (FALSE); // End try block
    // Return the results that we found. May be a array with 1 entry.
    return $resultArr;
  }

  /**
   * Split a string at it's groups, ie bracketed expressions
   * 
   * Returns an array of strings, when concatenated together would produce the original
   * string.  ie a(b)cde(f)(g) would map to:
   * array ('a', '(b)', cde', '(f)', '(g)')
   *
   * @param     $string  (string) The string to process
   * @param     $open    (string) The substring for the open of a group
   * @param     $close   (string) The substring for the close of a group
   * @return             (array)  The parsed string, see above
   */
  function _getEndGroups($string, $open='[', $close=']') {
    // Note that it doesn't make sense for $separator to itself contain (,),[ or ],
    // but as this is a private function we should be ok.
    $resultArr   = array();
    do { // BEGIN try block
      // Check if we have both an open and a close tag      
      if (empty($open) and empty($close)) { // no separator found so end now
        $resultArr[] = $string;
        break; // try-block
      }

      if (empty($string)) {
        $resultArr[] = $string;
        break; // try-block
      }

      
      while (!empty($string)) {
        // Now determine the first bracket '(' or '['.
        $openPos = strpos($string, $open);
        $closePos = strpos($string, $close);
        if ($openPos===FALSE || $closePos===FALSE) {
          // Oh, no more groups to be found then.  Quit
          $resultArr[] = $string;
          break;
        }

        // Sanity check
        if ($openPos > $closePos) {
          // Malformed string, dump the rest and quit.
          $resultArr[] = $string;
          break;
        }

        // Get prefix string part before the first bracket.
        $preStr = substr($string, 0, $openPos);
        // This is the first string that will go in our output
        if (!empty($preStr))
          $resultArr[] = $preStr;

        // Skip over what we've proceed, including the open char
        $string = substr($string, $openPos + 1 - strlen($string));

        // Find the next open char and adjust our close char
//echo "close: $closePos\nopen: $openPos\n\n";
        $closePos -= $openPos + 1;
        $openPos = strpos($string, $open);
//echo "close: $closePos\nopen: $openPos\n\n";

        // While we have found nesting...
        while ($openPos && $closePos && ($closePos > $openPos)) {
          // Find another close pos after the one we are looking at
          $closePos = strpos($string, $close, $closePos + 1);
          // And skip our open
          $openPos = strpos($string, $open, $openPos + 1);
        }
//echo "close: $closePos\nopen: $openPos\n\n";

        // If we now have a close pos, then it's the end of the group.
        if ($closePos === FALSE) {
          // We didn't... so bail dumping what was left
          $resultArr[] = $open.$string;
          break;
        }

        // We did, so we can extract the group
        $resultArr[] = $open.substr($string, 0, $closePos + 1);
        // Skip what we have processed
        $string = substr($string, $closePos + 1);
      }
    } while (FALSE); // End try block
    // Return the results that we found. May be a array with 1 entry.
    return $resultArr;
  }
  
  /**
   * Retrieves a substring before a delimiter.
   *
   * This method retrieves everything from a string before a given delimiter,
   * not including the delimiter.
   *
   * @param     $string     (string) String, from which the substring should be extracted.
   * @param     $delimiter  (string) String containing the delimiter to use.
   * @return                (string) Substring from the original string before the delimiter.
   * @see       _afterstr()
   */
  function _prestr(&$string, $delimiter, $offset=0) {
    // Return the substring.
    $offset = ($offset<0) ? 0 : $offset;
    $pos = strpos($string, $delimiter, $offset);
    if ($pos===FALSE) return $string; else return substr($string, 0, $pos);
  }
  
  /**
   * Retrieves a substring after a delimiter.
   *
   * This method retrieves everything from a string after a given delimiter,
   * not including the delimiter.
   *
   * @param     $string     (string) String, from which the substring should be extracted.
   * @param     $delimiter  (string) String containing the delimiter to use.
   * @return                (string) Substring from the original string after the delimiter.
   * @see       _prestr()
   */
  function _afterstr($string, $delimiter, $offset=0) {
    $offset = ($offset<0) ? 0 : $offset;
    // Return the substring.
    return substr($string, strpos($string, $delimiter, $offset) + strlen($delimiter));
  }
  
  //-----------------------------------------------------------------------------------------
  // XPathBase                ------  Debug Stuff  ------                                    
  //-----------------------------------------------------------------------------------------
  
  /**
   * Alter the verbose (error) level reporting.
   *
   * Pass an int. >0 to turn on, 0 to turn off.  The higher the number, the 
   * higher the level of verbosity. By default, the class has a verbose level 
   * of 1.
   *
   * @param $levelOfVerbosity (int) default is 1 = on
   */
  function setVerbose($levelOfVerbosity = 1) {
    $level = -1;
    if ($levelOfVerbosity === TRUE) {
      $level = 1;
    } elseif ($levelOfVerbosity === FALSE) {
      $level = 0;
    } elseif (is_numeric($levelOfVerbosity)) {
      $level = $levelOfVerbosity;
    }
    if ($level >= 0) $this->properties['verboseLevel'] = $levelOfVerbosity;
  }
   
  /**
   * Returns the last occured error message.
   *
   * @access public
   * @return string (may be empty if there was no error at all)
   * @see    _setLastError(), _lastError
   */
  function getLastError() {
    return $this->_lastError;
  }
  
  /**
   * Creates a textual error message and sets it. 
   * 
   * example: 'XPath error in THIS_FILE_NAME:LINE. Message: YOUR_MESSAGE';
   * 
   * I don't think the message should include any markup because not everyone wants to debug 
   * into the browser window.
   * 
   * You should call _displayError() rather than _setLastError() if you would like the message,
   * dependant on their verbose settings, echoed to the screen.
   * 
   * @param $message (string) a textual error message default is ''
   * @param $line    (int)    the line number where the error occured, use __LINE__
   * @see getLastError()
   */
  function _setLastError($message='', $line='-', $file='-') {
    $this->_lastError = 'XPath error in ' . basename($file) . ':' . $line . '. Message: ' . $message;
  }
  
  /**
   * Displays an error message.
   *
   * This method displays an error messages depending on the users verbose settings 
   * and sets the last error message.  
   *
   * If also possibly stops the execution of the script.
   * ### Terminate should not be allowed --fab.  Should it??  N.S.
   *
   * @param $message    (string)  Error message to be displayed.
   * @param $lineNumber (int)     line number given by __LINE__
   * @param $terminate  (bool)    (default TURE) End the execution of this script.
   */
  function _displayError($message, $lineNumber='-', $file='-', $terminate=TRUE) {
    // Display the error message.
    $err = '<b>XPath error in '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
    $this->_setLastError($message, $lineNumber, $file);
    if (($this->properties['verboseLevel'] > 0) OR ($terminate)) echo $err;
    // End the execution of this script.
    if ($terminate) exit;
  }

  /**
   * Displays a diagnostic message
   *
   * This method displays an error messages
   *
   * @param $message    (string)  Error message to be displayed.
   * @param $lineNumber (int)     line number given by __LINE__
   */
  function _displayMessage($message, $lineNumber='-', $file='-') {
    // Display the error message.
    $err = '<b>XPath message from '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
    if ($this->properties['verboseLevel'] > 0) echo $err;
  }
  
  /**
   * Called to begin the debug run of a function.
   *
   * This method starts a <DIV><PRE> tag so that the entry to this function
   * is clear to the debugging user.  Call _closeDebugFunction() at the
   * end of the function to create a clean box round the function call.
   *
   * @author    Nigel Swinson <hide@address.com>
   * @author    Sam   Blum    <hide@address.com>
   * @param     $functionName (string) the name of the function we are beginning to debug
   * @return                  (array)  the output from the microtime() function.
   * @see       _closeDebugFunction()
   */
  function _beginDebugFunction($functionName) {
    $fileName = basename(__FILE__);
    static $color = array('green','blue','red','lime','fuchsia', 'aqua');
    static $colIndex = -1;
    $colIndex++;
    echo '<div style="clear:both" align="left"> ';
    echo '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; padding:5">';
    echo '<a style="float:right;margin:5px" name="'.$this->iDebugNextLinkNumber.'Open" href="#'.$this->iDebugNextLinkNumber.'Close">Function Close '.$this->iDebugNextLinkNumber.'</a>';
    echo "<STRONG>{$fileName} : {$functionName}</STRONG>";
    echo '<hr style="clear:both">';
    array_push($this->aDebugOpenLinks, $this->iDebugNextLinkNumber);
    $this->iDebugNextLinkNumber++;
    return microtime();
  }
  
  /**
   * Called to end the debug run of a function.
   *
   * This method ends a <DIV><PRE> block and reports the time since $aStartTime
   * is clear to the debugging user.
   *
   * @author    Nigel Swinson <hide@address.com>
   * @param     $aStartTime   (array) the time that the function call was started.
   * @param     $return_value (mixed) the return value from the function call that 
   *                                  we are debugging
   */
  function _closeDebugFunction($aStartTime, $returnValue = "") {
    echo "<hr>";
    $iOpenLinkNumber = array_pop($this->aDebugOpenLinks);
    echo '<a style="float:right" name="'.$iOpenLinkNumber.'Close" href="#'.$iOpenLinkNumber.'Open">Function Open '.$iOpenLinkNumber.'</a>';
    if (isSet($returnValue)) {
      if (is_array($returnValue))
        echo "Return Value: ".print_r($returnValue)."\n";
      else if (is_numeric($returnValue)) 
        echo "Return Value: ".(string)$returnValue."\n";
      else if (is_bool($returnValue)) 
        echo "Return Value: ".($returnValue ? "TRUE" : "FALSE")."\n";
      else 
        echo "Return Value: \"".htmlspecialchars($returnValue)."\"\n";
    }
    $this->_profileFunction($aStartTime, "Function took");
    echo '<br style="clear:both">';
    echo " \n</pre></div>";
  }
  
  /**
   * Call to return time since start of function for Profiling
   *
   * @param     $aStartTime  (array)  the time that the function call was started.
   * @param     $alertString (string) the string to describe what has just finished happening
   */
  function _profileFunction($aStartTime, $alertString) {
    // Print the time it took to call this function.
    $now   = explode(' ', microtime());
    $last  = explode(' ', $aStartTime);
    $delta = (round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*1000 ));
    echo "\n{$alertString} <strong>{$delta} ms</strong>";
  }

  /**
   * Echo an XPath context for diagnostic purposes
   *
   * @param $context   (array)   An XPath context
   */
  function _printContext($context) {
    echo "{$context['nodePath']}({$context['pos']}/{$context['size']})";
  }
  
  /**
   * This is a debug helper function. It dumps the node-tree as HTML
   *
   * *QUICK AND DIRTY*. Needs some polishing.
   *
   * @param $node   (array)   A node 
   * @param $indent (string) (optional, default=''). For internal recursive calls.
   */
  function _treeDump($node, $indent = '') {
    $out = '';
    
    // Get rid of recursion
    $parentName = empty($node['parentNode']) ? "SUPER ROOT" :  $node['parentNode']['name'];
    unset($node['parentNode']);
    $node['parentNode'] = $parentName ;
    
    $out .= "NODE[{$node['name']}]\n";
    
    foreach($node as $key => $val) {
      if ($key === 'childNodes') continue;
      if (is_Array($val)) {
        $out .= $indent . "  [{$key}]\n" . arrayToStr($val, $indent . '    ');
      } else {
        $out .= $indent . "  [{$key}] => '{$val}' \n";
      }
    }
    
    if (!empty($node['childNodes'])) {
      $out .= $indent . "  ['childNodes'] (Size = ".sizeOf($node['childNodes']).")\n";
      foreach($node['childNodes'] as $key => $childNode) {
        $out .= $indent . "     [$key] => " . $this->_treeDump($childNode, $indent . '       ') . "\n";
      }
    }
    
    if (empty($indent)) {
      return "<pre>" . htmlspecialchars($out) . "</pre>";
    }
    return $out;
  }
} // END OF CLASS XPathBase


/************************************************************************************************
* ===============================================================================================
*                             X P a t h E n g i n e  -  Class                                    
* ===============================================================================================
************************************************************************************************/

class XPathEngine extends XPathBase {
  
  // List of supported XPath axes.
  // What a stupid idea from W3C to take axes name containing a '-' (dash)
  // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
  //       We will then do the same on the users Xpath querys
  //   -sibling => _sibling
  //   -or-     =>     _or_
  //  
  // This array contains a list of all valid axes that can be evaluated in an
  // XPath query.
  var $axes = array ( 'ancestor', 'ancestor_or_self', 'attribute', 'child', 'descendant', 
                        'descendant_or_self', 'following', 'following_sibling',  
                        'namespace', 'parent', 'preceding', 'preceding_sibling', 'self' 
     );
  
  // List of supported XPath functions.
  // What a stupid idea from W3C to take function name containing a '-' (dash)
  // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
  //       We will then do the same on the users Xpath querys 
  //   starts-with      => starts_with
  //   substring-before => substring_before
  //   substring-after  => substring_after
  //   string-length    => string_length
  //
  // This array contains a list of all valid functions that can be evaluated
  // in an XPath query.
  var $functions = array ( 'last', 'position', 'count', 'id', 'name',
    'string', 'concat', 'starts_with', 'contains', 'substring_before',
    'substring_after', 'substring', 'string_length', 'normalize_space', 'translate',
    'boolean', 'not', 'true', 'false', 'lang', 'number', 'sum', 'floor',
    'ceiling', 'round', 'x_lower', 'x_upper', 'generate_id' );
    
  // List of supported XPath operators.
  //
  // This array contains a list of all valid operators that can be evaluated
  // in a predicate of an XPath query. The list is ordered by the
  // precedence of the operators (lowest precedence first).
  var $operators = array( ' or ', ' and ', '=', '!=', '<=', '<', '>=', '>',
    '+', '-', '*', ' div ', ' mod ', ' | ');

  // List of literals from the xPath string.
  var $axPathLiterals = array();
  
  // The index and tree that is created during the analysis of an XML source.
  var $nodeIndex = array();
  var $nodeRoot  = array();
  var $emptyNode = array(
                     'name'        => '',       // The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
                     'attributes'  => array(),  // The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
                     'childNodes'  => array(),  // Array of pointers to child nodes.
                     'textParts'   => array(),  // Array of text parts between the cilderen E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd')
                     'parentNode'   => NULL,     // Pointer to parent node or NULL if this node is the 'super root'
                     //-- *!* Following vars are set by the indexer and is for optimisation only *!*
                     'depth'       => 0,  // The tag depth (or tree level) starting with the root tag at 0.
                     'pos'         => 0,  // Is the zero-based position this node has in the parents 'childNodes'-list.
                     'contextPos'  => 1,  // Is the one-based position this node has by counting the siblings tags (tags with same name)
                     'xpath'       => ''  // Is the abs. XPath to this node.
                   );
  var $_indexIsDirty = FALSE;

  
  // These variable used during the parse XML source
  var $nodeStack       = array(); // The elements that we have still to close.
  var $parseStackIndex = 0;       // The current element of the nodeStack[] that we are adding to while 
                                  // parsing an XML source.  Corresponds to the depth of the xml node.
                                  // in our input data.
  var $parseOptions    = array(); // Used to set the PHP's XML parser options (see xml_parser_set_option)
  var $parsedTextLocation   = ''; // A reference to where we have to put char data collected during XML parsing
  var $parsInCData     = 0 ;      // Is >0 when we are inside a CDATA section.  
  var $parseSkipWhiteCache = 0;   // A cache of the skip whitespace parse option to speed up the parse.

  // This is the array of error strings, to keep consistency.
  var $errorStrings = array(
    'AbsoluteXPathRequired' => "The supplied xPath '%s' does not *uniquely* describe a node in the xml document.",
    'NoNodeMatch'           => "The supplied xPath-query '%s' does not match *any* node in the xml document.",
    'RootNodeAlreadyExists' => "An xml document may have only one root node."
    );
    
  /**
   * Constructor
   *
   * Optionally you may call this constructor with the XML-filename to parse and the 
   * XML option vector. Each of the entries in the option vector will be passed to
   * xml_parser_set_option().
   *
   * A option vector sample: 
   *   $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE, 
   *                   XML_OPTION_SKIP_WHITE => TRUE);
   *
   * @param  $userXmlOptions (array) (optional) Vector of (<optionID>=><value>, 
   *                                 <optionID>=><value>, ...).  See PHP's
   *                                 xml_parser_set_option() docu for a list of possible
   *                                 options.
   * @see   importFromFile(), importFromString(), setXmlOptions()
   */
  function XPathEngine($userXmlOptions=array()) {
    parent::XPathBase();
    // Default to not folding case
    $this->parseOptions[XML_OPTION_CASE_FOLDING] = FALSE;
    // And not skipping whitespace
    $this->parseOptions[XML_OPTION_SKIP_WHITE] = FALSE;
    
    // Now merge in the overrides.
    // Don't use PHP's array_merge!
    if (is_array($userXmlOptions)) {
      foreach($userXmlOptions as $key => $val) $this->parseOptions[$key] = $val;
    }
  }
  
  /**
   * Resets the object so it's able to take a new xml sting/file
   *
   * Constructing objects is slow.  If you can, reuse ones that you have used already
   * by using this reset() function.
   */
  function reset() {
    parent::reset();
    $this->properties['xmlFile']  = ''; 
    $this->parseStackIndex = 0;
    $this->parsedTextLocation = '';
    $this->parsInCData   = 0;
    $this->nodeIndex     = array();
    $this->nodeRoot      = array();
    $this->nodeStack     = array();
    $this->aLiterals     = array();
    $this->_indexIsDirty = FALSE;
  }
  
  
  //-----------------------------------------------------------------------------------------
  // XPathEngine              ------  Get / Set Stuff  ------                                
  //-----------------------------------------------------------------------------------------
  
  /**
   * Returns the property/ies you want.
   * 
   * if $param is not given, all properties will be returned in a hash.
   *
   * @param  $param (string) the property you want the value of, or NULL for all the properties
   * @return        (mixed)  string OR hash of all params, or NULL on an unknown parameter.
   */
  function getProperties($param=NULL) {
    $this->properties['hasContent']      = !empty($this->nodeRoot);
    $this->properties['caseFolding']     = $this->parseOptions[XML_OPTION_CASE_FOLDING];
    $this->properties['skipWhiteSpaces'] = $this->parseOptions[XML_OPTION_SKIP_WHITE];
    
    if (empty($param)) return $this->properties;
    
    if (isSet($this->properties[$param])) {
      return $this->properties[$param];
    } else {
      return NULL;
    }
  }
  
  /**
   * Set an xml_parser_set_option()
   *
   * @param $optionID (int) The option ID (e.g. XML_OPTION_SKIP_WHITE)
   * @param $value    (int) The option value.
   * @see XML parser functions in PHP doc
   */
  function setXmlOption($optionID, $value) {
    if (!is_numeric($optionID)) return;
     $this->parseOptions[$optionID] = $value;
  }

  /**
   * Sets a number of xml_parser_set_option()s
   *
   * @param  $userXmlOptions (array) An array of parser options.
   * @see setXmlOption
   */
  function setXmlOptions($userXmlOptions=array()) {
    if (!is_array($userXmlOptions)) return;
    foreach($userXmlOptions as $key => $val) {
      $this->setXmlOption($key, $val);
    }
  }
  
  /**
   * Alternative way to control whether case-folding is enabled for this XML parser.
   *
   * Short cut to setXmlOptions(XML_OPTION_CASE_FOLDING, TRUE/FALSE)
   *
   * When it comes to XML, case-folding simply means uppercasing all tag- 
   * and attribute-names (NOT the content) if set to TRUE.  Note if you
   * have this option set, then your XPath queries will also be case folded 
   * for you.
   *
   * @param $onOff (bool) (default TRUE) 
   * @see XML parser functions in PHP doc
   */
  function setCaseFolding($onOff=TRUE) {
    $this->parseOptions[XML_OPTION_CASE_FOLDING] = $onOff;
  }
  
  /**
   * Alternative way to control whether skip-white-spaces is enabled for this XML parser.
   *
   * Short cut to setXmlOptions(XML_OPTION_SKIP_WHITE, TRUE/FALSE)
   *
   * When it comes to XML, skip-white-spaces will trim the tag content.
   * An XML file with no whitespace will be faster to process, but will make 
   * your data less human readable when you come to write it out.
   *
   * Running with this option on will slow the class down, so if you want to 
   * speed up your XML, then run it through once skipping white-spaces, then
   * write out the new version of your XML without whitespace, then use the
   * new XML file with skip whitespaces turned off.
   *
   * @param $onOff (bool) (default TRUE) 
   * @see XML parser functions in PHP doc
   */
  function setSkipWhiteSpaces($onOff=TRUE) {
    $this->parseOptions[XML_OPTION_SKIP_WHITE] = $onOff;
  }
   
  /**
   * Get the node defined by the $absoluteXPath.
   *
   * @param   $absoluteXPath (string) (optional, default is 'super-root') xpath to the node.
   * @return                 (array)  The node, or FALSE if the node wasn't found.
   */
  function &getNode($absoluteXPath='') {
    if ($absoluteXPath==='/') $absoluteXPath = '';
    if (!isSet($this->nodeIndex[$absoluteXPath])) return FALSE;
    if ($this->_indexIsDirty) $this->reindexNodeTree();
    return $this->nodeIndex[$absoluteXPath];
  }

  /**
   * Get a the content of a node text part or node attribute.
   * 
   * If the absolute Xpath references an attribute (Xpath ends with @ or attribute::), 
   * then the text value of that node-attribute is returned.
   * Otherwise the Xpath is referencing a text part of the node. This can be either a 
   * direct reference to a text part (Xpath ends with text()[<nr>]) or indirect reference 
   * (a simple abs. Xpath to a node).
   * 1) Direct Reference (xpath ends with text()[<part-number>]):
   *   If the 'part-number' is omitted, the first text-part is assumed; starting by 1.
   *   Negative numbers are allowed, where -1 is the last text-part a.s.o.
   * 2) Indirect Reference (a simple abs. Xpath to a node):
   *   Default is to return the *whole text*; that is the concated text-parts of the matching
   *   node. (NOTE that only in this case you'll only get a copy and changes to the returned  
   *   value wounld have no effect). Optionally you may pass a parameter 
   *   $textPartNr to define the text-part you want;  starting by 1.
   *   Negative numbers are allowed, where -1 is the last text-part a.s.o.
   *
   * NOTE I : The returned value can be fetched by reference
   *          E.g. $text =& wholeText(). If you wish to modify the text.
   * NOTE II: text-part numbers out of range will return FALSE
   * SIDENOTE:The function name is a suggestion from W3C in the XPath specification level 3.
   *
   * @param   $absoluteXPath  (string)  xpath to the node (See above).
   * @param   $textPartNr     (int)     If referring to a node, specifies which text part 
   *                                    to query.
   * @return                  (&string) A *reference* to the text if the node that the other 
   *                                    parameters describe or FALSE if the node is not found.
   */
  function &wholeText($absoluteXPath, $textPartNr=NULL) {
    $status = FALSE;
    $text   = NULL;
    if ($this->_indexIsDirty) $this->reindexNodeTree();
    
    do { // try-block
      if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $absoluteXPath, $matches)) {
        $absoluteXPath = $matches[1];
        $attribute = $matches[3];
        if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
          $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
          break; // try-block
        }
        $text =& $this->nodeIndex[$absoluteXPath]['attributes'][$attribute];
        $status = TRUE;
        break; // try-block
      }
            
      // Xpath contains a 'text()'-function, thus goes right to a text node. If so interpret the Xpath.
      if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $absoluteXPath, $matches)) {
        $absoluteXPath = $matches[1];
 
        if (!isSet($this->nodeIndex[$absoluteXPath])) {
            $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
            break; // try-block
        }

        // Get the amount of the text parts in the node.
        $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);

        // default to the first text node if a text node was not specified
        $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;

        // Support negative indexes like -1 === last a.s.o.
        if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
        if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
          $this->_displayError("The $absoluteXPath/text()[$textPartNr] value isn't a NODE in this document.", __LINE__, __FILE__, FALSE);
          break; // try-block
        }
        $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr - 1];
        $status = TRUE;
        break; // try-block
      }
      
      // At this point we have been given an xpath with neither a 'text()' nor 'attribute::' axis at the end
      // So we assume a get to text is wanted and use the optioanl fallback parameters $textPartNr
     
      if (!isSet($this->nodeIndex[$absoluteXPath])) {
          $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
          break; // try-block
      }

      // Get the amount of the text parts in the node.
      $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);

      // If $textPartNr == NULL we return a *copy* of the whole concated text-parts
      if (is_null($textPartNr)) {
        unset($text);
        $text = implode('', $this->nodeIndex[$absoluteXPath]['textParts']);
        $status = TRUE;
        break; // try-block
      }
      
      // Support negative indexes like -1 === last a.s.o.
      if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
      if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
        $this->_displayError("The $absoluteXPath has no text part at pos [$textPartNr] (Note: text parts start with 1).", __LINE__, __FILE__, FALSE);
        break; // try-block
      }
      $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr -1];
      $status = TRUE;
    } while (FALSE); // END try-block
    
    if (!$status) return FALSE;
    return $text;
  }

  /**
   * Obtain the string value of an object
   *
   * http://www.w3.org/TR/xpath#dt-string-value
   *
   * "For every type of node, there is a way of determining a string-value for a node of that type. 
   * For some types of node, the string-value is part of the node; for other types of node, the 
   * string-value is computed from the string-value of descendant nodes."
   *
   * @param $node   (node)   The node we have to convert
   * @return        (string) The string value of the node.  "" if the object has no evaluatable
   *                         string value
   */
  function _stringValue($node) {
    // Decode the entitites and then add the resulting literal string into our array.
    return $this->_addLiteral($this->decodeEntities($this->wholeText($node)));
  }
  
  //-----------------------------------------------------------------------------------------
  // XPathEngine           ------ Export the XML Document ------                             
  //-----------------------------------------------------------------------------------------
   
  /**
   * Returns the containing XML as marked up HTML with specified nodes hi-lighted
   *
   * @param $absoluteXPath    (string) The address of the node you would like to export.
   *                                   If empty the whole document will be exported.
   * @param $hilighXpathList  (array)  A list of nodes that you would like to highlight
   * @return                  (mixed)  The Xml document marked up as HTML so that it can
   *                                   be viewed in a browser, including any XML headers.
   *                                   FALSE on error.
   * @see _export()    
   */
  function exportAsHtml($absoluteXPath='', $hilightXpathList=array()) {
    $htmlString = $this->_export($absoluteXPath, $xmlHeader=NULL, $hilightXpathList);
    if (!$htmlString) return FALSE;
    return "<pre>\n" . $htmlString . "\n</pre>"; 
  }
  
  /**
   * Given a context this function returns the containing XML
   *
   * @param $absoluteXPath  (string) The address of the node you would like to export.
   *                                 If empty the whole document will be exported.
   * @param $xmlHeader      (array)  The string that you would like to appear before
   *                                 the XML content.  ie before the <root></root>.  If you
   *                                 do not specify this argument, the xmlHeader that was 
   *                                 found in the parsed xml file will be used instead.
   * @return                (mixed)  The Xml fragment/document, suitable for writing
   *                                 out to an .xml file or as part of a larger xml file, or
   *                                 FALSE on error.
   * @see _export()    
   */
  function exportAsXml($absoluteXPath='', $xmlHeader=NULL) {
    $this->hilightXpathList = NULL;
    return $this->_export($absoluteXPath, $xmlHeader); 
  }
    
  /**
   * Generates a XML string with the content of the current document and writes it to a file.
   *
   * Per default includes a <?xml ...> tag at the start of the data too. 
   *
   * @param     $fileName       (string) 
   * @param     $absoluteXPath  (string) The path to the parent node you want(see text above)
   * @param     $xmlHeader      (array)  The string that you would like to appear before
   *                                     the XML content.  ie before the <root></root>.  If you
   *                                     do not specify this argument, the xmlHeader that was 
   *                                     found in the parsed xml file will be used instead.
   * @return                    (string) The returned string contains well-formed XML data 
   *                                     or FALSE on error.
   * @see       exportAsXml(), exportAsHtml()
   */
  function exportToFile($fileName, $absoluteXPath='', $xmlHeader=NULL) {   
    $status = FALSE;
    do { // try-block
      if (!($hFile = fopen($fileName, "wb"))) {   // Did we open the file ok?
        $errStr = "Failed to open the $fileName xml file.";
        break; // try-block
      }
      
      if ($this->properties['OS_supports_flock']) {
        if (!flock($hFile, LOCK_EX + LOCK_NB)) {  // Lock the file
          $errStr = "Couldn't get an exclusive lock on the $fileName file.";
          break; // try-block
        }
      }
      if (!($xmlOut = $this->_export($absoluteXPath, $xmlHeader))) {
        $errStr = "Export failed";
        break; // try-block
      }
      
      $iBytesWritten = fwrite($hFile, $xmlOut);
      if ($iBytesWritten != strlen($xmlOut)) {
        $errStr = "Write error when writing back the $fileName file.";
        break; // try-block
      }
      
      // Flush and unlock the file
      @fflush($hFile);
      $status = TRUE;
    } while(FALSE);
    
    @flock($hFile, LOCK_UN);
    @fclose($hFile);
    // Sanity check the produced file.
    clearstatcache();
    if (filesize($fileName) < strlen($xmlOut)) {
      $errStr = "Write error when writing back the $fileName file.";
      $status = FALSE;
    }
    
    if (!$status)  $this->_displayError($errStr, __LINE__, __FILE__, FALSE);
    return $status;
  }

  /**
   * Generates a XML string with the content of the current document.
   *
   * This is the start for extracting the XML-data from the node-tree. We do some preperations
   * and then call _InternalExport() to fetch the main XML-data. You optionally may pass 
   * xpath to any node that will then be used as top node, to extract XML-parts of the 
   * document. Default is '', meaning to extract the whole document.
   *
   * You also may pass a 'xmlHeader' (usually something like <?xml version="1.0"? > that will
   * overwrite any other 'xmlHeader', if there was one in the original source.  If there
   * wasn't one in the original source, and you still don't specify one, then it will
   * use a default of <?xml version="1.0"? >
   * Finaly, when exporting to HTML, you may pass a vector xPaths you want to hi-light.
   * The hi-lighted tags and attributes will receive a nice color. 
   * 
   * NOTE I : The output can have 2 formats:
   *       a) If "skip white spaces" is/was set. (Not Recommended - slower)
   *          The output is formatted by adding indenting and carriage returns.
   *       b) If "skip white spaces" is/was *NOT* set.
   *          'as is'. No formatting is done. The output should the same as the 
   *          the original parsed XML source. 
   *
   * @param  $absoluteXPath (string) (optional, default is root) The node we choose as top-node
   * @param  $xmlHeader     (string) (optional) content before <root/> (see text above)
   * @param  $hilightXpath  (array)  (optional) a vector of xPaths to nodes we wat to 
   *                                 hi-light (see text above)
   * @return                (mixed)  The xml string, or FALSE on error.
   */
  function _export($absoluteXPath='', $xmlHeader=NULL, $hilightXpathList='') {
    // Check whether a root node is given.
    if (empty($absoluteXpath)) $absoluteXpath = '';
    if ($absoluteXpath == '/') $absoluteXpath = '';
    if ($this->_indexIsDirty) $this->reindexNodeTree();
    if (!isSet($this->nodeIndex[$absoluteXpath])) {
      // If the $absoluteXpath was '' and it didn't exist, then the document is empty
      // and we can safely return ''.
      if ($absoluteXpath == '') return '';
      $this->_displayError("The given xpath '{$absoluteXpath}' isn't a node in this document.", __LINE__, __FILE__, FALSE);
      return FALSE;
    }
    
    $this->hilightXpathList = $hilightXpathList;
    $this->indentStep = '  ';
    $hilightIsActive = is_array($hilightXpathList);
    if ($hilightIsActive) {
      $this->indentStep = '&nbsp;&nbsp;&nbsp;&nbsp;';
    }    
    
    // Cache this now
    $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;

    ///////////////////////////////////////
    // Get the starting node and begin with the header

    // Get the start node.  The super root is a special case.
    $startNode = NULL;
    if (empty($absoluteXPath)) {
      $superRoot = $this->nodeIndex[''];
      // If they didn't specify an xml header, use the one in the object
      if (is_null($xmlHeader)) {
        $xmlHeader = $this->parseSkipWhiteCache ? trim($superRoot['textParts'][0]) : $superRoot['textParts'][0];
        // If we still don't have an XML header, then use a suitable default
        if (empty($xmlHeader)) {
            $xmlHeader = '<?xml version="1.0"?>';
        }
      }

      if (isSet($superRoot['childNodes'][0])) $startNode = $superRoot['childNodes'][0];
    } else {
      $startNode = $this->nodeIndex[$absoluteXPath];
    }

    if (!empty($xmlHeader)) { 
      $xmlOut = $this->parseSkipWhiteCache ? $xmlHeader."\n" : $xmlHeader;
    } else {
      $xmlOut = '';
    }

    ///////////////////////////////////////
    // Output the document.

    if (($xmlOut .= $this->_InternalExport($startNode)) === FALSE) {
      return FALSE;
    }
    
    ///////////////////////////////////////

    // Convert our markers to hi-lights.
    if ($hilightIsActive) {
      $from = array('<', '>', chr(2), chr(3));
      $to = array('&lt;', '&gt;', '<font color="#FF0000"><b>', '</b></font>');
      $xmlOut = str_replace($from, $to, $xmlOut);
    }
    return $xmlOut; 
  }  

  /**
   * Export the xml document starting at the named node.
   *
   * @param $node (node)   The node we have to start exporting from
   * @return      (string) The string representation of the node.
   */
  function _InternalExport($node) {
    $bDebugThisFunction = FALSE;

    if ($bDebugThisFunction) {
      $aStartTime = $this->_beginDebugFunction("_InternalExport");
      echo "Exporting node: ".$node['xpath']."<br>\n";
    }

    ////////////////////////////////

    // Quick out.
    if (empty($node)) return '';

    // The output starts as empty.
    $xmlOut = '';
    // This loop will output the text before the current child of a parent then the 
    // current child.  Where the child is a short tag we output the child, then move
    // onto the next child.  Where the child is not a short tag, we output the open tag, 
    // then queue up on currentParentStack[] the child.  
    //
    // When we run out of children, we then output the last text part, and close the 
    // 'parent' tag before popping the stack and carrying on.
    //
    // To illustrate, the numbers in this xml file indicate what is output on each
    // pass of the while loop:
    //
    // 1
    // <1>2
    //  <2>3
    //   <3/>4
    //  </4>5
    //  <5/>6
    // </6>

    // Although this is neater done using recursion, there's a 33% performance saving
    // to be gained by using this stack mechanism.

    // Only add CR's if "skip white spaces" was set. Otherwise leave as is.
    $CR = ($this->parseSkipWhiteCache) ? "\n" : '';
    $currentIndent = '';
    $hilightIsActive = is_array($this->hilightXpathList);

    // To keep track of where we are in the document we use a node stack.  The node 
    // stack has the following parallel entries:
    //   'Parent'     => (array) A copy of the parent node that who's children we are 
    //                           exporting
    //   'ChildIndex' => (array) The child index of the corresponding parent that we
    //                           are currently exporting.
    //   'Highlighted'=> (bool)  If we are highlighting this node.  Only relevant if
    //                           the hilight is active.

    // Setup our node stack.  The loop is designed to output children of a parent, 
    // not the parent itself, so we must put the parent on as the starting point.
    $nodeStack['Parent'] = array($node['parentNode']);
    // And add the childpos of our node in it's parent to our "child index stack".
    $nodeStack['ChildIndex'] = array($node['pos']);
    // We start at 0.
    $nodeStackIndex = 0;

    // We have not to output text before/after our node, so blank it.  We will recover it
    // later
    $OldPreceedingStringValue = $nodeStack['Parent'][0]['textParts'][$node['pos']];
    $OldPreceedingStringRef =& $nodeStack['Parent'][0]['textParts'][$node['pos']];
    $OldPreceedingStringRef = "";
    $currentXpath = "";

    // While we still have data on our stack
    while ($nodeStackIndex >= 0) {
      // Count the children and get a copy of the current child.
      $iChildCount = count($nodeStack['Parent'][$nodeStackIndex]['childNodes']);
      $currentChild = $nodeStack['ChildIndex'][$nodeStackIndex];
      // Only do the auto indenting if the $parseSkipWhiteCache flag was set.
      if ($this->parseSkipWhiteCache)
        $currentIndent = str_repeat($this->indentStep, $nodeStackIndex);

      if ($bDebugThisFunction)
        echo "Exporting child ".($currentChild+1)." of node {$nodeStack['Parent'][$nodeStackIndex]['xpath']}\n";

      ///////////////////////////////////////////
      // Add the text before our child.

      // Add the text part before the current child
      $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild];
      if (isSet($tmpTxt) AND ($tmpTxt!="")) {
        // Only add CR indent if there were children
        if ($iChildCount)
          $xmlOut .= $CR.$currentIndent;
        // Hilight if necessary.
        $highlightStart = $highlightEnd = '';
        if ($hilightIsActive) {
          $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+1).']';
          if (in_array($currentXpath, $this->hilightXpathList)) {
           // Yes we hilight
            $highlightStart = chr(2);
            $highlightEnd   = chr(3);
          }
        }
        $xmlOut .= $highlightStart.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild].$highlightEnd;
      }
      if ($iChildCount && $nodeStackIndex) $xmlOut .= $CR;

      ///////////////////////////////////////////

      // Are there any more children?
      if ($iChildCount <= $currentChild) {
        // Nope, so output the last text before the closing tag
        $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1];
        if (isSet($tmpTxt) AND ($tmpTxt!="")) {
          // Hilight if necessary.
          $highlightStart = $highlightEnd = '';
          if ($hilightIsActive) {
            $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+2).']';
            if (in_array($currentXpath, $this->hilightXpathList)) {
             // Yes we hilight
              $highlightStart = chr(2);
              $highlightEnd   = chr(3);
            }
          }
          $xmlOut .= $highlightStart
                .$currentIndent.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1].$CR
                .$highlightEnd;
        }

        // Now close this tag, as we are finished with this child.

        // Potentially output an (slightly smaller indent).
        if ($this->parseSkipWhiteCache
          && count($nodeStack['Parent'][$nodeStackIndex]['childNodes'])) {
          $xmlOut .= str_repeat($this->indentStep, $nodeStackIndex - 1);
        }

        // Check whether the xml-tag is to be hilighted.
        $highlightStart = $highlightEnd = '';
        if ($hilightIsActive) {
          $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'];
          if (in_array($currentXpath, $this->hilightXpathList)) {
            // Yes we hilight
            $highlightStart = chr(2);
            $highlightEnd   = chr(3);
          }
        }
        $xmlOut .=  $highlightStart
                     .'</'.$nodeStack['Parent'][$nodeStackIndex]['name'].'>'
                     .$highlightEnd;
        // Decrement the $nodeStackIndex to go back to the next unfinished parent.
        $nodeStackIndex--;

        // If the index is 0 we are finished exporting the last node, as we may have been
        // exporting an internal node.
        if ($nodeStackIndex == 0) break;

        // Indicate to the parent that we are finished with this child.
        $nodeStack['ChildIndex'][$nodeStackIndex]++;

        continue;
      }

      ///////////////////////////////////////////
      // Ok, there are children still to process.

      // Queue up the next child (I can copy because I won't modify and copying is faster.)
      $nodeStack['Parent'][$nodeStackIndex + 1] = $nodeStack['Parent'][$nodeStackIndex]['childNodes'][$currentChild];

      // Work out if it is a short child tag.
      $iGrandChildCount = count($nodeStack['Parent'][$nodeStackIndex + 1]['childNodes']);
      $shortGrandChild = (($iGrandChildCount == 0) AND (implode('',$nodeStack['Parent'][$nodeStackIndex + 1]['textParts'])==''));

      ///////////////////////////////////////////
      // Assemble the attribute string first.
      $attrStr = '';
      foreach($nodeStack['Parent'][$nodeStackIndex + 1]['attributes'] as $key=>$val) {
        // Should we hilight the attribute?
        if ($hilightIsActive AND in_array($currentXpath.'/attribute::'.$key, $this->hilightXpathList)) {
          $hiAttrStart = chr(2);
          $hiAttrEnd   = chr(3);
        } else {
          $hiAttrStart = $hiAttrEnd = '';
        }
        $attrStr .= ' '.$hiAttrStart.$key.'="'.$val.'"'.$hiAttrEnd;
      }

      ///////////////////////////////////////////
      // Work out what goes before and after the tag content

      $beforeTagContent = $currentIndent;
      if ($shortGrandChild) $afterTagContent = '/>';
      else                  $afterTagContent = '>';

      // Check whether the xml-tag is to be hilighted.
      if ($hilightIsActive) {
        $currentXpath = $nodeStack['Parent'][$nodeStackIndex + 1]['xpath'];
        if (in_array($currentXpath, $this->hilightXpathList)) {
          // Yes we hilight
          $beforeTagContent .= chr(2);
          $afterTagContent  .= chr(3);
        }
      }
      $beforeTagContent .= '<';
//      if ($shortGrandChild) $afterTagContent .= $CR;
      
      ///////////////////////////////////////////
      // Output the tag

      $xmlOut .= $beforeTagContent
                  .$nodeStack['Parent'][$nodeStackIndex + 1]['name'].$attrStr
                  .$afterTagContent;

      ///////////////////////////////////////////
      // Carry on.            

      // If it is a short tag, then we've already done this child, we just move to the next
      if ($shortGrandChild) {
        // Move to the next child, we need not go deeper in the tree.
        $nodeStack['ChildIndex'][$nodeStackIndex]++;
        // But if we are just exporting the one node we'd go no further.
        if ($nodeStackIndex == 0) break;
      } else {
        // Else queue up the child going one deeper in the stack
        $nodeStackIndex++;
        // Start with it's first child
        $nodeStack['ChildIndex'][$nodeStackIndex] = 0;
      }
    }

    $result = $xmlOut;

    // Repair what we "undid"
    $OldPreceedingStringRef = $OldPreceedingStringValue;

    ////////////////////////////////////////////

    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($aStartTime, $result);
    }

    return $result;
  }
     
  //-----------------------------------------------------------------------------------------
  // XPathEngine           ------ Import the XML Source ------                               
  //-----------------------------------------------------------------------------------------
  
  /**
   * Reads a file or URL and parses the XML data.
   *
   * Parse the XML source and (upon success) store the information into an internal structure.
   *
   * @param     $fileName (string) Path and name (or URL) of the file to be read and parsed.
   * @return              (bool)   TRUE on success, FALSE on failure (check getLastError())
   * @see       importFromString(), getLastError(), 
   */
  function importFromFile($fileName) {
    $status = FALSE;
    $errStr = '';
    do { // try-block
      // Remember file name. Used in error output to know in which file it happend
      $this->properties['xmlFile'] = $fileName;
      // If we already have content, then complain.
      if (!empty($this->nodeRoot)) {
        $errStr = 'Called when this object already contains xml data. Use reset().';
        break; // try-block
      }
      // The the source is an url try to fetch it.
      if (preg_match(';^http(s)?://;', $fileName)) {
        // Read the content of the url...this is really prone to errors, and we don't really
        // check for too many here...for now, suppressing both possible warnings...we need
        // to check if we get a none xml page or something of that nature in the future
        $xmlString = @implode('', @file($fileName));
        if (!empty($xmlString)) {
          $status = TRUE;
        } else {
          $errStr = "The url '{$fileName}' could not be found or read.";
        }
        break; // try-block
      } 
      
      // Reaching this point we're dealing with a real file (not an url). Check if the file exists and is readable.
      if (!is_readable($fileName)) { // Read the content from the file
        $errStr = "File '{$fileName}' could not be found or read.";
        break; // try-block
      }
      if (is_dir($fileName)) {
        $errStr = "'{$fileName}' is a directory.";
        break; // try-block
      }
      // Read the file
      if (!($fp = @fopen($fileName, 'rb'))) {
        $errStr = "Failed to open '{$fileName}' for read.";
        break; // try-block
      }
      $xmlString = fread($fp, filesize($fileName));
      @fclose($fp);
      
      $status = TRUE;
    } while (FALSE);
    
    if (!$status) {
      $this->_displayError('In importFromFile(): '. $errStr, __LINE__, __FILE__, FALSE);
      return FALSE;
    }
    return $this->importFromString($xmlString);
  }
  
  /**
   * Reads a string and parses the XML data.
   *
   * Parse the XML source and (upon success) store the information into an internal structure.
   * If a parent xpath is given this means that XML data is to be *appended* to that parent.
   *
   * ### If a function uses setLastError(), then say in the function header that getLastError() is useful.
   *
   * @param  $xmlString           (string) Name of the string to be read and parsed.
   * @param  $absoluteParentPath  (string) Node to append data too (see above)
   * @return                      (bool)   TRUE on success, FALSE on failure 
   *                                       (check getLastError())
   */
  function importFromString($xmlString, $absoluteParentPath = '') {
    $bDebugThisFunction = FALSE;

    if ($bDebugThisFunction) {
      $aStartTime = $this->_beginDebugFunction("importFromString");
      echo "Importing from string of length ".strlen($xmlString)." to node '$absoluteParentPath'\n<br>";
      echo "Parser options:\n<br>";
      print_r($this->parseOptions);
    }

    $status = FALSE;
    $errStr = '';
    do { // try-block
      // If we already have content, then complain.
      if (!empty($this->nodeRoot) AND empty($absoluteParentPath)) {
        $errStr = 'Called when this object already contains xml data. Use reset() or pass the parent Xpath as 2ed param to where tie data will append.';
        break; // try-block
      }
      // Check whether content has been read.
      if (empty($xmlString)) {
        // Nothing to do!!
        $status = TRUE;
        // If we were importing to root, build a blank root.
        if (empty($absoluteParentPath)) {
          $this->_createSuperRoot();
        }
        $this->reindexNodeTree();
//        $errStr = 'This xml document (string) was empty';
        break; // try-block
      } else {
        $xmlString = $this->_translateAmpersand($xmlString);
      }
      
      // Restart our node index with a root entry.
      $nodeStack = array();
      $this->parseStackIndex = 0;

      // If a parent xpath is given this means that XML data is to be *appended* to that parent.
      if (!empty($absoluteParentPath)) {
        // Check if parent exists
        if (!isSet($nodeIndex[$absoluteParentPath])) {
          $errStr = "You tried to append XML data to a parent '$absoluteParentPath' that does not exist.";
          break; // try-block
        } 
        // Add it as the starting point in our array.
        $this->nodeStack[0] =& $nodeIndex[$absoluteParentPath];
      } else {
        // Build a 'super-root'
        $this->_createSuperRoot();
        // Put it in as the start of our node stack.
        $this->nodeStack[0] =& $this->nodeRoot;
      }

      // Point our text buffer reference at the next text part of the root
      $this->parsedTextLocation =& $this->nodeStack[0]['textParts'][];
      $this->parsInCData = 0;
      // We cache this now.
      $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;
      
      // Create an XML parser.
      $parser = xml_parser_create();
      // Set default XML parser options.
      if (is_array($this->parseOptions)) {
        foreach($this->parseOptions as $key => $val) {
          xml_parser_set_option($parser, $key, $val);
        }
      }
      
      // Set the object and the element handlers for the XML parser.
      xml_set_object($parser, $this);
      xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
      xml_set_character_data_handler($parser, '_handleCharacterData');
      xml_set_default_handler($parser, '_handleDefaultData');
      xml_set_processing_instruction_handler($parser, '_handlePI');
     
      if ($bDebugThisFunction)
       $this->_profileFunction($aStartTime, "Setup for parse");

      // Parse the XML source and on error generate an error message.
      if (!xml_parse($parser, $xmlString, TRUE)) {
        $source = empty($this->properties['xmlFile']) ? 'string' : 'file ' . basename($this->properties['xmlFile']) . "'";
        $errStr = "XML error in given {$source} on line ".
               xml_get_current_line_number($parser). '  column '. xml_get_current_column_number($parser) .
               '. Reason:'. xml_error_string(xml_get_error_code($parser));
        break; // try-block
      }
      
      // Free the parser.
      @xml_parser_free($parser);
      // And we don't need this any more.
      $this->nodeStack = array();

      if ($bDebugThisFunction)
        $this->_profileFunction($aStartTime, "Parse Object");

      $this->reindexNodeTree();

      if ($bDebugThisFunction) {
        print_r(array_keys($this->nodeIndex));
      }

      if ($bDebugThisFunction)
       $this->_profileFunction($aStartTime, "Reindex Object");
      
      $status = TRUE;
    } while (FALSE);
    
    if (!$status) {
      $this->_displayError('In importFromString(): '. $errStr, __LINE__, __FILE__, FALSE);
      $bResult = FALSE;
    } else {
      $bResult = TRUE;
    }

    ////////////////////////////////////////////

    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($aStartTime, $bResult);
    }

    return $bResult;
  }
  
  
  //-----------------------------------------------------------------------------------------
  // XPathEngine               ------  XML Handlers  ------                                  
  //-----------------------------------------------------------------------------------------
  
  /**
   * Handles opening XML tags while parsing.
   *
   * While parsing a XML document for each opening tag this method is
   * called. It'll add the tag found to the tree of document nodes.
   *
   * @param $parser     (int)    Handler for accessing the current XML parser.
   * @param $name       (string) Name of the opening tag found in the document.
   * @param $attributes (array)  Associative array containing a list of
   *                             all attributes of the tag found in the document.
   * @see _handleEndElement(), _handleCharacterData()
   */
  function _handleStartElement($parser, $nodeName, $attributes) {
    if (empty($nodeName)) {
      $this->_displayError('XML error in file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
      return;
    }

    // Trim accumulated text if necessary.
    if ($this->parseSkipWhiteCache) {
      $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
      $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
    } 

    if ($this->bDebugXmlParse) {
      echo "<blockquote>" . htmlspecialchars("Start node: <".$nodeName . ">")."<br>";
      echo "Appended to stack entry: $this->parseStackIndex<br>\n";
      echo "Text part before element is: ".htmlspecialchars($this->parsedTextLocation);
      /*
      echo "<pre>";
      $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
      for ($i = 0; $i < $dataPartsCount; $i++) {
        echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
      }
      echo "</pre>";
      */
    }

    // Add a node and set path to current.
    if (!$this->_internalAppendChild($this->parseStackIndex, $nodeName)) {
      $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
      return;
    }    

    // We will have gone one deeper then in the stack.
    $this->parseStackIndex++;

    // Point our parseTxtBuffer reference at the new node.
    $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][0];
    
    // Set the attributes.
    if (!empty($attributes)) {
      if ($this->bDebugXmlParse) {
        echo 'Attributes: <br>';
        print_r($attributes);
        echo '<br>';
      }
      $this->nodeStack[$this->parseStackIndex]['attributes'] = $attributes;
    }
  }
  
  /**
   * Handles closing XML tags while parsing.
   *
   * While parsing a XML document for each closing tag this method is called.
   *
   * @param $parser (int)    Handler for accessing the current XML parser.
   * @param $name   (string) Name of the closing tag found in the document.
   * @see       _handleStartElement(), _handleCharacterData()
   */
  function _handleEndElement($parser, $name) {
    if (($this->parsedTextLocation=='') 
        && empty($this->nodeStack[$this->parseStackIndex]['textParts'])) {
      // We reach this point when parsing a tag of format <foo/>. The 'textParts'-array 
      // should stay empty and not have an empty string in it.
    } else {
      // Trim accumulated text if necessary.
      if ($this->parseSkipWhiteCache) {
        $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
        $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
      }
    }

    if ($this->bDebugXmlParse) {
      echo "Text part after element is: ".htmlspecialchars($this->parsedTextLocation)."<br>\n";
      echo htmlspecialchars("Parent:<{$this->parseStackIndex}>, End-node:</$name> '".$this->parsedTextLocation) . "'<br>Text nodes:<pre>\n";
      $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
      for ($i = 0; $i < $dataPartsCount; $i++) {
        echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
      }
      var_dump($this->nodeStack[$this->parseStackIndex]['textParts']);
      echo "</pre></blockquote>\n";
    }

    // Jump back to the parent element.
    $this->parseStackIndex--;

    // Set our reference for where we put any more whitespace
    $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][];

    // Note we leave the entry in the stack, as it will get blanked over by the next element
    // at this level.  The safe thing to do would be to remove it too, but in the interests 
    // of performance, we will not bother, as were it to be a problem, then it would be an
    // internal bug anyway.
    if ($this->parseStackIndex < 0) {
      $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
      return;
    }    
  }
  
  /**
   * Handles character data while parsing.
   *
   * While parsing a XML document for each character data this method
   * is called. It'll add the character data to the document tree.
   *
   * @param $parser (int)    Handler for accessing the current XML parser.
   * @param $text   (string) Character data found in the document.
   * @see       _handleStartElement(), _handleEndElement()
   */
  function _handleCharacterData($parser, $text) {
  
    if ($this->parsInCData >0) $text = $this->_translateAmpersand($text, $reverse=TRUE);
    
    if ($this->bDebugXmlParse) echo "Handling character data: '".htmlspecialchars($text)."'<br>";
    if ($this->parseSkipWhiteCache AND !empty($text) AND !$this->parsInCData) {
      // Special case CR. CR always comes in a separate data. Trans. it to '' or ' '. 
      // If txtBuffer is already ending with a space use '' otherwise ' '.
      $bufferHasEndingSpace = (empty($this->parsedTextLocation) OR substr($this->parsedTextLocation, -1) === ' ') ? TRUE : FALSE;
      if ($text=="\n") {
        $text = $bufferHasEndingSpace ? '' : ' ';
      } else {
        if ($bufferHasEndingSpace) {
          $text = ltrim(preg_replace('/\s+/', ' ', $text));
        } else {
          $text = preg_replace('/\s+/', ' ', $text);
        }
      }
      if ($this->bDebugXmlParse) echo "'Skip white space' is ON. reduced to : '" .htmlspecialchars($text) . "'<br>";
    }
    $this->parsedTextLocation .= $text;
  }
  
  /**
   * Default handler for the XML parser.  
   *
   * While parsing a XML document for string not caught by one of the other
   * handler functions, we end up here.
   *
   * @param $parser (int)    Handler for accessing the current XML parser.
   * @param $text   (string) Character data found in the document.
   * @see       _handleStartElement(), _handleEndElement()
   */
  function _handleDefaultData($parser, $text) {
    do { // try-block
      if (!strcmp($text, '<![CDATA[')) {
        $this->parsInCData++;
      } elseif (!strcmp($text, ']]>')) {
        $this->parsInCData--;
        if ($this->parsInCData < 0) $this->parsInCData = 0;
      }
      $this->parsedTextLocation .= $this->_translateAmpersand($text, $reverse=TRUE);
      if ($this->bDebugXmlParse) echo "Default handler data: ".htmlspecialchars($text)."<br>";    
      break; // try-block
    } while (FALSE); // END try-block
  }
  
  /**
   * Handles processing instruction (PI)
   *
   * A processing instruction has the following format: 
   * <?  target data  ? > e.g.  <? dtd version="1.0" ? >
   *
   * Currently I have no bether idea as to left it 'as is' and treat the PI data as normal 
   * text (and adding the surrounding PI-tags <? ? >). 
   *
   * @param     $parser (int)    Handler for accessing the current XML parser.
   * @param     $target (string) Name of the PI target. E.g. XML, PHP, DTD, ... 
   * @param     $data   (string) Associative array containing a list of
   * @see       PHP's manual "xml_set_processing_instruction_handler"
   */
  function _handlePI($parser, $target, $data) {
    //echo("pi data=".$data."end"); exit;
    $data = $this->_translateAmpersand($data, $reverse=TRUE);
    $this->parsedTextLocation .= "<?{$target} {$data}?>";
    return TRUE;
  }
  
  //-----------------------------------------------------------------------------------------
  // XPathEngine          ------  Node Tree Stuff  ------                                    
  //-----------------------------------------------------------------------------------------

  /**
   * Creates a super root node.
   */
  function _createSuperRoot() {
    // Build a 'super-root'
    $this->nodeRoot = $this->emptyNode;
    $this->nodeRoot['name']      = '';
    $this->nodeRoot['parentNode'] = NULL;
    $this->nodeIndex[''] =& $this->nodeRoot;
  }

  /**
   * Adds a new node to the XML document tree during xml parsing.
   *
   * This method adds a new node to the tree of nodes of the XML document
   * being handled by this class. The new node is created according to the
   * parameters passed to this method.  This method is a much watered down
   * version of appendChild(), used in parsing an xml file only.
   * 
   * It is assumed that adding starts with root and progresses through the
   * document in parse order.  New nodes must have a corresponding parent. And
   * once we have read the </> tag for the element we will never need to add
   * any more data to that node.  Otherwise the add will be ignored or fail.
   *
   * The function is faciliated by a nodeStack, which is an array of nodes that
   * we have yet to close.
   *
   * @param   $stackParentIndex (int)    The index into the nodeStack[] of the parent
   *                                     node to which the new node should be added as 
   *                                     a child. *READONLY*
   * @param   $nodeName         (string) Name of the new node. *READONLY*
   * @return                    (bool)   TRUE if we successfully added a new child to 
   *                                     the node stack at index $stackParentIndex + 1,
   *                                     FALSE on error.
   */
  function _internalAppendChild($stackParentIndex, $nodeName) {
    // This call is likely to be executed thousands of times, so every 0.01ms counts.
    // If you want to debug this function, you'll have to comment the stuff back in
    //$bDebugThisFunction = FALSE;
    
    /*
    if ($bDebugThisFunction) {
      $aStartTime = $this->_beginDebugFunction("_internalAppendChild");
      echo "Current Node (parent-index) and the child to append : '{$stackParentIndex}' +  '{$nodeName}' \n<br>";
    }
    */
     //////////////////////////////////////

    if (!isSet($this->nodeStack[$stackParentIndex])) {
      $errStr = "Invalid parent. You tried to append the tag '{$nodeName}' to an non-existing parent in our node stack '{$stackParentIndex}'.";
      $this->_displayError('In _internalAppendChild(): '. $errStr, __LINE__, __FILE__, FALSE); 

      /*