<?php
// +----------------------------------------------------------------------+
// | Php.XPath Version 2.2 stable                                         |
// +----------------------------------------------------------------------+
// | This is the major update and part rewrite of M.Mehls phpxml project  |
// | It is the product of the updates from Nigel Swinson side             |
// | branches (V1.N.x) that include a DOM like interface and              |
// | a major restucturing and rewriting done by Sam Blum as well as       |
// | contributions of others of the open source comunity.  As of October  |
// | 2001, M.Mehl recognized Php.XPath as the main branch and renaming of |
// | the original phpxml project, and phpxml has thus been expired as a   |
// | project name.                                                        |
// +----------------------------------------------------------------------+
// | 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 Initial Developer of the Original Code is Michael P. Mehl.       |
// +----------------------------------------------------------------------+
// | Main Active Authors:                                                 |
// |   Nigel Swinson <nigelswinson@users.sourceforge.net>                 |
// |     Started around 2001-07 and creator of V1.N.x branches.           |
// |   Sam Blum <bs_php@infeer.com>                                       |
// |     Started around 2001-09 major restruct and testbench initiator.   |
// |   Daniel Allen <bigredlinux@yahoo.com>                               |
// |     Started around 2001-10 working to make Php.XPath adhere to specs |
// |   fab                                                                |
// |     minor changes, especially the return of status codes instead of  |
// |     echo and exit.                                                   |
// +----------------------------------------------------------------------+
// | Main Former Authors:                                                 |
// |   Michael P. Mehl <mpm@phpxml.org>                                   |
// |     Inital creator of V 1.0. Stoped activities around 2001-03        |
// +----------------------------------------------------------------------+
// | Requires PHP version  4.0.5 and up                                   |
// +----------------------------------------------------------------------+
// | Ref:                                                                 |
// |   @link http://sourceforge.net/projects/phpxpath/   Latest release   |
// |   @link http://www.w3.org/TR/xpath   W3C XPath Recommendation        |
// +----------------------------------------------------------------------+

//////////////////////////////////////////////////////////////////////////////////
// Class for accessing XML data through the XML Path Language
// XPath Version 1.0 defined by W3C
//
// This class offers methods for accessing the nodes of a XML document using 
// the XPath language. You can add or remove children (nodes), set or modify their 
// content and their attributes. No additional PHP extensions like DOM XML 
// or something similar are required to use these features.

class XPath {
  ////////////////////////////////////////////////////////////////////////////////
  //############### Old Public Interface #######################################//
  ////////////////////////////////////////////////////////////////////////////////

  // This is the interface that has been with us from version 1.0 - 1.N.4.  It's
  // a hotch potch of radomly named junk now, so is getting renamed to DOM style
  // function names.  
  // This interface will be expired after the 1.N.5, at first warning you and then
  // later stopping execution when still used. *So update your code*.
  // To turn warning/aborting on/off toggle the following flags.
  // But be warned: The interface will be flushed soon.
  var $deprecate_1N4_warning = TRUE;
  var $deprecate_1N4_abort = TRUE;

  /**
   * @deprecated  Use XPath() instead
   */
  function XML($fileName = '') {
    if ($this->deprecate_1N4_warning) {
      $this->_displayError("Class name 'XML' is deprecated sinc V1.N.5. Use XPath() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->XPath($fileName);
  }
  
  /**
   * @deprecated  Use importFromFile() instead
   */
  function load_file($fileName) {
    if ($this->deprecate_1N4_warning) {
      $this->_displayError("Methode 'load_file' is deprecated sinc V1.N.5. Use importFromFile() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->importFromFile($fileName);
  }
  
  /**
   * @deprecated  Use importFromString() instead
   */
  function load_string($xmlString) {
    if ($this->deprecate_1N4_warning) {
      $this->_displayError("Methode 'load_string' is deprecated sinc V1.N.5. Use importFromString() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->importFromString($xmlString);
  }
  
  /**
   * Given a context this function returns the containing XML
   *
   * This method takes a context, which is derived from the evaluate 
   * function, and it returns the XML that is contained within this
   * node
   *
   * @deprecated Use exportAsXml() instead
   * @param     array absoluteXPathArray an array of absolute XPath 
   *            addresses to nodes.
   * @param     int $index which of the results of the absoluteXPathArray 
   *            to use.
   * @return    string The string returned is valid XML
   * @see       exportAsXml(), evaluate()
   */
  function grab($absoluteXPathArray = '', $index = 0) {
    if ($this->deprecate_1N4_warning) {
      $this->_displayError("Methode 'grab' is deprecated sinc V1.N.5. Use exportAsXml() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->exportAsXml($absoluteXPathArray[$index]);
  }
   
  /**
   * @deprecated  Use exportAsXml() instead
   */
  function get_as_xml_file() {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'get_as_xml_file' is deprecated sinc V1.N.5. Use exportAsXml() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return '<?xml version="1.0"?>' . "\n" . $this->exportAsXml();
  }
   
  /**
   * @deprecated  Use exportAsHtml() instead
   */
  function get_as_html_file($highlight = array()) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'get_as_html_file' is deprecated sinc V1.N.5. Use exportAsHtml() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->exportAsHtml('', $highlight);
  }
    
  /**
   * @deprecated  Use appendChild() instead
   */
  function add_node($absoluteParentPath, $nodeName) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'add_node' is deprecated sinc V1.N.5. Use appendChild() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->appendChild($absoluteParentPath, $nodeName);
  }

  /**
   * @deprecated  Use removeChild() instead
   */
  function remove_node($absoluteXPath) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'remove_node' is deprecated sinc V1.N.5. Use removeChild() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->removeChild($absoluteXPath);
 }
  
  /**
   * @deprecated  Use appendData() instead
   */
  function add_content($absoluteXPath, $value ) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'add_content' is deprecated sinc V1.N.5. Use appendData() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    $this->_setContent(&$absoluteXPath, &$value, TRUE );
  }
  
  /**
   * @deprecated  Use replaceData() and appendData() instead
   */
  function set_content($absoluteXPath, $value, $append=FALSE) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'set_content' is deprecated sinc V1.N.5. Use replaceData() and appendData() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->_setContent(&$absoluteXPath, &$value, $append);
  }
  
  /**
   * @deprecated  Use substringData() instead
   */
  function get_content($absoluteXPath) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'get_content' is deprecated sinc V1.N.5. Use getData() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->substringData($absoluteXPath);
  }
  
  /**
   * Add attributes to a node.
   *
   * This method adds attributes to a node. Existing attributes *WILL BE*
   * overwritten unless $overwrite is set to FALSE.
   *
   * @deprecated  Use setAttributes() instead
   * @param     string $absoluteXPath Full document path of the node, the 
   *            attributes should be added to.  *READONLY*
   * @param     array $attributes Associative array containing the new
   *            attributes for the node.  *READONLY*
   * @param     bool $overwrite TRUE (=default): overwite attibutes / 
   *            FALSE:  Will not overwite existing attibutes *READONLY*
   * @see       getAttributes(), setAttributes(), removeAttributes()
   */
  function add_attributes($absoluteXPath, $attributes, $overwrite = TRUE) {
    if ($this->deprecate_1N4_warning) {
      $this->_displayError("Methode 'add_attributes' is deprecated sinc V1.N.5. Use setAttributes() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);    
    }
    // If overwrite is not set, then we must make sure that we don't overwrite any of
    // the existing attributes with the same name.
    if (!$overwrite) {
      $aExistingAttributes = $this->getAttributes($absoluteXPath);
      $aExistingAttributes = array_intersect($aExistingAttributes, $attributes);
      $aNewAttributes = $attributes;
      foreach($aExistingAttributes as $name => $value)
        unset($aNewAttributes[$name]);
      return $this->setAttributes($absoluteXPath, $aNewAttributes);
    } else {
      return $this->setAttributes($absoluteXPath, $attributes);
    }
  } 
  
  /**
   * Sets the attributes of a node.
   *
   * This method sets the attributes of a node and overwrites all existing
   * attributes by doing this.
   *
   * @deprecated  Use setAttributes() instead
   * @param     string $absoluteXPath Full document path of the node, 
   *            the attributes of which should be set. *READONLY*
   * @param     array $attributes Associative array containing the new
   *            attributes for the node. *READONLY*
   * @see       getAttributes(), setAttributes(), removeAttributes()
   */
  function set_attributes ($absoluteXPath, $attributes) {
    // Set the attributes of the node.
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'set_attributes' is deprecated sinc V1.N.5. Use setAttributes() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    if (!is_array($attributes)) return;    

    // Remove the existing attributes
    if ($overwrite) $this->removeAttributes(array_keys($this->getAttributes()));
    return $this->setAttributes($absoluteXPath, $attributes);
  }
    
  /**
   * @deprecated  Use getAttributes() instead
   */
  function get_attributes($absoluteXPath) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'get_attributes' is deprecated sinc V1.N.5. Use getAttributes() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->getAttributes($absoluteXPath);
  }
    
  /**
   * @deprecated  Use nodeName() instead
   */
  function get_name($absoluteXPath) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'get_name' is deprecated sinc V1.N.5. Use nodeName() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->nodeName($absoluteXPath);
  }
  
  
  /**
   * @deprecated  Use nodeName() instead
   */
  function get_names($absoluteXPaths) {
    if ($this->deprecate_1N4_warning) {      
      $this->_displayError("Methode 'get_names' is deprecated sinc V1.N.5. Use nodeName() instead and read the doc.", __LINE__, $this->deprecate_1N4_abort);
    }
    return $this->nodeName($absoluteXPaths);
  }
  
  ////////////////////////////////////////////////////////////////////////////////
  //#################### Public Interface ######################################//
  ////////////////////////////////////////////////////////////////////////////////
  
  /////////////////////////////////////////////////
  // ########################################### //
  // Constructor of the class.
  
  /**
   * Constructor of the class
   *
   * This constructor initializes the class and, when a filename is given,
   * tries to read and parse the given file.
   * You may also set the XML parsing parameters with an array. 
   * E.g. $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE);
   *
   * @param     $fileName string  Path and name of the file to read and parsed.
   * @param     $userXmlOptions array vector array of (<optionID>=><value>, <optionID>=><value>, ...)
   * @see       importFromFile(), importFromString()
   */
  function XPath($fileName='', $userXmlOptions=array()) {
    // Set the options for parsing the XML data.
    // Per default we want to keep spaces in the CDATA, as most people generally want to 
    // keep space, and you can call trim() but there's no untrim() is there? Francis Fillion <francisf@videotron.ca>
    $this->xmlOptions[XML_OPTION_CASE_FOLDING] = FALSE;
    $this->xmlOptions[XML_OPTION_SKIP_WHITE] = FALSE;
    // Don't use PHP's array_merge!
    reset($userXmlOptions);
    while (list($key) = each($userXmlOptions)) {
      $this->xmlOptions[$key] = $userXmlOptions[$key];
    }
    
    // Check whether a file was given.
    if (!empty($fileName)) {
      // Load the XML file.
      $this->importFromFile($fileName);
    }
  }
  
  /**
  * Returns the property/ies you want.
  * 
  * if $param is not given, all properties will be returned in a hash.
  * the following params are allowed:
  * - fileName
  * - hasContent
  * - caseFolding
  * - skipWhiteSpaces
  *
  * @author fab, sam
  * @param  string $param
  * @return mixed (string OR hash of all params)
  * @throws NULL on an unknown param.
  */
  function getProperties($param=NULL) {
    $properties = NULL;
    if (empty($properties)) {
      $properties = array(
        'fileName'        => $this->fileName, 
        'hasContent'      => $this->_objectHasContent(),
        'caseFolding'     => $this->xmlOptions[XML_OPTION_CASE_FOLDING],
        'skipWhiteSpaces' => $this->xmlOptions[XML_OPTION_SKIP_WHITE],
        'verboseLevel'    => $this->_verboseLevel
      );
    }
    
    if (empty($param)) return $properties;
    
    if (isSet($properties[$param])) {
      return $properties[$param];
    } else {
      return NULL;
    }
  }
  
  /**
  * returns the last occured error message.
  * @author fab
  * @access public
  * @return string (may be empty if there was no error at all)
  * @see    _setLastError(), _lastError
  */
  function getLastError() {
    return $this->_lastError;
  }
  
  
  /////////////////////////////////////////////////
  // ########################################### //
  // Input
  /**
   * Controls whether case-folding is enabled for this XML parser.
   *
   * In other words, when it comes to XML, case-folding simply means uppercasing
   * all tag- and attribute-names (NOT the content) if set to TRUE.
   *
   * @author    Sam Blum 
   * @param     $onOff bool (default TRUE) 
   */
   function setCaseFolding($onOff=TRUE) {
     $this->xmlOptions[XML_OPTION_CASE_FOLDING] = $onOff;
   }
   
  /**
   * Controls whether skip-white-spaces is enabled for this XML parser.
   *
   * In other words, when it comes to XML, skip-white-spaces will trim
   * the tag content (=the CDATA) 
   *
   * @author    Sam Blum 
   * @param     $onOff bool (default TRUE) 
   */
   function setSkipWhiteSpaces($onOff=TRUE) {
     $this->xmlOptions[XML_OPTION_SKIP_WHITE] = $onOff;
   }
   
  /**
   * xml_parser_set_option -- set options in an XML parser.
   *
   * See 'XML parser functions' in PHP doc
   *
   * @author    Sam Blum 
   * @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->xmlOptions[$optionID] = $value;
   }
   
  /**
   * Turn verbose (error) output ON or OFF
   * Pass a bool. TRUE to turn on, FALSE to turn off.
   * Pass a int >0 to reach higher levels of verbosity (for future use).
   *
   * @author    Sam Blum 
   * @param     mixed $levelOfVerbosity  (default is 0 = off)
   */
   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->_verboseLevel = $levelOfVerbosity;
   }
   

  /**
   * Reads a file and parses the XML data.
   *
   * This method reads the content of a XML file, tries to parse its
   * content and upon success stores the information retrieved from
   * the file into an array.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>, changes by fab
   * @param     string $fileName Path and name of the file to be read and parsed.
   * @return    bool TRUE on success, FALSE on failure (check getLastError())
   * @see       importFromString(), _getLastError(), 
   */
  function importFromFile($fileName) {
    // Remember file name. Used in error output to know in which file it happend
    $this->fileName = $fileName;
    // If we already have content, then complain.
    if ($this->_objectHasContent()) {
      $this->_displayError('importFromFile() called when this object already contains xml data. Use reset().', __LINE__, FALSE);
      return FALSE;
    }
    // Check whether the url exists or if the file exists and is readable.
    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
      $content = @implode('', @file($fileName));
      if ( empty($content) ) {
        // Display an error message.
        $this->_displayError("In importFromFile(): The url '{$fileName}' could not be found or read.", __LINE__, FALSE);
        return FALSE;
      }
    } 
    elseif (!is_readable($fileName)) { // Read the content from the file
      $this->_displayError("In importFromFile(): File '{$fileName}' could not be found or read.", __LINE__, FALSE);
      return FALSE;
    }
    elseif (is_dir($fileName)) {
      $this->_displayError("In importFromFile(): '{$fileName}' is a directory.", __LINE__, FALSE);
      return FALSE;
    }    
    // Read the content of the file.
    $content = implode("", file($fileName));
    return $this->importFromString(&$content);
  }
  
  /**
   * Reads a string and parses the XML data.
   *
   * This method reads the content of a XML string, tries to parse its
   * content and upon success stores the information retrieved from
   * the string into an array.
   *
   * @author    Francis Fillion <ffillion@infotopie.com> modified from Michael P. Mehl <mpm@phpxml.org>, changes by fab
   * @param     string $xmlString name of the string to be read and parsed.
   * @return    bool TRUE on success, FALSE on failure (check getLastError())
   * @see       _handleStartElement(), _handleEndElement(),
   *            _handleCharacterData(), importFromFile()
   */
  function importFromString($xmlString) {
    // If we already have content, then complain.
    if ($this->_objectHasContent()) {
      $this->_displayError('importFromString() called when this object already contains xml data. Use reset().', __LINE__, FALSE);
      return FALSE;
    }
    // Check whether content has been read.
    if (empty($xmlString)) {
      $this->_displayError('This xml document (string) was empty', __LINE__, FALSE);
      return FALSE;
    }
    // Create an XML parser.
    $parser = xml_parser_create();
    
    // Set default XML parser options.
    if (is_array($this->xmlOptions)) {
      reset($this->xmlOptions);
      while (list($k) = each($this->xmlOptions)) {
        xml_parser_set_option($parser, $k, $this->xmlOptions[$k]);
      }
    }
    
    // Set the object for the parser.
    xml_set_object($parser, &$this);
    // Set the element handlers for the parser.
    xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
    xml_set_character_data_handler($parser, '_handleCharacterData');
    xml_set_default_handler($parser, '_handleDefaultData');

    // Set the document node.
    $this->nodes['']['xml-declaration'] = '';
    $this->nodes['']['dtd-declaration'] = '';

    // Parse the XML file.
    if (!xml_parse($parser, $xmlString, TRUE)) {
      //error
      $source = empty($this->fileName) ? 'string' : 'file ' . basename($this->fileName) . "'";
	    $this->_displayError("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)), __LINE__ );
      return FALSE;
    }
    
    // Free the parser.
    xml_parser_free($parser);
    return TRUE;
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Output

  /**
   * Given a context this function returns the containing XML as marked up HTML
   *
   * This method takes the absolute path to a node in the XML object
   * which is derived from the evaluate function, and it returns the 
   * XML that is contained within this node as a string as marked up
   * html suitable for outputing inline to an HTML file for display or
   * debugging reasons.  So <> etc are replaced by &lt; and &gt;
   * NOTE: Instead of an absolute path you may also pass a xpath query.
   *       If the result of the query leads to 1 unique node, then the
   *       path to that node is taken.
   *
   * @author    Nigel Swinson <nigelswinson@users.sourceforge.net>
   * @param     string $absoluteXPath The path to the current node (see text above)
   * @return    string The string returned is valid XML
   * @throws    FALSE on error;
   * @see       exportAsXml(), exportToFile(), evaluate()
   */
  function &exportAsHtml($absoluteXPath = '', $highlight = array()) {
    // Numpty check
    if (!isSet($this->nodes[$absoluteXPath])) {
      // Try to evaluate the $absoluteXPath; if it returns only 1 node use it; otherwise give up.
      $resultArr = $this->match($absoluteXPath);
      if (sizeOf($resultArr)==1) {
        $absoluteXPath = $resultArr[0];
      } else {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $absoluteXPath), __LINE__, FALSE);
        return FALSE;
      }
    }
    
    if (is_array($absoluteXPath)) {
        $this->_displayError('exportAsHtml() called with array as $absoluteXPath parameter.  '.
                                        'This is not supported', __LINE__, FALSE);
        return FALSE;
    }
    $level = 0;
    if (!empty($absoluteXPath)) {
      // Check that they gave us the path to one of the nodes.
      if (!isSet($this->nodes[$absoluteXPath])) {
        // Display an error message.
        $this->_displayError('exportAsHtml() called with $absoluteXPath parameter that does not '.
                    'describe a single node in the XML object.', __LINE__, FALSE);
        return FALSE;
      }
      $level = $this->nodes[$absoluteXPath]['doc-pos'];
    }
    return $this->_export($highlight, $absoluteXPath, $level, 0);
  }

  /**
   * Given a context this function returns the containing XML
   *
   * This method takes the absolute path to a node in the XML object
   * which is derived from the evaluate function, and it returns the 
   * XML that is contained within this node as a string.
   * NOTE: Instead of an absolute path you may also pass a xpath query.
   *       If the result of the query leads to 1 unique node, then the
   *       path to that node is taken.
   *
   * @author    Nigel Swinson <nigelswinson@users.sourceforge.net>
   * @param     string $absoluteXPath The path to the current node (see text above)
   * @return    string The string returned is valid XML
   * @throws    FALSE on error;
   * @see       exportAsHtml(), exportToFile(), evaluate()
   */
  function &exportAsXml($absoluteXPath = '') {
    // Numpty check
    if (!isSet($this->nodes[$absoluteXPath])) {
      // Try to evaluate the $absoluteXPath; if it returns only 1 node use it; otherwise give up.
      $resultArr = $this->match($absoluteXPath);
      if (sizeOf($resultArr)==1) {
        $absoluteXPath = $resultArr[0];
      } else {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $absoluteXPath), __LINE__, FALSE);
        return FALSE;
      }
    }
    
    $level = 0;
    if (!empty($absoluteXPath)) {
      $level = $this->nodes[$absoluteXPath]['doc-pos'];
    }
    return $this->_export(array(), $absoluteXPath, $level, 1);
  }

  /**
   * Generates a XML file with the content of the relevant portion of the current document.
   *
   * This method creates a string containing the XML data being read
   * and modified by this class before. This string can be used to save
   * a modified document back to a file or doing other nice things with
   * it.  It encludes a <?xml ...> tag at the start of the data too.
   * NOTE: Instead of an absolute path you may also pass a xpath query.
   *       If the result of the query leads to 1 unique node, then the
   *       path to that node is taken.
   *
   * @author    Nigel Swinson <nigelswinson@users.sourceforge.net>
   * @param     string $fileName
   * @param     string $absoluteXPath The path to the current node (see text above)
   * @param     string $xmlHeader ( default is '< ? xml version="1.0" ? >' )
   * @return    string The returned string contains well-formed XML data
   *            representing the content of this document suitable for
   *            writing out to a file.
   * @throws    FALSE on error;
   * @see       importFromFile(), evaluate(), exportAsHtml()
   */
  function exportToFile($fileName, $absoluteXPath='', $xmlHeader='<?xml version="1.0"?>') {   
    // Numpty check
    if (!isSet($this->nodes[$absoluteXPath])) {
      // Try to evaluate the $absoluteXPath; if it returns only 1 node use it; otherwise give up.
      $resultArr = $this->match($absoluteXPath);
      if (sizeOf($resultArr)==1) {
        $absoluteXPath = $resultArr[0];
      } else {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $absoluteXPath), __LINE__, FALSE);
        return FALSE;
      }
    }
    
    // Open the file and we'll write out to it.
    $hFile = fopen($fileName, "w");
    
    // Did we open the file ok?
    $bResult = TRUE;
    if (!$hFile) {
      $this->_displayError("Failed to open the $fileName xml file.", __LINE__, FALSE);
      $bResult = FALSE;		
    } else {
      // Lock the file
    if (!flock($hFile, LOCK_EX)) {
      $this->_displayError("Couldn't get an exclusive lock on the $fileName file.", __LINE__, FALSE);
      fclose($hFile);
      return FALSE;
    }

    // setup the header, either by what came from the dom object or the default one
    if ( preg_match(";<\?xml.*?\?>;",$this->nodes['']['xml-declaration']) )
      $xmlHeader = trim($this->nodes['']['xml-declaration']);
    if ( trim($this->nodes['']['dtd-declaration']) != '' )
      $xmlHeader .= "\n".trim($this->nodes['']['dtd-declaration']);
    // Get the relevant object as a string and write it to file
    $xmlString = $this->exportAsXml($absoluteXPath);
    if (!fwrite($hFile, $xmlHeader."\n".$xmlString)) {
      $this->_displayError("Write error when writing back the $fileName file.", __LINE__, FALSE);
      $bResult = FALSE;		
    }

      // Flush and unlock the file
      fflush($hFile);
      flock($hFile, LOCK_UN);

      if (!fclose($hFile)) {
        $this->_displayError("Failed to close the $fileNamefile.", __LINE__, FALSE);
        $bResult = FALSE;		
      }
    }
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Search.
  
  /**
   * Evaluates an XPath expression.
   *
   * This method tries to evaluate an XPath expression by parsing it. A
   * XML document has to be read before this method is able to work.
   *
   * @param     string $xPathQuery XPath expression to be evaluated.
   * @param     string $context Full path of a document node, starting
   *            from which the XPath expression should be evaluated.
   * @return    array The returned array contains a list of the full
   *            document paths of all nodes that match the evaluated
   *            XPath expression.
   * @throws    FALSE on error;
   */
  function &match($xPathQuery, $baseXPath='') {
    return $this->evaluate($xPathQuery, $baseXPath);
  }
  
  /**
   * Alias for the match function
   * 
   * @see       evaluate()
   */
  function &evaluate($xPathQuery, $baseXPath='') {
    // Starting point of the user sending an xPath query
    static $slashes2descendant = array('//@'=>'/descendant::*/attribute::', '//'=>'/descendant::', '/@'=>'/attribute::');
    
    if (empty($xPathQuery)) return array();

    // Numpty check
    if (!empty($baseXPath) && !isSet($this->nodes[$baseXPath])) {
      $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'],$absoluteXPath), __LINE__, FALSE);
      return FALSE;
    }
    
    // Convert all entities.
    $xPathQuery = strtr($xPathQuery, array_flip($this->entities));
   
    // Replace a double slashes, because they'll cause problems otherwise.
    $xPathQuery = strtr($xPathQuery, $slashes2descendant);
    
    /* By Dan Allen
      I know the evaluate function is under review right now and I realize
      that it is very complex...however, I thought of one out that should
      definitely be in place...if a user specifies the full path
      like 
          /root[1]/node[1]/child[1]
      it should just return immediately
        if(in_array($xPathQuery,array_keys($this->nodes))) {
          return array($xPathQuery);
        }
      why even look?
    */
    
    // Stupid idea from W3C to take axes name containing a '-' (dash)
    // Instead of the '-' in the names we use '_'.
    $xPathQuery = strtr($xPathQuery, $this->dash2underscoreHash);
    
    return $this->_internalEvaluate($xPathQuery, $baseXPath);
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Element Name access

  /**
   * Retrieves the names of a group of document nodes.
   *          
   * This method retrieves the names of a group of document nodes
   * specified in the argument.  So if the argument was '/A[1]/B[2]' then it
   * would return 'B' if the node did exist in the tree.
   *          
   * @param     array or string $absoluteXPath Array or single full document 
   *            path(s) of the node(s), from which the names should be 
   *            retrieved.
   * @return    array or string The returned array contains either an array 
   *            of the names of the specified nodes, or just the individual
   *            name.
   */
  function &nodeName($absoluteXPath) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction = FALSE;
    
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction('nodeName');
      echo "Node: $absoluteXPath\n";
      echo "<hr>";
    }

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

    // Did they ask for more than one name?
    $parmIsString = FALSE;
    $paths = null;
    if (is_string($absoluteXPath)) {
      $parmIsString = TRUE;
      $paths[] = $absoluteXPath;
    } else {
      $paths = &$absoluteXPath;
    }
    
    // Build the results array
    $names = array();
    $size = sizeOf($paths);
    // Get each name in turn.
    for ($i=0; $i<$size; $i++) {
      $path = &$paths[$i];
      // Numpty check
      // Check that the path exists
      if (isSet($this->nodes[$path])) {
        $names[] = $this->nodes[$path]['name'];
      } else {
        $this->_displayError("The path '$path' isn't a path of this class.", __LINE__, FALSE);                                
      }
    }
    if ($parmIsString) {
      if (sizeOf($names)) $result = $names[0]; else $result = '';
    } else {
      $result = $names;
    }

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

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

  /////////////////////////////////////////////////
  // ########################################### //
  // Attribute modification

  /**
   * Retrieves a list of all attributes of a node.
   *
   * This method retrieves a list of all attributes of the node specified in
   * the argument.  If you only want the value of 1 attribute, then you may
   * specify that attribute as a parameter
   *
   * @param     string $absoluteXPath Full document path of the node, from 
   *            which the list of attributes should be retrieved. *READONLY*
   * @param     string $attribute the name of the attribute that you wish to
   *            retrieve or empty if you wish to retrive all of the attributes
   *            in an associative array. *READONLY*
   * @return    array or string The returned associative array contains the all
   *            attributes of the specified node, or the individual $attribute
   *            if that parameter was specified.
   * @throws    FALSE on error;
   * @see       removeAttribute()
   */
  function getAttributes($absoluteXPath, $attribute = '') {
    // Numpty check
    if (!isSet($this->nodes[$absoluteXPath])) {
      // Try to evaluate the $absoluteXPath; if it returns only 1 node use it; otherwise give up.
      $resultArr = $this->match($absoluteXPath);
      if (sizeOf($resultArr)==1) {
        $absoluteXPath = $resultArr[0];
      } else {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $absoluteXPath), __LINE__, FALSE);
        return FALSE;
      }
    }
    
    // Check that there is a node.
    if (!isSet($this->nodes[$absoluteXPath])) {
      if (is_array($attribute))  return array();
      else                      return '';
    }
    // Get the attributes of the node.
    $aAttributes = isset($this->nodes[$absoluteXPath]['attributes'])
                      ? $this->nodes[$absoluteXPath]['attributes'] 
                      : array();
    // Return the complete list or just the desired element
    if (empty($attribute) or is_array($attribute))
      return $aAttributes;
    else 
      return $aAttributes[$attribute];
  }

  /**
   * Set attributes to a node.
   *
   * This method sets a number of attributes.  Existing attributes
   * overwritten with the new values, but existing attributes will not be
   * overwritten.
   *
   * @param     string $xPathQuery Full document path of the node, the attributes
   *            should be added to.
   * @param     array $attributes Associative array containing the new
   *            attributes for the node
   * @see       getAttribute(), removeAttribute()
   */
  function setAttribute($absoluteXPath, $name, $value) {
    return $this->setAttributes($absoluteXPath, array($name => $value));
  }

  /**
   * Version of setAttribute() that sets multiple attributes.
   *
   * NOTE: Instead of an absolute path you may also pass a xpath-query.
   *       All nodes that match the xpath query will be modified.
   *
   * @param     string $xPathQuery The path to the current node (see text above)
   * @param     array  $attributes associative array of attributes to set.
   * @see              setAttribute()
   */
  function setAttributes($xPathQuery, $attributes) {
    // The attributes parameter should be an associative array.
    if (!is_array($attributes)) return;
    
    // Numpty check
    $xpv = array();
    if (isSet($this->nodes[$xPathQuery])) {
      $xpv[] = &$xPathQuery;
    } else {
      // Try to evaluate the $xPathQuery; if it returns only 1 node use it; otherwise give up.
      $xpv = &$this->match($xPathQuery);
      if (sizeOf($xpv) ==0) {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery), __LINE__);
      }
    }
    
    $xpvSize = sizeOf($xpv);
    for ($k=0; $k < $xpvSize; $k++) {
      $absoluteXPath = &$xpv[$k];
      // Add the attributes to the node.
      if (isSet($this->nodes[$absoluteXPath]['attributes'])) {
        $this->nodes[$absoluteXPath]['attributes'] =
        array_merge($this->nodes[$absoluteXPath]['attributes'],$attributes);
      } else {
        $this->nodes[$absoluteXPath]['attributes'] = $attributes;
      }
    } // END for ($k=0; $k < $xpvSize; $k++) 
  }


  /**
   * Removes an attribute of a node.
   *
   * This method removes either a single, or a group of attributes from a node.
   *
   * @param     string $absoluteXPath Full document path of the node, from 
   *            which the list of attributes should be retrieved. *READONLY*
   * @param     string or array $attribute the name or names of the attribute(s)
   *            that you wish to remove.  If $attribute is empty, then all
   *            attributes will be removed for the node. *READONLY*
   * @see       getAttribute(), setAttribute()
   */
  function removeAttribute($absoluteXPath, $attribute = '') {
    // Numpty check
    if (!isSet($this->nodes[$absoluteXPath])) {
      // Try to evaluate the $absoluteXPath; if it returns only 1 node use it; otherwise give up.
      $resultArr = $this->match($absoluteXPath);
      if (sizeOf($resultArr)==1) {
        $absoluteXPath = $resultArr[0];
      } else {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $absoluteXPath), __LINE__);
      }
    }
    
    // If the attribute parameter wasn't set then remove all the attributes
    if (!isSet($attribute)) {
      unset($this->nodes[$absoluteXPath]['attributes']);
      return;
    }
    
    // If the attribute parameter isn't an array then we have just to remove the 
    // one attribute
    if (!is_array($attribute)) {
      if (isset($this->nodes[$absoluteXPath]['attributes']))
        unset($this->nodes[$absoluteXPath]['attributes'][$attribute]);
      return;
    }
    
    // Remove all the elements in the array then.
    foreach($attribute as $name) {
      unset($this->nodes[$absoluteXPath]['attributes'][$name]);
    }
    return;
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Element Content modification

  /**
   * Retrieve all the text from a node as a single string.
   *
   * Simplified wholeText(). This function is not actually in any spec and is 
   * therefore more of a helper "shortcut."
   *
   * @author    Sam Blume <bs_php@infeer.com> and Daniel Allen <bigredlinux@yahoo.com>
   * @param     string $absoluteXPath Full document path of the node, from 
   *            which the text should be retrieved (any text() will be truncated). *READONLY*
   * @return    string The returned string contains either the value or the
   *            character data of the node.  If the node had mixed data, it concats all the
   *            text nodes into a single string
   * @see       wholeText()
   */
  function &getData($absoluteXPath) {
    // If this node had a text() node specified at the end, we want to truncate
    // text() part of the XPath, since this function is intended primarily for novice users
    if ( preg_match(":(.*)/text\(\)(\[(.*)\])?$:U",$absoluteXPath) ) {
      $absoluteXPath = substr($absoluteXPath,0,strrpos($absoluteXPath,"/text()"));
    }
    return $this->wholeText($absoluteXPath);
  }
  
  /**
   * Retrieves the normalized text of a node as an array of all the text() node contents.
   *
   * Retrieves the text content of a node as an array, where each element
   * of the array was interrupted by a non-text child element.  So if the node
   * was <a>1<b>2</b>3<c/>4</a> Then getDataParts('a[1]') would return ('1','3','4').
   * If the xpath terminated as a text() node, it will be truncated since this function
   * is intended primarily for novice users.
   *
   * @author    Sam Blume <bs_php@infeer.com> and Daniel Allen <bigredlinux@yahoo.com>
   * @param     string $absoluteXPath Full document path of the node, from 
   *            which the content should be retrieved as an array (xpath ending in a text()
   *            node will be truncated). *READONLY*
   * @return    array The returned array contains strings that are either the value or the
   *            character data of the node.
   * @throws    FALSE on error;
   * @see       getData(), wholeText()
   */
  function &getDataParts($absoluteXPath) {
    $text = array();
    // If this node had a text() node specified at the end, we want to truncate
    // text() part of the XPath, since this function is intended primarily for novice users
    if ( preg_match(":(.*)/text\(\)(\[(.*)\])?$:U",$absoluteXPath) ) {
      $absoluteXPath = substr($absoluteXPath,0,strrpos($absoluteXPath,"/text()"));
    }
    // Numpty check
    if (!isSet($this->nodes[$absoluteXPath])) {
      // Try to evaluate the $absoluteXPath; if it returns only 1 node use it; otherwise give up.
      $resultArr = $this->match($absoluteXPath);
      if (sizeOf($resultArr)==1) {
        $absoluteXPath = $resultArr[0];
      } else {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $absoluteXPath), __LINE__, FALSE);
        return FALSE;
      }
    }
    // Return the cdata of the node.
    foreach($this->nodes[$absoluteXPath]["text"] as $textNode)
    {
      $text[] = $textNode;
    }
    return $text;
  }

  /**
   * Retrieves and concats the contents of all the text nodes for a given parent
   *
   * In the recommendation for the XPath specification level 3, W3C makes the suggestion
   * for a function called wholeText() which returns all of the content of the text nodes
   * for a particular parent XPath, so as to eliminate only the non-text child nodes and
   * return the data...what I am not sure of, is if this is supposed to be recursive into
   * the text of the child nodes along side the text nodes of the original parent
   *
   * @author   Daniel Allen <bigredlinux@yahoo.com>
   * @param    string $absoluteXPath Full document path of the node, from 
   *           which the content should be retrieved. *READONLY*
   * @return   string The returned string contains the concat cdata of all of the
               text nodes
   * @see      getData(), getDataParts(), wholeText(), substringData()
  **/
  function wholeText($absoluteXPath) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction= FALSE;

    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction('wholeText');
      echo "Node: $absoluteXPath\n";
      echo "<hr>";
    }

    // Try block
    do {
      // if a specific text node was given, we actually just want the substringData function
      if ( preg_match(":text\(\)(\[\d*\])?$:",$absoluteXPath) ) {
        if ($bDebugThisFunction) echo "The xpath expression contained a :text() function.\n";
        $result = $this->substringData($absoluteXPath);
        break;
      }

      // if an attribute node was given, we actually just want the substringData function
      if ( preg_match(";(.*)/(attribute::|@)([^/]*)$;U",$absoluteXPath) ) {
        if ($bDebugThisFunction) echo "The xpath expression pointed to an attribute node.\n";
        $result = $this->substringData($absoluteXPath);
        break;
      }

      // Must have been just a node then.

      // Does it point direct to a single node?
      if ( !isSet($this->nodes[$absoluteXPath]) ) {
        if ($bDebugThisFunction) echo "The xpath expression isn't a node in the tree\n".
                            "Checking to see if it is an XPath expression for a single node.\n";
        // Try to evaluate the absoluteXPath (since it really isn't an absolutePath)
        $resultArr = $this->match($absoluteXPath);
        if ( sizeOf($resultArr) == 1 ) {
          $absoluteXPath = $resultArr[0];
        } 
        else {
          $this->_displayError("The $absoluteXPath does not evaluate to a single node in this document.", __LINE__, FALSE);
          $result = '';
          break;
        }
      } 

      if ($bDebugThisFunction) {
        echo "The xpath expression $absoluteXPath points to a single node in the tree with text nodes:\n";
        print_r($this->nodes[$absoluteXPath]['text']);
      }
      $result = implode('',$this->nodes[$absoluteXPath]['text']);
    } while (0);

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

  /**
   * Retrieves all or part of the content of a text node.
   *
   * This method retrieves the content of a specific text node. If it's an attribute
   * node, then the value of the attribute will be retrieved, otherwise
   * it'll be the character data of the node.  The node specified must be
   * a text node (if not an attribute), or else the first text node of the parent will be used.
   * The consideration just mentioned will only pertain to elements with mixed content, where
   * the text is divided by other non-text child nodes, such as in html data.  In that case,
   * these text nodes are actually distinct nodes called text() nodes.  Obviously, elements with
   * only text as a child will have only a single text() node.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org> modified by Daniel Allen <bigredlinux@yahoo.com>
   * @param     string $absoluteXPath Full document path of the text() node, from 
   *            which the content should be retrieved. *READONLY*
   * @param     number $offset Return the string starting at this offset. 
   *            *READONLY*
   * @param     number $count Return a maximum of count characters.  0 means 
   *            entire string. *READONLY*
   * @return    string The returned string contains either the cdata of the text node 
   * @see       wholeText(), getData(), getDataParts()
   */
  function &substringData($absoluteXPath, $offset = 0, $count = 0) {
    if ( $offset < 0 or !is_int($offset) ) $offset = 0;
    // we can just use _setContent and append nothing and then return it, same exact
    // code, no reason to write it twice
    return $this->_setContent($absoluteXPath, "", FALSE, $offset, $count);
  }

  /**
   * Set the content of a node
   *
   * This method sets the content of a node.  If it's an attribute node, then
   * the value of the attribute will be set, otherwise the character data of
   * the node will be set.  It throws an error if data is already present
   *
   * @param    string $absoluteXPath Full document path to the text node
   * @param    string $value String containing the context to be set
   * @see      appendData, replaceData(), deleteData()
   */
   function insertData($absoluteXPath, $value, $offset = 0) {
     if ( $offset < 0 or !is_int($offset) ) $offset = 0;
     $this->_setContent($absoluteXPath, $value, FALSE, $offset);
   }

   /**
    * Replace the content of a text() node.
    *
    * This method replaces the content of a node. If it's an attribute node, then
    * the value of the attribute will be set, otherwise the character data of
    * the node will be set. Existing content will be overwritten.
    *
    * @param     string $absoluteXPath Full document path of the text node. *READONLY*
    * @param     string $value String containing the content to be set. *READONLY*
    * @see       appendData(), deleteData()
    */
   function replaceData($absoluteXPath, $value, $offset = 0, $count = 0) {
     if ( $offset < 0 or !is_int($offset) ) $offset = 0;
     // TRUE for replace the data for the selection, offset and count self explanatory
     $this->_setContent($absoluteXPath, $value, TRUE, $offset, $count);
   }

  /**
   * Replace a node with an unprocessed (unparsed) text string.
   *
   * This function will delete the node you define by $absoluteXPath
   * (plus it's sub-nodes) and substitute it by the string $text.
   * Often used to push in not well formed HTML.
   * WARNING: 
   *     The $data is taken 1:1 (= not even an entity converting is done). 
   *     Your in charge that the data you enter is valid XML if you intend
   *     to export and import it again the content.
   * NOTE: Instead of an absolute path you may also pass a xpath-query.
   *       All nodes that match the xpath query will be modified.
   *
   * @author    Sam Blum
   * @param     string $xPathQuery path to the node (See text above). *READONLY*
   * @param     string $data String containing the content to be set. *READONLY*
   * @return    TRUE on success
   * @throws    FALSE on error;
   * @see       replaceChild()
   */
  function replaceChildByData($xPathQuery, $data) {
    // Numpty check
    $xpv = array();
    if (isSet($this->nodes[$xPathQuery])) {
      $xpv[] = &$xPathQuery;
    } else {
      // Try to evaluate the $xPathQuery; if it returns only 1 node use it; otherwise give up.
      $xpv = &$this->match($xPathQuery);
      if (sizeOf($xpv) ==0) {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery), __LINE__, FALSE);
        return FALSE;
      }
    }
    
    $xpvSize = sizeOf($xpv);
    for ($k=0; $k < $xpvSize; $k++) {
      $absoluteXPath = &$xpv[$k];
      $theNode = &$this->nodes[$absoluteXPath];
      $contextPos = $theNode['context-pos'];
      $parentNode = &$this->nodes[$theNode['parent']];
      /**
      echo "** " .$contextPos ." **<br>";
      echo "** " . var_dump($parentNode['text']) ." **<br>";
      **/
      $parentNode['text'][$contextPos-1] .= $data;
      $this->removeChild($absoluteXPath, $keepSubnodes=FALSE);
    }
    return TRUE;
  }
  
  
  /**
   * Replaces a node in the xml document
   *
   * This function will replace the entire content of the specified node(s) with
   * the specified string.  The string may either be xml data that will be parsed
   * and inserted into the tree creating new grand children as necessary, or can 
   * be a plain text string that may be not well formed xml data and will be handled
   * as text.  Additionally to preserve the integrity of the xml structure facilitating 
   * export of the object you may specify that the string is to be treated as 
   * <![CDATA[ ]]>.
   *
   * examples:
   *    replaceChild('/root[1]/element[1]','<newelement><a><b/><b/></a></newelement>');
   *    replaceChild('/root[1]/element[1]','Html <br> Data', FALSE);
   *    replaceChild('/root[1]/element[1]','Html <br> Data that id like to be able '.
   *                                   'to export to file <hr> some day', FALSE, TRUE);
   *
   * NOTE: Instead of an absolute path you may also pass a xpath-query.
   *       All nodes that match the xpath query will be modified.
   * NOTE: If you replace as CData, then when you exportAsXml, you will need to pass
   *       the $bTrimCData parameter.
   * 
   * @param     string $xPathQuery Full document path of the node. *READONLY*
   * @param     string $xmlData String containing either the XML data or the
   *                non well formed string that will be inserted as is.  *READONLY*
   * @param     bool $asXml Flag that specifies whether xmlData holds XML data that
   *                will be parsed as xml and inserted, modifying the existing tree, 
   *                or whether it should just be replace as text content, unparsed.
   * @param     bool $asCData places <![CData[ ]]> tags round xmlData so that the class
   *                may be exported to file.
   */
  function replaceChild($absoluteXPath, $xmlData, $asXml=TRUE, $asCData=FALSE) {
    // #### Not realy implemented yet.
    
    /**--------------------------------**/ 
    /** --sam Following is only temp.  **/
    $this->replaceChildByData($absoluteXPath, $xmlData);
    $xmlData = &$this->exportAsXml();
    $this->reset();
    return $this->importFromString($xmlData);
    /**--------------------------------**/ 
     
     $this->_displayError("Not implemented yet", __LINE__);
  }

  /**
   * Appends an xml string to the end of the text section of a node.
   *
   * This function will append XML after a node defined by the parameter 
   * xPathQuery (that is a absoluteXPath or xpath-query).
   * NOTE: Instead of an absolute path you may also pass a xpath-query.
   *       All nodes that match the xpath query will be modified.
   * 
   * #### Remove appendXml() and put it's functionality into appendChild()
   *
   * Not optimized at all!! 
   *
   * @author    Sam Blum
   * @param     string $xPathQuery Full document path of the node. *READONLY*
   * @param     string $xmlData String containing the XML data. *READONLY*
   * @see       replaceNodeByXml()
   */
  function appendXml($xPathQuery, $xmlData) {
    // Numpty check
    $xpv = array();
    if (isSet($this->nodes[$xPathQuery])) {
      $xpv[] = &$xPathQuery;
    } else {
      // Try to evaluate the $xPathQuery; if it returns only 1 node use it; otherwise give up.
      $xpv = &$this->match($xPathQuery);
      if (sizeOf($xpv) ==0) {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery), __LINE__);
      }
    }
    
    $xpvSize = sizeOf($xpv);
    for ($k=0; $k < $xpvSize; $k++) {
      $absoluteXPath = &$xpv[$k];
      
      $theNode = &$this->nodes[$absoluteXPath];
      // Check boarders
      if (!isSet($theNode['text'])) $theNode['text'][] = '';
      $maxPos = sizeOf($theNode['text'])-1;
      $theNode['text'][$maxPos] .= $xmlData;
    }
    $xmlData = &$this->exportAsXml();
    $this->reset();
    return $this->importFromString($xmlData);
  }
  
  /**
   * Append text content to the end of the text for a node.
   *
   * This method adds content to a node. If it's an attribute node, then
   * the value of the attribute will be set, otherwise the character data of
   * the node will be set. The content is appended to existing content,
   * so nothing will be overwritten.
   *
   * @param     string $xPathQuery Full document path of the node. *READONLY*
   * @param     string $value String containing the content to be added. *READONLY*
   * @see       replaceData(), deleteData()
   */
  function appendData( $absoluteXPath, $value ) {
    // FALSE for no replace, -1 offset for the end of the string
    $this->_setContent( $absoluteXPath, $value, FALSE );
  }

  /**
   * Delete text content of a node.
   *
   * Deletes a max of $count characters starting at $offset from the text content 
   * for a node.
   *
   * @param     string $xPathQuery Full document path of the node. *READONLY*
   * @param     number $offset Return the string starting at this offset. 
   *            *READONLY*
   * @param     number $count Return a maximum of count characters.  0 means 
   *            entire string. *READONLY*
   * @return    string The new text value.
   */
  function deleteData($absoluteXPath, $offset = 0, $count = 0) {
    if ( $offset < 0 or !is_int($offset) ) $offset = 0;
    // Now delete the data starting at offset for count characters...
    // count defaults to the length string, whereas $offset defaults to the beginning
    // if neither offset nor count are given, the whole string is deleted
    $this->_setContent( $absoluteXPath, "", TRUE, $offset, $count );
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Element alteration

  /**
   * Removes a node from the XML document.
   *
   * This method removes a node from the tree of nodes of the XML document.
   * If the node is a document node, remove it. Depending on the parameter 
   * $keepSubnodes all children nodes are moved up one level (TRUE) or 
   * the node and the subnodes are removed with all character data. 
   * If the node is an attribute node,
   * only this attribute will be removed, the node to which the attribute
   * belongs as well as its children will remain unmodified.
   * NOTE: Instead of an absolute path you may also pass a xpath query.
   *       If the result of the query leads to 1 unique node, then the
   *       path to that node is taken.
   *
   * @author    Sam Blum
   * @param     string $xPathQuery The path to the current node (see text above)
   * @param     bool   $keepSubnodes default TRUE = move subnodes up / FALSE cascaded delete 
   * @return    TRUE on success
   * @throws    FALSE on error;
   * @see       appendChild(),  evaluate()
   */
  function removeChildSam($xPathQuery, $keepSubnodes=FALSE) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction= FALSE;
    
    // Numpty check
    $xpv = array();
    if (isSet($this->nodes[$xPathQuery])) {
      $xpv[] = &$xPathQuery;
    } else {
      // Try to evaluate the $xPathQuery; if it returns 0 nodes give up; otherwise loop through the result.
      $xpv = &$this->match($xPathQuery);
      if (sizeOf($xpv) ==0) {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery), __LINE__, FALSE);
        return FALSE;
      }
    }
    
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction('removeChild');
      echo "Node: $xPathQuery\n";
      echo "<hr>";
    }
    $xpvSize = sizeOf($xpv);
    for ($k=0; $k < $xpvSize; $k++) {
      $absoluteXPath = &$xpv[$k]; 
      do { // try block
        //////////////////////////////////////////////
        // Check whether it's an attribute node.
        $lastSlashPos = strrpos($absoluteXPath, '/') -1;
        $attrPos = ($lastSlashPos>=0) ? strpos($absoluteXPath, '/attribute::', $lastSlashPos) : FALSE;
        
        if ($attrPos !== FALSE) {
          if ($bDebugThisFunction) echo "We are removing an attribute node\n";
          // Extract the path to the node.
          $thePath = substr($absoluteXPath, 0, $attrPos);
          
          // Get the name of the attribute.
          $attribute = $this->_afterstr($absoluteXPath, '/attribute::');
          
          // Exception empty attribute, ignor it.
          if (strLen($attribute)==0) break;
          
          // Numpty check
          if (isSet($this->nodes[$thePath])) {
            // Unset the attribute
            unSet($this->nodes[$thePath]['attributes'][$attribute]);
          }
          break; // try block
        }
        
        $nodeList = array();
        // Get abs. parent xpath
        $absoluteParentPath = $this->nodes[$absoluteXPath]['parent'];
        
        if ($bDebugThisFunction) {
          echo "\nWe want to remove '$absoluteXPath'. The parent of the 'to remove' node at '$absoluteParentPath' is ";
          echo "<div style=\"".$this->styleSheet['Node']."\">\n";
          print_r($this->nodes[$absoluteParentPath]);
          echo "</div>\n";
        }
        
        // Get a copy of the current parent
        $copyOfParrent = $this->nodes[$absoluteParentPath];
        
        // Remember the node name fragment to be removed e.g. BBB[2]
        $nodenameToBeRemoved = substr($absoluteXPath, strrpos($absoluteXPath, '/')+1);
        // Remember the pos of the node in the parents child list e.g. it's the 3rd child.
        $nodePosInParent = array_search($nodenameToBeRemoved, $this->nodes[$absoluteParentPath]['children']);
        if ($bDebugThisFunction) echo "Node to remove is at pos [{$nodePosInParent}] in the parents child list.\n";
        
        // Do we have to move the sub nodes 1 leval up
        if ($keepSubnodes) {
          // Get a list of all child nodes that must be move up
          $vxpChildrenNodes = $this->evaluate($absoluteXPath.'/*');
           
          // Save chiled list in a sub tree stucture by cutting off the 
          // parent path fragment. That is chopping off the head.
          // Unset the values in the main tree.
          $headLeng = strLen($absoluteXPath);
          $size = sizeOf($vxpChildrenNodes);
          for ($i=0; $i<$size; $i++) {
            $corePath = subStr($vxpChildrenNodes[$i], $headLeng);
            $nodeList[$corePath] = &$this->nodes[$vxpChildrenNodes[$i]];
            unset($this->nodes[$vxpChildrenNodes[$i]]);
          }
          unset($this->nodes[$absoluteXPath]);
          
          // Now add the nodes again
          reset($nodeList);
          while (list($xpath) = each($nodeList)) {
            $splitPos = strrpos($xpath, '/');
            $frontNodePart = substr($xpath, 0, $splitPos);
            $lastNode = substr($xpath, $splitPos);
            $lastNode = substr($lastNode, 1, strrpos($lastNode, '[')-1);
            $newParentPath = $absoluteParentPath . $frontNodePart;
            $newParentPath =$this->appendChild($newParentPath, $lastNode);
            if (!empty($nodeList[$xpath]['attributes'])) {
              $this->nodes[$newParentPath]['attributes'] = $nodeList[$xpath]['attributes'];
            }
            $this->nodes[$newParentPath]['text'] = $nodeList[$xpath]['text'];
            //echo "<br>newParentPath:[$newParentPath], lastNode:[$lastNode]"; 
          }
        }
        //////////////////////////////////////
        // Now we have to clean up the parent.
        // Make a new children list
        $newChildSize = sizeOf($this->nodes[$absoluteParentPath]['children']);
        $oldChildSize = sizeOf($copyOfParrent['children']);
        $newCildren = array();
        $offset = 0;
        for ($i=0; $i<$oldChildSize; $i++) {
          if ($i == $nodePosInParent) {
            for ($j=$oldChildSize; $j<$newChildSize; $j++) {
              $newCildren[$offset++] = $this->nodes[$absoluteParentPath]['children'][$j];
            }
          } else {
            $newCildren[$offset++] = $copyOfParrent['children'][$i];
          }
        }
        $this->nodes[$absoluteParentPath]['children'] = $newCildren;
        
        // Correct the childCount
        // Node fragment to be removed snip from  BBB[2] to BBB
        $nodenameToBeRemoved = substr($nodenameToBeRemoved, 0, strrpos($nodenameToBeRemoved, '['));
        $chiledCountHash = &$this->nodes[$absoluteParentPath]['childCount'];
        if (isSet($chiledCountHash[$nodenameToBeRemoved])) {
          if ($chiledCountHash[$nodenameToBeRemoved] > 1) {
            $chiledCountHash[$nodenameToBeRemoved]--;
          } else {
            unset($chiledCountHash[$nodenameToBeRemoved]);
          }
        }
        
        // Now merge the 2 text parts that used to be split by the node
        $newTextParts = array();
        $offset=0;
        $textArray = &$this->nodes[$absoluteParentPath]['text'];
        for ($i=0; $i<sizeOf($textArray); $i++) {
          $newTextParts[$offset] = $textArray[$i];
          if ($i == $nodePosInParent) {
            $i++;
            $newTextParts[$offset] .= $textArray[$i];
          }
          $offset++;
        }
        $textArray = $newTextParts;
        if ($bDebugThisFunction) {
          echo "\nThe new fixed parent node at $absoluteParentPath is ";
          echo "<div style=\"".$this->styleSheet['ParentNode']."\">\n";
          print_r($this->nodes[$absoluteParentPath]);
          echo "</div>\n";
        }      
      } while(FALSE); // END try block
    } // END for ($k=0; $k < $xpvSize; $k++) 
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, '');
    }
    return;
  }
 
  /**
   * Removes a node from the XML document.
   *
   * This method removes a node from the tree of nodes of the XML document.
   * If the node is a document node, all children of the node and its
   * character data will be removed. If the node is an attribute node,
   * only this attribute will be removed, the node to which the attribute
   * belongs as well as its children will remain unmodified.
   *
   * @author    Nigel Swinson <nigelswinson@users.sourceforge.net>
   * @param     string $absoluteXPath Full path of the node to be removed.
   * @return    TRUE on success
   * @throws    FALSE on error;
   * @see       appendChild(), hasChildNodes(), evaluate()
   */
  function removeChild($absoluteXPath) {
    // Numpty check
    if (!isSet($this->nodes[$absoluteXPath])) {
      // Try to evaluate the $absoluteXPath; if it returns only 1 node use it; otherwise give up.
      $resultArr = $this->match($absoluteXPath);
      if (sizeOf($resultArr)==1) {
        $absoluteXPath = $resultArr[0];
      } else {
        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $absoluteXPath), __LINE__, FALSE);
        return FALSE;
      }
    }
    
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction = FALSE;
    
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction('removeChild');
      echo "Node: $absoluteXPath\n";
      echo "<hr>";
    }
    
    // Numpty check
    if (empty($absoluteXPath)) {
      $this->_displayError('No child to remove. Passsed parameter was empty.', __LINE__, FALSE);
      return FALSE;
    }
    //////////////////////////////////////////////
    // Check whether the node is an attribute node.
    if (preg_match('/\/attribute::/', $absoluteXPath)) {
      if ($bDebugThisFunction) echo "We are removing an attribute node\n";
      // Get the path to the attribute node's parent.
      $parent = $this->_prestr($absoluteXPath, '/attribute::');
      
      // Get the name of the attribute.
      $attribute = $this->_afterstr($absoluteXPath, '/attribute::');
      
      // Unset the attribute
      unSet($this->nodes[$parent]['attributes'][$attribute]);
    } else {  
      if ($bDebugThisFunction) echo "We are removing an element node\n";

      // Find out if the node that they gave us exists.
      if (!isSet($this->nodes[$absoluteXPath])) {
        // Not sure if this is quite so fatal, but I think it likely.  Typically
        // you will evaluate() then remove.  The alternative would be just to quit.
        $this->_displayError("The $absoluteXPath argument does not uniquely refer to a node in the XML file  use /AAA[1]/BBB[1] format. ". basename(__FILE__).':'.__LINE__);
      }

      /////////////////////////////////////
      // Get some stats about the environment of the deceased.
      
      // Get the name, the parent and the siblings of current node.
      $nameOfDeadChild    = $this->nodes[$absoluteXPath]['name'];
      $parentOfDeadChild  = $this->nodes[$absoluteXPath]['parent'];
      $sameNameSiblings   = $this->nodes[$parentOfDeadChild]['childCount'][$nameOfDeadChild];
      $siblingsCount      = count($this->nodes[$parentOfDeadChild]['children']);
      // Get base node number e.g. /AAA[1]/BBB[3]/CCC[2]  => 2
      $contextPosOfDeadChild = $this->nodes[$absoluteXPath]['context-pos'];
      $fullNameOfDeadChild = $nameOfDeadChild.'['.$contextPosOfDeadChild.']';
      // Get the child number, ie the element number within the parent.
      $aChildToIndex = array_flip($this->nodes[$parentOfDeadChild]['children']);
      $deadChildSiblingOrder = $aChildToIndex[$fullNameOfDeadChild];      
      // Construct the common element of all the same name siblings.
      $commonComponent = $parentOfDeadChild.'/'.$nameOfDeadChild;

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

      // Create an associative array, which contains information about
      // all nodes that required to be renamed.  The linear order of the
      // elements is important, as the first rename should be done first, then
      // the second, etc, otherwise we will "overwrite" the members on the
      // way through
      $rename = array();
      if ($bDebugThisFunction) echo "\nCreating list of nodes that need renamed.\n";
            
      // Now run through the younger siblings, as they must be renamed
      for ( $iRunner = $contextPosOfDeadChild + 1; $iRunner <= $sameNameSiblings; $iRunner++) {
        // Create the renaming entry.
        $old = $parentOfDeadChild.'/'.$nameOfDeadChild.'['.$iRunner.']';
        $new = $parentOfDeadChild.'/'.$nameOfDeadChild.'['.($iRunner - 1).']';

        $rename[$old] = $new;
      }
      
      if ($bDebugThisFunction) {
        echo "The following nodes all have to be renamed\n";
        print_r($rename);
      }

      /////////////////////////////////////
      // Fixing parent.

      if ($bDebugThisFunction) {
        echo "<table border='1'><tr><td><pre>\n";
        echo "\nThe parent was:\n";
        print_r($this->nodes[$parentOfDeadChild]);
        echo ".\n";
      }

      // Decrease the number of children.
      $this->nodes[$parentOfDeadChild]['childCount'][$nameOfDeadChild]--;
      if ($this->nodes[$parentOfDeadChild]['childCount'][$nameOfDeadChild] == 0)
          unset($this->nodes[$parentOfDeadChild]['childCount'][$nameOfDeadChild]);
      
      // Merge the text before and after the child
      if (!empty($this->nodes[$parentOfDeadChild]['text'][$deadChildSiblingOrder])) {
        $this->nodes[$parentOfDeadChild]['text'][$deadChildSiblingOrder] .= $this->nodes[$parentOfDeadChild]['text'][$deadChildSiblingOrder+1];
      }
      // Shift all the next text nodes down in the array.
      for ($iRunner = $deadChildSiblingOrder + 1; $iRunner < $siblingsCount; $iRunner++) {
        $this->nodes[$parentOfDeadChild]['text'][$iRunner] = $this->nodes[$parentOfDeadChild]['text'][$iRunner+1];
      }
      // Unset the last text node.
      unset($this->nodes[$parentOfDeadChild]['text'][$siblingsCount]);

      // Remove the child from the parents memory.  Sniff sniff :o( We must finish the grieving process!
      unSet($this->nodes[$parentOfDeadChild]['children'][$fullNameOfDeadChild]);
      // Go through the array, and rename all the younger same-name siblings, shifting the kids up
      // the array as we go.
      $contextPosOfNextSameNameSibling = $contextPosOfDeadChild + 1;
      $fullNameOfNextSameNameSibling = $nameOfDeadChild .'['.$contextPosOfNextSameNameSibling.']';
      for ($iRunner = $deadChildSiblingOrder + 1; $iRunner < $siblingsCount; $iRunner++) {
        $childName = &$this->nodes[$parentOfDeadChild]['children'][$iRunner];
        if ($childName == $fullNameOfNextSameNameSibling) {
          $childName = $nameOfDeadChild .'['.($contextPosOfNextSameNameSibling-1).']';
          $fullNameOfNextSameNameSibling = $nameOfDeadChild .'['.++$contextPosOfNextSameNameSibling.']';
        }
        $this->nodes[$parentOfDeadChild]['children'][$iRunner-1] = $childName;
      }
      // Unset the last child to indicate we have one less child now.
      unset($this->nodes[$parentOfDeadChild]['children'][$siblingsCount-1]);

      if ($bDebugThisFunction) {
        echo "\n</pre></td><td><pre>\n";
        echo "\nThe parent is now:\n";
        print_r($this->nodes[$parentOfDeadChild]);
        echo ".\n";
        echo "\n</pre></td></tr></table>\n";
      }

      /////////////////////////////////////
      // Rename all the entries in the $nodes array to correct the object for the dead child.
      
      if ($bDebugThisFunction) echo "\nModifying node list.\n";

      // Run through all nodes of the document.
      $aNodeKeys = array_keys($this->nodes);
      reset($aNodeKeys);
      while (list($key, $absoluteXPathRunner) = each($aNodeKeys)) {
        // skip super-Root
        if (empty($absoluteXPathRunner)) continue;

        // Check to see if this node starts with the common component of all nodes that are to be renamed
        if ($commonComponent == substr($absoluteXPathRunner, 0, strlen($commonComponent))) {
          if ($absoluteXPath == substr($absoluteXPathRunner, 0, strlen($absoluteXPath))) {
            if ($bDebugThisFunction) echo "Removing node: $absoluteXPathRunner\n";
            unset($this->nodes[$absoluteXPathRunner]);
            continue;
          }

          // Run through the array of nodes to be renamed.
          reset($rename);
          while (list($old, $new) = each($rename)) {
            // Does this rename prefix match our node?
            $oldLength = strlen($old);
            if ($old == substr($absoluteXPathRunner, 0, $oldLength)) {
              // Build the new name of this node in the nodes array.
              $nameOfRenamedChild  = $new.substr($absoluteXPathRunner, $oldLength);

              // Get the complete values for this node.
              $values = $this->nodes[$absoluteXPathRunner];

              // Update the context pos as we have renamed it.
              if ($values['parent'] == $parentOfDeadChild)
                $values['context-pos'] = $values['context-pos'] - 1;

              // We may need to rename the 'parent' of this node too.
              if ($old == substr($values['parent'], 0, $oldLength)) {
                $values['parent'] = $new.substr($values['parent'], $oldLength);
              }

              if ($bDebugThisFunction) {
                echo "Renaming node: $absoluteXPathRunner from $old to $new.\n";
//                echo "Values:\n"; print_r($values);
              }
              
              // Add the node to the list of nodes, remove it's old entry
              $this->nodes[$nameOfRenamedChild] = $values;
              unset($this->nodes[$absoluteXPathRunner]);

              break;
            }
          }
        } 
      }
      
      if ($bDebugThisFunction) {
        echo "The new node list is:\n";
        print_r($this->nodes);
      }
    }
    //////////////////////////////////////////////
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time);
    }
    return TRUE;
  }
  
  /**
   * Adds a new node to the XML document.
   *
   * 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.
   * 
   * It it assumed that adding starts with root and new nodes must have a 
   * corresponding parent. Otherwise the add will be ignored.
   * Node stucture:
   *      [<path>]['name']          // <nodeName>
   *      [<path>]['doc-pos']       // Path-'depth' starting with 0
   *      [<path>]['context-pos']   // child order
   *      [<path>]['parent']        // <parent path>
   *      [<path>]['children']      // array(<xPathFragment>, ...)         e.g. array(AAA[1],AAA[2],BBB[1])
   *      [<path>]['childCount']    // array(<nodeName> => <child Count>, ...)
   *      [<path>]['attributes']    // array(<attrName> => <attrVal>, ...)
   *      [<path>]['text']          // array of text parts: E.g. <A>hello<B/>world</A> -> array('hello','world')
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $absoluteParentPath Full path of the parent, to which the new
   *            node should be added as a child. *READONLY*
   * @param     string $nodeName Name of the new node. *READONLY*
   * @return    string The string returned by this method will contain the
   *            full document path of the created node.
   * @see       removeChild(), hasChildNodes(), evaluate()
   */
  function appendChild($absoluteParentPath, $nodeName) {
    $bDebugThisFunction= FALSE;
    
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction("appendChild");
      echo "Current Node (parent): '{$absoluteParentPath}'\n";
	    echo "NewNodeName  (child) : '{$nodeName}' \n<br>";
      echo implode(' - ', array_keys($this->nodes));
    }
    
    static $emptyNode = array(
                       'name'        => '',
                       'doc-pos'     => 0, 
                       'context-pos' => 1,
                       'parent'      => '',
                       'children'    => array(),
                       'childCount'  => array(),
                       'attributes'  => array(),
                       'text'        => array()
                     );
                     
    // If first call assume it's root.
    if (empty($this->root)) {
      // Use $nodeName root element as it is the first tag.
      $this->root = '/'.$nodeName.'[1]';
      $fullNameOfNewChild = $this->root;
      $this->nodes[$fullNameOfNewChild] = $emptyNode;
      $this->nodes[$fullNameOfNewChild]['name'] = $nodeName;
      // Make a super-root.  Don't use PHP's array_merge!!
      reset($emptyNode);
      while (list($key) = each($emptyNode)) {
        $this->nodes[''][$key] = $emptyNode[$key];
      }
      $this->nodes['']['childCount'] = array($nodeName=>1);
      $this->nodes['']['children'][] = $nodeName.'[1]';
    } else {
      // Assemble the basic path for this element.      
      if ($absoluteParentPath == '/')
        $absoluteParentPath = '';

      $basicPath = $absoluteParentPath.'/'.$nodeName;
      
      // Modify the child count for this type of node      
      $tmpCount = &$this->nodes[$absoluteParentPath]['childCount'][$nodeName];
      $position = isSet($tmpCount) ? $tmpCount+1: 1;
            
      if ($bDebugThisFunction) {
        echo "\nIn parent '{$absoluteParentPath}' the child '{$nodeName}' has position '{$position}'.";
      }
      
      ///////////////////////////////////////
      // Add the new node to them nodes array.
      
      // Assable the full path for this element.
      $fullNameOfNewChild = $basicPath.'['.$position.']';
      
      // Init node if not exsisting
      if (!isSet($this->nodes[$fullNameOfNewChild]))
        $this->nodes[$fullNameOfNewChild] = $emptyNode;
      
      // Use node directly
      $newChildNode = &$this->nodes[$fullNameOfNewChild];
      // Calculate the position for the following and preceding axis detection.
      $newChildNode['doc-pos'] = $this->nodes[$absoluteParentPath]['doc-pos'] + 1;
      // Calculate the context position, which is the position of this
      // element within elements of the same name in the parent node.
      $newChildNode['context-pos'] = $position;
      // Save the information about the node.
      $newChildNode['name'] = $nodeName;
      // Set the parent
      $newChildNode['parent'] = $absoluteParentPath;
      
      if ($bDebugThisFunction) {
        echo "\nThe new node at $fullNameOfNewChild is ";
        echo "<div style=\"".$this->styleSheet['Node']."\">\n";
        print_r($newChildNode);
        echo "</div>\n";
      }      

      ///////////////////////////////////////
      // Fix the parent.
      
      // Get the parent node.
      $newParentNode = &$this->nodes[$absoluteParentPath];
      // There a bug some where that will trigger the following check. --sam 12.12.2001
      if (!isSet($newParentNode['children'])) {
        $this->_displayError("Internal node problem. When adding node '{$nodeName}' to an invalid parent node '{$absoluteParentPath}'." , __LINE__, FALSE);
      }
      
      // Count the siblings
      $siblingCount = count($newParentNode['children']);
      // Add children in the *order* they come in.
      $newParentNode['children'][$siblingCount] = $nodeName.'['.$position.']';
      // Update the number of childer in parent path
      $newParentNode['childCount'][$nodeName] = $position;
      // Add some default sensible whitespace before this new node.
      
      // Work out who the grandparent is.
      if ($newParentNode['parent']) {
        $newGrandParentNode = &$this->nodes[$newParentNode['parent']];
        $beforeText = $newGrandParentNode['text'][$newParentNode['context-pos']-1];
        $afterText = $newGrandParentNode['text'][$newParentNode['context-pos']];
        if ($bDebugThisFunction) {
          echo "Text before this child is '$beforeText'\n";
          echo "Text after this child is '$afterText'\n";
        }
        // If there is no text before or after, then default to something sensible
        if ( $beforeText == '' && $afterText == '' ) {
          $newParentNode['text'][(int)$siblingCount] = "\n\t";
          $newParentNode['text'][$siblingCount+1] = "\n";
        // If the text before and after is purely whitespace, derive from it.
        } else if (!preg_match('/[^\s]/',$beforeText) && !preg_match('/[^\s]/',$afterText)) { // --sam regex shoud be /^\s*$/ I guess
          $newParentNode['text'][(int)$siblingCount] = "$beforeText\t";
          $newParentNode['text'][$siblingCount+1] = "$afterText";
        // Otherwise default to something sensible.
        } else {
          $newParentNode['text'][(int)$siblingCount] = "\n\t";
          $newParentNode['text'][$siblingCount+1] = "\n";
        }
      // We are adding to the root, so we know what these should be.
      } else {
        $newParentNode['text'][(int)$siblingCount] = "\n\t";
        $newParentNode['text'][$siblingCount+1] = "\n";
      }

      if ($bDebugThisFunction) {
        echo "\nThe fixed parent at $absoluteParentPath is ";
        echo "<div style=\"".$this->styleSheet['ParentNode']."\">\n";
        print_r($this->nodes[$absoluteParentPath]);
        echo "</div>\n";
      }
    }

    //////////////////////////////////////////////
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, $fullNameOfNewChild);
    }

    // Return the path of the new node.
    return $fullNameOfNewChild;
  }


  /**
   * Returns TRUE if the given node has child nodes below it
   *
   * @author    Dietrich Ayala <dietrich@ganx4.com>
   * @param     string $absoluteXPath full path of the potentail parent node
   *            *READONLY*
   * @return    bool TRUE if this node exists and has a child, FALSE otherwise
   * @see       removeChild(), appendChild(), evaluate()
   */
  function hasChildNodes($absoluteXPath) {
    if (!isSet($this->nodes[$absoluteXPath])) return FALSE;
		if (count($this->nodes[$absoluteXPath]['children']) >= 1)	return TRUE;
		return FALSE;
  }

  /**
   * Resets the object so it's able to take a new xml sting/file
   *
   * @author    Sam Blume bs_php@infeer.com
   *
   */
  function reset() {
    $this->nodes        = array();
    $this->path         = '';
    $this->xpath        = '';
    $this->root         = '';
    $this->position     = 0;
    $this->xmlTxtBuffer = '';
    $this->fileName     = ''; //added by fab
    $this->inCData      = 0;
    $this->_lastError   = '';
  }

  ////////////////////////////////////////////////////////////////////////////////
  //################### Private Members ########################################//
  ////////////////////////////////////////////////////////////////////////////////
  
  // 0=silent, 1 and above produce verbose output (an echo to screen). 
  // @see getLastError()
  var $_verboseLevel = 1;
  
  // xml_parser_set_option -- set options in an XML parser.
  var $xmlOptions = array();
  
  // The filename, if XML was imported by a file (instead of a string)
  var $fileName = '';
  
  // List of all document nodes.
  //
  // This array contains a list of all document nodes saved as an
  // associative array.
  var $nodes = array();
   
  // Current document path.
  //
  // This variable saves the current path while parsing a XML file and adding
  // the nodes being read from the file.
  var $path = '';
  
  // Current document position.
  //
  // This variable counts the current document position while parsing a XML
  // file and adding the nodes being read from the file.
  var $position = 0;
  
  // Path of the document root.
  //
  // This string contains the full path to the node that acts as the root
  // node of the whole document.
  var $root = '';
  
  // Current XPath expression.
  //
  // This string contains the full XPath expression being parsed currently.
  var $xpath    = '';
                                                         
  // Used as tmp storage for the char data collected during xml parsing
  var $xmlTxtBuffer = '';

  // When parsing the xml file, sets to true when we are inside a CDATA section.
  var $inCData = 0;

  // As debugging of the xml parse is spread across several functions, we need
  // to make this a member.
  var $bDebugXmlParse = FALSE;

  // List of entities to be converted.
  //
  // This array contains a list of entities to be converted when an XPath
  // expression is evaluated.
  //
  // ### People seem to think that &apos is a bad idea for charset ISO-8859-1 
  //var $entities = array ( "&" => "&amp;", "<" => "&lt;", ">" => "&gt;",
  //    "'" => "&apos;", '"' => "&quot;" );
  var $entities = array('&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;', '"'=>'&quot;');
  
  // List of supported XPath axes.
  // What a stupid idea from W3C to take axes name containing a '-' (dash)
  // NOTE: Instead of the '-' in the names we use '_'.
  //       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 expression.
  var $axes = array ( 'child', 'descendant', 'parent', 'ancestor',
    'following_sibling', 'preceding_sibling', 'following', 'preceding',
    'attribute', 'text', 'namespace', 'self', 'descendant_or_self',
    'ancestor_or_self' );
  
  // List of supported XPath functions.
  // What a stupid idea from W3C to take function name containing a '-' (dash)
  // NOTE: Instead of the '-' in the names we use '_'.
  //       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 expression.
  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' );
    
  // What a stupid idea from W3C to take axes name containing a '-' (dash)
  // It's hard to distinguish from a minus operator. 
  // NOTE: Instead of the '-' in the names we use '_'.
  //       We will then do the same on the users Xpath querys
  var $dash2underscoreHash = array( 
      '-sibling'    => '_sibling', 
      '-or-'        => '_or_',
      'starts-with' => 'starts_with', 
      'substring-before' => 'substring_before',
      'substring-after'  => 'substring_after', 
      'string-length'    => 'string_length',
      'normalize-space'  => 'normalize_space');
   
  // List of supported XPath operators.
  //
  // This array contains a list of all valid operators that can be evaluated
  // in a predicate of an XPath expression. The list is ordered by the
  // precedence of the operators (lowest precedence first).
  var $operators = array( ' or ', ' and ', '=', '!=', '<=', '<', '>=', '>',
    '+', '-', '*', ' div ', ' mod ' );

  // This is the array of error strings, to keep consistency.
  var $errorStrings = array(
    'AbsoluteXPathRequired' => 'The supplied string does not uniquely describe a node in the xml document: %s'
    );

  // Style sheet of strings used to make output prettier.
  var $styleSheet = array(
    'Node' => 'background-color: #FFFFEE;',
    'ParentNode' => 'background-color: #FFEEFF;',
    );

  ////////////////////////////////////////////////////////////////////////////////
  //################### Private Interface ######################################//
  ////////////////////////////////////////////////////////////////////////////////

  /////////////////////////////////////////////////
  // ########################################### //
  // Export functions
  
  /**
   * Generates a XML file with the content of the current document.
   *
   * This method creates a string containing the XML data being read
   * and modified by this class before. This string can be used to save
   * a modified document back to a file or doing other nice things with
   * it.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $highlight Array containing a list of full document
   *            paths of nodes to be highlighted by <font>...</font> tags
   *            in the generated XML string.
   * @param     string $currentXpath While doing a recursion with this method, this
   *            parameter is used for internal purpose.
   * @param     int $level While doing a recursion with this method, this
   *            parameter is used for internal purpose.
   * @param     $outputAsXml specifies whether or not you want to escape
   *            <> as &gt; and &lt;  Added by N.S.
   * @return    string The returned string contains well-formed XML data
   *            representing the content of this document.
   */
  function &_export($highlight = array(), $currentXpath = '', $level = 0, $outputAsXml = 0) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction= FALSE;

    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction("_export");
      echo "Highlights:\n";
      print_r($highlight);
      echo "Root: $currentXpath\n";
      echo "Level: $level\n";
      echo "Output As Xml: $outputAsXml\n\n";
    }
    //////////////////////////////////////////////
    // Create a string to save the generated XML data.
    $xml = '';

    // Create two strings containing the tags for highlighting a node.
    static $highlight_start = '<font color="#FF0000"><b>';
    static $highlight_end   = '</b></font>';

    // Check whether a root node is given.
    if (empty($currentXpath)) {
      // Set it to the document root.
      $currentXpath = $this->root;
      if ($bDebugThisFunction) echo "Changing root to $this->root as it is empty\n";
    }

    // Generate a string to be displayed before the tags.
    $before = '';
    // Calculate the amount of whitespaces to display.
    for ( $i=0; $i<$level; $i++) {
       //Add a whitespaces to the string.
       $before .= '  ';
    }

    // If there is no node at $currentXpath then we have nothing to export.  Quit now
    if (!isSet($this->nodes[$currentXpath])) {
      if ($bDebugThisFunction) echo "No node at $currentXpath, returning null\n";        
      // Not completely sure yet if this is fatal, but I think it is.
      $this->_displayError("When exporting the class, the node at $currentXpath ".
                              "was not found.  This is probably due to previous internal corruption.");       
      return '';
    }

    $theNode = &$this->nodes[$currentXpath];


    // Check whether the node is selected.
    $selected = empty($highlight) ? FALSE : in_array($currentXpath, $highlight);
    $hasChildren = (sizeOf($theNode['children'])>0) ? TRUE : FALSE;

    // Check whether the node is selected for highlight.
    if ($selected ) $xml .= $highlight_start;

    // Now open the tag adding the whitespaces to the XML data.
    if (!$outputAsXml && $level>0)  $xml .= "\n".$before;

    if (empty($theNode['name'])) {
      // If the node has no name, then ring alarm bells.
      $this->_displayError("When exporting the class, the node at '{$currentXpath}' ".
                              "was found to have no node name.", __LINE__, FALSE);
      return '';
    }

    $xml .= ($outputAsXml) ? '<' : '&lt;';
    $xml .= htmlspecialchars($theNode['name']);

    // Check whether there are attributes for this node.
    if (count($theNode['attributes']) > 0) {
      if ($bDebugThisFunction) echo "Outputing the attributes\n";
      // Run through all attributes.
      $highlighting = FALSE;
      reset($theNode['attributes']);
      while (list($key) = each($theNode['attributes'])) {
        // Check whether this attribute is highlighted.
        if (is_array($highlight) and in_array($currentXpath.'/attribute::'.$key, $highlight)) {
          // Add the highlight code to the XML data.
          $xml .= $highlight_start;
          $highlighting = TRUE;
        }
    
        // Add the attribute to the XML data.
        $xml .= ' '.$key.'="'.htmlspecialchars($theNode['attributes'][$key]).'"';
    
        // Check whether this attribute is highlighted.
        if ($highlighting) {
          // Add the highlight code to the XML data.
          $xml .= $highlight_end;
          $highlighting = FALSE;
        }
      }
    } 

    if (!isset($theNode['text'])) {
      $mergedText = '';
      $useShortEnd = !$hasChildren;
    } else {
      $mergedText = implode('', $theNode['text']);
      $useShortEnd = (!$hasChildren && ($mergedText == ''));
    }

    // Check whether the node contains character data or has children.
    if ($useShortEnd) {
      // Add the end to the tag.
      $xml .= ($outputAsXml) ? '/>' : "/&gt;";
    } else {
      // Close the tag.
      $xml .= ($outputAsXml) ? '>' : '&gt;';
    }

    // Check whether the node is selected. Add the highlight code to the XML data.
    if ($selected) $xml .= $highlight_end;

    // Check whether the node has children or not.
    if (!$hasChildren) {
      $xml .= $mergedText;
    } else {
      // Run through all children in the order they where set.
      $childSize = sizeOf($theNode['children']);
      if ($bDebugThisFunction) {
		    echo "$childSize children to output.\n"; 
		    print_r($theNode['children']);
	    }
      for ($i=0; $i<$childSize; $i++) {
        if (!empty($theNode['text'][$i])) $xml .=  $theNode['text'][$i];
        // Generate the full path of the child.
        $fullchild = $currentXpath.'/'.$theNode['children'][$i];
	      if ($bDebugThisFunction) echo "Outputing child $i: $fullchild\n";
        // Add the child's XML data to the existing data.
        $xml .= $this->_export(&$highlight, $fullchild, $level + 1, $outputAsXml);
      }
      // Add the text fagment after the child node
      if (!empty($theNode['text'][$i])) {
        if ($outputAsXml) $xml .= $theNode['text'][$i];
        else              $xml .= htmlspecialchars($theNode['text'][$i]);
      }
    }
  
    // Check if we have to set a ending </foo> tag
    if (! $useShortEnd) {
      // Add the whitespaces to the XML data, but only if there were kids.
      if (!$outputAsXml && $hasChildren) $xml .= "\n".$before;
  
      // Check whether the node is selected. Add the highlight code to the XML data.
      if ($selected) $xml .= $highlight_start;
  
      // Add the closing tag.
      $xml .= ($outputAsXml) ? '</' : '&lt;/';
      $xml .= $theNode['name'];
      $xml .= ($outputAsXml) ? '>' : '&gt;';
  
      // Check whether the node is selected. Add the highlight code to the XML data.
      if ($selected) $xml .= $highlight_end;
      
      // Add a linebreak.
      if (!$outputAsXml) $xml .= "\n";
    }
    //////////////////////////////////////////////
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, $xml);
    }
  
    // Return the XML data.
    return $xml;
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Xml parsing utitilties
  
  /**
   * 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.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     int $parser Handler for accessing the current XML parser.
   * @param     string $name Name of the opening tag found in the document.
   * @param     array $attributes 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__);
      return;
    }
    if ($this->bDebugXmlParse)
      echo "<blockquote>Start node: ".$nodeName . "<br>";

    $oldPath = $this->path;

    // Add a node.
    $this->path = $this->appendChild($this->path, $nodeName);

    // Add text fragments to the old node, resetting the defaults applied by appendChild()
    if (!empty($this->path)) {
      // When we read a start element, we replace the before text for this node with
      // what we actually read.  Hence the -2.
      if (isset($this->nodes[$oldPath]['text']))
        $iIndex = count($this->nodes[$oldPath]['text'])-2;
      else
        $iIndex = 0;
      $this->nodes[$oldPath]['text'][$iIndex?$iIndex:0] = $this->xmlTxtBuffer;
      $this->xmlTxtBuffer = '';
    }
  
    // Set the attributes.
    if (!empty($attributes))
      $this->nodes[$this->path]['attributes'] = $attributes;
  }
  
  /**
   * Handles closing XML tags while parsing.
   *
   * While parsing a XML document for each closing tag this method is called.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     int $parser Handler for accessing the current XML parser.
   * @param     string $name Name of the closing tag found in the document.
   * @see       _handleStartElement(), _handleCharacterData()
   */
  function _handleEndElement($parser, $name) {
    // Add text fragments.  If the node has kids, then there will be at least two
    // text entries, before and after.  So we replace the last entry with the txtBuffer.
    // If the node had no kids, then we need to set the 1 and only text node.
    $iIndex = count($this->nodes[$this->path]['text']);
    $this->nodes[$this->path]['text'][$iIndex?$iIndex-1:0] = $this->xmlTxtBuffer;
    if ($this->bDebugXmlParse) {
      echo "End node:$name: [{$this->path}]: '".htmlspecialchars($this->xmlTxtBuffer)."'<br>Text nodes:<pre>\n";
      print_r($this->nodes[$this->path]['text']);
      echo "</pre></blockquote>\n";
    }
    $this->xmlTxtBuffer = '';
    // Jump back to the parent element.
    $this->path = substr($this->path, 0, strrpos($this->path, '/'));
  }
  
  /**
   * 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.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     int $parser Handler for accessing the current XML parser.
   * @param     string $text Character data found in the document.
   * @see       _handleStartElement(), _handleEndElement()
   */
  function _handleCharacterData($parser, $text) {
    // Replace entities.
    if (!$this->inCData)
      $text = strtr($text, $this->entities);
    
    $this->xmlTxtBuffer .= $text;
    if ($this->bDebugXmlParse)
      echo "Handling character data: ".htmlspecialchars($text)."<br>";
  }

  /**
   * 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     int $parser Handler for accessing the current XML parser.
   * @param     string $text Character data found in the document.
   * @see       _handleStartElement(), _handleEndElement()
   */
  function _handleDefaultData($parser, $text) {
    // Replace entities.
    do { // try-block
      if ($this->path) {
        $this->xmlTxtBuffer .= $text;
        if (!strcmp($text, '<![CDATA[')) {
          $this->inCData++;
        } else if (!strcmp($text, ']]>')) {
          $this->inCData--;
          if ($this->inCData < 0) $this->inCData = 0;
        }
        if ($this->bDebugXmlParse) echo "Default handler data: ".htmlspecialchars($text)."<br>";    
        break; // try-block
      }
           
      // handle the dtd and xml declarations
      if ( preg_match(";\?>$;", $this->nodes['']['xml-declaration']) ) {
        $this->nodes['']['dtd-declaration'] .= $text;
        break; // try-block
      }
       
      $this->nodes['']['xml-declaration'] .= $text;
      
    } while (FALSE); // END try-block
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // XPath expression parsing functions

  /**
   * Split a string by a searator-string -- BUT the searator-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     string $separator String that should be searched.
   * @param     string $term 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.  If we are inside
                          // a () or [] bracket, then we won't be able to reliably
                          // extract strings.
    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 determin 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 befor 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;
  }
  
  
  /**
   * Retrieves axis information from an XPath expression step.
   *
   * This method tries to extract the name of the axis and its node-test
   * from a given step of an XPath expression at a given node.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $step String containing a step of an XPath expression.
   * @param     string $nodePath Full document path of the node on which the
   *            step is executed.
   * @return    array This method returns an array containing information
   *            about the axis found in the step.
   * @see       _evaluateStep()
   */
  function _getAxis($step, $nodePath) {
    // Create an array to save the axis information.
    $axis = array(
      'axis'      => '',
      'node-test' => '',
      'predicate' => array()
    );
    
    do { // parse block
      $parseBlock = 1;
      
      // Check whether the step is empty or only self. 
      if ( empty($step) OR ($step == '.') OR ($step == 'current()') ) {
        // Set it to the default value.
        $step = '.';
        $axis['axis']      = 'self';
        $axis['node-test'] = '*';
        break $parseBlock;
      }
      
      // Check whether is an abbreviated syntax.
      if ($step == '*') {
        // Use the child axis and select all children.
        $axis['axis']      = 'child';
        $axis['node-test'] = '*';
        break $parseBlock;
      }

      // Check whether it's all wrapped in a function.  will be like count(.*) where .* is anything
      // text() will try to be matched here, so just explicitly ignore it
      $regex = ":(.*)\s*\((.*)\)$:U";
      if (preg_match($regex, $step, $match) && $step != "text()") {
        $function = $match[1];
        $data    = $match[2];
        if ($this->_isFunction($function)) {
          // Save the evaluated function.
          $axis['axis']      = 'function';
          $axis['node-test'] = $this->_evaluateFunction($function, $data, $nodePath, $nodeTest);
        } 
        else {
          // Use the child axis and a function.
          $axis['axis']      = 'child';
          $axis['node-test'] = $step;
        }
        break $parseBlock;
      }

      // Check whether there are predicates and add the predicate 
      // to the list of predicates without []. Get contents of
      // every [] found.
      $regex = '/\[(.*)\]/';
      preg_match_all($regex, $step, $regs); 
      if (!empty($regs[1])) {
        $axis['predicate'] = $regs[1];
        // Reduce the step.
        $step = preg_replace($regex,"",$step); //$this->_prestr($step, '[');
      }
      
      // Check whether the axis is given in plain text.
      if ($this->_searchString($step, '::') > -1) {
        // Split the step to extract axis and node-test.
        $axis['axis']      = $this->_prestr($step, '::');
        $axis['node-test'] = $this->_afterstr($step, '::');
        break $parseBlock;
      }
      
      if ($step[0] == '@') {
        // Use the attribute axis and select the attribute.
        $axis['axis']      = 'attribute';
        $axis['node-test'] = substr($step, 1);
        break $parseBlock;
      }
      
      if (eregi('\]$', $step)) {
        // Use the child axis and select a position.
        $axis['axis']      = 'child';
        $axis['node-test'] = substr($step, strpos($step, '['));
        break $parseBlock;
      }

      if ($step == '..') {
        // Select the parent axis.
        $axis['axis']      = 'parent';
        $axis['node-test'] = '*';
        break $parseBlock;
      }
      
      if (preg_match('/^[a-zA-Z0-9\-_]+$/', $step)) {
        // Select the child axis and the child.
        $axis['axis']      = 'child';
        $axis['node-test'] = $step;
        break $parseBlock;
      } 

      if ( $step == "text()" ) {
        // Handle the text node
        $axis["axis"]      = "child";
        $axis["node-test"] = "cdata";
        break $parseBlock;
      }

      // Default will be to fall back to using the child axis and a name.
      $axis['axis']      = 'child';
      $axis['node-test'] = $step;

    } while(FALSE); // end parse block
    
    // Check whether it's a valid axis.
    if (!in_array($axis['axis'], array_merge($this->axes, array('function')))) {
      // Display an error message.
      $this->_displayError('While parsing an XPath expression, in the step ' .
        str_replace($step, '<b>'.$step.'</b>', $this->xpath) .
        ' the invalid axis ' . $axis['axis'] . ' was found.', __LINE__);
    }
    
    // Return the axis information.
    return $axis;
  }    

  /**
   * Checks for a valid function name.
   *
   * This method check whether an expression contains a valid name of an
   * XPath function.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $expression Name of the function to be checked.
   * @return    boolean This method returns TRUE if the given name is a valid
   *            XPath function name, otherwise FALSE.
   * @see       evaluate()
   */
  function _isFunction(&$expression) {
    // Check whether it's in the list of supported functions.
    if (in_array($expression, $this->functions)) {
      // It's a function.
      return TRUE;
    } else {
      // It's not a function.
      return FALSE;
    }
  }
  
  /////////////////////////////////////////////////
  // ########################################### //
  // Evaluation functions
  
  /**
   * Internal recursive evaluate an-XPath-expression function.
   *
   * $this->evaluate() is the entry point and does some inits, while this 
   * function is called recursive internaly for every sub-xPath expresion we find.
   *
   * @param     string $xPathQuery XPath expression to be evaluated.
   * @param     string or array $context Full path of a document node, starting
   *            from which the XPath expression should be evaluated.
   * @see       evaluate()
   */
  function _internalEvaluate($xPathQuery, $context='') {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction= FALSE;
    
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction("evaluate");
      echo "Path: $xPathQuery\n";
      echo "Context: $context\n";
    }
    
    // Numpty check
    if (empty($xPathQuery)) {
      $this->_displayError("The $xPathQuery argument must have a value.", __LINE__);
      return FALSE;
    }
    
    // Split the paths that are sparated by '|' into distinct xPath expresions.
    $xPaths = &$this->_bracketExplode('|', $xPathQuery);
    if ($bDebugThisFunction) { echo "<hr>Split the paths that are sparated by '|'\n"; print_r($xPaths); }
    
    // Create an empty set to save the result.
    $result = array();
    
    // Run through all paths.
    reset($xPaths);
    while (list(,$xPath) = each($xPaths)) {
      // mini syntax check
      if (!$this->_bracketsCheck($xPath)) {
        $this->_displayError('While parsing an XPath expression, in the predicate ' .
        str_replace($xPath, '<b>'.$xPath.'</b>', $xPathQuery) .
        ', there was an invalid number of brackets or a bracket mismatch.', __LINE__);
      }
      // Save the current path.
      $this->xpath = $xPath;
      // Split the path at every slash *outside* a bracket.
      $steps = &$this->_bracketExplode('/', $xPath);
      if ($bDebugThisFunction) { echo "<hr>Split the path '$xPath' at every slash *outside* a bracket.\n "; print_r($steps); }
      // Check whether the first element is empty.
      if (empty($steps[0])) {
        // Remove the first and empty element. It's a starting  '//'.
        array_shift($steps);
      }
      // Start to evaluate the steps.
      $nodes = $this->_evaluateStep($context, $steps);
      // Remove duplicated nodes.
      $nodes = array_unique($nodes);
      // Add the nodes to the result set.
      $result = array_merge($result, $nodes);
    }
    //////////////////////////////////////////////
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, $result);
    }
    
    // Return the result.
    return $result;
  }
  
  /**
   * Evaluates a step of an XPath expression.
   *
   * This method tries to evaluate a step from an XPath expression at a
   * specific context.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string or array $context Full document path of the context from
   *            which starting the step should be evaluated.  Either a single
   *             context, or an array of contexts.
   * @param     array $steps Array containing the remaining steps of the
   *            current XPath expression.
   * @return    array This method returns an array containing all nodes
   *            that are the result of evaluating the given XPath step.
   * @see       evaluate()
   */
  function _evaluateStep($context, $steps) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction = FALSE;
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction("_evaluateStep(Context:[$context], steps:[$steps])");
      if (is_array($context)) {
        echo "Context:\n";
        print_r($context);
      } else {
        echo "Context: $context\n";
      }
      echo "Steps: ";
      print_r($steps);
      echo "<hr>\n";
    }
    //////////////////////////////////////////////
    // Create an empty array for saving the nodes found.
    $nodes = array();
    // We may have an "array" of one context.  If so convert it from 
    // array to single string.  Often, this function will be called with
    // a /Path1[1]/Path[3]/Path[2] sytle predicate.
    if (is_array($context) && (count($context) == 1)) $context = $context[0];
    // Check whether the context is an array of contexts.
    if (is_array($context)) {
      // Run through the array.
      $size = sizeOf($context);
      for ($i=0; $i<$size; $i++) {
        if ($bDebugThisFunction) echo "Evaluating step for the {$context[$i]} context...\n";        
        // Call this method for this single path.
        $nodes = array_merge($nodes, $this->_evaluateStep($context[$i], $steps));
      }
    } else {
      // Get this step.
      $step = trim(array_shift($steps));
     
      if ($bDebugThisFunction) echo "Evaluating step $step\n";        
      // Create an array to save the new contexts.
      $contexts = array();
      
      // Get the axis of the current step.
      $axis = $this->_getAxis($step, $context);
      if ($bDebugThisFunction) {
        echo __LINE__.":Axis of step is:\n";
        print_r($axis);
        echo "\n";
      }
      // Check whether it's a function.
      if ($axis['axis'] == 'function') {
        // Check whether an array was return by the function.
        if (is_array($axis['node-test'])) {
          // Add the results to the list of contexts.
          $contexts = array_merge($contexts, $axis['node-test']);
        } else {
          // Add the result to the list of contexts.
          $contexts[] = $axis['node-test'];
        }
      } else {
        // Create the name of the method.
        $method = '_handleAxis_' . $axis['axis'];
      
        // Check whether the axis handler is defined.
        if (!method_exists(&$this, $method)) {
          // Display an error message.
          $this->_displayError('While parsing an XPath expression, the axis ' .
          $axis['axis'] . ' could not be handled, because this version does not support this axis.', __LINE__);
        }
        if ($bDebugThisFunction) echo "Calling user method $method\n";        
        
        // Perform an axis action.
        $contexts = $this->$method($axis, $context);
        if ($bDebugThisFunction) {
          echo "We found these contexts from this step:\n";        
          print_r( $contexts );
          echo "\n";
        }
        
        // Check whether there are predicates.
        if (count($axis['predicate']) > 0) {
          if ($bDebugThisFunction) echo "Filtering contexts by predicate...\n";        
          
          // Check whether each node fits the predicates.
          $contexts = $this->_checkPredicates($contexts, $axis['predicate'], $axis['node-test']);
        }
      }
      
      // Check whether there are more steps left.
      if (count($steps) > 0) {
        if ($bDebugThisFunction) echo "Evaluating next step given the context of the first step...\n";        
        // Continue the evaluation of the next steps.
        $nodes = $this->_evaluateStep($contexts, $steps);
      } else {
        // Save the found contexts.
        $nodes = $contexts;
      }
    }
    
    //////////////////////////////////////////////
    // Return the nodes found.
    $result =  $nodes;
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, $result);
    }
    
    // Return the result.
    return $result;
  }
  
  /**
   * Evaluates an XPath function
   *
   * This method evaluates a given XPath function with its arguments on a
   * specific node of the document.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $function Name of the function to be evaluated.
   * @param     string $arguments String containing the arguments being
   *            passed to the function.
   * @param     string $node Full path to the document node on which the
   *            function should be evaluated.
   * @return    mixed This method returns the result of the evaluation of
   *            the function. Depending on the function the type of the 
   *            return value can be different.
   * @see       evaluate()
   */
  function _evaluateFunction($function, $arguments, $node, $nodeTest) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction= FALSE;
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction("_evaluateFunction(Function:[$function], Arguments:[$arguments], node:[$node], nodeTest:[$nodeTest])");
      if (is_array($arguments)) {
        echo "Arguments:\n";
        print_r($arguments);
      } else {
        echo "Arguments: $arguments\n";
      }
      echo "<hr>\n";
    }

    /////////////////////////////////////
    // Remove whitespaces.
    $function  = trim($function);
    $arguments = trim($arguments);
    // Create the name of the function handling function.
    $method = '_handleFunction_'. $function;
    
    // Check whether the function handling function is available.
    if (!method_exists(&$this, $method)) {
      // Display an error message.
      $this->_displayError("While parsing an XPath expression, ".
        "the function \"$function\" could not be handled, because this ".
        "version does not support this function.", __LINE__);
    }

    if ($bDebugThisFunction) echo "Calling function $method($node, $arguments)\n";        
    
    // Return the result of the function.
    $result = $this->$method($node, $arguments, $nodeTest);

    //////////////////////////////////////////////
    // Return the nodes found.
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, $result);
    }
    
    // Return the result.
    return $result;
  }
  
  /**
   * Evaluates a predicate on a node.
   *
   * This method tries to evaluate a predicate on a given node.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the predicate
   *            should be evaluated.
   * @param     string $predicate String containing the predicate expression
   *            to be evaluated.
   * @return    mixed This method is called recursively. The first call should
   *            return a boolean value, whether the node matches the predicate
   *            or not. Any call to the method being made during the recursion
   *            may also return other types for further processing.
   * @see       evaluate()
   */
  function _evaluatePredicate($node, $predicate, $nodeTest) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction= FALSE;
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction("_evaluatePredicate");
      echo "Node: [$node]\n";
      echo "Predicate: [$predicate]\n";
      echo "<hr>";
    }
    
    // Numpty check
    if (!is_string($predicate)) {
      // Display an error message.
      $this->_displayError("While parsing an XPath expression ".
        "there was an error in the following predicate, ".
        "because it was not a string. It was a '".$predicate."'", __LINE__);
      return FALSE;
    }
    $predicate = trim($predicate);
    // Numpty check.  If they give us an empty string, then this is an error. ## N.S
    if ($predicate === '') { 
      // Display an error message.
      $this->_displayError("While parsing an XPath expression ". 
        "there was an error in the predicate " .
        "because it was the null string.  If you wish to seach ".
        "for the empty string, you must use ''.", __LINE__);
      return FALSE;
    }
    /////////////////////////////////////////////
    // Quick ways out.
    // If it is a literal string, then we return the literal string.  ## N.S. --sb
    $stringDelimiterMismatsh = 0;
    if (preg_match(':^"(.*)"$:', $predicate, $regs)) {
      $result = $regs[1];
      $stringDelimiterMismatsh = strpos(' ' . $result, '"');
      if ($bDebugThisFunction) echo "Predicate is literal\n";        
    } elseif (preg_match(":^'(.*)'$:", $predicate, $regs)) {
      $result = $regs[1];
      $stringDelimiterMismatsh = strpos(' ' . $result, "'");
      if ($bDebugThisFunction) echo "Predicate is literal\n";        
    }
    
    if ($stringDelimiterMismatsh>0) {
      $this->_displayError("While parsing an XPath expression ".
            "there was an string delimiter miss match at pos [{$stringDelimiterMismatsh}] in the predicate string '{$predicate}'.", __LINE__);
      return FALSE;
    }
    
    // Check whether the predicate is just a digit.
    if (!isSet($result)) {
      if (is_numeric($predicate)) {
        // Return the value of the digit.
        $result = doubleval($predicate);
        if ($bDebugThisFunction) echo "Predicate is double\n";        
      }
    }
    /////////////////////////////////////////////
    // Check for operators.
    // Set the default position and the type of the operator.
    $position = 0;
    $operator = '';
    
    // Run through all operators and try to find one.
    if (!isSet($result)) {
      for ($i=0; $i<sizeOf($this->operators); $i++) {
        if ($position >0) break;
        $operator = $this->operators[$i];
        // Quickcheck. If not present don't wast time searching 'the hard way'
        if (strpos($predicate, $operator)===FALSE) continue;
        // Special check
        $position = $this->_searchString($predicate, $operator);
        // Check whether a operator was found.
        if ($position <= 0 ) continue;
        // Check whether it's the equal operator.
        if ($operator == '=') {
          // Also look for other operators containing the equal sign.
          switch ($predicate[$position-1]) {
            case '<' : 
              $position--;
              $operator = '<=';
              break;
            case '>' : 
              $position--;
              $operator = '>=';
              break;
            case '!' : 
              $position--;
              $operator = '!=';
              break;
            default:
          }
        }
        if ($operator == '*') {
          // Get some substrings.
          $character = substr($predicate, $position - 1, 1);
          $attribute = substr($predicate, $position - 11, 11);
        
          // Check whether it's an attribute selection.
          if (( $character == '@' ) || ( $attribute == 'attribute::' )) {
            // Don't use the operator.
            $operator = '';
            $position = -1;
          }
        }
      } // end while each($this->operators)
      
      // Check whether an operator was found.        
      if ($position > 0) {
        if ($bDebugThisFunction) echo "\nPredicate operator is a [$operator] at pos '$position'";        
        // Get the left and the right part of the expression.
        $left_predicate  = trim(substr($predicate, 0, $position));
        $right_predicate = trim(substr($predicate, $position + strlen($operator)));
        if ($bDebugThisFunction) echo "\nLEFT:[$left_predicate]  oper:[$operator]  RIGHT:[$right_predicate]";        
      
        // Remove whitespaces.
        $left_predicate  = trim($left_predicate);
        $right_predicate = trim($right_predicate);
        // Evaluate the left and the right part.
        if ($bDebugThisFunction) echo "\nEvaluating LEFT:[$left_predicate]";
        $left = $this->_evaluatePredicate($node, $left_predicate, $nodeTest);
        if ($bDebugThisFunction) echo "$left_predicate evals as: $left - ";
        // Only evaluate the right part if we need to.
        $right = FALSE;
        if (!$left and ($operator == ' and ')) {
          if ($bDebugThisFunction) echo "\nNo point in evaluating the right predicate: [$right_predicate]";
        } else {
          if ($bDebugThisFunction) echo "\nEvaluating RIGHT:[$right_predicate]";
          $right = $this->_evaluatePredicate($node, $right_predicate, $nodeTest);
          if ($bDebugThisFunction) echo "$right_predicate evals as: $right \n";
        }
        // Check the kind of operator.
        $b_result = FALSE;
        switch ( $operator) {
          case ' or ':
            // Return the two results connected by an 'or'.
            $b_result = (bool)( $left or $right );
            break;
          case ' and ':
            // Return the two results connected by an 'and'.
            $b_result = (bool)( $left and $right );
            break;
          case '=':
            // Compare the two results.
            $b_result = (bool)( $left == $right ); 
            break;                    
          case '!=':
            // Check whether the two results are not equal.
            $b_result = (bool)( $left != $right );
            break;                    
          case '<=':
            // Compare the two results.
            $b_result = (bool)( $left <= $right );
            break;                    
          case '<':
            // Compare the two results.
            $b_result = (bool)( $left < $right );
            break;                
          case '>=':
            // Compare the two results.
            $b_result = (bool)( $left >= $right );
            break;                    
          case '>':
            // Compare the two results.
            $b_result = (bool)( $left > $right );
            break;                    
          case '+':
            // Return the result by adding one result to the other.
            $b_result = $left + $right;
            break;                    
          case '-':
            // Return the result by decrease one result by the other.
            $b_result = $left - $right;
            break;                    
          case '*':
            // Return a multiplication of the two results.
            $b_result =  $left * $right;
            break;                    
          case ' div ':
            // Return a division of the two results.
            if ($right == 0) {
              // Display an error message.
              $this->_displayError('While parsing an XPath '.
                'predicate, a error due a division by zero '.
                'occured.', __LINE__);
            } else {
              // Return the result of the division.
              $b_result = $left / $right;
            }
            break;
          case ' mod ':
            // Return a modulo of the two results.
            $b_result = $left % $right;
            break;                    
        }
        $result = $b_result;
      }
    }

    /////////////////////////////////////////////
    // Check for functions.
    // Check whether the predicate is a function.
    if (!isSet($result)) {
      // do not catch the text() node, which looks like a function in its pattern
      if (preg_match(':\(:U', $predicate) && !preg_match(":text\(\)(\[\d*\])?$:",$predicate) ) {
        // Get the position of the first bracket.
        $start = strpos($predicate, '(');
        // If we search for the right bracket from the end of the string, we can 
        // support nested function calls.  Fix by Andrei Zmievski
        $end   = strrpos($predicate, ')');
      
        // Get everything before, between and after the brackets.
        $before  = substr($predicate, 0, $start);
        $between = substr($predicate, $start + 1, $end - $start - 1);
        $after   = substr($predicate, $end + 1);
      
        // Trim each string.
        $before  = trim($before);
        $between = trim($between);
        $after   = trim($after);
      
        if ($bDebugThisFunction) echo "\nPredicate is function \"$before\"";        
        // Check whether there's something after the bracket.
        if (!empty($after)) {
          // Display an error message.
          $this->_displayError('While parsing an XPath expression there was an error in the predicate ' .
            str_replace($predicate,'<b>'.$predicate.'</b>', $this->xpath) .
            '. After a closing bracket there was something unknown: "'. $after .'"', __LINE__);
        }
      
        // Check whether it's a function.
        if (empty($before) && empty($after)) {
          // Evaluate the content of the brackets.
          $result = $this->_evaluatePredicate($node, $between, $nodeTest);
        }
        elseif ($this->_isFunction($before)) {
          // Return the evaluated function.
          $result = $this->_evaluateFunction($before, $between, $node, $nodeTest);
        } 
        else {
          // Display an error message.
          $this->_displayError('While parsing a predicate in an XPath expression, a function '.
            str_replace($before, '<b>'.$before.'</b>', $this->xpath) . 
            ' was found, which is not yet supported by the parser.', __LINE__);
        }
      }
    }
    
    /////////////////////////////////////////////
    // Else it must just be an XPath expression.
    // Check whether it's an XPath expression.
    if (!isSet($result)) {
      if ($bDebugThisFunction) echo "\nPredicate is XPath expression.";
      $a_xpath_result = $this->_internalEvaluate($predicate, $node);
      if (count($a_xpath_result) > 0) {
        // Convert the array.
        $result = explode("|", implode("|", $a_xpath_result));
        // Get the value of the first result (which means we want to concat all the text...unless
        // a specific text() node has been given, and it will switch off to substringData
        $result = $this->wholeText($a_xpath_result[0]);            
      }
    }
    
    // Else no content so return the empty string.  ## N.S
    if (!isSet($result)) $result = '';
    //////////////////////////////////////////////
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, $result);
    }
    
    // Return the array of nodes.
    return $result;
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Check functions for tailoring a node set
  
  /**
   * -- sb:stoped
   *
   * Checks whether a node matches predicates.
   *
   * This method checks whether a list of nodes passed to this method match
   * a given list of predicates. 
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $nodes Array of full paths of all nodes to be tested.
   * @param     array $predicates Array of predicates to use.
   * @return    array The array returned by this method contains a list of
   *            all nodes matching the given predicates.
   * @see       _evaluateStep()
   */
  function _checkPredicates($nodes, $predicates, $nodeTest) {
    // If you are having difficulty using this function.  Then set this to TRUE and 
    // you'll get diagnostic info displayed to the output.
    $bDebugThisFunction = FALSE;
    if ($bDebugThisFunction) {
      $a_start_time = $this->_beginDebugFunction("_checkPredicates(Nodes:[$nodes], Predicates:[$predicates])");
      echo "Nodes:";
      print_r($nodes);
      echo "Predicates:";
      print_r($predicates);
      echo "<hr>";
    }
    //////////////////////////////////////////////
    // Create an empty set of nodes.
    $result = array();
    
    // Run through all nodes.
    for ($i=0; $i<sizeOf($nodes); $i++) {
      $node = &$nodes[$i];
      // Create a variable whether to add this node to the node-set.
      $add = TRUE;
      
      // Run through all predicates.
      for ($j=0; $j<sizeOf($predicates); $j++) {
        $predicate = &$predicates[$j]; 
        if ($bDebugThisFunction) echo "Evaluating predicate \"$predicate\"\n";
        // Check whether the predicate is just an number.
        if (preg_match('/^\d+$/', $predicate)) {
          if ($bDebugThisFunction) echo "Taking short cut and calling _handleFunction_position() directly.\n";
          // Take a short cut.  If it is just a position, then call 
          // _handleFunction_position() directly.  70% of the
          // time this will be the case. ## N.S
          $check = (bool) ($predicate == $this->_handleFunction_position($node, '', $nodeTest));
          // Enhance the predicate.
          //                    $predicate .= "=position()";
        } else {                
          // Else do the predicate check the long and thorough way.
          $check = $this->_evaluatePredicate($node, $predicate, $nodeTest);
        }
        // Check whether it's a string.
        if (is_string($check) && ( ( $check == '' ) 
           || ( $check == $predicate ) )) {
          // Set the result to FALSE.
          $check = FALSE;
        } 
        else if (is_bool($check) )  {
          // 0 and 1 are both bools and ints.  We need to capture the bools
          // as they might have been the intended result                    ## N.S
        } else
        // Check whether it's an integer.
        if (is_int($check)) {
          // Check whether it's the current position.
          if ($check == $this->_handleFunction_position($node, '', $nodeTest)) {
            // Set it to TRUE.
            $check = TRUE;
          }
          else {
            // Set it to FALSE.
            $check = FALSE;
          }
        }
        if ($bDebugThisFunction) echo "Node $node matches predicate $predicate: " . (($check) ? "TRUE" : "FALSE") ."\n";
        // Check whether the predicate is OK for this node.
        $add = $add && $check;
      }
       
      // Check whether to add this node to the node-set.
      if ($add) {
        // Add the node to the node-set.
        $result[] = $node;
      }            
      if ($bDebugThisFunction) echo "Node $node matches: " . (($add) ? "TRUE" : "FALSE") ."\n\n";        
    }
    //////////////////////////////////////////////
    if ($bDebugThisFunction) {
      $this->_closeDebugFunction($a_start_time, $result);
    }
    
    // Return the array of nodes.
    return $result;
  }
  
  /**
   * Checks whether a node matches a node-test.
   *
   * This method checks whether a node in the document matches a given
   * node-test.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $context Full path of the node, which should be tested
   *            for matching the node-test.
   * @param     string $nodeTest String containing the node-test for the
   *            node.
   * @return    boolean This method returns TRUE if the node matches the
   *            node-test, otherwise FALSE.
   * @see       evaluate()
   */
  function _checkNodeTest($context, $nodeTest) {
    // Check whether it's a function.
    if (preg_match('/\(/U', $nodeTest)) {
      // Get the type of function to use.
      $function = $this->_prestr($nodeTest, '(');
      
      // Check whether the node fits the method.
      switch ( $function) {
        case 'node':
          // Add this node to the list of nodes.
          return TRUE;
        case 'text':
          // Check whether the node has some text.
          $tmp = implode('', $this->nodes[$context]['text']);
          if (!empty($tmp)) {
            // Add this node to the list of nodes.
            return TRUE;
          }
          break;
        case 'comment':
          // Check whether the node has some comment.
          if (!empty($this->nodes[$context]['comment'])) {
            // Add this node to the list of nodes.
            return TRUE;
          }
          break;
        case 'processing-instruction':
          // Get the literal argument.
          $literal = $this->_afterstr($axis['node-test'], '(');
          
          // Cut the literal.
          $literal = substr($literal, 0, strlen($literal) - 1);
          
          // Check whether a literal was given.
          if (!empty($literal)) {
            // Check whether the node's processing instructions
            // are matching the literals given.
            if ($this->nodes[$context]['processing-instructions'] == $literal) {
              // Add this node to the node-set.
              return TRUE;
            }
          } else {
            // Check whether the node has processing
            // instructions.
            if (!empty($this->nodes[$context]['processing-instructions'])) {
              // Add this node to the node-set.
              return TRUE;
            }
          }
          break;
        default:
          // Display an error message.
          $this->_displayError('While parsing an XPath expression there was an undefined function called "' .
             str_replace($function, '<b>'.$function.'</b>', $this->xpath) .'"', __LINE__);
      }
    }
    elseif ($nodeTest == '*') {
      // Add this node to the node-set.
      return TRUE;
    }
    elseif (preg_match('/^[a-zA-Z0-9\-_]+/', $nodeTest)) {
      // Check whether the node-test can be fulfilled.
      if ($this->nodes[$context]['name'] == $nodeTest) {
        // Add this node to the node-set.
        return TRUE;
      }
    }
    else {
      // Display an error message.
      $this->_displayError("While parsing the XPath expression \"{$this->xpath}\" ".
        "an empty and therefore invalid node-test has been found.", __LINE__);
    }
    
    // Don't add this context.
    return FALSE;
  }
   
  /////////////////////////////////////////////////
  // ########################################### //
  // Functions to handle each of the different xpath axes.

  /**
   * Handles the XPath child axis.
   *
   * This method handles the XPath child axis.  It essentially filters out the
   * children to match the name specified after the /
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_Child($axis, $context) {
    // Create an empty node-set to hold the results of the child matches
    $nodes = array();

    if ( $axis["node-test"] == "cdata" ) {
      if ( !isSet($this->nodes[$context]["text"]) ) return "";
      foreach ($this->nodes[$context]['text'] as $index => $text) {
        $nodes[] = $context . "/text()[".($index + 1)."]";
      }
    }
    else {
      // Get a list of all children.
      $allChildren = &$this->nodes[$context]['children'];
      // Run through all children in the order they were set.
      for ( $i=0; $i < sizeOf($allChildren); $i++ ) {
        $child = $context.'/'.$allChildren[$i];
        // Check whether 
        if ($this->_checkNodeTest($child, $axis['node-test'])) 
        {
          // Add the child to the node-set.
          $nodes[] = $child;
        }
      }
    }
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath parent axis.
   *
   * This method handles the XPath parent axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_parent ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Check whether the parent matches the node-test.
    if ($this->_checkNodeTest($this->nodes[$context]['parent'], $axis['node-test'])) {
      // Add this node to the list of nodes.
      $nodes[] = $this->nodes[$context]['parent'];
    }
    
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath attribute axis.
   *
   * This method handles the XPath attribute axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_attribute ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Check whether all nodes should be selected.
    $nodeAttr = &$this->nodes[$context]['attributes'];
    if ($axis['node-test'] == '*') {
      // Check whether there are attributes.
      if (count($nodeAttr) > 0) {
        // Run through the attributes.
        reset($nodeAttr);
        while (list($key) = each($nodeAttr)) {
          // Add this node to the node-set.
          $nodes[] = $context.'/attribute::'.$key;
        }
      }
    }
    elseif (isSet($nodeAttr[$axis['node-test']]) AND strlen($nodeAttr[$axis['node-test']])) {
      // Add this node to the node-set.
      $nodes[] = $context . '/attribute::'. $axis['node-test'];
    }
      
    // Return the nodeset.
    return $nodes;
  }
   
  /**
   * Handles the XPath self axis.
   *
   * This method handles the XPath self axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_self ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Check whether the context match the node-test.
    if ($this->_checkNodeTest($context, $axis['node-test'])) {
      // Add this node to the node-set.
      $nodes[] = $context;
    }
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath descendant axis.
   *
   * This method handles the XPath descendant axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_descendant ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();        
    // Get a list of all children.
    $children = &$this->nodes[$context]['children'];
    // Run through all children in the order they where set.
    $childSize = sizeOf($children);
    for ($i=0; $i<$childSize; $i++) {
      $child = $context.'/'.$children[$i];
      // Check whether the child matches the node-test.
      if ($this->_checkNodeTest($child, $axis['node-test'])) {
        // Add the child to the list of nodes.
        $nodes[] = $child;
      }
      // Recurse to the next level.
      $nodes = array_merge($nodes, $this->_handleAxis_descendant($axis, $child));
    }
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath ancestor axis.
   *
   * This method handles the XPath ancestor axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_ancestor ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Get the parent of the current node.
    $parent = $this->nodes[$context]['parent'];
    
    // Check whether the parent isn't empty.
    if (!empty($parent)) {
      // Check whether the parent matches the node-test.
      if ($this->_checkNodeTest($parent, $axis['node-test'])) {
        // Add the parent to the list of nodes.
        $nodes[] = $parent;
      }
      
      // Handle all other ancestors.
      $nodes = array_merge($nodes, $this->_handleAxis_ancestor($axis, $parent));
    }
    
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath namespace axis.
   *
   * This method handles the XPath namespace axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_namespace ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Check whether all nodes should be selected.
    if (!empty($this->nodes[$context]['namespace'])) {
      // Add this node to the node-set.
      $nodes[] = $context;
    }
      
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath following axis.
   *
   * This method handles the XPath following axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_following ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Get the current document position.
    $position = $this->nodes[$context]['doc-pos'];
    
    // Run through all nodes of the document.
    reset($this->nodes);
    while (list($node) = each($this->nodes)) {
      // Check whether this is the context node.
      if ($node == $context ) break;
    }
    while (list($node) = each($this->nodes)) {
      // Check whether this is the context node.
      if ($this->nodes[$node]['doc-pos'] <= $position) break;
    }
    do {
        // Check whether the node fits the node-test.
        if ($this->_checkNodeTest($node, $axis['node-test'])) {
          // Add the node to the list of nodes.
          $nodes[] = $node;
        }
      
    } while (list($node) = each($this->nodes));
      
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath preceding axis.
   *
   * This method handles the XPath preceding axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_preceding ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Get the current document position.
    $position = $this->nodes[$context]['doc-pos'];
    
    // Run through all nodes of the document.
    reset($this->nodes);
    while (list($node) = each($this->nodes)) {
      // skip super-Root
      if (empty($node)) continue;
      // Check whether this is the context node.
      
      if ($node == $context) {
        // After this we won't look for more nodes.
        break;
      }
      if (!strncmp($node, $context, strLen($node))) {
        continue;
      }
      // Check whether the node fits the node-test.
      if ($this->_checkNodeTest($node, $axis['node-test'])) {
        // Add the node to the list of nodes.
        $nodes[] = $node;
      }
    }
      
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath following-sibling axis.
   *
   * This method handles the XPath following-sibling axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_following_sibling ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Get all children from the parent.
    $siblings = &$this->_handleAxis_child($axis, $this->nodes[$context]['parent']);
    // Create a flag whether the context node was already found.
    $found = FALSE;
    
    // Run through all siblings.
    $size = sizeOf($siblings);
    for ($i=0; $i<$size; $i++) {
      $sibling = &$siblings[$i];
      
      // Check whether the context node was already found.
      if ($found) {
        // Check whether the sibling matches the node-test.
        if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
          // Add the sibling to the list of nodes.
          $nodes[] = $sibling;
        }
      }
      
      // Check if we reached *this* context node.
      if ($sibling == $context) {
        // Continue looking for other siblings.
        $found = TRUE;
      }
    }
      
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath preceding-sibling axis.
   *
   * This method handles the XPath preceding-sibling axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_preceding_sibling ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Get all children from the parent.
    $siblings = $this->_handleAxis_child($axis, $this->nodes[$context]['parent']);
    
    // Run through all siblings.
    $size = sizeOf($siblings);
    for ($i=0; $i<$size; $i++) {
      $sibling = &$siblings[$i];
      // Check whether this is the context node.
      if ($sibling == $context) {
        // Don't continue looking for other siblings.
        break;
      }
    
      // Check whether the sibling matches the node-test.
      if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
        // Add the sibling to the list of nodes.
        $nodes[] = $sibling;
      }
    }
      
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath descendant-or-self axis.
   *
   * This method handles the XPath descendant-or-self axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_descendant_or_self ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Read the nodes.
    $nodes = array_merge(
      $this->_handleAxis_self($axis, $context),
      $this->_handleAxis_descendant($axis, $context)
      );
    
    // Return the nodeset.
    return $nodes;
  }
  
  /**
   * Handles the XPath ancestor-or-self axis.
   *
   * This method handles the XPath ancestor-or-self axis.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     array $axis Array containing information about the axis.
   * @param     string $context Node from which starting the axis should
   *            be processed.
   * @return    array This method returns an array containing all nodes 
   *            that were found during the evaluation of the given axis.
   * @see       evaluate()
   */
  function &_handleAxis_ancestor_or_self ( $axis, $context) {
    // Create an empty node-set.
    $nodes = array();
    
    // Read the nodes.
    $nodes = array_merge(
      $this->_handleAxis_self($axis, $context),
      $this->_handleAxis_ancestor($axis, $context)
    );
    
    // Return the nodeset.
    return $nodes;
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Functions to handle each of the different xpath functions.
  
  /**
   * Handles the XPath function last.
   *
   * This method handles the XPath function last.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_last ( $node, $arguments, $nodeTest) {
    // Calculate the size of the context.
    $parent   = $this->nodes[$node]['parent'];
    if ($nodeTest == "*")
    {
      $context = sizeOf($this->nodes[$parent]['children']);
    }
    else if ($nodeTest == "cdata")
    {
      $node = substr($node,0,strrpos($node,"/text()"));
      $context = sizeOf($this->nodes[$node]["text"]);
    }
    else
    {
      $children = $this->nodes[$parent]['childCount'];
      $context  = $children[$this->nodes[$node]['name']];
    }
    // Return the size.
    return $context;
  }
  
  /**
   * Handles the XPath function position.
   *
   * This method handles the XPath function position.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_position ( $node, $arguments, $nodeTest) {
    // return the context-position.
    if ($nodeTest == "*")
    {
      // if we are matching all children, then we need to find the position regardless of name
      $parent = $this->nodes[$node]['parent'];
      $indexChildren = array_flip($this->nodes[$parent]['children']); 
      $currentChild = substr($node,strrpos($node,"/")+1,strlen($node));
      $context = $indexChildren[$currentChild] + 1;
    }
    // if we are looking for text nodes, we go about it a bit differently
    else if( $nodeTest == "cdata" ) {
      $context = substr($node,strrpos($node,"[")+1,-1);
    }
    else
    {
      $context = $this->nodes[$node]['context-pos'];
    }
    return $context;
  }
  
  /**
   * Handles the XPath function count.
   *
   * This method handles the XPath function count.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_count ( $node, $arguments, $nodeTest) {
    // Evaluate the argument of the method as an XPath and return
    // the number of results.
    return count($this->_internalEvaluate($arguments, $node));
  }
  
  /**
   * Handles the XPath function id.
   *
   * This method handles the XPath function id.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_id ( $node, $arguments, $nodeTest) {
    // Trim the arguments.
    $arguments = trim($arguments);
    
    // Now split the arguments.
    $arguments = explode(' ', $arguments);
    
    // Check whether 
    
    // Create a list of nodes.
    $nodes = array();
    
    // Run through all nodes of the document.
    reset($this->nodes);
    while (list($node) = each($this->nodes)) {
      // skip super-Root
      if (empty($node)) continue;
      // Check whether the node has the ID we're looking for.
      if (in_array($this->nodes[$node]['attributes']['id'], $arguments)) {
        // Add this node to the list of nodes.
        $nodes[] = $node;
      }
    }
    
    // Return the list of nodes.
    return $nodes;
  }
  
  /**
   * Handles the XPath function name.
   *
   * This method handles the XPath function name.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_name ( $node, $arguments, $nodeTest) {
    // Return the name of the node.
    return $this->nodes[$node]['name'];
  }
  
  /**
   * Handles the XPath function string.
   *
   * This method handles the XPath function string.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_string ( $node, $arguments, $nodeTest) {
    // Check what type of parameter is given
    if (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) 
       || preg_match('/^\.[0-9]+$/', $arguments)) {
      // Convert the digits to a number.
      $number = doubleval($arguments);
        
      // Return the number.
      return strval($number);
    }
    elseif (is_bool($arguments)) {
      // Check whether it's TRUE.
      if ($arguments == TRUE) {
        // Return TRUE as a string.
        return 'TRUE';
      }
      else {
        // Return FALSE as a string.
        return 'FALSE';
      }
    }
    elseif (!empty($arguments)) {
      // Use the argument as an XPath.
      $result = $this->_internalEvaluate($arguments, $node);
        
      // Get the first argument.
      $result = explode('|', implode('|', $result));
        
      // Return the first result as a string.
      return $result[0];
    }
    elseif (empty($arguments)) {
      // Return the current node.
      return $node;
    }
    else {
      // Return an empty string.
      return '';
    }
  }
  
  /**
   * Handles the XPath function concat.
   *
   * This method handles the XPath function concat.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_concat ( $node, $arguments, $nodeTest) {
    // Split the arguments.
    $arguments = explode(',', $arguments);
      
    // Run through each argument and evaluate it.
    for ( $i = 0; $i < sizeof($arguments); $i++) {
      // Trim each argument.
      $arguments[$i] = trim($arguments[$i]);
        
      // Evaluate it.
      $arguments[$i] = $this->_evaluatePredicate($node, $arguments[$i], $nodeTest);
    }
      
    // Put the string together.
    $arguments = implode('', $arguments);
      
    // Return the string.
    return $arguments;
  }
  
  /**
   * Handles the XPath function starts-with.
   *
   * This method handles the XPath function starts-with.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_starts_with ($node, $arguments, $nodeTest) {
    // Get the arguments.
    $first  = trim($this->_prestr($arguments, ','));
    $second = trim($this->_afterstr($arguments, ','));
      
    // Evaluate each argument.
    $first  = $this->_evaluatePredicate($node, $first, $nodeTest);
    $second = $this->_evaluatePredicate($node, $second, $nodeTest);
      
    // Check whether the first string starts with the second one.
    if (ereg('^'.$second, $first)) {
      // Return TRUE.
      return TRUE;
    } else {
      // Return FALSE.
      return FALSE;
    }
  }
  
  /**
   * Handles the XPath function contains.
   *
   * This method handles the XPath function contains.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_contains ( $node, $arguments, $nodeTest) {
    // Get the arguments.
    $first  = trim($this->_prestr($arguments, ','));
    $second = trim($this->_afterstr($arguments, ','));
    //echo "Predicate: $arguments First: ".$first." Second: ".$second."\n";
    
    // Evaluate each argument.
    $first = $this->_evaluatePredicate($node, $first, $nodeTest);
    $second = $this->_evaluatePredicate($node, $second, $nodeTest);
    //echo $second.": ".$first."\n";
    // If the search string is null, then the provided there is a value it will contain it as
    // it is considered that all strings contain the empty string. ## N.S.
    if ($second==='') return TRUE;
     
    // Check whether the first string starts with the second one.
    if (strpos($first, $second) === FALSE) {
      return FALSE;
    } else {
      return TRUE;
    }
  }
  
  /**
   * Handles the XPath function substring-before.
   *
   * This method handles the XPath function substring-before.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_substring_before ( $node, $arguments, $nodeTest) {
    // Get the arguments.
    $first  = trim($this->_prestr($arguments, ','));
    $second = trim($this->_afterstr($arguments, ','));
      
    // Evaluate each argument.
    $first  = $this->_evaluatePredicate($node, $first, $nodeTest);
    $second = $this->_evaluatePredicate($node, $second, $nodeTest);
      
    // Return the substring.
    return $this->_prestr(strval($first), strval($second));
  }
  
  /**
   * Handles the XPath function substring-after.
   *
   * This method handles the XPath function substring-after.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_substring_after ( $node, $arguments, $nodeTest) {
    // Get the arguments.
    $first  = trim($this->_prestr($arguments, ','));
    $second = trim($this->_afterstr($arguments, ','));
      
    // Evaluate each argument.
    $first  = $this->_evaluatePredicate($node, $first, $nodeTest);
    $second = $this->_evaluatePredicate($node, $second, $nodeTest);
      
    // Return the substring.
    return $this->_afterstr(strval($first), strval($second));
  }
  
  /**
   * Handles the XPath function substring.
   *
   * This method handles the XPath function substring.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_substring ( $node, $arguments, $nodeTest) {
    // Split the arguments.
    $arguments = explode(",", $arguments);
      
    // Run through all arguments.
    for ( $i = 0; $i < sizeof($arguments); $i++) {
      // Trim the string.
      $arguments[$i] = trim($arguments[$i]);
        
      // Evaluate each argument.
      $arguments[$i] = $this->_evaluatePredicate($node, $arguments[$i], $nodeTest);
    }
      
    // Check whether a third argument was given.
    if (!empty($arguments[2])) {
      // Return the substring.
      return substr(strval($arguments[0]), $arguments[1] - 1,
        $arguments[2]);
    } else {
      // Return the substring.
      return substr(strval($arguments[0]), $arguments[1] - 1);
    }
  }
  
  /**
   * Handles the XPath function string-length.
   *
   * This method handles the XPath function string-length.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_string_length ( $node, $arguments, $nodeTest) {
    // Trim the argument.
    $arguments = trim($arguments);
    // Evaluate the argument.
    $arguments = $this->_evaluatePredicate($node, $arguments, $nodeTest);
    // Return the length of the string.
    return strlen(strval($arguments));
  }
  
  /**
   * Handles the XPath function normalize-space.
   *
   * The normalize-space function returns the argument string with whitespace 
   * normalized by stripping leading and trailing whitespace and replacing sequences 
   * of whitespace characters by a single space.
   * If the argument is omitted, it defaults to the context node converted to a string, 
   * in other words the string-value of the context node
   *
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    string trimed string
   * @see       evaluate()
   */
  function _handleFunction_normalize_space ( $node, $arguments, $nodeTest) {
      // Trim the argument.
      if (empty($arguments)) {
        $arguments = $this->nodes[$node]['parent'].'/'.$this->nodes[$node]['name'].'['.$this->nodes[$node]['context-pos'].']';
      } else {
         $arguments = $this->_evaluatePredicate($node, $arguments, $nodeTest);
      }
      $arguments = trim(preg_replace (";[[:space:]]+;s",' ',$arguments));
      return $arguments;
  }
  
  /**
   * Handles the XPath function translate.
   *
   * This method handles the XPath function translate.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_translate ( $node, $arguments, $nodeTest) {
    // Split the arguments.
    $arguments = explode(',', $arguments);
    
    // Run through all arguments.
    for ( $i = 0; $i < sizeof($arguments); $i++) {
      // Trim the argument.
      $arguments[$i] = trim($arguments[$i]);
      // Evaluate the argument.
      $arguments[$i] = $this->_evaluatePredicate($node, $arguments[$i], $nodeTest);
    }
      
    // Return the translated string.
    return strtr($arguments[0], $arguments[1], $arguments[2]);
  }
  
  /**
   * Handles the XPath function boolean.
   *
   * This method handles the XPath function boolean.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_boolean ( $node, $arguments, $nodeTest) {
    // Trim the arguments.
    $arguments = trim($arguments);
    
    // Check what type of parameter is given
    if (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) 
       || preg_match('/^\.[0-9]+$/', $arguments)) {
      // Convert the digits to a number.
      $number = doubleval($arguments);
      
      // Check whether the number zero.
      if ($number == 0) {
        // Return FALSE.
        return FALSE;
      } else {
        // Return TRUE.
        return TRUE;
      }
    }
    elseif (empty($arguments)) {
      // Sorry, there were no arguments.
      return FALSE;
    }
    else {
      // Try to evaluate the argument as an XPath.
      $result = $this->_internalEvaluate($arguments, $node);
      
      // Check whether we found something.
      if (count($result) > 0) {
        // Return TRUE.
        return TRUE;
      } else {
        // Return FALSE.
        return FALSE;
      }
    }
  }
  
  /**
   * Handles the XPath function not.
   *
   * This method handles the XPath function not.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_not ( $node, $arguments, $nodeTest) {
    // Trim the arguments.
    $arguments = trim($arguments);
    
    // Return the negative value of the content of the brackets.
    return !$this->_evaluatePredicate($node, $arguments, $nodeTest);
  }
  
  /**
   * Handles the XPath function TRUE.
   *
   * This method handles the XPath function TRUE.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_true ( $node, $arguments, $nodeTest) {
    // Return TRUE.
    return TRUE;
  }
  
  /**
   * Handles the XPath function FALSE.
   *
   * This method handles the XPath function FALSE.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_false ( $node, $arguments, $nodeTest) {
    // Return FALSE.
    return FALSE;
  }
  
  /**
   * Handles the XPath function lang.
   *
   * This method handles the XPath function lang.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_lang ( $node, $arguments, $nodeTest) {
    // Trim the arguments.
    $arguments = trim($arguments);
    
    // Check whether the node has an language attribute.
    if (empty($this->nodes[$node]['attributes']['xml:lang'])) {
      // Run through the ancestors.
      while ( !empty($node)) {
        // Select the parent node.
        $node = $this->nodes[$node]['parent'];
        
        // Check whether there's a language definition.
        if (!empty($this->nodes[$node]['attributes']['xml:lang'])) {
          // Check whether it's the language, the user asks for.
          if (eregi('^'.$arguments, $this->nodes[$node]['attributes']['xml:lang'])) {
            // Return TRUE.
            return TRUE;
          } else {
            // Return FALSE.
            return FALSE;
          }
        }
      }
      
      // Return FALSE.
      return FALSE;
    } else {
      // Check whether it's the language, the user asks for.
      if (eregi('^'.$arguments, $this->nodes[$node]['attributes']['xml:lang'])) {
        // Return TRUE.
        return TRUE;
      } else {
        // Return FALSE.
        return FALSE;
      }
    }
  }
  
  /**
   * Handles the XPath function number.
   *
   * This method handles the XPath function number.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_number ( $node, $arguments, $nodeTest) {
    if (!is_numeric($arguments)) {
      $arguments = $this->_evaluatePredicate($node, $arguments, $nodeTest);
    }
    // Check the type of argument.
    if (is_numeric($arguments)) {
      // Return the argument as a number.
      return doubleval($arguments);
    }
    elseif (is_bool($arguments)) {
      // Check whether it's TRUE.
      if ($arguments == TRUE) {
        // Return 1.
        return 1;
      } else {
        // Return 0.
        return 0;
      }
    }
  }
  
  /**
   * Handles the XPath function sum.
   *
   * This method handles the XPath function sum.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_sum ( $node, $arguments, $nodeTest) {
    // Trim the arguments.
    $arguments = trim($arguments);
    
    // Evaluate the arguments as an XPath expression.
    $result = $this->_internalEvaluate($arguments, $node);
    
    // Create a variable to save the sum.
    $sum = 0;
    
    // Run through all results.
    for ($i=0; $i<sizeOf($result); $i++) {
      // Get the value of the node.
      $value = $this->substringData($result[$i]);
      // Add it to the sum.
      $sum += doubleval($value);
    }
    
    // Return the sum.
    return $sum;
  }
  
  /**
   * Handles the XPath function floor.
   *
   * This method handles the XPath function floor.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_floor ( $node, $arguments, $nodeTest) {
    if (!is_numeric($arguments)) {
      $arguments = $this->_evaluatePredicate($node, $arguments, $nodeTest);
    }
    // Convert the arguments to a number.
    $arguments = doubleval($arguments);
    
    // Return the result
    return floor($arguments);
  }
  
  /**
   * Handles the XPath function ceiling.
   *
   * This method handles the XPath function ceiling.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_ceiling ( $node, $arguments, $nodeTest) {
    if (!is_numeric($arguments)) {
      $arguments = $this->_evaluatePredicate($node, $arguments, $nodeTest);
    }
    
    // Convert the arguments to a number.
    $arguments = doubleval($arguments);
    
    // Return the result
    return ceil($arguments);
  }
  
  /**
   * Handles the XPath function round.
   *
   * This method handles the XPath function round.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $node Full path of the node on which the function
   *            should be processed.
   * @param     string $arguments String containing the arguments that were
   *            passed to the function.
   * @return    mixed Depending on the type of function being processed this 
   *            method returns different types.
   * @see       evaluate()
   */
  function _handleFunction_round ( $node, $arguments, $nodeTest) {
    if (!is_numeric($arguments)) {
      $arguments = $this->_evaluatePredicate($node, $arguments, $nodeTest);
    }
    
    // Convert the arguments to a number.
    $arguments = doubleval($arguments);
    
    // Return the result
    return round($arguments);
  }
  
  /////////////////////////////////////////////////
  // ########################################### //
  // General helper functions

  /**
   * Set the content of a node.
   *
   * This method modifies the content of a text node. If it's an attribute node, then
   * the value of the attribute will be modified, otherwise the complete character 
   * data of the specific text node will be set.  This function is used for all the Data
   * functions, as the modification can be reduced to a single statment once all the checks
   * and initializations are done
   *
   * @param     string $xPathQuery path to the node (See text above). *READONLY*
   * @param     string $content String containing the content to be set. *READONLY*
   * @param     bool   $replace if TRUE the given substring will be replaced with the $content...
   *            else it will be inserted or appended
   * @param     int    $offset will be where the string will be inserted or begin to be replaced..
   *            a value of 1 is the beginning, 0 will be the end
   * @see       appendData(), insertData(), replaceData(), deleteData(), substringData()
   */
  function _setContent($absoluteXPath, $content, $replace, $offset = -1, $count = 0 ) {
    // we default offset to -1 for the end of the string
    // Check whether this path goes right to a text node, if so then modify the cdata
    if ( preg_match(":(.*)/text\(\)(\[(.*)\])?$:U",$absoluteXPath,$matches) ) {
      $absoluteXPath = $matches[1];
      // default to the first text node if a text node was not specified
      $textPart = isset($matches[2]) ? substr($matches[2],1,-1) : 1;
      // Numpty check
      if ( !isSet($this->nodes[$absoluteXPath]) ) {
        // Try to evaluate the absoluteXPath (since it really isn't an absolutePath)
        $resultArr = $this->match("$absoluteXPath/text()[$textPart]");
        if ( sizeOf($resultArr) == 1 ) {
          preg_match(":(.*)/text\(\)(\[(.*)\])?$:U",$resultArr[0],$matches); 
          $absoluteXPath = $matches[1];
          $textPart = isset($matches[2]) ? substr($matches[2],1,-1) : 1;
        } 
        else {
          $this->_displayError("The $absoluteXPath/text() does not evaluate to a single text node in this document.", __LINE__, FALSE);
          return;
        }
      }
      if ( !isSet($this->nodes[$absoluteXPath]["text"][$textPart - 1]) )
      {
        $this->_displayError("The $absoluteXPath does not have a text value at position ".($textPart-1), __LINE__, FALSE);
        return;
      }
      // Get a reference to the text node
      $textNode = &$this->nodes[$absoluteXPath]["text"][$textPart - 1];
    }
    else if ( preg_match(";(.*)/(attribute::|@)([^/]*)$;U",$absoluteXPath,$matches) ) {
      $absoluteXPath = $matches[1];
      $attribute = $matches[3];
      // Numpty check (1. make sure root path exists 2. make sure attribute exists)
      if ( !isSet($this->nodes[$absoluteXPath]) ) {
        // Try to evaluate the absoluteXPath (since it really isn't an absolutePath)
        $resultArr = $this->match("$absoluteXPath/attribute::$attribute");
        if ( sizeOf($resultArr) == 1 ) {
          preg_match(";(.*)/attribute::([^/]*)$;U",$resultArr[0],$matches);
          $absoluteXPath = $matches[1];
          $attribute = $matches[2];
        } 
        else {
          $this->_displayError("The $absoluteXPath/attribute::$attribute does not evaluate to a single node.", __LINE__, FALSE);
          break;
        }
      }
      else if ( !isSet($this->nodes[$absoluteXPath]["attributes"][$attribute]) ) {
        $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, FALSE);
        break;
      }
      // Get a reference to the attribute node 
      $textNode = &$this->nodes[$absoluteXPath]['attributes'][$attribute];
    }
    else {
    // we have been given an xpath with neither a text() or attribute axis at the end
    // find the first text() node and return that
      // Numpty check
      if ( !isSet($this->nodes[$absoluteXPath]) ) {
        // Try to evaluate the absoluteXPath (since it really isn't an absolutePath)
        $resultArr = $this->match($absoluteXPath);
        if ( sizeOf($resultArr) == 1 ) {
          $absoluteXPath = $resultArr[0];
        } 
        else {
          $this->_displayError("The $absoluteXPath does not evaluate to a single node in this document.", __LINE__, FALSE);
          return;
        }
      }
      if ( !is_array($this->nodes[$absoluteXPath]["text"]) ) {
        $this->_displayError("The $absoluteXPath is not a valid element node in the document.", __LINE__, FALSE);
        return;
      }
      $textNode = &$this->nodes[$absoluteXPath]["text"][0];
    }

    // Now modify the cdata, using the reference $textNode
    if ($replace) {
      $textNode = substr($textNode, 0, $offset) . $content . (($count) ? substr($textNode, $offset+$count) : '');
    }
    else {
      // if the offset is -1, append it, else insert it at the offset
      $textNode = ($offset == -1) ? "$textNode$content" : substr($textNode, 0, $offset).$content.substr($textNode, $offset);
      // This return is for substringData, ignored by rest of the functions
      return ($count) ? substr($textNode, $offset, $count) : substr($textNode, $offset);
    }
     
  }

  /////////////////////////////////////////////////
  // ########################################### //
  // Auxilliary functions for dealing with bracketed strings.
    
  /**
   * This method checks the right ammount and match of brackets
   *
   * @author    Sam Blume <bs_php@infeer.com>
   * @param     string $term String in which is checked.
   * @return    bool TRUE: OK / FALSE: KO  
   * @see       _evaluateStep()
   */
  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.
   *
   * 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.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $term String in which the search shall take place.
   * @param     string $expression 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.
   * @see       _evaluateStep()
   */
  function _searchString($term, $expression) {
    $bracketCounter = 0;
    $leng = strlen($term);
    for ( $i = 0; $i < $leng; $i++) {
      $char = $term[$i];
      if ($char=='(' || $char=='[') {
        $bracketCounter++;
        continue;
      }
      elseif ($char==')' || $char==']') {
        $bracketCounter--;
        continue;
      }
      if ($bracketCounter == 0) {
        // Check whether we can find the expression at this index.
        if (substr($term, $i, strlen($expression)) == $expression) {
          // Return the current index.
          return $i;
        }
      }
    }
    // Check whether we had a valid number of brackets.
    if ($bracketCounter != 0) {
      // Display an error message.
      $this->_displayError('While parsing an XPath expression, in the predicate ' .
        str_replace($term, '<b>'.$term.'</b>', $this->xpath) .
        ', there was an invalid number of brackets.', __LINE__);
    }
    // Nothing was found.
    return (-1);
  }
  
  /////////////////////////////////////////////////
  // ########################################### //
  // Auxilliary utilities
  
  /**
   * Retrieves a substring before a delimiter.
   *
   * This method retrieves everything from a string before a given delimiter,
   * not including the delimiter.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $string String, from which the substring should be
   *            extracted.
   * @param     string $delimiter 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.
    //return substr($string, 0, strlen($string) - strlen(strstr($string, "$delimiter")));
    $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.
   *
   * @author    Michael P. Mehl <mpm@phpxml.org>
   * @param     string $string String, from which the substring should be
   *            extracted.
   * @param     string $delimiter 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));
  }
  
  /**
   * !! terminate should not be allowed !! --fab
   *
   * Displays an error message.
   *
   * This method displays an error messages and stops the execution of the
   * script.
   *
   * @author     Michael P. Mehl <mpm@phpxml.org>
   * @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='-', $terminate=TRUE) {
    // Display the error message.
    $err = '<b>XPath error in '.basename(__FILE__).':'.$lineNumber.'</b> '.$message."<br \>\n";
    $this->_setLastError($message, $lineNumber);
    if (($this->_verboseLevel > 0) OR ($terminate)) echo $err;
    // End the execution of this script.
    if ($terminate) exit;
  }
  
  /**
  * 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.
  * 
  * based on the deprecated _displayError() and replaces it.
  * 
  * @author fab
  * @param  string $message a textual error message default is ''
  * @param  int    $line    the line number where the error occured, use __LINE__
  * @return void
  * @see getLastError()
  */
  function _setLastError($message='', $line='-') {
    $this->_lastError = 'XPath error in ' . basename(__FILE__) . ':' . $line . '. Message: ' . $message;
  }
  
  /**
   * Determine if the function has any content
   *
   * Returns TRUE if this object has any xml content.  i.e. after a successfull
   * load_XXX() call we will have content, but before we shouldn't.
   *
   * @author    Nigel Swinson <nigelswinson@users.sourceforge.net>
   * @return    TRUE if the object holds any content, FALSE otherwise.
   */
  function _objectHasContent() {
    return (count($this->nodes));
  }
  
  /////////////////////////////////////////////////
  // ########################################### //
  // Auxilliary debug utilities to help debug functions.

  /**
   * 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 <nigelswinson@users.sourceforge.net>
   * @author    Sam   Blum    <bs_php@infeer.com>
   * @param     string $FunctionName the name of the function we are beginning to debug
   * @return    array the output from the gettimeofday function.
   * @see       _closeDebugFunction()
   */
  function _beginDebugFunction($function_name) {
    $fileName = basename(__FILE__);
    static $color = array('green','blue','red','lime','fuchsia', 'aqua');
    static $colIndex = -1;
    $colIndex++;
    $pre = '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; padding:5">';
    $out = '<div align="left"> ' . $pre . "<STRONG>{$fileName} : {$function_name}</STRONG><HR>";
    echo $out;
    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 <nigelswinson@users.sourceforge.net>
   * @param     array $a_starttime the time that the function call was started.
   * @param     any $return_value the return value from the function call that 
   *            we are debugging
   */
  function _closeDebugFunction($a_starttime, $return_value = "") {
    echo "<hr>";
    if (isSet($return_value)) {
      if (is_array($return_value))
        echo "Return Value: ".print_r($return_value)."\n";
      else if (is_numeric($return_value)) 
        echo "Return Value: '$return_value'\n";
      else if (is_bool($return_value)) 
        echo "Return Value: ".($return_value ? "TRUE" : "FALSE")."\n";
      else 
        echo "Return Value: \"".htmlspecialchars($return_value)."\"\n";
    }
    $this->_profileFunction($a_starttime, "Function took");
    echo " \n</pre></div>";
  }
  
  /**
   * Call to return time since start of function for Profiling
   *
   * @param     array $a_starttime the time that the function call was started.
   * @param     string $alert_string the string to describe what has just finished happening
   */
  function _profileFunction($a_starttime, $alert_string) {
    // Print the time it took to call this function.
    $now   = explode(' ', microtime());
    $last  = explode(' ', $a_starttime);
    $delta = (round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*1000 ));
    echo "\n{$alert_string} <strong>{$delta} ms</strong>";
  }
  ////////////////////////////////////////////////////////////////////////////////////////////////
  
}
?>
