[ Index ]
 

Code source de eGroupWare 1.2.106-2

Accédez au Source d'autres logiciels libresSoutenez Angelica Josefina !

title

Body

[fermer]

/phpsysinfo/includes/ -> XPath.class.php (source)

   1  <?php
   2  /**
   3   * Php.XPath
   4   *
   5   * +======================================================================================================+
   6   * | A php class for searching an XML document using XPath, and making modifications using a DOM 
   7   * | style API. Does not require the DOM XML PHP library. 
   8   * |
   9   * +======================================================================================================+
  10   * | What Is XPath:
  11   * | --------------
  12   * | - "What SQL is for a relational database, XPath is for an XML document." -- Sam Blum
  13   * | - "The primary purpose of XPath is to address parts of an XML document. In support of this 
  14   * |    primary purpose, it also provides basic facilities for manipulting it." -- W3C
  15   * | 
  16   * | XPath in action and a very nice intro is under:
  17   * |    http://www.zvon.org/xxl/XPathTutorial/General/examples.html
  18   * | Specs Can be found under:
  19   * |    http://www.w3.org/TR/xpath     W3C XPath Recommendation 
  20   * |    http://www.w3.org/TR/xpath20   W3C XPath Recommendation 
  21   * |
  22   * | NOTE: Most of the XPath-spec has been realized, but not all. Usually this should not be
  23   * |       problem as the missing part is either rarely used or it's simpler to do with PHP itself.
  24   * +------------------------------------------------------------------------------------------------------+
  25   * | Requires PHP version  4.0.5 and up
  26   * +------------------------------------------------------------------------------------------------------+
  27   * | Main Active Authors:
  28   * | --------------------
  29   * | Nigel Swinson <nigelswinson@users.sourceforge.net>
  30   * |   Started around 2001-07, saved phpxml from near death and renamed to Php.XPath
  31   * |   Restructured XPath code to stay in line with XPath spec.
  32   * | Sam Blum <bs_php@infeer.com>
  33   * |   Started around 2001-09 1st major restruct (V2.0) and testbench initiator.   
  34   * |   2nd (V3.0) major rewrite in 2002-02
  35   * | Daniel Allen <bigredlinux@yahoo.com>
  36   * |   Started around 2001-10 working to make Php.XPath adhere to specs 
  37   * | Main Former Author: Michael P. Mehl <mpm@phpxml.org>
  38   * |   Inital creator of V 1.0. Stoped activities around 2001-03        
  39   * +------------------------------------------------------------------------------------------------------+
  40   * | Code Structure:
  41   * | --------------_
  42   * | The class is split into 3 main objects. To keep usability easy all 3 
  43   * | objects are in this file (but may be split in 3 file in future).
  44   * |   +-------------+ 
  45   * |   |  XPathBase  | XPathBase holds general and debugging functions. 
  46   * |   +------+------+
  47   * |          v      
  48   * |   +-------------+ XPathEngine is the implementation of the W3C XPath spec. It contains the 
  49   * |   | XPathEngine | XML-import (parser), -export  and can handle xPathQueries. It's a fully 
  50   * |   +------+------+ functional class but has no functions to modify the XML-document (see following).
  51   * |          v      
  52   * |   +-------------+ 
  53   * |   |    XPath    | XPath extends the functionality with actions to modify the XML-document.
  54   * |   +-------------+ We tryed to implement a DOM - like interface.
  55   * +------------------------------------------------------------------------------------------------------+
  56   * | Usage:
  57   * | ------
  58   * | Scroll to the end of this php file and you will find a short sample code to get you started
  59   * +------------------------------------------------------------------------------------------------------+
  60   * | Glossary:
  61   * | ---------
  62   * | To understand how to use the functions and to pass the right parameters, read following:
  63   * |     
  64   * | Document: (full node tree, XML-tree)
  65   * |     After a XML-source has been imported and parsed, it's stored as a tree of nodes sometimes 
  66   * |     refered to as 'document'.
  67   * |     
  68   * | AbsoluteXPath: (xPath, xPathSet)
  69   * |     A absolute XPath is a string. It 'points' to *one* node in the XML-document. We use the
  70   * |     term 'absolute' to emphasise that it is not an xPath-query (see xPathQuery). A valid xPath 
  71   * |     has the form like '/AAA[1]/BBB[2]/CCC[1]'. Usually functions that require a node (see Node) 
  72   * |     will also accept an abs. XPath.
  73   * |     
  74   * | Node: (node, nodeSet, node-tree)
  75   * |     Some funtions require or return a node (or a whole node-tree). Nodes are only used with the 
  76   * |     XPath-interface and have an internal structure. Every node in a XML document has a unique 
  77   * |     corresponding abs. xPath. That's why public functions that accept a node, will usually also 
  78   * |     accept a abs. xPath (a string) 'pointing' to an existing node (see absolutXPath).
  79   * |     
  80   * | XPathQuery: (xquery, query)
  81   * |     A xPath-query is a string that is matched against the XML-document. The result of the match 
  82   * |     is a xPathSet (vector of xPath's). It's always possible to pass a single absoluteXPath 
  83   * |     instead of a xPath-query. A valid xPathQuery could look like this:
  84   * |     '//XXX/*[contains(., "foo")]/..' (See the link in 'What Is XPath' to learn more).
  85   * |     
  86   * |     
  87   * +------------------------------------------------------------------------------------------------------+
  88   * | Internals:
  89   * | ----------
  90   * | - The Node Tree
  91   * |   -------------
  92   * | A central role of the package is how the XML-data is stored. The whole data is in a node-tree.
  93   * | A node can be seen as the equvalent to a tag in the XML soure with some extra info.
  94   * | For instance the following XML 
  95   * |                        <AAA foo="x">***<BBB/><CCC/>**<BBB/>*</AAA>
  96   * | Would produce folowing node-tree:
  97   * |                              'super-root'      <-- $nodeRoot (Very handy)  
  98   * |                                    |                                           
  99   * |             'depth' 0            AAA[1]        <-- top node. The 'textParts' of this node would be
 100   * |                                /   |   \                     'textParts' => array('***','','**','*')
 101   * |             'depth' 1     BBB[1] CCC[1] BBB[2]               (NOTE: Is always size of child nodes+1)
 102   * | - The Node
 103   * |   --------
 104   * | The node itself is an structure desiged mainly to be used in connection with the interface of PHP.XPath.
 105   * | That means it's possible for functions to return a sub-node-tree that can be used as input of an other 
 106   * | PHP.XPath function.
 107   * | 
 108   * | The main structure of a node is:
 109   * |   $node = array(
 110   * |     'name'        => '',      # The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
 111   * |     'attributes'  => array(), # The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
 112   * |     'textParts'   => array(), # Array of text parts surrounding the children E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd')
 113   * |     'childNodes'  => array(), # Array of refences (pointers) to child nodes.
 114   * |     
 115   * | For optimisation reasions some additional data is stored in the node too:
 116   * |     'parentNode'  => NULL     # Reference (pointer) to the parent node (or NULL if it's 'super root')
 117   * |     'depth'       => 0,       # The tag depth (or tree level) starting with the root tag at 0.
 118   * |     'pos'         => 0,       # Is the zero-based position this node has in the parent's 'childNodes'-list.
 119   * |     'contextPos'  => 1,       # Is the one-based position this node has by counting the siblings tags (tags with same name)
 120   * |     'xpath'       => ''       # Is the abs. XPath to this node.
 121   * |     'generated_id'=> ''       # The id returned for this node by generate-id() (attribute and text nodes not supported)
 122   * | 
 123   * | - The NodeIndex
 124   * |   -------------
 125   * | Every node in the tree has an absolute XPath. E.g '/AAA[1]/BBB[2]' the $nodeIndex is a hash array
 126   * | to all the nodes in the node-tree. The key used is the absolute XPath (a string).
 127   * |    
 128   * +------------------------------------------------------------------------------------------------------+
 129   * | License:
 130   * | --------
 131   * | The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); 
 132   * | you may not use this file except in compliance with the License. You may obtain a copy of the 
 133   * | License at http://www.mozilla.org/MPL/ 
 134   * | 
 135   * | Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY
 136   * | OF ANY KIND, either express or implied. See the License for the specific language governing 
 137   * | rights and limitations under the License. 
 138   * |
 139   * | The Original Code is <phpXML/>. 
 140   * | 
 141   * | The Initial Developer of the Original Code is Michael P. Mehl. Portions created by Michael 
 142   * | P. Mehl are Copyright (C) 2001 Michael P. Mehl. All Rights Reserved.
 143   * |
 144   * | Contributor(s): N.Swinson / S.Blum / D.Allen
 145   * | 
 146   * | Alternatively, the contents of this file may be used under the terms of either of the GNU 
 147   * | General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public 
 148   * | License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the 
 149   * | LGPL License are applicable instead of those above.  If you wish to allow use of your version 
 150   * | of this file only under the terms of the GPL or the LGPL License and not to allow others to 
 151   * | use your version of this file under the MPL, indicate your decision by deleting the 
 152   * | provisions above and replace them with the notice and other provisions required by the 
 153   * | GPL or the LGPL License.  If you do not delete the provisions above, a recipient may use 
 154   * | your version of this file under either the MPL, the GPL or the LGPL License. 
 155   * | 
 156   * +======================================================================================================+
 157   *
 158   * @author  S.Blum / N.Swinson / D.Allen / (P.Mehl)
 159   * @link    http://sourceforge.net/projects/phpxpath/
 160   * @version 3.5
 161   * @CVS $Id: XPath.class.php 21228 2006-04-06 13:52:08Z ralfbecker $
 162   */
 163  
 164  // Include guard, protects file being included twice
 165  $ConstantName = 'INCLUDED_'.strtoupper(__FILE__);
 166  if (defined($ConstantName)) return;
 167  define($ConstantName,1, TRUE);
 168  
 169  /************************************************************************************************
 170  * ===============================================================================================
 171  *                               X P a t h B a s e  -  Class                                      
 172  * ===============================================================================================
 173  ************************************************************************************************/
 174  class XPathBase {
 175    var $_lastError;
 176    
 177    // As debugging of the xml parse is spread across several functions, we need to make this a member.
 178    var $bDebugXmlParse = FALSE;
 179  
 180    // do we want to do profiling?
 181    var $bClassProfiling = FALSE;
 182  
 183    // Used to help navigate through the begin/end debug calls
 184    var $iDebugNextLinkNumber = 1;
 185    var $aDebugOpenLinks = array();
 186    var $aDebugFunctions = array(
 187            //'_evaluatePrimaryExpr',
 188            //'_evaluateExpr',
 189            //'_evaluateStep',
 190            //'_checkPredicates',
 191            //'_evaluateFunction',
 192            //'_evaluateOperator',
 193            //'_evaluatePathExpr',
 194                 );
 195  
 196    /**
 197     * Constructor
 198     */
 199    function XPathBase() {
 200      # $this->bDebugXmlParse = TRUE;
 201      $this->properties['verboseLevel'] = 1;  // 0=silent, 1 and above produce verbose output (an echo to screen). 
 202      
 203      if (!isSet($_ENV)) {  // Note: $_ENV introduced in 4.1.0. In earlier versions, use $HTTP_ENV_VARS.
 204        $_ENV = $GLOBALS['HTTP_ENV_VARS'];
 205      }
 206      
 207      // Windows 95/98 do not support file locking. Detecting OS (Operation System) and setting the 
 208      // properties['OS_supports_flock'] to FALSE if win 95/98 is detected. 
 209      // This will surpress the file locking error reported from win 98 users when exportToFile() is called.
 210      // May have to add more OS's to the list in future (Macs?).
 211      // ### Note that it's only the FAT and NFS file systems that are really a problem.  NTFS and
 212      // the latest php libs do support flock()
 213      $_ENV['OS'] = isSet($_ENV['OS']) ? $_ENV['OS'] : 'Unknown OS';
 214      switch ($_ENV['OS']) { 
 215        case 'Windows_95':
 216        case 'Windows_98':
 217        case 'Unknown OS':
 218          // should catch Mac OS X compatible environment 
 219          if (!empty($_SERVER['SERVER_SOFTWARE']) 
 220              && preg_match('/Darwin/',$_SERVER['SERVER_SOFTWARE'])) { 
 221             // fall-through 
 222          } else { 
 223             $this->properties['OS_supports_flock'] = FALSE; 
 224             break; 
 225          }
 226        default:
 227          $this->properties['OS_supports_flock'] = TRUE;
 228      }
 229    }
 230    
 231    
 232    /**
 233     * Resets the object so it's able to take a new xml sting/file
 234     *
 235     * Constructing objects is slow.  If you can, reuse ones that you have used already
 236     * by using this reset() function.
 237     */
 238    function reset() {
 239      $this->_lastError   = '';
 240    }
 241    
 242    //-----------------------------------------------------------------------------------------
 243    // XPathBase                    ------  Helpers  ------                                    
 244    //-----------------------------------------------------------------------------------------
 245    
 246    /**
 247     * This method checks the right amount and match of brackets
 248     *
 249     * @param     $term (string) String in which is checked.
 250     * @return          (bool)   TRUE: OK / FALSE: KO  
 251     */
 252    function _bracketsCheck($term) {
 253      $leng = strlen($term);
 254      $brackets = 0;
 255      $bracketMisscount = $bracketMissmatsh = FALSE;
 256      $stack = array();
 257      for ($i=0; $i<$leng; $i++) {
 258        switch ($term[$i]) {
 259          case '(' : 
 260          case '[' : 
 261            $stack[$brackets] = $term[$i]; 
 262            $brackets++; 
 263            break;
 264          case ')': 
 265            $brackets--;
 266            if ($brackets<0) {
 267              $bracketMisscount = TRUE;
 268              break 2;
 269            }
 270            if ($stack[$brackets] != '(') {
 271              $bracketMissmatsh = TRUE;
 272              break 2;
 273            }
 274            break;
 275          case ']' : 
 276            $brackets--;
 277            if ($brackets<0) {
 278              $bracketMisscount = TRUE;
 279              break 2;
 280            }
 281            if ($stack[$brackets] != '[') {
 282              $bracketMissmatsh = TRUE;
 283              break 2;
 284            }
 285            break;
 286        }
 287      }
 288      // Check whether we had a valid number of brackets.
 289      if ($brackets != 0) $bracketMisscount = TRUE;
 290      if ($bracketMisscount || $bracketMissmatsh) {
 291        return FALSE;
 292      }
 293      return TRUE;
 294    }
 295    
 296    /**
 297     * Looks for a string within another string -- BUT the search-string must be located *outside* of any brackets.
 298     *
 299     * This method looks for a string within another string. Brackets in the
 300     * string the method is looking through will be respected, which means that
 301     * only if the string the method is looking for is located outside of
 302     * brackets, the search will be successful.
 303     *
 304     * @param     $term       (string) String in which the search shall take place.
 305     * @param     $expression (string) String that should be searched.
 306     * @return                (int)    This method returns -1 if no string was found, 
 307     *                                 otherwise the offset at which the string was found.
 308     */
 309    function _searchString($term, $expression) {
 310      $bracketCounter = 0; // Record where we are in the brackets. 
 311      $leng = strlen($term);
 312      $exprLeng = strlen($expression);
 313      for ($i=0; $i<$leng; $i++) {
 314        $char = $term[$i];
 315        if ($char=='(' || $char=='[') {
 316          $bracketCounter++;
 317          continue;
 318        }
 319        elseif ($char==')' || $char==']') {
 320          $bracketCounter--;
 321        }
 322        if ($bracketCounter == 0) {
 323          // Check whether we can find the expression at this index.
 324          if (substr($term, $i, $exprLeng) == $expression) return $i;
 325        }
 326      }
 327      // Nothing was found.
 328      return (-1);
 329    }
 330    
 331    /**
 332     * Split a string by a searator-string -- BUT the separator-string must be located *outside* of any brackets.
 333     * 
 334     * Returns an array of strings, each of which is a substring of string formed 
 335     * by splitting it on boundaries formed by the string separator. 
 336     *
 337     * @param     $separator  (string) String that should be searched.
 338     * @param     $term       (string) String in which the search shall take place.
 339     * @return                (array)  see above
 340     */
 341    function _bracketExplode($separator, $term) {
 342      // Note that it doesn't make sense for $separator to itself contain (,),[ or ],
 343      // but as this is a private function we should be ok.
 344      $resultArr   = array();
 345      $bracketCounter = 0;  // Record where we are in the brackets. 
 346      do { // BEGIN try block
 347        // Check if any separator is in the term
 348        $sepLeng =  strlen($separator);
 349        if (strpos($term, $separator)===FALSE) { // no separator found so end now
 350          $resultArr[] = $term;
 351          break; // try-block
 352        }
 353        
 354        // Make a substitute separator out of 'unused chars'.
 355        $substituteSep = str_repeat(chr(2), $sepLeng);
 356        
 357        // Now determine the first bracket '(' or '['.
 358        $tmp1 = strpos($term, '(');
 359        $tmp2 = strpos($term, '[');
 360        if ($tmp1===FALSE) {
 361          $startAt = (int)$tmp2;
 362        } elseif ($tmp2===FALSE) {
 363          $startAt = (int)$tmp1;
 364        } else {
 365          $startAt = min($tmp1, $tmp2);
 366        }
 367        
 368        // Get prefix string part before the first bracket.
 369        $preStr = substr($term, 0, $startAt);
 370        // Substitute separator in prefix string.
 371        $preStr = str_replace($separator, $substituteSep, $preStr);
 372        
 373        // Now get the rest-string (postfix string)
 374        $postStr = substr($term, $startAt);
 375        // Go all the way through the rest-string.
 376        $strLeng = strlen($postStr);
 377        for ($i=0; $i < $strLeng; $i++) {
 378          $char = $postStr[$i];
 379          // Spot (,),[,] and modify our bracket counter.  Note there is an
 380          // assumption here that you don't have a string(with[mis)matched]brackets.
 381          // This should be ok as the dodgy string will be detected elsewhere.
 382          if ($char=='(' || $char=='[') {
 383            $bracketCounter++;
 384            continue;
 385          } 
 386          elseif ($char==')' || $char==']') {
 387            $bracketCounter--;
 388          }
 389          // If no brackets surround us check for separator
 390          if ($bracketCounter == 0) {
 391            // Check whether we can find the expression starting at this index.
 392            if ((substr($postStr, $i, $sepLeng) == $separator)) {
 393              // Substitute the found separator 
 394              for ($j=0; $j<$sepLeng; $j++) {
 395                $postStr[$i+$j] = $substituteSep[$j];
 396              }
 397            }
 398          }
 399        }
 400        // Now explod using the substitute separator as key.
 401        $resultArr = explode($substituteSep, $preStr . $postStr);
 402      } while (FALSE); // End try block
 403      // Return the results that we found. May be a array with 1 entry.
 404      return $resultArr;
 405    }
 406  
 407    /**
 408     * Split a string at it's groups, ie bracketed expressions
 409     * 
 410     * Returns an array of strings, when concatenated together would produce the original
 411     * string.  ie a(b)cde(f)(g) would map to:
 412     * array ('a', '(b)', cde', '(f)', '(g)')
 413     *
 414     * @param     $string  (string) The string to process
 415     * @param     $open    (string) The substring for the open of a group
 416     * @param     $close   (string) The substring for the close of a group
 417     * @return             (array)  The parsed string, see above
 418     */
 419    function _getEndGroups($string, $open='[', $close=']') {
 420      // Note that it doesn't make sense for $separator to itself contain (,),[ or ],
 421      // but as this is a private function we should be ok.
 422      $resultArr   = array();
 423      do { // BEGIN try block
 424        // Check if we have both an open and a close tag      
 425        if (empty($open) and empty($close)) { // no separator found so end now
 426          $resultArr[] = $string;
 427          break; // try-block
 428        }
 429  
 430        if (empty($string)) {
 431          $resultArr[] = $string;
 432          break; // try-block
 433        }
 434  
 435        
 436        while (!empty($string)) {
 437          // Now determine the first bracket '(' or '['.
 438          $openPos = strpos($string, $open);
 439          $closePos = strpos($string, $close);
 440          if ($openPos===FALSE || $closePos===FALSE) {
 441            // Oh, no more groups to be found then.  Quit
 442            $resultArr[] = $string;
 443            break;
 444          }
 445  
 446          // Sanity check
 447          if ($openPos > $closePos) {
 448            // Malformed string, dump the rest and quit.
 449            $resultArr[] = $string;
 450            break;
 451          }
 452  
 453          // Get prefix string part before the first bracket.
 454          $preStr = substr($string, 0, $openPos);
 455          // This is the first string that will go in our output
 456          if (!empty($preStr))
 457            $resultArr[] = $preStr;
 458  
 459          // Skip over what we've proceed, including the open char
 460          $string = substr($string, $openPos + 1 - strlen($string));
 461  
 462          // Find the next open char and adjust our close char
 463  //echo "close: $closePos\nopen: $openPos\n\n";
 464          $closePos -= $openPos + 1;
 465          $openPos = strpos($string, $open);
 466  //echo "close: $closePos\nopen: $openPos\n\n";
 467  
 468          // While we have found nesting...
 469          while ($openPos && $closePos && ($closePos > $openPos)) {
 470            // Find another close pos after the one we are looking at
 471            $closePos = strpos($string, $close, $closePos + 1);
 472            // And skip our open
 473            $openPos = strpos($string, $open, $openPos + 1);
 474          }
 475  //echo "close: $closePos\nopen: $openPos\n\n";
 476  
 477          // If we now have a close pos, then it's the end of the group.
 478          if ($closePos === FALSE) {
 479            // We didn't... so bail dumping what was left
 480            $resultArr[] = $open.$string;
 481            break;
 482          }
 483  
 484          // We did, so we can extract the group
 485          $resultArr[] = $open.substr($string, 0, $closePos + 1);
 486          // Skip what we have processed
 487          $string = substr($string, $closePos + 1);
 488        }
 489      } while (FALSE); // End try block
 490      // Return the results that we found. May be a array with 1 entry.
 491      return $resultArr;
 492    }
 493    
 494    /**
 495     * Retrieves a substring before a delimiter.
 496     *
 497     * This method retrieves everything from a string before a given delimiter,
 498     * not including the delimiter.
 499     *
 500     * @param     $string     (string) String, from which the substring should be extracted.
 501     * @param     $delimiter  (string) String containing the delimiter to use.
 502     * @return                (string) Substring from the original string before the delimiter.
 503     * @see       _afterstr()
 504     */
 505    function _prestr(&$string, $delimiter, $offset=0) {
 506      // Return the substring.
 507      $offset = ($offset<0) ? 0 : $offset;
 508      $pos = strpos($string, $delimiter, $offset);
 509      if ($pos===FALSE) return $string; else return substr($string, 0, $pos);
 510    }
 511    
 512    /**
 513     * Retrieves a substring after a delimiter.
 514     *
 515     * This method retrieves everything from a string after a given delimiter,
 516     * not including the delimiter.
 517     *
 518     * @param     $string     (string) String, from which the substring should be extracted.
 519     * @param     $delimiter  (string) String containing the delimiter to use.
 520     * @return                (string) Substring from the original string after the delimiter.
 521     * @see       _prestr()
 522     */
 523    function _afterstr($string, $delimiter, $offset=0) {
 524      $offset = ($offset<0) ? 0 : $offset;
 525      // Return the substring.
 526      return substr($string, strpos($string, $delimiter, $offset) + strlen($delimiter));
 527    }
 528    
 529    //-----------------------------------------------------------------------------------------
 530    // XPathBase                ------  Debug Stuff  ------                                    
 531    //-----------------------------------------------------------------------------------------
 532    
 533    /**
 534     * Alter the verbose (error) level reporting.
 535     *
 536     * Pass an int. >0 to turn on, 0 to turn off.  The higher the number, the 
 537     * higher the level of verbosity. By default, the class has a verbose level 
 538     * of 1.
 539     *
 540     * @param $levelOfVerbosity (int) default is 1 = on
 541     */
 542    function setVerbose($levelOfVerbosity = 1) {
 543      $level = -1;
 544      if ($levelOfVerbosity === TRUE) {
 545        $level = 1;
 546      } elseif ($levelOfVerbosity === FALSE) {
 547        $level = 0;
 548      } elseif (is_numeric($levelOfVerbosity)) {
 549        $level = $levelOfVerbosity;
 550      }
 551      if ($level >= 0) $this->properties['verboseLevel'] = $levelOfVerbosity;
 552    }
 553     
 554    /**
 555     * Returns the last occured error message.
 556     *
 557     * @access public
 558     * @return string (may be empty if there was no error at all)
 559     * @see    _setLastError(), _lastError
 560     */
 561    function getLastError() {
 562      return $this->_lastError;
 563    }
 564    
 565    /**
 566     * Creates a textual error message and sets it. 
 567     * 
 568     * example: 'XPath error in THIS_FILE_NAME:LINE. Message: YOUR_MESSAGE';
 569     * 
 570     * I don't think the message should include any markup because not everyone wants to debug 
 571     * into the browser window.
 572     * 
 573     * You should call _displayError() rather than _setLastError() if you would like the message,
 574     * dependant on their verbose settings, echoed to the screen.
 575     * 
 576     * @param $message (string) a textual error message default is ''
 577     * @param $line    (int)    the line number where the error occured, use __LINE__
 578     * @see getLastError()
 579     */
 580    function _setLastError($message='', $line='-', $file='-') {
 581      $this->_lastError = 'XPath error in ' . basename($file) . ':' . $line . '. Message: ' . $message;
 582    }
 583    
 584    /**
 585     * Displays an error message.
 586     *
 587     * This method displays an error messages depending on the users verbose settings 
 588     * and sets the last error message.  
 589     *
 590     * If also possibly stops the execution of the script.
 591     * ### Terminate should not be allowed --fab.  Should it??  N.S.
 592     *
 593     * @param $message    (string)  Error message to be displayed.
 594     * @param $lineNumber (int)     line number given by __LINE__
 595     * @param $terminate  (bool)    (default TURE) End the execution of this script.
 596     */
 597    function _displayError($message, $lineNumber='-', $file='-', $terminate=TRUE) {
 598      // Display the error message.
 599      $err = '<b>XPath error in '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
 600      $this->_setLastError($message, $lineNumber, $file);
 601      if (($this->properties['verboseLevel'] > 0) OR ($terminate)) echo $err;
 602      // End the execution of this script.
 603      if ($terminate) exit;
 604    }
 605  
 606    /**
 607     * Displays a diagnostic message
 608     *
 609     * This method displays an error messages
 610     *
 611     * @param $message    (string)  Error message to be displayed.
 612     * @param $lineNumber (int)     line number given by __LINE__
 613     */
 614    function _displayMessage($message, $lineNumber='-', $file='-') {
 615      // Display the error message.
 616      $err = '<b>XPath message from '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
 617      if ($this->properties['verboseLevel'] > 0) echo $err;
 618    }
 619    
 620    /**
 621     * Called to begin the debug run of a function.
 622     *
 623     * This method starts a <DIV><PRE> tag so that the entry to this function
 624     * is clear to the debugging user.  Call _closeDebugFunction() at the
 625     * end of the function to create a clean box round the function call.
 626     *
 627     * @author    Nigel Swinson <nigelswinson@users.sourceforge.net>
 628     * @author    Sam   Blum    <bs_php@infeer.com>
 629     * @param     $functionName (string) the name of the function we are beginning to debug
 630     * @param     $bDebugFlag   (bool) TRUE if we are to draw a call stack, FALSE otherwise
 631     * @return                  (array)  the output from the microtime() function.
 632     * @see       _closeDebugFunction()
 633     */
 634    function _beginDebugFunction($functionName, $bDebugFlag) {
 635      if ($bDebugFlag) {
 636        $fileName = basename(__FILE__);
 637        static $color = array('green','blue','red','lime','fuchsia', 'aqua');
 638        static $colIndex = -1;
 639        $colIndex++;
 640        echo '<div style="clear:both" align="left"> ';
 641        echo '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; padding:5">';
 642        echo '<a style="float:right;margin:5px" name="'.$this->iDebugNextLinkNumber.'Open" href="#'.$this->iDebugNextLinkNumber.'Close">Function Close '.$this->iDebugNextLinkNumber.'</a>';
 643        echo "<STRONG>{$fileName} : {$functionName}</STRONG>";
 644        echo '<hr style="clear:both">';
 645        array_push($this->aDebugOpenLinks, $this->iDebugNextLinkNumber);
 646        $this->iDebugNextLinkNumber++;
 647      }
 648  
 649      if ($this->bClassProfiling)
 650        $this->_ProfBegin($FunctionName);
 651  
 652      return TRUE;
 653    }
 654    
 655    /**
 656     * Called to end the debug run of a function.
 657     *
 658     * This method ends a <DIV><PRE> block and reports the time since $aStartTime
 659     * is clear to the debugging user.
 660     *
 661     * @author    Nigel Swinson <nigelswinson@users.sourceforge.net>
 662     * @param     $functionName (string) the name of the function we are beginning to debug
 663     * @param     $return_value (mixed) the return value from the function call that 
 664     *                                  we are debugging
 665     * @param     $bDebugFlag   (bool) TRUE if we are to draw a call stack, FALSE otherwise
 666     */
 667    function _closeDebugFunction($functionName, $returnValue = "", $bDebugFlag) {
 668      if ($bDebugFlag) {
 669        echo "<hr>";
 670        $iOpenLinkNumber = array_pop($this->aDebugOpenLinks);
 671        echo '<a style="float:right" name="'.$iOpenLinkNumber.'Close" href="#'.$iOpenLinkNumber.'Open">Function Open '.$iOpenLinkNumber.'</a>';
 672        if (isSet($returnValue)) {
 673        if (is_array($returnValue))
 674          echo "Return Value: ".print_r($returnValue)."\n";
 675        else if (is_numeric($returnValue)) 
 676          echo "Return Value: ".(string)$returnValue."\n";
 677        else if (is_bool($returnValue)) 
 678          echo "Return Value: ".($returnValue ? "TRUE" : "FALSE")."\n";
 679        else 
 680          echo "Return Value: \"".htmlspecialchars($returnValue)."\"\n";
 681      }
 682        echo '<br style="clear:both">';
 683        echo " \n</pre></div>";
 684      }
 685    
 686      if ($this->bClassProfiling)
 687        $this->_ProfEnd($FunctionName);
 688  
 689      return TRUE;
 690    }
 691    
 692    /**
 693      * Profile begin call
 694      */
 695    function _ProfBegin($sonFuncName) {
 696      static $entryTmpl = array ( 'start' => array(),
 697                    'recursiveCount' => 0,
 698                    'totTime' => 0,
 699                    'callCount' => 0  );
 700      $now = explode(' ', microtime());
 701  
 702      if (empty($this->callStack)) {
 703        $fatherFuncName = '';
 704      }
 705      else {
 706        $fatherFuncName = $this->callStack[sizeOf($this->callStack)-1];
 707        $fatherEntry = &$this->profile[$fatherFuncName];
 708      }
 709      $this->callStack[] = $sonFuncName;
 710  
 711      if (!isSet($this->profile[$sonFuncName])) {
 712        $this->profile[$sonFuncName] = $entryTmpl;
 713      }
 714  
 715      $sonEntry = &$this->profile[$sonFuncName];
 716      $sonEntry['callCount']++;
 717      // if we call the t's the same function let the time run, otherwise sum up
 718      if ($fatherFuncName == $sonFuncName) {
 719        $sonEntry['recursiveCount']++;
 720      }
 721      if (!empty($fatherFuncName)) {
 722        $last = $fatherEntry['start'];
 723      $fatherEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 );
 724        $fatherEntry['start'] = 0;
 725      }
 726      $sonEntry['start'] = explode(' ', microtime());
 727    }
 728  
 729    /**
 730     * Profile end call
 731     */
 732    function _ProfEnd($sonFuncName) {
 733      $now = explode(' ', microtime());
 734  
 735      array_pop($this->callStack);
 736      if (empty($this->callStack)) {
 737        $fatherFuncName = '';
 738      }
 739      else {
 740        $fatherFuncName = $this->callStack[sizeOf($this->callStack)-1];
 741        $fatherEntry = &$this->profile[$fatherFuncName];
 742      }
 743      $sonEntry = &$this->profile[$sonFuncName];
 744      if (empty($sonEntry)) {
 745        echo "ERROR in profEnd(): '$funcNam' not in list. Seams it was never started ;o)";
 746      }
 747  
 748      $last = $sonEntry['start'];
 749      $sonEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 );
 750      $sonEntry['start'] = 0;
 751      if (!empty($fatherEntry)) $fatherEntry['start'] = explode(' ', microtime());
 752    }
 753  
 754      /**
 755     * Show profile gathered so far as HTML table
 756     */
 757    function _ProfileToHtml() {
 758      $sortArr = array();
 759      if (empty($this->profile)) return '';
 760      reset($this->profile);
 761      while (list($funcName) = each($this->profile)) {
 762        $sortArrKey[] = $this->profile[$funcName]['totTime'];
 763        $sortArrVal[] = $funcName;
 764      }
 765      //echo '<pre>';var_dump($sortArrVal);echo '</pre>';
 766      array_multisort ($sortArrKey, SORT_DESC, $sortArrVal );
 767      //echo '<pre>';var_dump($sortArrVal);echo '</pre>';
 768  
 769      $totTime = 0;
 770      $size = sizeOf($sortArrVal);
 771      for ($i=0; $i<$size; $i++) {
 772        $funcName = &$sortArrVal[$i];
 773        $totTime += $this->profile[$funcName]['totTime'];
 774      }
 775      $out = '<table border="1">';
 776      $out .='<tr align="center" bgcolor="#bcd6f1"><th>Function</th><th> % </th><th>Total [ms]</th><th># Call</th><th>[ms] per Call</th><th># Recursive</th></tr>';
 777      for ($i=0; $i<$size; $i++) {
 778        $funcName = &$sortArrVal[$i];
 779        $row = &$this->profile[$funcName];
 780        $procent = round($row['totTime']*100/$totTime);
 781        if ($procent>20) $bgc = '#ff8080';
 782        elseif ($procent>15) $bgc = '#ff9999';
 783        elseif ($procent>10) $bgc = '#ffcccc';
 784        elseif ($procent>5) $bgc = '#ffffcc';
 785        else $bgc = '#66ff99';
 786  
 787        $out .="<tr align='center' bgcolor='{$bgc}'>";
 788        $out .='<td>'. $funcName .'</td><td>'. $procent .'% '.'</td><td>'. $row['totTime']/10 .'</td><td>'. $row['callCount'] .'</td><td>'. round($row['totTime']/10/$row['callCount'],2) .'</td><td>'. $row['recursiveCount'].'</td>';
 789        $out .='</tr>';
 790      }
 791      $out .= '</table> Total Time [' . $totTime/10 .'ms]' ;
 792  
 793      echo $out;
 794      return TRUE;
 795    }
 796  
 797    /**
 798     * Echo an XPath context for diagnostic purposes
 799     *
 800     * @param $context   (array)   An XPath context
 801     */
 802    function _printContext($context) {
 803      echo "{$context['nodePath']}({$context['pos']}/{$context['size']})";
 804    }
 805    
 806    /**
 807     * This is a debug helper function. It dumps the node-tree as HTML
 808     *
 809     * *QUICK AND DIRTY*. Needs some polishing.
 810     *
 811     * @param $node   (array)   A node 
 812     * @param $indent (string) (optional, default=''). For internal recursive calls.
 813     */
 814    function _treeDump($node, $indent = '') {
 815      $out = '';
 816      
 817      // Get rid of recursion
 818      $parentName = empty($node['parentNode']) ? "SUPER ROOT" :  $node['parentNode']['name'];
 819      unset($node['parentNode']);
 820      $node['parentNode'] = $parentName ;
 821      
 822      $out .= "NODE[{$node['name']}]\n";
 823      
 824      foreach($node as $key => $val) {
 825        if ($key === 'childNodes') continue;
 826        if (is_Array($val)) {
 827          $out .= $indent . "  [{$key}]\n" . arrayToStr($val, $indent . '    ');
 828        } else {
 829          $out .= $indent . "  [{$key}] => '{$val}' \n";
 830        }
 831      }
 832      
 833      if (!empty($node['childNodes'])) {
 834        $out .= $indent . "  ['childNodes'] (Size = ".sizeOf($node['childNodes']).")\n";
 835        foreach($node['childNodes'] as $key => $childNode) {
 836          $out .= $indent . "     [$key] => " . $this->_treeDump($childNode, $indent . '       ') . "\n";
 837        }
 838      }
 839      
 840      if (empty($indent)) {
 841        return "<pre>" . htmlspecialchars($out) . "</pre>";
 842      }
 843      return $out;
 844    }
 845  } // END OF CLASS XPathBase
 846  
 847  
 848  /************************************************************************************************
 849  * ===============================================================================================
 850  *                             X P a t h E n g i n e  -  Class                                    
 851  * ===============================================================================================
 852  ************************************************************************************************/
 853  
 854  class XPathEngine extends XPathBase {
 855    
 856    // List of supported XPath axes.
 857    // What a stupid idea from W3C to take axes name containing a '-' (dash)
 858    // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
 859    //       We will then do the same on the users Xpath querys
 860    //   -sibling => _sibling
 861    //   -or-     =>     _or_
 862    //  
 863    // This array contains a list of all valid axes that can be evaluated in an
 864    // XPath query.
 865    var $axes = array ( 'ancestor', 'ancestor_or_self', 'attribute', 'child', 'descendant', 
 866                          'descendant_or_self', 'following', 'following_sibling',  
 867                          'namespace', 'parent', 'preceding', 'preceding_sibling', 'self' 
 868       );
 869    
 870    // List of supported XPath functions.
 871    // What a stupid idea from W3C to take function name containing a '-' (dash)
 872    // NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
 873    //       We will then do the same on the users Xpath querys 
 874    //   starts-with      => starts_with
 875    //   substring-before => substring_before
 876    //   substring-after  => substring_after
 877    //   string-length    => string_length
 878    //
 879    // This array contains a list of all valid functions that can be evaluated
 880    // in an XPath query.
 881    var $functions = array ( 'last', 'position', 'count', 'id', 'name',
 882      'string', 'concat', 'starts_with', 'contains', 'substring_before',
 883      'substring_after', 'substring', 'string_length', 'normalize_space', 'translate',
 884      'boolean', 'not', 'true', 'false', 'lang', 'number', 'sum', 'floor',
 885      'ceiling', 'round', 'x_lower', 'x_upper', 'generate_id' );
 886      
 887    // List of supported XPath operators.
 888    //
 889    // This array contains a list of all valid operators that can be evaluated
 890    // in a predicate of an XPath query. The list is ordered by the
 891    // precedence of the operators (lowest precedence first).
 892    var $operators = array( ' or ', ' and ', '=', '!=', '<=', '<', '>=', '>',
 893      '+', '-', '*', ' div ', ' mod ', ' | ');
 894  
 895    // List of literals from the xPath string.
 896    var $axPathLiterals = array();
 897    
 898    // The index and tree that is created during the analysis of an XML source.
 899    var $nodeIndex = array();
 900    var $nodeRoot  = array();
 901    var $emptyNode = array(
 902                       'name'        => '',       // The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
 903                       'attributes'  => array(),  // The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
 904                       'childNodes'  => array(),  // Array of pointers to child nodes.
 905                       'textParts'   => array(),  // Array of text parts between the cilderen E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd')
 906                       'parentNode'   => NULL,     // Pointer to parent node or NULL if this node is the 'super root'
 907                       //-- *!* Following vars are set by the indexer and is for optimisation only *!*
 908                       'depth'       => 0,  // The tag depth (or tree level) starting with the root tag at 0.
 909                       'pos'         => 0,  // Is the zero-based position this node has in the parents 'childNodes'-list.
 910                       'contextPos'  => 1,  // Is the one-based position this node has by counting the siblings tags (tags with same name)
 911                       'xpath'       => ''  // Is the abs. XPath to this node.
 912                     );
 913    var $_indexIsDirty = FALSE;
 914  
 915    
 916    // These variable used during the parse XML source
 917    var $nodeStack       = array(); // The elements that we have still to close.
 918    var $parseStackIndex = 0;       // The current element of the nodeStack[] that we are adding to while 
 919                                    // parsing an XML source.  Corresponds to the depth of the xml node.
 920                                    // in our input data.
 921    var $parseOptions    = array(); // Used to set the PHP's XML parser options (see xml_parser_set_option)
 922    var $parsedTextLocation   = ''; // A reference to where we have to put char data collected during XML parsing
 923    var $parsInCData     = 0 ;      // Is >0 when we are inside a CDATA section.  
 924    var $parseSkipWhiteCache = 0;   // A cache of the skip whitespace parse option to speed up the parse.
 925  
 926    // This is the array of error strings, to keep consistency.
 927    var $errorStrings = array(
 928      'AbsoluteXPathRequired' => "The supplied xPath '%s' does not *uniquely* describe a node in the xml document.",
 929      'NoNodeMatch'           => "The supplied xPath-query '%s' does not match *any* node in the xml document.",
 930      'RootNodeAlreadyExists' => "An xml document may have only one root node."
 931      );
 932      
 933    /**
 934     * Constructor
 935     *
 936     * Optionally you may call this constructor with the XML-filename to parse and the 
 937     * XML option vector. Each of the entries in the option vector will be passed to
 938     * xml_parser_set_option().
 939     *
 940     * A option vector sample: 
 941     *   $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE, 
 942     *                   XML_OPTION_SKIP_WHITE => TRUE);
 943     *
 944     * @param  $userXmlOptions (array) (optional) Vector of (<optionID>=><value>, 
 945     *                                 <optionID>=><value>, ...).  See PHP's
 946     *                                 xml_parser_set_option() docu for a list of possible
 947     *                                 options.
 948     * @see   importFromFile(), importFromString(), setXmlOptions()
 949     */
 950    function XPathEngine($userXmlOptions=array()) {
 951      parent::XPathBase();
 952      // Default to not folding case
 953      $this->parseOptions[XML_OPTION_CASE_FOLDING] = FALSE;
 954      // And not skipping whitespace
 955      $this->parseOptions[XML_OPTION_SKIP_WHITE] = FALSE;
 956      
 957      // Now merge in the overrides.
 958      // Don't use PHP's array_merge!
 959      if (is_array($userXmlOptions)) {
 960        foreach($userXmlOptions as $key => $val) $this->parseOptions[$key] = $val;
 961      }
 962    }
 963    
 964    /**
 965     * Resets the object so it's able to take a new xml sting/file
 966     *
 967     * Constructing objects is slow.  If you can, reuse ones that you have used already
 968     * by using this reset() function.
 969     */
 970    function reset() {
 971      parent::reset();
 972      $this->properties['xmlFile']  = ''; 
 973      $this->parseStackIndex = 0;
 974      $this->parsedTextLocation = '';
 975      $this->parsInCData   = 0;
 976      $this->nodeIndex     = array();
 977      $this->nodeRoot      = array();
 978      $this->nodeStack     = array();
 979      $this->aLiterals     = array();
 980      $this->_indexIsDirty = FALSE;
 981    }
 982    
 983    
 984    //-----------------------------------------------------------------------------------------
 985    // XPathEngine              ------  Get / Set Stuff  ------                                
 986    //-----------------------------------------------------------------------------------------
 987    
 988    /**
 989     * Returns the property/ies you want.
 990     * 
 991     * if $param is not given, all properties will be returned in a hash.
 992     *
 993     * @param  $param (string) the property you want the value of, or NULL for all the properties
 994     * @return        (mixed)  string OR hash of all params, or NULL on an unknown parameter.
 995     */
 996    function getProperties($param=NULL) {
 997      $this->properties['hasContent']      = !empty($this->nodeRoot);
 998      $this->properties['caseFolding']     = $this->parseOptions[XML_OPTION_CASE_FOLDING];
 999      $this->properties['skipWhiteSpaces'] = $this->parseOptions[XML_OPTION_SKIP_WHITE];
1000      
1001      if (empty($param)) return $this->properties;
1002      
1003      if (isSet($this->properties[$param])) {
1004        return $this->properties[$param];
1005      } else {
1006        return NULL;
1007      }
1008    }
1009    
1010    /**
1011     * Set an xml_parser_set_option()
1012     *
1013     * @param $optionID (int) The option ID (e.g. XML_OPTION_SKIP_WHITE)
1014     * @param $value    (int) The option value.
1015     * @see XML parser functions in PHP doc
1016     */
1017    function setXmlOption($optionID, $value) {
1018      if (!is_numeric($optionID)) return;
1019       $this->parseOptions[$optionID] = $value;
1020    }
1021  
1022    /**
1023     * Sets a number of xml_parser_set_option()s
1024     *
1025     * @param  $userXmlOptions (array) An array of parser options.
1026     * @see setXmlOption
1027     */
1028    function setXmlOptions($userXmlOptions=array()) {
1029      if (!is_array($userXmlOptions)) return;
1030      foreach($userXmlOptions as $key => $val) {
1031        $this->setXmlOption($key, $val);
1032      }
1033    }
1034    
1035    /**
1036     * Alternative way to control whether case-folding is enabled for this XML parser.
1037     *
1038     * Short cut to setXmlOptions(XML_OPTION_CASE_FOLDING, TRUE/FALSE)
1039     *
1040     * When it comes to XML, case-folding simply means uppercasing all tag- 
1041     * and attribute-names (NOT the content) if set to TRUE.  Note if you
1042     * have this option set, then your XPath queries will also be case folded 
1043     * for you.
1044     *
1045     * @param $onOff (bool) (default TRUE) 
1046     * @see XML parser functions in PHP doc
1047     */
1048    function setCaseFolding($onOff=TRUE) {
1049      $this->parseOptions[XML_OPTION_CASE_FOLDING] = $onOff;
1050    }
1051    
1052    /**
1053     * Alternative way to control whether skip-white-spaces is enabled for this XML parser.
1054     *
1055     * Short cut to setXmlOptions(XML_OPTION_SKIP_WHITE, TRUE/FALSE)
1056     *
1057     * When it comes to XML, skip-white-spaces will trim the tag content.
1058     * An XML file with no whitespace will be faster to process, but will make 
1059     * your data less human readable when you come to write it out.
1060     *
1061     * Running with this option on will slow the class down, so if you want to 
1062     * speed up your XML, then run it through once skipping white-spaces, then
1063     * write out the new version of your XML without whitespace, then use the
1064     * new XML file with skip whitespaces turned off.
1065     *
1066     * @param $onOff (bool) (default TRUE) 
1067     * @see XML parser functions in PHP doc
1068     */
1069    function setSkipWhiteSpaces($onOff=TRUE) {
1070      $this->parseOptions[XML_OPTION_SKIP_WHITE] = $onOff;
1071    }
1072     
1073    /**
1074     * Get the node defined by the $absoluteXPath.
1075     *
1076     * @param   $absoluteXPath (string) (optional, default is 'super-root') xpath to the node.
1077     * @return                 (array)  The node, or FALSE if the node wasn't found.
1078     */
1079    function &getNode($absoluteXPath='') {
1080      if ($absoluteXPath==='/') $absoluteXPath = '';
1081      if (!isSet($this->nodeIndex[$absoluteXPath])) return FALSE;
1082      if ($this->_indexIsDirty) $this->reindexNodeTree();
1083      return $this->nodeIndex[$absoluteXPath];
1084    }
1085  
1086    /**
1087     * Get a the content of a node text part or node attribute.
1088     * 
1089     * If the absolute Xpath references an attribute (Xpath ends with @ or attribute::), 
1090     * then the text value of that node-attribute is returned.
1091     * Otherwise the Xpath is referencing a text part of the node. This can be either a 
1092     * direct reference to a text part (Xpath ends with text()[<nr>]) or indirect reference 
1093     * (a simple abs. Xpath to a node).
1094     * 1) Direct Reference (xpath ends with text()[<part-number>]):
1095     *   If the 'part-number' is omitted, the first text-part is assumed; starting by 1.
1096     *   Negative numbers are allowed, where -1 is the last text-part a.s.o.
1097     * 2) Indirect Reference (a simple abs. Xpath to a node):
1098     *   Default is to return the *whole text*; that is the concated text-parts of the matching
1099     *   node. (NOTE that only in this case you'll only get a copy and changes to the returned  
1100     *   value wounld have no effect). Optionally you may pass a parameter 
1101     *   $textPartNr to define the text-part you want;  starting by 1.
1102     *   Negative numbers are allowed, where -1 is the last text-part a.s.o.
1103     *
1104     * NOTE I : The returned value can be fetched by reference
1105     *          E.g. $text =& wholeText(). If you wish to modify the text.
1106     * NOTE II: text-part numbers out of range will return FALSE
1107     * SIDENOTE:The function name is a suggestion from W3C in the XPath specification level 3.
1108     *
1109     * @param   $absoluteXPath  (string)  xpath to the node (See above).
1110     * @param   $textPartNr     (int)     If referring to a node, specifies which text part 
1111     *                                    to query.
1112     * @return                  (&string) A *reference* to the text if the node that the other 
1113     *                                    parameters describe or FALSE if the node is not found.
1114     */
1115    function &wholeText($absoluteXPath, $textPartNr=NULL) {
1116      $status = FALSE;
1117      $text   = NULL;
1118      if ($this->_indexIsDirty) $this->reindexNodeTree();
1119      
1120      do { // try-block
1121        if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $absoluteXPath, $matches)) {
1122          $absoluteXPath = $matches[1];
1123          $attribute = $matches[3];
1124          if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
1125            $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
1126            break; // try-block
1127          }
1128          $text =& $this->nodeIndex[$absoluteXPath]['attributes'][$attribute];
1129          $status = TRUE;
1130          break; // try-block
1131        }
1132              
1133        // Xpath contains a 'text()'-function, thus goes right to a text node. If so interpret the Xpath.
1134        if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $absoluteXPath, $matches)) {
1135          $absoluteXPath = $matches[1];
1136   
1137          if (!isSet($this->nodeIndex[$absoluteXPath])) {
1138              $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
1139              break; // try-block
1140          }
1141  
1142          // Get the amount of the text parts in the node.
1143          $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
1144  
1145          // default to the first text node if a text node was not specified
1146          $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
1147  
1148          // Support negative indexes like -1 === last a.s.o.
1149          if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
1150          if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
1151            $this->_displayError("The $absoluteXPath/text()[$textPartNr] value isn't a NODE in this document.", __LINE__, __FILE__, FALSE);
1152            break; // try-block
1153          }
1154          $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr - 1];
1155          $status = TRUE;
1156          break; // try-block
1157        }
1158        
1159        // At this point we have been given an xpath with neither a 'text()' nor 'attribute::' axis at the end
1160        // So we assume a get to text is wanted and use the optioanl fallback parameters $textPartNr
1161       
1162        if (!isSet($this->nodeIndex[$absoluteXPath])) {
1163            $this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
1164            break; // try-block
1165        }
1166  
1167        // Get the amount of the text parts in the node.
1168        $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
1169  
1170        // If $textPartNr == NULL we return a *copy* of the whole concated text-parts
1171        if (is_null($textPartNr)) {
1172          unset($text);
1173          $text = implode('', $this->nodeIndex[$absoluteXPath]['textParts']);
1174          $status = TRUE;
1175          break; // try-block
1176        }
1177        
1178        // Support negative indexes like -1 === last a.s.o.
1179        if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
1180        if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
1181          $this->_displayError("The $absoluteXPath has no text part at pos [$textPartNr] (Note: text parts start with 1).", __LINE__, __FILE__, FALSE);
1182          break; // try-block
1183        }
1184        $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr -1];
1185        $status = TRUE;
1186      } while (FALSE); // END try-block
1187      
1188      if (!$status) return FALSE;
1189      return $text;
1190    }
1191  
1192    /**
1193     * Obtain the string value of an object
1194     *
1195     * http://www.w3.org/TR/xpath#dt-string-value
1196     *
1197     * "For every type of node, there is a way of determining a string-value for a node of that type. 
1198     * For some types of node, the string-value is part of the node; for other types of node, the 
1199     * string-value is computed from the string-value of descendant nodes."
1200     *
1201     * @param $node   (node)   The node we have to convert
1202     * @return        (string) The string value of the node.  "" if the object has no evaluatable
1203     *                         string value
1204     */
1205    function _stringValue($node) {
1206      // Decode the entitites and then add the resulting literal string into our array.
1207      return $this->_addLiteral($this->decodeEntities($this->wholeText($node)));
1208    }
1209    
1210    //-----------------------------------------------------------------------------------------
1211    // XPathEngine           ------ Export the XML Document ------                             
1212    //-----------------------------------------------------------------------------------------
1213     
1214    /**
1215     * Returns the containing XML as marked up HTML with specified nodes hi-lighted
1216     *
1217     * @param $absoluteXPath    (string) The address of the node you would like to export.
1218     *                                   If empty the whole document will be exported.
1219     * @param $hilighXpathList  (array)  A list of nodes that you would like to highlight
1220     * @return                  (mixed)  The Xml document marked up as HTML so that it can
1221     *                                   be viewed in a browser, including any XML headers.
1222     *                                   FALSE on error.
1223     * @see _export()    
1224     */
1225    function exportAsHtml($absoluteXPath='', $hilightXpathList=array()) {
1226      $htmlString = $this->_export($absoluteXPath, $xmlHeader=NULL, $hilightXpathList);
1227      if (!$htmlString) return FALSE;
1228      return "<pre>\n" . $htmlString . "\n</pre>"; 
1229    }
1230    
1231    /**
1232     * Given a context this function returns the containing XML
1233     *
1234     * @param $absoluteXPath  (string) The address of the node you would like to export.
1235     *                                 If empty the whole document will be exported.
1236     * @param $xmlHeader      (array)  The string that you would like to appear before
1237     *                                 the XML content.  ie before the <root></root>.  If you
1238     *                                 do not specify this argument, the xmlHeader that was 
1239     *                                 found in the parsed xml file will be used instead.
1240     * @return                (mixed)  The Xml fragment/document, suitable for writing
1241     *                                 out to an .xml file or as part of a larger xml file, or
1242     *                                 FALSE on error.
1243     * @see _export()    
1244     */
1245    function exportAsXml($absoluteXPath='', $xmlHeader=NULL) {
1246      $this->hilightXpathList = NULL;
1247      return $this->_export($absoluteXPath, $xmlHeader); 
1248    }
1249      
1250    /**
1251     * Generates a XML string with the content of the current document and writes it to a file.
1252     *
1253     * Per default includes a <?xml ...> tag at the start of the data too. 
1254     *
1255     * @param     $fileName       (string) 
1256     * @param     $absoluteXPath  (string) The path to the parent node you want(see text above)
1257     * @param     $xmlHeader      (array)  The string that you would like to appear before
1258     *                                     the XML content.  ie before the <root></root>.  If you
1259     *                                     do not specify this argument, the xmlHeader that was 
1260     *                                     found in the parsed xml file will be used instead.
1261     * @return                    (string) The returned string contains well-formed XML data 
1262     *                                     or FALSE on error.
1263     * @see       exportAsXml(), exportAsHtml()
1264     */
1265    function exportToFile($fileName, $absoluteXPath='', $xmlHeader=NULL) {   
1266      $status = FALSE;
1267      do { // try-block
1268        if (!($hFile = fopen($fileName, "wb"))) {   // Did we open the file ok?
1269          $errStr = "Failed to open the $fileName xml file.";
1270          break; // try-block
1271        }
1272        
1273        if ($this->properties['OS_supports_flock']) {
1274          if (!flock($hFile, LOCK_EX + LOCK_NB)) {  // Lock the file
1275            $errStr = "Couldn't get an exclusive lock on the $fileName file.";
1276            break; // try-block
1277          }
1278        }
1279        if (!($xmlOut = $this->_export($absoluteXPath, $xmlHeader))) {
1280          $errStr = "Export failed";
1281          break; // try-block
1282        }
1283        
1284        $iBytesWritten = fwrite($hFile, $xmlOut);
1285        if ($iBytesWritten != strlen($xmlOut)) {
1286          $errStr = "Write error when writing back the $fileName file.";
1287          break; // try-block
1288        }
1289        
1290        // Flush and unlock the file
1291        @fflush($hFile);
1292        $status = TRUE;
1293      } while(FALSE);
1294      
1295      @flock($hFile, LOCK_UN);
1296      @fclose($hFile);
1297      // Sanity check the produced file.
1298      clearstatcache();
1299      if (filesize($fileName) < strlen($xmlOut)) {
1300        $errStr = "Write error when writing back the $fileName file.";
1301        $status = FALSE;
1302      }
1303      
1304      if (!$status)  $this->_displayError($errStr, __LINE__, __FILE__, FALSE);
1305      return $status;
1306    }
1307  
1308    /**
1309     * Generates a XML string with the content of the current document.
1310     *
1311     * This is the start for extracting the XML-data from the node-tree. We do some preperations
1312     * and then call _InternalExport() to fetch the main XML-data. You optionally may pass 
1313     * xpath to any node that will then be used as top node, to extract XML-parts of the 
1314     * document. Default is '', meaning to extract the whole document.
1315     *
1316     * You also may pass a 'xmlHeader' (usually something like <?xml version="1.0"? > that will
1317     * overwrite any other 'xmlHeader', if there was one in the original source.  If there
1318     * wasn't one in the original source, and you still don't specify one, then it will
1319     * use a default of <?xml version="1.0"? >
1320     * Finaly, when exporting to HTML, you may pass a vector xPaths you want to hi-light.
1321     * The hi-lighted tags and attributes will receive a nice color. 
1322     * 
1323     * NOTE I : The output can have 2 formats:
1324     *       a) If "skip white spaces" is/was set. (Not Recommended - slower)
1325     *          The output is formatted by adding indenting and carriage returns.
1326     *       b) If "skip white spaces" is/was *NOT* set.
1327     *          'as is'. No formatting is done. The output should the same as the 
1328     *          the original parsed XML source. 
1329     *
1330     * @param  $absoluteXPath (string) (optional, default is root) The node we choose as top-node
1331     * @param  $xmlHeader     (string) (optional) content before <root/> (see text above)
1332     * @param  $hilightXpath  (array)  (optional) a vector of xPaths to nodes we wat to 
1333     *                                 hi-light (see text above)
1334     * @return                (mixed)  The xml string, or FALSE on error.
1335     */
1336    function _export($absoluteXPath='', $xmlHeader=NULL, $hilightXpathList='') {
1337      // Check whether a root node is given.
1338      if (empty($absoluteXpath)) $absoluteXpath = '';
1339      if ($absoluteXpath == '/') $absoluteXpath = '';
1340      if ($this->_indexIsDirty) $this->reindexNodeTree();
1341      if (!isSet($this->nodeIndex[$absoluteXpath])) {
1342        // If the $absoluteXpath was '' and it didn't exist, then the document is empty
1343        // and we can safely return ''.
1344        if ($absoluteXpath == '') return '';
1345        $this->_displayError("The given xpath '{$absoluteXpath}' isn't a node in this document.", __LINE__, __FILE__, FALSE);
1346        return FALSE;
1347      }
1348      
1349      $this->hilightXpathList = $hilightXpathList;
1350      $this->indentStep = '  ';
1351      $hilightIsActive = is_array($hilightXpathList);
1352      if ($hilightIsActive) {
1353        $this->indentStep = '&nbsp;&nbsp;&nbsp;&nbsp;';
1354      }    
1355      
1356      // Cache this now
1357      $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;
1358  
1359      ///////////////////////////////////////
1360      // Get the starting node and begin with the header
1361  
1362      // Get the start node.  The super root is a special case.
1363      $startNode = NULL;
1364      if (empty($absoluteXPath)) {
1365        $superRoot = $this->nodeIndex[''];
1366        // If they didn't specify an xml header, use the one in the object
1367        if (is_null($xmlHeader)) {
1368          $xmlHeader = $this->parseSkipWhiteCache ? trim($superRoot['textParts'][0]) : $superRoot['textParts'][0];
1369          // If we still don't have an XML header, then use a suitable default
1370          if (empty($xmlHeader)) {
1371              $xmlHeader = '<?xml version="1.0"?>';
1372          }
1373        }
1374  
1375        if (isSet($superRoot['childNodes'][0])) $startNode = $superRoot['childNodes'][0];
1376      } else {
1377        $startNode = $this->nodeIndex[$absoluteXPath];
1378      }
1379  
1380      if (!empty($xmlHeader)) { 
1381        $xmlOut = $this->parseSkipWhiteCache ? $xmlHeader."\n" : $xmlHeader;
1382      } else {
1383        $xmlOut = '';
1384      }
1385  
1386      ///////////////////////////////////////
1387      // Output the document.
1388  
1389      if (($xmlOut .= $this->_InternalExport($startNode)) === FALSE) {
1390        return FALSE;
1391      }
1392      
1393      ///////////////////////////////////////
1394  
1395      // Convert our markers to hi-lights.
1396      if ($hilightIsActive) {
1397        $from = array('<', '>', chr(2), chr(3));
1398        $to = array('&lt;', '&gt;', '<font color="#FF0000"><b>', '</b></font>');
1399        $xmlOut = str_replace($from, $to, $xmlOut);
1400      }
1401      return $xmlOut; 
1402    }  
1403  
1404    /**
1405     * Export the xml document starting at the named node.
1406     *
1407     * @param $node (node)   The node we have to start exporting from
1408     * @return      (string) The string representation of the node.
1409     */
1410    function _InternalExport($node) {
1411      $ThisFunctionName = '_InternalExport';
1412      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
1413      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
1414      if ($bDebugThisFunction) {
1415        echo "Exporting node: ".$node['xpath']."<br>\n";
1416      }
1417  
1418      ////////////////////////////////
1419  
1420      // Quick out.
1421      if (empty($node)) return '';
1422  
1423      // The output starts as empty.
1424      $xmlOut = '';
1425      // This loop will output the text before the current child of a parent then the 
1426      // current child.  Where the child is a short tag we output the child, then move
1427      // onto the next child.  Where the child is not a short tag, we output the open tag, 
1428      // then queue up on currentParentStack[] the child.  
1429      //
1430      // When we run out of children, we then output the last text part, and close the 
1431      // 'parent' tag before popping the stack and carrying on.
1432      //
1433      // To illustrate, the numbers in this xml file indicate what is output on each
1434      // pass of the while loop:
1435      //
1436      // 1
1437      // <1>2
1438      //  <2>3
1439      //   <3/>4
1440      //  </4>5
1441      //  <5/>6
1442      // </6>
1443  
1444      // Although this is neater done using recursion, there's a 33% performance saving
1445      // to be gained by using this stack mechanism.
1446  
1447      // Only add CR's if "skip white spaces" was set. Otherwise leave as is.
1448      $CR = ($this->parseSkipWhiteCache) ? "\n" : '';
1449      $currentIndent = '';
1450      $hilightIsActive = is_array($this->hilightXpathList);
1451  
1452      // To keep track of where we are in the document we use a node stack.  The node 
1453      // stack has the following parallel entries:
1454      //   'Parent'     => (array) A copy of the parent node that who's children we are 
1455      //                           exporting
1456      //   'ChildIndex' => (array) The child index of the corresponding parent that we
1457      //                           are currently exporting.
1458      //   'Highlighted'=> (bool)  If we are highlighting this node.  Only relevant if
1459      //                           the hilight is active.
1460  
1461      // Setup our node stack.  The loop is designed to output children of a parent, 
1462      // not the parent itself, so we must put the parent on as the starting point.
1463      $nodeStack['Parent'] = array($node['parentNode']);
1464      // And add the childpos of our node in it's parent to our "child index stack".
1465      $nodeStack['ChildIndex'] = array($node['pos']);
1466      // We start at 0.
1467      $nodeStackIndex = 0;
1468  
1469      // We have not to output text before/after our node, so blank it.  We will recover it
1470      // later
1471      $OldPreceedingStringValue = $nodeStack['Parent'][0]['textParts'][$node['pos']];
1472      $OldPreceedingStringRef =& $nodeStack['Parent'][0]['textParts'][$node['pos']];
1473      $OldPreceedingStringRef = "";
1474      $currentXpath = "";
1475  
1476      // While we still have data on our stack
1477      while ($nodeStackIndex >= 0) {
1478        // Count the children and get a copy of the current child.
1479        $iChildCount = count($nodeStack['Parent'][$nodeStackIndex]['childNodes']);
1480        $currentChild = $nodeStack['ChildIndex'][$nodeStackIndex];
1481        // Only do the auto indenting if the $parseSkipWhiteCache flag was set.
1482        if ($this->parseSkipWhiteCache)
1483          $currentIndent = str_repeat($this->indentStep, $nodeStackIndex);
1484  
1485        if ($bDebugThisFunction)
1486          echo "Exporting child ".($currentChild+1)." of node {$nodeStack['Parent'][$nodeStackIndex]['xpath']}\n";
1487  
1488        ///////////////////////////////////////////
1489        // Add the text before our child.
1490  
1491        // Add the text part before the current child
1492        $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild];
1493        if (isSet($tmpTxt) AND ($tmpTxt!="")) {
1494          // Only add CR indent if there were children
1495          if ($iChildCount)
1496            $xmlOut .= $CR.$currentIndent;
1497          // Hilight if necessary.
1498          $highlightStart = $highlightEnd = '';
1499          if ($hilightIsActive) {
1500            $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+1).']';
1501            if (in_array($currentXpath, $this->hilightXpathList)) {
1502             // Yes we hilight
1503              $highlightStart = chr(2);
1504              $highlightEnd   = chr(3);
1505            }
1506          }
1507          $xmlOut .= $highlightStart.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild].$highlightEnd;
1508        }
1509        if ($iChildCount && $nodeStackIndex) $xmlOut .= $CR;
1510  
1511        ///////////////////////////////////////////
1512  
1513        // Are there any more children?
1514        if ($iChildCount <= $currentChild) {
1515          // Nope, so output the last text before the closing tag
1516          $tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1];
1517          if (isSet($tmpTxt) AND ($tmpTxt!="")) {
1518            // Hilight if necessary.
1519            $highlightStart = $highlightEnd = '';
1520            if ($hilightIsActive) {
1521              $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+2).']';
1522              if (in_array($currentXpath, $this->hilightXpathList)) {
1523               // Yes we hilight
1524                $highlightStart = chr(2);
1525                $highlightEnd   = chr(3);
1526              }
1527            }
1528            $xmlOut .= $highlightStart
1529                  .$currentIndent.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1].$CR
1530                  .$highlightEnd;
1531          }
1532  
1533          // Now close this tag, as we are finished with this child.
1534  
1535          // Potentially output an (slightly smaller indent).
1536          if ($this->parseSkipWhiteCache
1537            && count($nodeStack['Parent'][$nodeStackIndex]['childNodes'])) {
1538            $xmlOut .= str_repeat($this->indentStep, $nodeStackIndex - 1);
1539          }
1540  
1541          // Check whether the xml-tag is to be hilighted.
1542          $highlightStart = $highlightEnd = '';
1543          if ($hilightIsActive) {
1544            $currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'];
1545            if (in_array($currentXpath, $this->hilightXpathList)) {
1546              // Yes we hilight
1547              $highlightStart = chr(2);
1548              $highlightEnd   = chr(3);
1549            }
1550          }
1551          $xmlOut .=  $highlightStart
1552                       .'</'.$nodeStack['Parent'][$nodeStackIndex]['name'].'>'
1553                       .$highlightEnd;
1554          // Decrement the $nodeStackIndex to go back to the next unfinished parent.
1555          $nodeStackIndex--;
1556  
1557          // If the index is 0 we are finished exporting the last node, as we may have been
1558          // exporting an internal node.
1559          if ($nodeStackIndex == 0) break;
1560  
1561          // Indicate to the parent that we are finished with this child.
1562          $nodeStack['ChildIndex'][$nodeStackIndex]++;
1563  
1564          continue;
1565        }
1566  
1567        ///////////////////////////////////////////
1568        // Ok, there are children still to process.
1569  
1570        // Queue up the next child (I can copy because I won't modify and copying is faster.)
1571        $nodeStack['Parent'][$nodeStackIndex + 1] = $nodeStack['Parent'][$nodeStackIndex]['childNodes'][$currentChild];
1572  
1573        // Work out if it is a short child tag.
1574        $iGrandChildCount = count($nodeStack['Parent'][$nodeStackIndex + 1]['childNodes']);
1575        $shortGrandChild = (($iGrandChildCount == 0) AND (implode('',$nodeStack['Parent'][$nodeStackIndex + 1]['textParts'])==''));
1576  
1577        ///////////////////////////////////////////
1578        // Assemble the attribute string first.
1579        $attrStr = '';
1580        foreach($nodeStack['Parent'][$nodeStackIndex + 1]['attributes'] as $key=>$val) {
1581          // Should we hilight the attribute?
1582          if ($hilightIsActive AND in_array($currentXpath.'/attribute::'.$key, $this->hilightXpathList)) {
1583            $hiAttrStart = chr(2);
1584            $hiAttrEnd   = chr(3);
1585          } else {
1586            $hiAttrStart = $hiAttrEnd = '';
1587          }
1588          $attrStr .= ' '.$hiAttrStart.$key.'="'.$val.'"'.$hiAttrEnd;
1589        }
1590  
1591        ///////////////////////////////////////////
1592        // Work out what goes before and after the tag content
1593  
1594        $beforeTagContent = $currentIndent;
1595        if ($shortGrandChild) $afterTagContent = '/>';
1596        else                  $afterTagContent = '>';
1597  
1598        // Check whether the xml-tag is to be hilighted.
1599        if ($hilightIsActive) {
1600          $currentXpath = $nodeStack['Parent'][$nodeStackIndex + 1]['xpath'];
1601          if (in_array($currentXpath, $this->hilightXpathList)) {
1602            // Yes we hilight
1603            $beforeTagContent .= chr(2);
1604            $afterTagContent  .= chr(3);
1605          }
1606        }
1607        $beforeTagContent .= '<';
1608  //      if ($shortGrandChild) $afterTagContent .= $CR;
1609        
1610        ///////////////////////////////////////////
1611        // Output the tag
1612  
1613        $xmlOut .= $beforeTagContent
1614                    .$nodeStack['Parent'][$nodeStackIndex + 1]['name'].$attrStr
1615                    .$afterTagContent;
1616  
1617        ///////////////////////////////////////////
1618        // Carry on.            
1619  
1620        // If it is a short tag, then we've already done this child, we just move to the next
1621        if ($shortGrandChild) {
1622          // Move to the next child, we need not go deeper in the tree.
1623          $nodeStack['ChildIndex'][$nodeStackIndex]++;
1624          // But if we are just exporting the one node we'd go no further.
1625          if ($nodeStackIndex == 0) break;
1626        } else {
1627          // Else queue up the child going one deeper in the stack
1628          $nodeStackIndex++;
1629          // Start with it's first child
1630          $nodeStack['ChildIndex'][$nodeStackIndex] = 0;
1631        }
1632      }
1633  
1634      $result = $xmlOut;
1635  
1636      // Repair what we "undid"
1637      $OldPreceedingStringRef = $OldPreceedingStringValue;
1638  
1639      ////////////////////////////////////////////
1640  
1641      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
1642  
1643      return $result;
1644    }
1645       
1646    //-----------------------------------------------------------------------------------------
1647    // XPathEngine           ------ Import the XML Source ------                               
1648    //-----------------------------------------------------------------------------------------
1649    
1650    /**
1651     * Reads a file or URL and parses the XML data.
1652     *
1653     * Parse the XML source and (upon success) store the information into an internal structure.
1654     *
1655     * @param     $fileName (string) Path and name (or URL) of the file to be read and parsed.
1656     * @return              (bool)   TRUE on success, FALSE on failure (check getLastError())
1657     * @see       importFromString(), getLastError(), 
1658     */
1659    function importFromFile($fileName) {
1660      $status = FALSE;
1661      $errStr = '';
1662      do { // try-block
1663        // Remember file name. Used in error output to know in which file it happend
1664        $this->properties['xmlFile'] = $fileName;
1665        // If we already have content, then complain.
1666        if (!empty($this->nodeRoot)) {
1667          $errStr = 'Called when this object already contains xml data. Use reset().';
1668          break; // try-block
1669        }
1670        // The the source is an url try to fetch it.
1671        if (preg_match(';^http(s)?://;', $fileName)) {
1672          // Read the content of the url...this is really prone to errors, and we don't really
1673          // check for too many here...for now, suppressing both possible warnings...we need
1674          // to check if we get a none xml page or something of that nature in the future
1675          $xmlString = @implode('', @file($fileName));
1676          if (!empty($xmlString)) {
1677            $status = TRUE;
1678          } else {
1679            $errStr = "The url '{$fileName}' could not be found or read.";
1680          }
1681          break; // try-block
1682        } 
1683        
1684        // Reaching this point we're dealing with a real file (not an url). Check if the file exists and is readable.
1685        if (!is_readable($fileName)) { // Read the content from the file
1686          $errStr = "File '{$fileName}' could not be found or read.";
1687          break; // try-block
1688        }
1689        if (is_dir($fileName)) {
1690          $errStr = "'{$fileName}' is a directory.";
1691          break; // try-block
1692        }
1693        // Read the file
1694        if (!($fp = @fopen($fileName, 'rb'))) {
1695          $errStr = "Failed to open '{$fileName}' for read.";
1696          break; // try-block
1697        }
1698        $xmlString = fread($fp, filesize($fileName));
1699        @fclose($fp);
1700        
1701        $status = TRUE;
1702      } while (FALSE);
1703      
1704      if (!$status) {
1705        $this->_displayError('In importFromFile(): '. $errStr, __LINE__, __FILE__, FALSE);
1706        return FALSE;
1707      }
1708      return $this->importFromString($xmlString);
1709    }
1710    
1711    /**
1712     * Reads a string and parses the XML data.
1713     *
1714     * Parse the XML source and (upon success) store the information into an internal structure.
1715     * If a parent xpath is given this means that XML data is to be *appended* to that parent.
1716     *
1717     * ### If a function uses setLastError(), then say in the function header that getLastError() is useful.
1718     *
1719     * @param  $xmlString           (string) Name of the string to be read and parsed.
1720     * @param  $absoluteParentPath  (string) Node to append data too (see above)
1721     * @return                      (bool)   TRUE on success, FALSE on failure 
1722     *                                       (check getLastError())
1723     */
1724    function importFromString($xmlString, $absoluteParentPath = '') {
1725      $ThisFunctionName = 'importFromString';
1726      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
1727      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
1728      if ($bDebugThisFunction) {
1729        echo "Importing from string of length ".strlen($xmlString)." to node '$absoluteParentPath'\n<br>";
1730        echo "Parser options:\n<br>";
1731        print_r($this->parseOptions);
1732      }
1733  
1734      $status = FALSE;
1735      $errStr = '';
1736      do { // try-block
1737        // If we already have content, then complain.
1738        if (!empty($this->nodeRoot) AND empty($absoluteParentPath)) {
1739          $errStr = 'Called when this object already contains xml data. Use reset() or pass the parent Xpath as 2ed param to where tie data will append.';
1740          break; // try-block
1741        }
1742        // Check whether content has been read.
1743        if (empty($xmlString)) {
1744          // Nothing to do!!
1745          $status = TRUE;
1746          // If we were importing to root, build a blank root.
1747          if (empty($absoluteParentPath)) {
1748            $this->_createSuperRoot();
1749          }
1750          $this->reindexNodeTree();
1751  //        $errStr = 'This xml document (string) was empty';
1752          break; // try-block
1753        } else {
1754          $xmlString = $this->_translateAmpersand($xmlString);
1755        }
1756        
1757        // Restart our node index with a root entry.
1758        $nodeStack = array();
1759        $this->parseStackIndex = 0;
1760  
1761        // If a parent xpath is given this means that XML data is to be *appended* to that parent.
1762        if (!empty($absoluteParentPath)) {
1763          // Check if parent exists
1764          if (!isSet($this->nodeIndex[$absoluteParentPath])) {
1765            $errStr = "You tried to append XML data to a parent '$absoluteParentPath' that does not exist.";
1766            break; // try-block
1767          } 
1768          // Add it as the starting point in our array.
1769          $this->nodeStack[0] =& $this->nodeIndex[$absoluteParentPath];
1770        } else {
1771          // Build a 'super-root'
1772          $this->_createSuperRoot();
1773          // Put it in as the start of our node stack.
1774          $this->nodeStack[0] =& $this->nodeRoot;
1775        }
1776  
1777        // Point our text buffer reference at the next text part of the root
1778        $this->parsedTextLocation =& $this->nodeStack[0]['textParts'][];
1779        $this->parsInCData = 0;
1780        // We cache this now.
1781        $this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;
1782        
1783        // Create an XML parser.
1784        $parser = xml_parser_create();
1785        // Set default XML parser options.
1786        if (is_array($this->parseOptions)) {
1787          foreach($this->parseOptions as $key => $val) {
1788            xml_parser_set_option($parser, $key, $val);
1789          }
1790        }
1791        
1792        // Set the object and the element handlers for the XML parser.
1793        xml_set_object($parser, $this);
1794        xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
1795        xml_set_character_data_handler($parser, '_handleCharacterData');
1796        xml_set_default_handler($parser, '_handleDefaultData');
1797        xml_set_processing_instruction_handler($parser, '_handlePI');
1798       
1799        // Parse the XML source and on error generate an error message.
1800        if (!xml_parse($parser, $xmlString, TRUE)) {
1801          $source = empty($this->properties['xmlFile']) ? 'string' : 'file ' . basename($this->properties['xmlFile']) . "'";
1802          $errStr = "XML error in given {$source} on line ".
1803                 xml_get_current_line_number($parser). '  column '. xml_get_current_column_number($parser) .
1804                 '. Reason:'. xml_error_string(xml_get_error_code($parser));
1805          break; // try-block
1806        }
1807        
1808        // Free the parser.
1809        @xml_parser_free($parser);
1810        // And we don't need this any more.
1811        $this->nodeStack = array();
1812  
1813        $this->reindexNodeTree();
1814  
1815        if ($bDebugThisFunction) {
1816          print_r(array_keys($this->nodeIndex));
1817        }
1818  
1819        $status = TRUE;
1820      } while (FALSE);
1821      
1822      if (!$status) {
1823        $this->_displayError('In importFromString(): '. $errStr, __LINE__, __FILE__, FALSE);
1824        $bResult = FALSE;
1825      } else {
1826        $bResult = TRUE;
1827      }
1828  
1829      ////////////////////////////////////////////
1830  
1831      $this->_closeDebugFunction($ThisFunctionName, $bResult, $bDebugThisFunction);
1832  
1833      return $bResult;
1834    }
1835    
1836    
1837    //-----------------------------------------------------------------------------------------
1838    // XPathEngine               ------  XML Handlers  ------                                  
1839    //-----------------------------------------------------------------------------------------
1840    
1841    /**
1842     * Handles opening XML tags while parsing.
1843     *
1844     * While parsing a XML document for each opening tag this method is
1845     * called. It'll add the tag found to the tree of document nodes.
1846     *
1847     * @param $parser     (int)    Handler for accessing the current XML parser.
1848     * @param $name       (string) Name of the opening tag found in the document.
1849     * @param $attributes (array)  Associative array containing a list of
1850     *                             all attributes of the tag found in the document.
1851     * @see _handleEndElement(), _handleCharacterData()
1852     */
1853    function _handleStartElement($parser, $nodeName, $attributes) {
1854      if (empty($nodeName)) {
1855        $this->_displayError('XML error in file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
1856        return;
1857      }
1858  
1859      // Trim accumulated text if necessary.
1860      if ($this->parseSkipWhiteCache) {
1861        $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
1862        $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
1863      } 
1864  
1865      if ($this->bDebugXmlParse) {
1866        echo "<blockquote>" . htmlspecialchars("Start node: <".$nodeName . ">")."<br>";
1867        echo "Appended to stack entry: $this->parseStackIndex<br>\n";
1868        echo "Text part before element is: ".htmlspecialchars($this->parsedTextLocation);
1869        /*
1870        echo "<pre>";
1871        $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
1872        for ($i = 0; $i < $dataPartsCount; $i++) {
1873          echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
1874        }
1875        echo "</pre>";
1876        */
1877      }
1878  
1879      // Add a node and set path to current.
1880      if (!$this->_internalAppendChild($this->parseStackIndex, $nodeName)) {
1881        $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
1882        return;
1883      }    
1884  
1885      // We will have gone one deeper then in the stack.
1886      $this->parseStackIndex++;
1887  
1888      // Point our parseTxtBuffer reference at the new node.
1889      $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][0];
1890      
1891      // Set the attributes.
1892      if (!empty($attributes)) {
1893        if ($this->bDebugXmlParse) {
1894          echo 'Attributes: <br>';
1895          print_r($attributes);
1896          echo '<br>';
1897        }
1898        $this->nodeStack[$this->parseStackIndex]['attributes'] = $attributes;
1899      }
1900    }
1901    
1902    /**
1903     * Handles closing XML tags while parsing.
1904     *
1905     * While parsing a XML document for each closing tag this method is called.
1906     *
1907     * @param $parser (int)    Handler for accessing the current XML parser.
1908     * @param $name   (string) Name of the closing tag found in the document.
1909     * @see       _handleStartElement(), _handleCharacterData()
1910     */
1911    function _handleEndElement($parser, $name) {
1912      if (($this->parsedTextLocation=='') 
1913          && empty($this->nodeStack[$this->parseStackIndex]['textParts'])) {
1914        // We reach this point when parsing a tag of format <foo/>. The 'textParts'-array 
1915        // should stay empty and not have an empty string in it.
1916      } else {
1917        // Trim accumulated text if necessary.
1918        if ($this->parseSkipWhiteCache) {
1919          $iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
1920          $this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
1921        }
1922      }
1923  
1924      if ($this->bDebugXmlParse) {
1925        echo "Text part after element is: ".htmlspecialchars($this->parsedTextLocation)."<br>\n";
1926        echo htmlspecialchars("Parent:<{$this->parseStackIndex}>, End-node:</$name> '".$this->parsedTextLocation) . "'<br>Text nodes:<pre>\n";
1927        $dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
1928        for ($i = 0; $i < $dataPartsCount; $i++) {
1929          echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
1930        }
1931        var_dump($this->nodeStack[$this->parseStackIndex]['textParts']);
1932        echo "</pre></blockquote>\n";
1933      }
1934  
1935      // Jump back to the parent element.
1936      $this->parseStackIndex--;
1937  
1938      // Set our reference for where we put any more whitespace
1939      $this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][];
1940  
1941      // Note we leave the entry in the stack, as it will get blanked over by the next element
1942      // at this level.  The safe thing to do would be to remove it too, but in the interests 
1943      // of performance, we will not bother, as were it to be a problem, then it would be an
1944      // internal bug anyway.
1945      if ($this->parseStackIndex < 0) {
1946        $this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
1947        return;
1948      }    
1949    }
1950    
1951    /**
1952     * Handles character data while parsing.
1953     *
1954     * While parsing a XML document for each character data this method
1955     * is called. It'll add the character data to the document tree.
1956     *
1957     * @param $parser (int)    Handler for accessing the current XML parser.
1958     * @param $text   (string) Character data found in the document.
1959     * @see       _handleStartElement(), _handleEndElement()
1960     */
1961    function _handleCharacterData($parser, $text) {
1962    
1963      if ($this->parsInCData >0) $text = $this->_translateAmpersand($text, $reverse=TRUE);
1964      
1965      if ($this->bDebugXmlParse) echo "Handling character data: '".htmlspecialchars($text)."'<br>";
1966      if ($this->parseSkipWhiteCache AND !empty($text) AND !$this->parsInCData) {
1967        // Special case CR. CR always comes in a separate data. Trans. it to '' or ' '. 
1968        // If txtBuffer is already ending with a space use '' otherwise ' '.
1969        $bufferHasEndingSpace = (empty($this->parsedTextLocation) OR substr($this->parsedTextLocation, -1) === ' ') ? TRUE : FALSE;
1970        if ($text=="\n") {
1971          $text = $bufferHasEndingSpace ? '' : ' ';
1972        } else {
1973          if ($bufferHasEndingSpace) {
1974            $text = ltrim(preg_replace('/\s+/', ' ', $text));
1975          } else {
1976            $text = preg_replace('/\s+/', ' ', $text);
1977          }
1978        }
1979        if ($this->bDebugXmlParse) echo "'Skip white space' is ON. reduced to : '" .htmlspecialchars($text) . "'<br>";
1980      }
1981      $this->parsedTextLocation .= $text;
1982    }
1983    
1984    /**
1985     * Default handler for the XML parser.  
1986     *
1987     * While parsing a XML document for string not caught by one of the other
1988     * handler functions, we end up here.
1989     *
1990     * @param $parser (int)    Handler for accessing the current XML parser.
1991     * @param $text   (string) Character data found in the document.
1992     * @see       _handleStartElement(), _handleEndElement()
1993     */
1994    function _handleDefaultData($parser, $text) {
1995      do { // try-block
1996        if (!strcmp($text, '<![CDATA[')) {
1997          $this->parsInCData++;
1998        } elseif (!strcmp($text, ']]>')) {
1999          $this->parsInCData--;
2000          if ($this->parsInCData < 0) $this->parsInCData = 0;
2001        }
2002        $this->parsedTextLocation .= $this->_translateAmpersand($text, $reverse=TRUE);
2003        if ($this->bDebugXmlParse) echo "Default handler data: ".htmlspecialchars($text)."<br>";    
2004        break; // try-block
2005      } while (FALSE); // END try-block
2006    }
2007    
2008    /**
2009     * Handles processing instruction (PI)
2010     *
2011     * A processing instruction has the following format: 
2012     * <?  target data  ? > e.g.  <? dtd version="1.0" ? >
2013     *
2014     * Currently I have no bether idea as to left it 'as is' and treat the PI data as normal 
2015     * text (and adding the surrounding PI-tags <? ? >). 
2016     *
2017     * @param     $parser (int)    Handler for accessing the current XML parser.
2018     * @param     $target (string) Name of the PI target. E.g. XML, PHP, DTD, ... 
2019     * @param     $data   (string) Associative array containing a list of
2020     * @see       PHP's manual "xml_set_processing_instruction_handler"
2021     */
2022    function _handlePI($parser, $target, $data) {
2023      //echo("pi data=".$data."end"); exit;
2024      $data = $this->_translateAmpersand($data, $reverse=TRUE);
2025      $this->parsedTextLocation .= "<?{$target} {$data}?>";
2026      return TRUE;
2027    }
2028    
2029    //-----------------------------------------------------------------------------------------
2030    // XPathEngine          ------  Node Tree Stuff  ------                                    
2031    //-----------------------------------------------------------------------------------------
2032  
2033    /**
2034     * Creates a super root node.
2035     */
2036    function _createSuperRoot() {
2037      // Build a 'super-root'
2038      $this->nodeRoot = $this->emptyNode;
2039      $this->nodeRoot['name']      = '';
2040      $this->nodeRoot['parentNode'] = NULL;
2041      $this->nodeIndex[''] =& $this->nodeRoot;
2042    }
2043  
2044    /**
2045     * Adds a new node to the XML document tree during xml parsing.
2046     *
2047     * This method adds a new node to the tree of nodes of the XML document
2048     * being handled by this class. The new node is created according to the
2049     * parameters passed to this method.  This method is a much watered down
2050     * version of appendChild(), used in parsing an xml file only.
2051     * 
2052     * It is assumed that adding starts with root and progresses through the
2053     * document in parse order.  New nodes must have a corresponding parent. And
2054     * once we have read the </> tag for the element we will never need to add
2055     * any more data to that node.  Otherwise the add will be ignored or fail.
2056     *
2057     * The function is faciliated by a nodeStack, which is an array of nodes that
2058     * we have yet to close.
2059     *
2060     * @param   $stackParentIndex (int)    The index into the nodeStack[] of the parent
2061     *                                     node to which the new node should be added as 
2062     *                                     a child. *READONLY*
2063     * @param   $nodeName         (string) Name of the new node. *READONLY*
2064     * @return                    (bool)   TRUE if we successfully added a new child to 
2065     *                                     the node stack at index $stackParentIndex + 1,
2066     *                                     FALSE on error.
2067     */
2068    function _internalAppendChild($stackParentIndex, $nodeName) {
2069      // This call is likely to be executed thousands of times, so every 0.01ms counts.
2070      // If you want to debug this function, you'll have to comment the stuff back in
2071      //$bDebugThisFunction = FALSE;
2072      
2073      /*
2074      $ThisFunctionName = '_internalAppendChild';
2075      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
2076      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
2077      if ($bDebugThisFunction) {
2078        echo "Current Node (parent-index) and the child to append : '{$stackParentIndex}' +  '{$nodeName}' \n<br>";
2079      }
2080      */
2081       //////////////////////////////////////
2082  
2083      if (!isSet($this->nodeStack[$stackParentIndex])) {
2084        $errStr = "Invalid parent. You tried to append the tag '{$nodeName}' to an non-existing parent in our node stack '{$stackParentIndex}'.";
2085        $this->_displayError('In _internalAppendChild(): '. $errStr, __LINE__, __FILE__, FALSE); 
2086  
2087        /*
2088        $this->_closeDebugFunction($ThisFunctionName, FALSE, $bDebugThisFunction);
2089        */
2090  
2091        return FALSE;
2092      }
2093  
2094      // Retrieve the parent node from the node stack.  This is the last node at that 
2095      // depth that we have yet to close.  This is where we should add the text/node.
2096      $parentNode =& $this->nodeStack[$stackParentIndex];
2097            
2098      // Brand new node please
2099      $newChildNode = $this->emptyNode;
2100      
2101      // Save the vital information about the node.
2102      $newChildNode['name'] = $nodeName;
2103      $parentNode['childNodes'][] =& $newChildNode;
2104      
2105      // Add to our node stack
2106      $this->nodeStack[$stackParentIndex + 1] =& $newChildNode;
2107  
2108      /*
2109      if ($bDebugThisFunction) {
2110        echo "The new node received index: '".($stackParentIndex + 1)."'\n";
2111        foreach($this->nodeStack as $key => $val) echo "$key => ".$val['name']."\n"; 
2112      }
2113      $this->_closeDebugFunction($ThisFunctionName, TRUE, $bDebugThisFunction);
2114      */
2115  
2116      return TRUE;
2117    }
2118    
2119    /**
2120     * Update nodeIndex and every node of the node-tree. 
2121     *
2122     * Call after you have finished any tree modifications other wise a match with 
2123     * an xPathQuery will produce wrong results.  The $this->nodeIndex[] is recreated 
2124     * and every nodes optimization data is updated.  The optimization data is all the
2125     * data that is duplicate information, would just take longer to find. Child nodes 
2126     * with value NULL are removed from the tree.
2127     *
2128     * By default the modification functions in this component will automatically re-index
2129     * the nodes in the tree.  Sometimes this is not the behaver you want. To surpress the 
2130     * reindex, set the functions $autoReindex to FALSE and call reindexNodeTree() at the 
2131     * end of your changes.  This sometimes leads to better code (and less CPU overhead).
2132     *
2133     * Sample:
2134     * =======
2135     * Given the xml is <AAA><B/>.<B/>.<B/></AAA> | Goal is <AAA>.<B/>.</AAA>  (Delete B[1] and B[3])
2136     *   $xPathSet = $xPath->match('//B'); # Will result in array('/AAA[1]/B[1]', '/AAA[1]/B[2]', '/AAA[1]/B[3]');
2137     * Three ways to do it.
2138     * 1) Top-Down  (with auto reindexing) - Safe, Slow and you get easily mix up with the the changing node index
2139     *    removeChild('/AAA[1]/B[1]'); // B[1] removed, thus all B[n] become B[n-1] !!
2140     *    removeChild('/AAA[1]/B[2]'); // Now remove B[2] (That originaly was B[3])
2141     * 2) Bottom-Up (with auto reindexing) -  Safe, Slow and the changing node index (caused by auto-reindex) can be ignored.
2142     *    for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
2143     *      if ($i==1) continue; 
2144     *      removeChild($xPathSet[$i]);
2145     *    }
2146     * 3) // Top-down (with *NO* auto reindexing) - Fast, Safe as long as you call reindexNodeTree()
2147     *    foreach($xPathSet as $xPath) {
2148     *      // Specify no reindexing
2149     *      if ($xPath == $xPathSet[1]) continue; 
2150     *      removeChild($xPath, $autoReindex=FALSE);
2151     *      // The object is now in a slightly inconsistent state.
2152     *    }
2153     *    // Finally do the reindex and the object is consistent again
2154     *    reindexNodeTree();
2155     *
2156     * @return (bool) TRUE on success, FALSE otherwise.
2157     * @see _recursiveReindexNodeTree()
2158     */
2159    function reindexNodeTree() {
2160      //return;
2161      $this->_indexIsDirty = FALSE;
2162      $this->nodeIndex = array();
2163      $this->nodeIndex[''] =& $this->nodeRoot;
2164      // Quick out for when the tree has no data.
2165      if (empty($this->nodeRoot)) return TRUE;
2166      return $this->_recursiveReindexNodeTree('');
2167    }
2168    
2169  
2170    /**
2171     * Create the ids that are accessable through the generate-id() function
2172     */
2173    function _generate_ids() {
2174      // If we have generated them already, then bail.
2175      if (isset($this->nodeIndex['']['generate_id'])) return;
2176  
2177      // keys generated are the string 'id0' . hexatridecimal-based (0..9,a-z) index
2178      $aNodeIndexes = array_keys($this->nodeIndex);
2179      $idNumber = 0;
2180      foreach($aNodeIndexes as $index => $key) {
2181  //      $this->nodeIndex[$key]['generated_id'] = 'id' . base_convert($index,10,36);
2182        // Skip attribute and text nodes.
2183        // ### Currently don't support attribute and text nodes.
2184        if (strstr($key, 'text()') !== FALSE) continue;
2185        if (strstr($key, 'attribute::') !== FALSE) continue;
2186        $this->nodeIndex[$key]['generated_id'] = 'idPhpXPath' . $idNumber;
2187  
2188        // Make the id's sequential so that we can test predictively.
2189        $idNumber++;
2190      }
2191    }
2192  
2193    /**
2194     * Here's where the work is done for reindexing (see reindexNodeTree)
2195     *
2196     * @param  $absoluteParentPath (string) the xPath to the parent node
2197     * @return                     (bool)   TRUE on success, FALSE otherwise.
2198     * @see reindexNodeTree()
2199     */
2200    function _recursiveReindexNodeTree($absoluteParentPath) {
2201      $parentNode =& $this->nodeIndex[$absoluteParentPath];
2202      
2203      // Check for any 'dead' child nodes first and concate the text parts if found.
2204      for ($iChildIndex=sizeOf($parentNode['childNodes'])-1; $iChildIndex>=0; $iChildIndex--) {
2205        // Check if the child node still exits (it may have been removed).
2206        if (!empty($parentNode['childNodes'][$iChildIndex])) continue;
2207        // Child node was removed. We got to merge the text parts then.
2208        $parentNode['textParts'][$iChildIndex] .= $parentNode['textParts'][$iChildIndex+1];
2209        array_splice($parentNode['textParts'], $iChildIndex+1, 1); 
2210        array_splice($parentNode['childNodes'], $iChildIndex, 1);
2211      }
2212  
2213      // Now start a reindex.
2214      $contextHash = array();
2215      $childSize = sizeOf($parentNode['childNodes']);
2216  
2217      // If there are no children, we have to treat this specially:
2218      if ($childSize == 0) {
2219        // Add a dummy text node.
2220        $this->nodeIndex[$absoluteParentPath.'/text()[1]'] =& $parentNode;
2221      } else {
2222        for ($iChildIndex=0; $iChildIndex<$childSize; $iChildIndex++) {
2223          $childNode =& $parentNode['childNodes'][$iChildIndex];
2224          // Make sure that there is a text-part in front of every node. (May be empty)
2225          if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = '';
2226          // Count the nodes with same name (to determine their context position)
2227          $childName = $childNode['name'];
2228          if (empty($contextHash[$childName])) { 
2229            $contextPos = $contextHash[$childName] = 1;
2230          } else {
2231            $contextPos = ++$contextHash[$childName];
2232          }
2233          // Make the node-index hash
2234          $newPath = $absoluteParentPath . '/' . $childName . '['.$contextPos.']';
2235  
2236          // ### Note ultimately we will end up supporting text nodes as actual nodes.
2237  
2238          // Preceed with a dummy entry for the text node.
2239          $this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+1).']'] =& $childNode;
2240          // Then the node itself
2241          $this->nodeIndex[$newPath] =& $childNode;
2242  
2243          // Now some dummy nodes for each of the attribute nodes.
2244          $iAttributeCount = sizeOf($childNode['attributes']);
2245          if ($iAttributeCount > 0) {
2246            $aAttributesNames = array_keys($childNode['attributes']);
2247            for ($iAttributeIndex = 0; $iAttributeIndex < $iAttributeCount; $iAttributeIndex++) {
2248              $attribute = $aAttributesNames[$iAttributeIndex];
2249              $newAttributeNode = $this->emptyNode;
2250              $newAttributeNode['name'] = $attribute;
2251              $newAttributeNode['textParts'] = array($childNode['attributes'][$attribute]);
2252              $newAttributeNode['contextPos'] = $iAttributeIndex;
2253              $newAttributeNode['xpath'] = "$newPath/attribute::$attribute";
2254              $newAttributeNode['parentNode'] =& $childNode;
2255              $newAttributeNode['depth'] =& $parentNode['depth'] + 2;
2256              // Insert the node as a master node, not a reference, otherwise there will be 
2257              // variable "bleeding".
2258              $this->nodeIndex["$newPath/attribute::$attribute"] = $newAttributeNode;
2259            }
2260          }
2261  
2262          // Update the node info (optimisation)
2263          $childNode['parentNode'] =& $parentNode;
2264          $childNode['depth'] = $parentNode['depth'] + 1;
2265          $childNode['pos'] = $iChildIndex;
2266          $childNode['contextPos'] = $contextHash[$childName];
2267          $childNode['xpath'] = $newPath;
2268          $this->_recursiveReindexNodeTree($newPath);
2269  
2270          // Follow with a dummy entry for the text node.
2271          $this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+2).']'] =& $childNode;
2272        }
2273  
2274        // Make sure that their is a text-part after the last node.
2275        if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = '';
2276      }
2277  
2278      return TRUE;
2279    }
2280    
2281    /** 
2282     * Clone a node and it's child nodes.
2283     *
2284     * NOTE: If the node has children you *MUST* use the reference operator!
2285     *       E.g. $clonedNode =& cloneNode($node);
2286     *       Otherwise the children will not point back to the parent, they will point 
2287     *       back to your temporary variable instead.
2288     *
2289     * @param   $node (mixed)  Either a node (hash array) or an abs. Xpath to a node in 
2290     *                         the current doc
2291     * @return        (&array) A node and it's child nodes.
2292     */
2293    function &cloneNode($node, $recursive=FALSE) {
2294      if (is_string($node) AND isSet($this->nodeIndex[$node])) {
2295        $node = $this->nodeIndex[$node];
2296      }
2297      // Copy the text-parts ()
2298      $textParts = $node['textParts'];
2299      $node['textParts'] = array();
2300      foreach ($textParts as $key => $val) {
2301        $node['textParts'][] = $val;
2302      }
2303      
2304      $childSize = sizeOf($node['childNodes']);
2305      for ($i=0; $i<$childSize; $i++) {
2306        $childNode =& $this->cloneNode($node['childNodes'][$i], TRUE);  // copy child 
2307        $node['childNodes'][$i] =& $childNode; // reference the copy
2308        $childNode['parentNode'] =& $node;      // child references the parent.
2309      }
2310      
2311      if (!$recursive) {
2312        //$node['childNodes'][0]['parentNode'] = null;
2313        //print "<pre>";
2314        //var_dump($node);
2315      }
2316      return $node;
2317    }
2318    
2319    
2320  /** Nice to have but __sleep() has a bug. 
2321      (2002-2 PHP V4.1. See bug #15350)
2322    
2323    /**
2324     * PHP cals this function when you call PHP's serialize. 
2325     *
2326     * It prevents cyclic referencing, which is why print_r() of an XPath object doesn't work.
2327     *
2328    function __sleep() {
2329      // Destroy recursive pointers
2330      $keys = array_keys($this->nodeIndex);
2331      $size = sizeOf($keys);
2332      for ($i=0; $i<$size; $i++) {
2333        unset($this->nodeIndex[$keys[$i]]['parentNode']);
2334      }
2335      unset($this->nodeIndex);
2336    }
2337    
2338    /**
2339     * PHP cals this function when you call PHP's unserialize. 
2340     *
2341     * It reindexes the node-tree
2342     *
2343    function __wakeup() {
2344      $this->reindexNodeTree();
2345    }
2346    
2347  */
2348    
2349    //-----------------------------------------------------------------------------------------
2350    // XPath            ------  XPath Query / Evaluation Handlers  ------                      
2351    //-----------------------------------------------------------------------------------------
2352    
2353    /**
2354     * Matches (evaluates) an XPath query
2355     *
2356     * This method tries to evaluate an XPath query by parsing it. A XML source must 
2357     * have been imported before this method is able to work.
2358     *
2359     * @param     $xPathQuery  (string) XPath query to be evaluated.
2360     * @param     $baseXPath   (string) (default is super-root) XPath query to a single document node, 
2361     *                                  from which the XPath query should  start evaluating.
2362     * @return                 (mixed)  The result of the XPath expression.  Either:
2363     *                                    node-set (an ordered collection of absolute references to nodes without duplicates) 
2364     *                                    boolean (true or false) 
2365     *                                    number (a floating-point number) 
2366     *                                    string (a sequence of UCS characters) 
2367     */
2368    function match($xPathQuery, $baseXPath='') {
2369      if ($this->_indexIsDirty) $this->reindexNodeTree();
2370      
2371      // Replace a double slashes, because they'll cause problems otherwise.
2372      static $slashes2descendant = array(
2373          '//@' => '/descendant_or_self::*/attribute::', 
2374          '//'  => '/descendant_or_self::node()/', 
2375          '/@'  => '/attribute::');
2376      // Stupid idea from W3C to take axes name containing a '-' (dash) !!!
2377      // We replace the '-' with '_' to avoid the conflict with the minus operator.
2378      static $dash2underscoreHash = array( 
2379          '-sibling'    => '_sibling', 
2380          '-or-'        => '_or_',
2381          'starts-with' => 'starts_with', 
2382          'substring-before' => 'substring_before',
2383          'substring-after'  => 'substring_after', 
2384          'string-length'    => 'string_length',
2385          'normalize-space'  => 'normalize_space',
2386          'x-lower'          => 'x_lower',
2387          'x-upper'          => 'x_upper',
2388          'generate-id'      => 'generate_id');
2389      
2390      if (empty($xPathQuery)) return array();
2391  
2392      // Special case for when document is empty.
2393      if (empty($this->nodeRoot)) return array();
2394  
2395      if (!isSet($this->nodeIndex[$baseXPath])) {
2396              $xPathSet = $this->_resolveXPathQuery($baseXPath,'match');
2397              if (sizeOf($xPathSet) !== 1) {
2398                  $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
2399                  return FALSE;
2400              }
2401              $baseXPath = $xPathSet[0];
2402      }
2403  
2404      // We should possibly do a proper syntactical parse, but instead we will cheat and just
2405      // remove any literals that could make things very difficult for us, and replace them with
2406      // special tags.  Then we can treat the xPathQuery much more easily as JUST "syntax".  Provided 
2407      // there are no literals in the string, then we can guarentee that most of the operators and 
2408      // syntactical elements are indeed elements and not just part of a literal string.
2409      $processedxPathQuery = $this->_removeLiterals($xPathQuery);
2410      
2411      // Replace a double slashes, and '-' (dash) in axes names.
2412      $processedxPathQuery = strtr($processedxPathQuery, $slashes2descendant);
2413      $processedxPathQuery = strtr($processedxPathQuery, $dash2underscoreHash);
2414  
2415      // Build the context
2416      $context = array('nodePath' => $baseXPath, 'pos' => 1, 'size' => 1);
2417  
2418      // The primary syntactic construct in XPath is the expression.
2419      $result = $this->_evaluateExpr($processedxPathQuery, $context);
2420  
2421      // We might have been returned a string.. If so convert back to a literal
2422      $literalString = $this->_asLiteral($result);
2423      if ($literalString != FALSE) return $literalString;
2424      else return $result;
2425    }
2426  
2427    /**
2428     * Alias for the match function
2429     *
2430     * @see match()
2431     */
2432    function evaluate($xPathQuery, $baseXPath='') {
2433      return $this->match($xPathQuery, $baseXPath);
2434    }
2435  
2436    /**
2437     * Parse out the literals of an XPath expression.
2438     *
2439     * Instead of doing a full lexical parse, we parse out the literal strings, and then
2440     * Treat the sections of the string either as parts of XPath or literal strings.  So
2441     * this function replaces each literal it finds with a literal reference, and then inserts
2442     * the reference into an array of strings that we can access.  The literals can be accessed
2443     * later from the literals associative array.
2444     *
2445     * Example:
2446     *  XPathExpr = /AAA[@CCC = "hello"]/BBB[DDD = 'world'] 
2447     *  =>  literals: array("hello", "world")
2448     *      return value: /AAA[@CCC = $1]/BBB[DDD = $2] 
2449     *
2450     * Note: This does not interfere with the VariableReference syntactical element, as these 
2451     * elements must not start with a number.
2452     *
2453     * @param  $xPathQuery  (string) XPath expression to be processed
2454     * @return              (string) The XPath expression without the literals.
2455     *                              
2456     */
2457    function _removeLiterals($xPathQuery) {
2458      // What comes first?  A " or a '?
2459      if (!preg_match(":^([^\"']*)([\"'].*)$:", $xPathQuery, $aMatches)) {
2460        // No " or ' means no more literals.
2461        return $xPathQuery;
2462      }
2463      
2464      $result = $aMatches[1];
2465      $remainder = $aMatches[2];
2466      // What kind of literal?
2467      if (preg_match(':^"([^"]*)"(.*)$:', $remainder, $aMatches)) {
2468        // A "" literal.
2469        $literal = $aMatches[1];
2470        $remainder = $aMatches[2];
2471      } else if (preg_match(":^'([^']*)'(.*)$:", $remainder, $aMatches)) {
2472        // A '' literal.
2473        $literal = $aMatches[1];
2474        $remainder = $aMatches[2];
2475      } else {
2476        $this->_displayError("The '$xPathQuery' argument began a literal, but did not close it.", __LINE__, __FILE__);
2477      }
2478  
2479      // Store the literal
2480      $literalNumber = count($this->axPathLiterals);
2481      $this->axPathLiterals[$literalNumber] = $literal;
2482      $result .= '$'.$literalNumber;
2483      return $result.$this->_removeLiterals($remainder);
2484    }
2485  
2486    /**
2487     * Returns the given string as a literal reference.
2488     *
2489     * @param $string (string) The string that we are processing
2490     * @return        (mixed)  The literal string.  FALSE if the string isn't a literal reference.
2491     */
2492    function _asLiteral($string) {
2493      if (empty($string)) return FALSE;
2494      if (empty($string[0])) return FALSE;
2495      if ($string[0] == '$') {
2496        $remainder = substr($string, 1);
2497        if (is_numeric($remainder)) {
2498          // We have a string reference then.
2499          $stringNumber = (int)$remainder;
2500          if ($stringNumber >= count($this->axPathLiterals)) {
2501              $this->_displayError("Internal error.  Found a string reference that we didn't set in xPathQuery: '$xPathQuery'.", __LINE__, __FILE__);
2502              return FALSE;
2503          }
2504          return $this->axPathLiterals[$stringNumber];
2505        }
2506      }
2507  
2508      // It's not a reference then.
2509      return FALSE;
2510    }
2511    
2512    /**
2513     * Adds a literal to our array of literals
2514     *
2515     * In order to make sure we don't interpret literal strings as XPath expressions, we have to
2516     * encode literal strings so that we know that they are not XPaths.
2517     *
2518     * @param $string (string) The literal string that we need to store for future access
2519     * @return        (mixed)  A reference string to this literal.
2520     */
2521    function _addLiteral($string) {
2522      // Store the literal
2523      $literalNumber = count($this->axPathLiterals);
2524      $this->axPathLiterals[$literalNumber] = $string;
2525      $result = '$'.$literalNumber;
2526      return $result;
2527    }
2528  
2529    /**
2530     * Look for operators in the expression
2531     *
2532     * Parses through the given expression looking for operators.  If found returns
2533     * the operands and the operator in the resulting array.
2534     *
2535     * @param  $xPathQuery  (string) XPath query to be evaluated.
2536     * @return              (array)  If an operator is found, it returns an array containing
2537     *                               information about the operator.  If no operator is found
2538     *                               then it returns an empty array.  If an operator is found,
2539     *                               but has invalid operands, it returns FALSE.
2540     *                               The resulting array has the following entries:
2541     *                                'operator' => The string version of operator that was found,
2542     *                                              trimmed for whitespace
2543     *                                'left operand' => The left operand, or empty if there was no
2544     *                                              left operand for this operator.
2545     *                                'right operand' => The right operand, or empty if there was no
2546     *                                              right operand for this operator.
2547     */
2548    function _GetOperator($xPathQuery) {
2549      $position = 0;
2550      $operator = '';
2551  
2552      // The results of this function can easily be cached.
2553      static $aResultsCache = array();
2554      if (isset($aResultsCache[$xPathQuery])) {
2555        return $aResultsCache[$xPathQuery];
2556      }
2557  
2558      // Run through all operators and try to find one.
2559      $opSize = sizeOf($this->operators);
2560      for ($i=0; $i<$opSize; $i++) {
2561        // Pick an operator to try.
2562        $operator = $this->operators[$i];
2563        // Quickcheck. If not present don't wast time searching 'the hard way'
2564        if (strpos($xPathQuery, $operator)===FALSE) continue;
2565        // Special check
2566        $position = $this->_searchString($xPathQuery, $operator);
2567        // Check whether a operator was found.
2568        if ($position <= 0 ) continue;
2569  
2570        // Check whether it's the equal operator.
2571        if ($operator == '=') {
2572          // Also look for other operators containing the equal sign.
2573          switch ($xPathQuery[$position-1]) {
2574            case '<' : 
2575              $position--;
2576              $operator = '<=';
2577              break;
2578            case '>' : 
2579              $position--;
2580              $operator = '>=';
2581              break;
2582            case '!' : 
2583              $position--;
2584              $operator = '!=';
2585              break;
2586            default:
2587              // It's a pure = operator then.
2588          }
2589          break;
2590        }
2591  
2592        if ($operator == '*') {
2593          // http://www.w3.org/TR/xpath#exprlex:
2594          // "If there is a preceding token and the preceding token is not one of @, ::, (, [, 
2595          // or an Operator, then a * must be recognized as a MultiplyOperator and an NCName must 
2596          // be recognized as an OperatorName."
2597  
2598          // Get some substrings.
2599          $character = substr($xPathQuery, $position - 1, 1);
2600        
2601          // Check whether it's a multiply operator or a name test.
2602          if (strchr('/@:([', $character) != FALSE) {
2603            // Don't use the operator.
2604              $position = -1;
2605            continue;
2606          } else {
2607            // The operator is good.  Lets use it.
2608            break;
2609          }
2610        }
2611  
2612        // Extremely annoyingly, we could have a node name like "for-each" and we should not
2613        // parse this as a "-" operator.  So if the first char of the right operator is alphabetic,
2614        // then this is NOT an interger operator.
2615        if (strchr('-+*', $operator) != FALSE) {
2616          $rightOperand = trim(substr($xPathQuery, $position + strlen($operator)));
2617          if (strlen($rightOperand) > 1) {
2618            if (preg_match(':^\D$:', $rightOperand[0])) {
2619              // Don't use the operator.
2620              $position = -1;
2621              continue;
2622            } else {
2623              // The operator is good.  Lets use it.
2624              break;
2625            }
2626          }
2627        }
2628  
2629        // The operator must be good then :o)
2630        break;
2631  
2632      } // end while each($this->operators)
2633  
2634      // Did we find an operator?
2635      if ($position == -1) {
2636        $aResultsCache[$xPathQuery] = array();
2637        return array();
2638      }
2639  
2640      /////////////////////////////////////////////
2641      // Get the operands
2642  
2643      // Get the left and the right part of the expression.
2644      $leftOperand  = trim(substr($xPathQuery, 0, $position));
2645      $rightOperand = trim(substr($xPathQuery, $position + strlen($operator)));
2646    
2647      // Remove whitespaces.
2648      $leftOperand  = trim($leftOperand);
2649      $rightOperand = trim($rightOperand);
2650  
2651      /////////////////////////////////////////////
2652      // Check the operands.
2653  
2654      if ($leftOperand == '') {
2655        $aResultsCache[$xPathQuery] = FALSE;
2656        return FALSE;
2657      }
2658  
2659      if ($rightOperand == '') {
2660        $aResultsCache[$xPathQuery] = FALSE;
2661        return FALSE;
2662      }
2663  
2664      // Package up and return what we found.
2665      $aResult = array('operator' => $operator,
2666                  'left operand' => $leftOperand,
2667                  'right operand' => $rightOperand);
2668  
2669      $aResultsCache[$xPathQuery] = $aResult;
2670  
2671      return $aResult;
2672    }
2673  
2674    /**
2675     * Evaluates an XPath PrimaryExpr
2676     *
2677     * http://www.w3.org/TR/xpath#section-Basics
2678     *
2679     *  [15]    PrimaryExpr    ::= VariableReference  
2680     *                             | '(' Expr ')'  
2681     *                             | Literal  
2682     *                             | Number  
2683     *                             | FunctionCall 
2684     *
2685     * @param  $xPathQuery  (string)   XPath query to be evaluated.
2686     * @param  $context     (array)    The context from which to evaluate
2687     * @param  $results     (mixed)    If the expression could be parsed and evaluated as one of these
2688     *                                 syntactical elements, then this will be either:
2689     *                                    - node-set (an ordered collection of nodes without duplicates) 
2690     *                                    - boolean (true or false) 
2691     *                                    - number (a floating-point number) 
2692     *                                    - string (a sequence of UCS characters) 
2693     * @return              (string)    An empty string if the query was successfully parsed and 
2694     *                                  evaluated, else a string containing the reason for failing.
2695     * @see    evaluate()
2696     */
2697    function _evaluatePrimaryExpr($xPathQuery, $context, &$result) {
2698      $ThisFunctionName = '_evaluatePrimaryExpr';
2699      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
2700      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
2701      if ($bDebugThisFunction) {
2702        echo "Path: $xPathQuery\n";
2703        echo "Context:";
2704        $this->_printContext($context);
2705        echo "\n";
2706      }
2707  
2708      // Certain expressions will never be PrimaryExpr, so to speed up processing, cache the
2709      // results we do find from this function.
2710      static $aResultsCache = array();
2711      
2712      // Do while false loop
2713      $error = "";
2714      // If the result is independant of context, then we can cache the result and speed this function
2715      // up on future calls.
2716      $bCacheableResult = FALSE;
2717      do {
2718        if (isset($aResultsCache[$xPathQuery])) {
2719          $error = $aResultsCache[$xPathQuery]['Error'];
2720          $result = $aResultsCache[$xPathQuery]['Result'];
2721          break;
2722        }
2723  
2724        // VariableReference 
2725        // ### Not supported.
2726  
2727        // Is it a number?
2728        // | Number  
2729        if (is_numeric($xPathQuery)) {
2730          $result = doubleval($xPathQuery);
2731          $bCacheableResult = TRUE;
2732          break;
2733        }
2734  
2735        // If it starts with $, and the remainder is a number, then it's a string.
2736        // | Literal  
2737        $literal = $this->_asLiteral($xPathQuery);
2738        if ($literal !== FALSE) {
2739          $result = $xPathQuery;
2740          $bCacheableResult = TRUE;
2741          break;
2742        }
2743  
2744        // Is it a function?
2745        // | FunctionCall 
2746        {
2747          // Check whether it's all wrapped in a function.  will be like count(.*) where .* is anything
2748          // text() will try to be matched here, so just explicitly ignore it
2749          $regex = ":^([^\(\)\[\]/]*)\s*\((.*)\)$:U";
2750          if (preg_match($regex, $xPathQuery, $aMatch) && $xPathQuery != "text()") {
2751            $function = $aMatch[1];
2752            $data     = $aMatch[2];
2753            // It is possible that we will get "a() or b()" which will match as function "a" with
2754            // arguments ") or b(" which is clearly wrong... _bracketsCheck() should catch this.
2755            if ($this->_bracketsCheck($data)) {
2756              if (in_array($function, $this->functions)) {
2757                if ($bDebugThisFunction) echo "XPathExpr: $xPathQuery is a $function() function call:\n";
2758                $result = $this->_evaluateFunction($function, $data, $context);
2759                break;
2760              } 
2761            }
2762          }
2763        }
2764  
2765        // Is it a bracketed expression?
2766        // | '(' Expr ')'  
2767        // If it is surrounded by () then trim the brackets
2768        $bBrackets = FALSE;
2769        if (preg_match(":^\((.*)\):", $xPathQuery, $aMatches)) {
2770          // Do not keep trimming off the () as we could have "(() and ())"
2771          $bBrackets = TRUE;
2772          $xPathQuery = $aMatches[1];
2773        }
2774  
2775        if ($bBrackets) {
2776          // Must be a Expr then.
2777          $result = $this->_evaluateExpr($xPathQuery, $context);
2778          break;
2779        }
2780  
2781        // Can't be a PrimaryExpr then.
2782        $error = "Expression is not a PrimaryExpr";
2783        $bCacheableResult = TRUE;
2784      } while (FALSE);
2785      //////////////////////////////////////////////    
2786  
2787      // If possible, cache the result.
2788      if ($bCacheableResult) {
2789          $aResultsCache[$xPathQuery]['Error'] = $error;
2790          $aResultsCache[$xPathQuery]['Result'] = $result;
2791      }
2792  
2793      $this->_closeDebugFunction($ThisFunctionName, array('result' => $result, 'error' => $error), $bDebugThisFunction);
2794  
2795      // Return the result.
2796      return $error;
2797    }
2798  
2799    /**
2800     * Evaluates an XPath Expr
2801     *
2802     * $this->evaluate() is the entry point and does some inits, while this 
2803     * function is called recursive internaly for every sub-xPath expresion we find.
2804     * It handles the following syntax, and calls evaluatePathExpr if it finds that none
2805     * of this grammer applies.
2806     *
2807     * http://www.w3.org/TR/xpath#section-Basics
2808     *
2809     * [14]    Expr               ::= OrExpr 
2810     * [21]    OrExpr             ::= AndExpr  
2811     *                                | OrExpr 'or' AndExpr  
2812     * [22]    AndExpr            ::= EqualityExpr  
2813     *                                | AndExpr 'and' EqualityExpr  
2814     * [23]    EqualityExpr       ::= RelationalExpr  
2815     *                                | EqualityExpr '=' RelationalExpr  
2816     *                                | EqualityExpr '!=' RelationalExpr  
2817     * [24]    RelationalExpr     ::= AdditiveExpr  
2818     *                                | RelationalExpr '<' AdditiveExpr  
2819     *                                | RelationalExpr '>' AdditiveExpr  
2820     *                                | RelationalExpr '<=' AdditiveExpr  
2821     *                                | RelationalExpr '>=' AdditiveExpr  
2822     * [25]    AdditiveExpr       ::= MultiplicativeExpr  
2823     *                                | AdditiveExpr '+' MultiplicativeExpr  
2824     *                                | AdditiveExpr '-' MultiplicativeExpr  
2825     * [26]    MultiplicativeExpr ::= UnaryExpr  
2826     *                                | MultiplicativeExpr MultiplyOperator UnaryExpr  
2827     *                                | MultiplicativeExpr 'div' UnaryExpr  
2828     *                                | MultiplicativeExpr 'mod' UnaryExpr  
2829     * [27]    UnaryExpr          ::= UnionExpr  
2830     *                                | '-' UnaryExpr 
2831     * [18]    UnionExpr          ::= PathExpr  
2832     *                                | UnionExpr '|' PathExpr 
2833     *
2834     * NOTE: The effect of the above grammar is that the order of precedence is 
2835     * (lowest precedence first): 
2836     * 1) or 
2837     * 2) and 
2838     * 3) =, != 
2839     * 4) <=, <, >=, > 
2840     * 5) +, -
2841     * 6) *, div, mod
2842     * 7) - (negate)
2843     * 8) |
2844     *
2845     * @param  $xPathQuery  (string)   XPath query to be evaluated.
2846     * @param  $context     (array)    An associative array the describes the context from which
2847     *                                 to evaluate the XPath Expr.  Contains three members:
2848     *                                  'nodePath' => The absolute XPath expression to the context node
2849     *                                  'size' => The context size
2850     *                                  'pos' => The context position
2851     * @return              (mixed)    The result of the XPath expression.  Either:
2852     *                                 node-set (an ordered collection of nodes without duplicates) 
2853     *                                 boolean (true or false) 
2854     *                                 number (a floating-point number) 
2855     *                                 string (a sequence of UCS characters) 
2856     * @see    evaluate()
2857     */
2858    function _evaluateExpr($xPathQuery, $context) {
2859      $ThisFunctionName = '_evaluateExpr';
2860      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
2861      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
2862      if ($bDebugThisFunction) {
2863        echo "Path: $xPathQuery\n";
2864        echo "Context:";
2865        $this->_printContext($context);
2866        echo "\n";    
2867      }
2868  
2869      // Numpty check
2870      if (!isset($xPathQuery) || ($xPathQuery == '')) {
2871        $this->_displayError("The \$xPathQuery argument must have a value.", __LINE__, __FILE__);
2872        return FALSE;
2873      }
2874  
2875      // At the top level we deal with booleans.  Only if the Expr is just an AdditiveExpr will 
2876      // the result not be a boolean.
2877      //
2878      //
2879      // Between these syntactical elements we get PathExprs.
2880  
2881      // Do while false loop
2882      do {
2883        static $aKnownPathExprCache = array();
2884  
2885        if (isset($aKnownPathExprCache[$xPathQuery])) {
2886          if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
2887          $result = $this->_evaluatePathExpr($xPathQuery, $context);
2888          break;
2889        }
2890  
2891        // Check for operators first, as we could have "() op ()" and the PrimaryExpr will try to
2892        // say that that is an Expr called ") op ("
2893        // Set the default position and the type of the operator.
2894        $aOperatorInfo = $this->_GetOperator($xPathQuery);
2895  
2896        // An expression can be one of these, and we should catch these "first" as they are most common
2897        if (empty($aOperatorInfo)) {
2898          $error = $this->_evaluatePrimaryExpr($xPathQuery, $context, $result);
2899          if (empty($error)) {
2900            // It could be parsed as a PrimaryExpr, so look no further :o)
2901            break;
2902          }
2903        }
2904  
2905        // Check whether an operator was found.
2906        if (empty($aOperatorInfo)) {
2907          if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
2908          $aKnownPathExprCache[$xPathQuery] = TRUE;
2909          // No operator.  Means we have a PathExpr then.  Go to the next level.
2910          $result = $this->_evaluatePathExpr($xPathQuery, $context);
2911          break;
2912        } 
2913  
2914        if ($bDebugThisFunction) { echo "\nFound and operator:"; print_r($aOperatorInfo); }//LEFT:[$leftOperand]  oper:[$operator]  RIGHT:[$rightOperand]";
2915  
2916        $operator = $aOperatorInfo['operator'];
2917  
2918        /////////////////////////////////////////////
2919        // Recursively process the operator
2920  
2921        // Check the kind of operator.
2922        switch ($operator) {
2923          case ' or ': 
2924          case ' and ':
2925            $operatorType = 'Boolean';
2926            break;
2927          case '+': 
2928          case '-': 
2929          case '*':
2930          case ' div ':
2931          case ' mod ':
2932            $operatorType = 'Integer';
2933            break;
2934          case ' | ':
2935            $operatorType = 'NodeSet';
2936            break;
2937          case '<=':
2938          case '<': 
2939          case '>=':
2940          case '>':
2941          case '=': 
2942          case '!=':
2943            $operatorType = 'Multi';
2944            break;
2945          default:
2946              $this->_displayError("Internal error.  Default case of switch statement reached.", __LINE__, __FILE__);
2947        }
2948  
2949        if ($bDebugThisFunction) echo "\nOperator is a [$operator]($operatorType operator)";
2950  
2951        /////////////////////////////////////////////
2952        // Evaluate the operands
2953  
2954        // Evaluate the left part.
2955        if ($bDebugThisFunction) echo "\nEvaluating LEFT:[{$aOperatorInfo['left operand']}]\n";
2956        $left = $this->_evaluateExpr($aOperatorInfo['left operand'], $context);
2957        if ($bDebugThisFunction) {echo "{$aOperatorInfo['left operand']} evals as:\n"; print_r($left); }
2958        
2959        // If it is a boolean operator, it's possible we don't need to evaluate the right part.
2960  
2961        // Only evaluate the right part if we need to.
2962        $right = '';
2963        if ($operatorType == 'Boolean') {
2964          // Is the left part false?
2965          $left = $this->_handleFunction_boolean($left, $context);
2966          if (!$left and ($operator == ' and ')) {
2967            $result = FALSE;
2968            break;
2969          } else if ($left and ($operator == ' or ')) {
2970            $result = TRUE;
2971            break;
2972          }
2973        } 
2974  
2975        // Evaluate the right part
2976        if ($bDebugThisFunction) echo "\nEvaluating RIGHT:[{$aOperatorInfo['right operand']}]\n";
2977        $right = $this->_evaluateExpr($aOperatorInfo['right operand'], $context);
2978        if ($bDebugThisFunction) {echo "{$aOperatorInfo['right operand']} evals as:\n"; print_r($right); echo "\n";}
2979  
2980        /////////////////////////////////////////////
2981        // Combine the operands
2982  
2983        // If necessary, work out how to treat the multi operators
2984        if ($operatorType != 'Multi') {
2985          $result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context);
2986        } else {
2987          // http://www.w3.org/TR/xpath#booleans
2988          // If both objects to be compared are node-sets, then the comparison will be true if and 
2989          // only if there is a node in the first node-set and a node in the second node-set such 
2990          // that the result of performing the comparison on the string-values of the two nodes is 
2991          // true. 
2992          // 
2993          // If one object to be compared is a node-set and the other is a number, then the 
2994          // comparison will be true if and only if there is a node in the node-set such that the 
2995          // result of performing the comparison on the number to be compared and on the result of 
2996          // converting the string-value of that node to a number using the number function is true. 
2997          //
2998          // If one object to be compared is a node-set and the other is a string, then the comparison 
2999          // will be true if and only if there is a node in the node-set such that the result of performing 
3000          // the comparison on the string-value of the node and the other string is true. 
3001          // 
3002          // If one object to be compared is a node-set and the other is a boolean, then the comparison 
3003          // will be true if and only if the result of performing the comparison on the boolean and on 
3004          // the result of converting the node-set to a boolean using the boolean function is true.
3005          if (is_array($left) || is_array($right)) {
3006            if ($bDebugThisFunction) echo "As one of the operands is an array, we will need to loop\n";
3007            if (is_array($left) && is_array($right)) {
3008              $operatorType = 'String';
3009            } elseif (is_numeric($left) || is_numeric($right)) {
3010              $operatorType = 'Integer';
3011            } elseif (is_bool($left)) {
3012              $operatorType = 'Boolean';
3013              $right = $this->_handleFunction_boolean($right, $context);
3014            } elseif (is_bool($right)) {
3015              $operatorType = 'Boolean';
3016              $left = $this->_handleFunction_boolean($left, $context);
3017            } else {
3018              $operatorType = 'String';
3019            }
3020            if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n";
3021            // Turn both operands into arrays to simplify logic
3022            $aLeft = $left;
3023            $aRight = $right;
3024            if (!is_array($aLeft)) $aLeft = array($aLeft);
3025            if (!is_array($aRight)) $aRight = array($aRight);
3026            $result = FALSE;
3027            if (!empty($aLeft)) {
3028              foreach ($aLeft as $leftItem) {
3029                if (empty($aRight)) break;
3030                // If the item is from a node set, we should evaluate it's string-value
3031                if (is_array($left)) {
3032                  if ($bDebugThisFunction) echo "\tObtaining string-value of LHS:$leftItem as it's from a nodeset\n";
3033                  $leftItem = $this->_stringValue($leftItem);
3034                }
3035                foreach ($aRight as $rightItem) {
3036                  // If the item is from a node set, we should evaluate it's string-value
3037                  if (is_array($right)) {
3038                    if ($bDebugThisFunction) echo "\tObtaining string-value of RHS:$rightItem as it's from a nodeset\n";
3039                    $rightItem = $this->_stringValue($rightItem);
3040                  }
3041  
3042                  if ($bDebugThisFunction) echo "\tEvaluating $leftItem $operator $rightItem\n";
3043                  $result = $this->_evaluateOperator($leftItem, $operator, $rightItem, $operatorType, $context);
3044                  if ($result === TRUE) break;
3045                }
3046                if ($result === TRUE) break;
3047              }
3048            }
3049          } 
3050          // When neither object to be compared is a node-set and the operator is = or !=, then the 
3051          // objects are compared by converting them to a common type as follows and then comparing 
3052          // them. 
3053          //
3054          // If at least one object to be compared is a boolean, then each object to be compared 
3055          // is converted to a boolean as if by applying the boolean function. 
3056          //
3057          // Otherwise, if at least one object to be compared is a number, then each object to be 
3058          // compared is converted to a number as if by applying the number function. 
3059          //
3060          // Otherwise, both objects to be compared are converted to strings as if by applying 
3061          // the string function. 
3062          //  
3063          // The = comparison will be true if and only if the objects are equal; the != comparison 
3064          // will be true if and only if the objects are not equal. Numbers are compared for equality 
3065          // according to IEEE 754 [IEEE 754]. Two booleans are equal if either both are true or 
3066          // both are false. Two strings are equal if and only if they consist of the same sequence 
3067          // of UCS characters.
3068          else {
3069            if (is_bool($left) || is_bool($right)) {
3070              $operatorType = 'Boolean';
3071            } elseif (is_numeric($left) || is_numeric($right)) {
3072              $operatorType = 'Integer';
3073            } else {
3074              $operatorType = 'String';
3075            }
3076            if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n";
3077            $result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context);
3078          }
3079        }
3080  
3081      } while (FALSE);
3082      //////////////////////////////////////////////
3083  
3084      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
3085  
3086      // Return the result.
3087      return $result;
3088    }
3089  
3090    /**
3091     * Evaluate the result of an operator whose operands have been evaluated
3092     *
3093     * If the operator type is not "NodeSet", then neither the left or right operators 
3094     * will be node sets, as the processing when one or other is an array is complex,
3095     * and should be handled by the caller.
3096     *
3097     * @param  $left          (mixed)   The left operand
3098     * @param  $right         (mixed)   The right operand
3099     * @param  $operator      (string)  The operator to use to combine the operands
3100     * @param  $operatorType  (string)  The type of the operator.  Either 'Boolean', 
3101     *                                  'Integer', 'String', or 'NodeSet'
3102     * @param  $context     (array)    The context from which to evaluate
3103     * @return              (mixed)    The result of the XPath expression.  Either:
3104     *                                 node-set (an ordered collection of nodes without duplicates) 
3105     *                                 boolean (true or false) 
3106     *                                 number (a floating-point number) 
3107     *                                 string (a sequence of UCS characters) 
3108     */
3109    function _evaluateOperator($left, $operator, $right, $operatorType, $context) {
3110      $ThisFunctionName = '_evaluateOperator';
3111      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
3112      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
3113      if ($bDebugThisFunction) {
3114        echo "left: $left\n";
3115        echo "right: $right\n";
3116        echo "operator: $operator\n";
3117        echo "operator type: $operatorType\n";
3118      }
3119  
3120      // Do while false loop
3121      do {
3122        // Handle the operator depending on the operator type.
3123        switch ($operatorType) {
3124          case 'Boolean':
3125            {
3126              // Boolify the arguments.  (The left arg is already a bool)
3127              $right = $this->_handleFunction_boolean($right, $context);
3128              switch ($operator) {
3129                case '=': // Compare the two results.
3130                  $result = (bool)($left == $right); 
3131                  break;
3132                case ' or ': // Return the two results connected by an 'or'.
3133                  $result = (bool)( $left or $right );
3134                  break;
3135                case ' and ': // Return the two results connected by an 'and'.
3136                  $result = (bool)( $left and $right );
3137                  break;
3138                case '!=': // Check whether the two results are not equal.
3139                  $result = (bool)( $left != $right );
3140                  break;
3141                default:
3142                  $this->_displayError("Internal error.  Default case of switch statement reached.", __LINE__, __FILE__);
3143              }
3144            }
3145            break;
3146          case 'Integer':
3147            {
3148              // Convert both left and right operands into numbers.
3149              if (empty($left) && ($operator == '-')) {
3150                // There may be no left operator if the op is '-'
3151                $left = 0;
3152              } else {
3153                $left = $this->_handleFunction_number($left, $context);
3154              }
3155              $right = $this->_handleFunction_number($right, $context);
3156              if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
3157              switch ($operator) {
3158                case '=': // Compare the two results.
3159                  $result = (bool)($left == $right); 
3160                  break;
3161                case '!=': // Compare the two results.
3162                  $result = (bool)($left != $right); 
3163                  break;
3164                case '+': // Return the result by adding one result to the other.
3165                  $result = $left + $right;
3166                  break;
3167                case '-': // Return the result by decrease one result by the other.
3168                  $result = $left - $right;
3169                  break;
3170                case '*': // Return a multiplication of the two results.
3171                  $result =  $left * $right;
3172                  break;
3173                case ' div ': // Return a division of the two results.
3174                  $result = $left / $right;
3175                  break;
3176                case ' mod ': // Return a modulo division of the two results.
3177                  $result = $left % $right;
3178                  break;
3179                case '<=': // Compare the two results.
3180                  $result = (bool)( $left <= $right );
3181                  break;
3182                case '<': // Compare the two results.
3183                  $result = (bool)( $left < $right );
3184                  break;
3185                case '>=': // Compare the two results.
3186                  $result = (bool)( $left >= $right );
3187                  break;
3188                case '>': // Compare the two results.
3189                  $result = (bool)( $left > $right );
3190                  break;
3191                default:
3192                  $this->_displayError("Internal error.  Default case of switch statement reached.", __LINE__, __FILE__);
3193              }
3194            }
3195            break;
3196          case 'NodeSet':
3197            // Add the nodes to the result set
3198            $result = array_merge($left, $right);
3199            // Remove duplicated nodes.
3200            $result = array_unique($result);
3201  
3202            // Preserve doc order if there was more than one query.
3203            if (count($result) > 1) {
3204              $result = $this->_sortByDocOrder($result);
3205            }
3206            break;
3207          case 'String':
3208              $left = $this->_handleFunction_string($left, $context);
3209              $right = $this->_handleFunction_string($right, $context);
3210              if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
3211              switch ($operator) {
3212                case '=': // Compare the two results.
3213                  $result = (bool)($left == $right); 
3214                  break;
3215                case '!=': // Compare the two results.
3216                  $result = (bool)($left != $right); 
3217                  break;
3218                default:
3219                  $this->_displayError("Internal error.  Default case of switch statement reached.", __LINE__, __FILE__);
3220              }
3221            break;
3222          default:
3223            $this->_displayError("Internal error.  Default case of switch statement reached.", __LINE__, __FILE__);
3224        }
3225      } while (FALSE);
3226  
3227      //////////////////////////////////////////////
3228  
3229      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
3230  
3231      // Return the result.
3232      return $result;
3233    }
3234    
3235    /**
3236     * Evaluates an XPath PathExpr
3237     *
3238     * It handles the following syntax:
3239     *
3240     * http://www.w3.org/TR/xpath#node-sets
3241     * http://www.w3.org/TR/xpath#NT-LocationPath
3242     * http://www.w3.org/TR/xpath#path-abbrev
3243     * http://www.w3.org/TR/xpath#NT-Step
3244     *
3245     * [19]   PathExpr              ::= LocationPath  
3246     *                                  | FilterExpr  
3247     *                                  | FilterExpr '/' RelativeLocationPath  
3248     *                                  | FilterExpr '//' RelativeLocationPath
3249     * [20]   FilterExpr            ::= PrimaryExpr  
3250     *                                  | FilterExpr Predicate 
3251     * [1]    LocationPath          ::= RelativeLocationPath  
3252     *                                  | AbsoluteLocationPath  
3253     * [2]    AbsoluteLocationPath  ::= '/' RelativeLocationPath?  
3254     *                                  | AbbreviatedAbsoluteLocationPath
3255     * [3]    RelativeLocationPath  ::= Step  
3256     *                                  | RelativeLocationPath '/' Step  
3257     *                                  | AbbreviatedRelativeLocationPath
3258     * [4]    Step                  ::= AxisSpecifier NodeTest Predicate*  
3259     *                                  | AbbreviatedStep  
3260     * [5]    AxisSpecifier         ::= AxisName '::'  
3261     *                                  | AbbreviatedAxisSpecifier  
3262     * [10]   AbbreviatedAbsoluteLocationPath
3263     *                              ::= '//' RelativeLocationPath
3264     * [11]   AbbreviatedRelativeLocationPath
3265     *                              ::= RelativeLocationPath '//' Step
3266     * [12]   AbbreviatedStep       ::= '.'  
3267     *                                  | '..'  
3268     * [13]   AbbreviatedAxisSpecifier    
3269     *                              ::= '@'? 
3270     *
3271     * If you expand all the abbreviated versions, then the grammer simplifies to:
3272     *
3273     * [19]   PathExpr              ::= RelativeLocationPath  
3274     *                                  | '/' RelativeLocationPath?  
3275     *                                  | FilterExpr  
3276     *                                  | FilterExpr '/' RelativeLocationPath  
3277     * [20]   FilterExpr            ::= PrimaryExpr  
3278     *                                  | FilterExpr Predicate 
3279     * [3]    RelativeLocationPath  ::= Step  
3280     *                                  | RelativeLocationPath '/' Step  
3281     * [4]    Step                  ::= AxisName '::' NodeTest Predicate*  
3282     *
3283     * Conceptually you can say that we should split by '/' and try to treat the parts
3284     * as steps, and if that fails then try to treat it as a PrimaryExpr.  
3285     * 
3286     * @param  $PathExpr   (string) PathExpr syntactical element
3287     * @param  $context    (array)  The context from which to evaluate
3288     * @return             (mixed)  The result of the XPath expression.  Either:
3289     *                               node-set (an ordered collection of nodes without duplicates) 
3290     *                               boolean (true or false) 
3291     *                               number (a floating-point number) 
3292     *                               string (a sequence of UCS characters) 
3293     * @see    evaluate()
3294     */
3295    function _evaluatePathExpr($PathExpr, $context) {
3296      $ThisFunctionName = '_evaluatePathExpr';
3297      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
3298      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
3299      if ($bDebugThisFunction) {
3300        echo "PathExpr: $PathExpr\n";
3301        echo "Context:";
3302        $this->_printContext($context);
3303        echo "\n";
3304      }
3305      
3306      // Numpty check
3307      if (empty($PathExpr)) {
3308        $this->_displayError("The \$PathExpr argument must have a value.", __LINE__, __FILE__);
3309        return FALSE;
3310      }
3311      //////////////////////////////////////////////
3312  
3313      // Parsing the expression into steps is a cachable operation as it doesn't depend on the context
3314      static $aResultsCache = array();
3315  
3316      if (isset($aResultsCache[$PathExpr])) {
3317        $steps = $aResultsCache[$PathExpr];
3318      } else {
3319        // Note that we have used $this->slashes2descendant to simplify this logic, so the 
3320        // "Abbreviated" paths basically never exist as '//' never exists.
3321  
3322        // mini syntax check
3323        if (!$this->_bracketsCheck($PathExpr)) {
3324          $this->_displayError('While parsing an XPath query, in the PathExpr "' .
3325          $PathExpr.
3326          '", there was an invalid number of brackets or a bracket mismatch.', __LINE__, __FILE__);
3327        }
3328        // Save the current path.
3329        $this->currentXpathQuery = $PathExpr;
3330        // Split the path at every slash *outside* a bracket.
3331        $steps = $this->_bracketExplode('/', $PathExpr);
3332        if ($bDebugThisFunction) { echo "<hr>Split the path '$PathExpr' at every slash *outside* a bracket.\n "; print_r($steps); }
3333        // Check whether the first element is empty.
3334        if (empty($steps[0])) {
3335          // Remove the first and empty element. It's a starting  '//'.
3336          array_shift($steps);
3337        }
3338        $aResultsCache[$PathExpr] = $steps;
3339      }
3340  
3341      // Start to evaluate the steps.
3342      // ### Consider implementing an evaluateSteps() function that removes recursion from
3343      // evaluateStep()
3344      $result = $this->_evaluateStep($steps, $context);
3345  
3346      // Preserve doc order if there was more than one result
3347      if (count($result) > 1) {
3348        $result = $this->_sortByDocOrder($result);
3349      }
3350      //////////////////////////////////////////////
3351  
3352      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
3353  
3354      // Return the result.
3355      return $result;
3356    }
3357  
3358    /**
3359     * Sort an xPathSet by doc order.
3360     *
3361     * @param  $xPathSet (array) Array of full paths to nodes that need to be sorted
3362     * @return           (array) Array containing the same contents as $xPathSet, but
3363     *                           with the contents in doc order
3364     */
3365    function _sortByDocOrder($xPathSet) {
3366      $ThisFunctionName = '_sortByDocOrder';
3367      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
3368      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
3369      if ($bDebugThisFunction) {
3370        echo "_sortByDocOrder(xPathSet:[".count($xPathSet)."])";
3371        echo "xPathSet:\n";
3372        print_r($xPathSet);
3373        echo "<hr>\n";
3374      }
3375      //////////////////////////////////////////////
3376  
3377      $aResult = array();
3378  
3379      // Spot some common shortcuts.
3380      if (count($xPathSet) < 1) {
3381        $aResult = $xPathSet;
3382      } else {
3383        // Build an array of doc-pos indexes.
3384        $aDocPos = array();
3385        $nodeCount = count($this->nodeIndex);
3386        $aPaths = array_keys($this->nodeIndex);
3387        if ($bDebugThisFunction) {
3388          echo "searching for path indices in array_keys(this->nodeIndex)...\n";
3389          //print_r($aPaths);
3390        }
3391  
3392        // The last index we found.  In general the elements will be in groups
3393        // that are themselves in order.
3394        $iLastIndex = 0;
3395        foreach ($xPathSet as $path) {
3396          // Cycle round the nodes, starting at the last index, looking for the path.
3397          $foundNode = FALSE;
3398          for ($iIndex = $iLastIndex; $iIndex < $nodeCount + $iLastIndex; $iIndex++) {
3399            $iThisIndex = $iIndex % $nodeCount;
3400            if (!strcmp($aPaths[$iThisIndex],$path)) {
3401              // we have found the doc-position index of the path 
3402              $aDocPos[] = $iThisIndex;
3403              $iLastIndex = $iThisIndex;
3404              $foundNode = TRUE;
3405              break;
3406            }
3407          }
3408          if ($bDebugThisFunction) {
3409            if (!$foundNode)
3410              echo "Error: $path not found in \$this->nodeIndex\n";
3411            else 
3412              echo "Found node after ".($iIndex - $iLastIndex)." iterations\n";
3413          }
3414        }
3415        // Now count the number of doc pos we have and the number of results and
3416        // confirm that we have the same number of each.
3417        $iDocPosCount = count($aDocPos);
3418        $iResultCount = count($xPathSet);
3419        if ($iDocPosCount != $iResultCount) {
3420          if ($bDebugThisFunction) {
3421            echo "count(\$aDocPos)=$iDocPosCount; count(\$result)=$iResultCount\n";
3422            print_r(array_keys($this->nodeIndex));
3423          }
3424          $this->_displayError('Results from _InternalEvaluate() are corrupt.  '.
3425                                        'Do you need to call reindexNodeTree()?', __LINE__, __FILE__);
3426        }
3427  
3428        // Now sort the indexes.
3429        sort($aDocPos);
3430  
3431        // And now convert back to paths.
3432        $iPathCount = count($aDocPos);
3433        for ($iIndex = 0; $iIndex < $iPathCount; $iIndex++) {
3434          $aResult[] = $aPaths[$aDocPos[$iIndex]];
3435        }
3436      }
3437  
3438      // Our result from the function is this array.
3439      $result = $aResult;
3440  
3441      //////////////////////////////////////////////
3442  
3443      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
3444  
3445      // Return the result.
3446      return $result;
3447    }
3448  
3449    /**
3450     * Evaluate a step from a XPathQuery expression at a specific contextPath.
3451     *
3452     * Steps are the arguments of a XPathQuery when divided by a '/'. A contextPath is a 
3453     * absolute XPath (or vector of XPaths) to a starting node(s) from which the step should 
3454     * be evaluated.
3455     *
3456     * @param  $steps        (array) Vector containing the remaining steps of the current 
3457     *                               XPathQuery expression.
3458     * @param  $context      (array) The context from which to evaluate
3459     * @return               (array) Vector of absolute XPath's as a result of the step 
3460     *                               evaluation.  The results will not necessarily be in doc order
3461     * @see    _evaluatePathExpr()
3462     */
3463    function _evaluateStep($steps, $context) {
3464      $ThisFunctionName = '_evaluateStep';
3465      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
3466      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
3467      if ($bDebugThisFunction) {
3468        echo "Context:";
3469        $this->_printContext($context);
3470        echo "\n";
3471        echo "Steps: ";
3472        print_r($steps);
3473        echo "<hr>\n";
3474      }
3475      //////////////////////////////////////////////
3476  
3477      $result = array(); // Create an empty array for saving the abs. XPath's found.
3478  
3479      $contextPaths = array();   // Create an array to save the new contexts.
3480      $step = trim(array_shift($steps)); // Get this step.
3481      if ($bDebugThisFunction) echo __LINE__.":Evaluating step $step\n";
3482      
3483      $axis = $this->_getAxis($step); // Get the axis of the current step.
3484  
3485      // If there was no axis, then it must be a PrimaryExpr
3486      if ($axis == FALSE) {
3487        if ($bDebugThisFunction) echo __LINE__.":Step is not an axis but a PrimaryExpr\n";
3488        // ### This isn't correct, as the result of this function might not be a node set.
3489        $error = $this->_evaluatePrimaryExpr($step, $context, $contextPaths);
3490        if (!empty($error)) {
3491          $this->_displayError("Expression failed to parse as PrimaryExpr because: $error"
3492                  , __LINE__, __FILE__, FALSE);
3493        }
3494      } else {
3495        if ($bDebugThisFunction) { echo __LINE__.":Axis of step is:\n"; print_r($axis); echo "\n";}
3496        $method = '_handleAxis_' . $axis['axis']; // Create the name of the method.
3497      
3498        // Check whether the axis handler is defined. If not display an error message.
3499        if (!method_exists($this, $method)) {
3500          $this->_displayError('While parsing an XPath query, the axis ' .
3501          $axis['axis'] . ' could not be handled, because this version does not support this axis.', __LINE__, __FILE__);
3502        }
3503        if ($bDebugThisFunction) echo __LINE__.":Calling user method $method\n";        
3504        
3505        // Perform an axis action.
3506        $contextPaths = $this->$method($axis, $context['nodePath']);
3507        if ($bDebugThisFunction) { echo __LINE__.":We found these contexts from this step:\n"; print_r( $contextPaths ); echo "\n";}
3508      }
3509  
3510      // Check whether there are predicates.
3511      if (count($contextPaths) > 0 && count($axis['predicate']) > 0) {
3512        if ($bDebugThisFunction) echo __LINE__.":Filtering contexts by predicate...\n";
3513        
3514        // Check whether each node fits the predicates.
3515        $contextPaths = $this->_checkPredicates($contextPaths, $axis['predicate']);
3516      }
3517  
3518      // Check whether there are more steps left.
3519      if (count($steps) > 0) {
3520        if ($bDebugThisFunction) echo __LINE__.":Evaluating next step given the context of the first step...\n";        
3521        
3522        // Continue the evaluation of the next steps.
3523  
3524        // Run through the array.
3525        $size = sizeOf($contextPaths);
3526        for ($pos=0; $pos<$size; $pos++) {
3527          // Build new context
3528          $newContext = array('nodePath' => $contextPaths[$pos], 'size' => $size, 'pos' => $pos + 1);
3529          if ($bDebugThisFunction) echo __LINE__.":Evaluating step for the {$contextPaths[$pos]} context...\n";
3530          // Call this method for this single path.
3531          $xPathSetNew = $this->_evaluateStep($steps, $newContext);
3532          if ($bDebugThisFunction) {echo "New results for this context:\n"; print_r($xPathSetNew);}
3533          $result = array_merge($result, $xPathSetNew);
3534        }
3535  
3536        // Remove duplicated nodes.
3537        $result = array_unique($result);
3538      } else {
3539        $result = $contextPaths; // Save the found contexts.
3540      }
3541      
3542      //////////////////////////////////////////////
3543  
3544      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
3545  
3546      // Return the result.
3547      return $result;
3548    }
3549    
3550    /**
3551     * Checks whether a node matches predicates.
3552     *
3553     * This method checks whether a list of nodes passed to this method match
3554     * a given list of predicates. 
3555     *
3556     * @param  $xPathSet   (array)  Array of full paths of all nodes to be tested.
3557     * @param  $predicates (array)  Array of predicates to use.
3558     * @return             (array)  Vector of absolute XPath's that match the given predicates.
3559     * @see    _evaluateStep()
3560     */
3561    function _checkPredicates($xPathSet, $predicates) {
3562      $ThisFunctionName = '_checkPredicates';
3563      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
3564      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
3565      if ($bDebugThisFunction) {
3566        echo "XPathSet:";
3567        print_r($xPathSet);
3568        echo "Predicates:";
3569        print_r($predicates);
3570        echo "<hr>";
3571      }
3572      //////////////////////////////////////////////
3573      // Create an empty set of nodes.
3574      $result = array();
3575  
3576      // Run through all predicates.
3577      $pSize = sizeOf($predicates);
3578      for ($j=0; $j<$pSize; $j++) {
3579        $predicate = $predicates[$j]; 
3580        if ($bDebugThisFunction) echo "Evaluating predicate \"$predicate\"\n";
3581  
3582        // This will contain all the nodes that match this predicate
3583        $aNewSet = array();
3584        
3585        // Run through all nodes.
3586        $contextSize = count($xPathSet);
3587        for ($contextPos=0; $contextPos<$contextSize; $contextPos++) {
3588          $xPath = $xPathSet[$contextPos];
3589  
3590          // Build the context for this predicate
3591          $context = array('nodePath' => $xPath, 'size' => $contextSize, 'pos' => $contextPos + 1);
3592        
3593          // Check whether the predicate is just an number.
3594          if (preg_match('/^\d+$/', $predicate)) {
3595            if ($bDebugThisFunction) echo "Taking short cut and calling _handleFunction_position() directly.\n";
3596            // Take a short cut.  If it is just a position, then call 
3597            // _handleFunction_position() directly.  70% of the
3598            // time this will be the case. ## N.S
3599  //          $check = (bool) ($predicate == $context['pos']);
3600            $check = (bool) ($predicate == $this->_handleFunction_position('', $context));
3601          } else {                
3602            // Else do the predicate check the long and through way.
3603            $check = $this->_evaluateExpr($predicate, $context);
3604          }
3605          if ($bDebugThisFunction) {
3606            echo "Evaluating the predicate returned "; 
3607            var_dump($check); 
3608            echo "\n";
3609          }
3610  
3611          if (is_int($check)) { // Check whether it's an integer.
3612            // Check whether it's the current position.
3613            $check = (bool) ($check == $this->_handleFunction_position('', $context));
3614          } else {
3615            $check = (bool) ($this->_handleFunction_boolean($check, $context));
3616  //          if ($bDebugThisFunction) {echo $this->_handleFunction_string($check, $context);}
3617          }
3618  
3619          if ($bDebugThisFunction) echo "Node $xPath matches predicate $predicate: " . (($check) ? "TRUE" : "FALSE") ."\n";
3620  
3621          // Do we add it?
3622          if ($check) $aNewSet[] = $xPath;
3623        }
3624         
3625        // Use the newly filtered list.
3626        $xPathSet = $aNewSet;
3627  
3628        if ($bDebugThisFunction) {echo "Node set now contains : "; print_r($xPathSet); }
3629      }
3630  
3631      $result = $xPathSet;
3632  
3633      //////////////////////////////////////////////
3634  
3635      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
3636  
3637      // Return the array of nodes.
3638      return $result;
3639    }
3640    
3641    /**
3642     * Evaluates an XPath function
3643     *
3644     * This method evaluates a given XPath function with its arguments on a
3645     * specific node of the document.
3646     *
3647     * @param  $function      (string) Name of the function to be evaluated.
3648     * @param  $arguments     (string) String containing the arguments being
3649     *                                 passed to the function.
3650     * @param  $context       (array)  The context from which to evaluate
3651     * @return                (mixed)  This method returns the result of the evaluation of
3652     *                                 the function. Depending on the function the type of the 
3653     *                                 return value can be different.
3654     * @see    evaluate()
3655     */
3656    function _evaluateFunction($function, $arguments, $context) {
3657      $ThisFunctionName = '_evaluateFunction';
3658      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
3659      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
3660      if ($bDebugThisFunction) {
3661        if (is_array($arguments)) {
3662          echo "Arguments:\n";
3663          print_r($arguments);
3664        } else {
3665          echo "Arguments: $arguments\n";
3666        }
3667        echo "Context:";
3668        $this->_printContext($context);
3669        echo "\n";
3670        echo "<hr>\n";
3671      }
3672      /////////////////////////////////////
3673      // Remove whitespaces.
3674      $function  = trim($function);
3675      $arguments = trim($arguments);
3676      // Create the name of the function handling function.
3677      $method = '_handleFunction_'. $function;
3678      
3679      // Check whether the function handling function is available.
3680      if (!method_exists($this, $method)) {
3681        // Display an error message.
3682        $this->_displayError("While parsing an XPath query, ".
3683          "the function \"$function\" could not be handled, because this ".
3684          "version does not support this function.", __LINE__, __FILE__);
3685      }
3686      if ($bDebugThisFunction) echo "Calling function $method($arguments)\n"; 
3687      
3688      // Return the result of the function.
3689      $result = $this->$method($arguments, $context);
3690      
3691      //////////////////////////////////////////////
3692      // Return the nodes found.
3693  
3694      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
3695  
3696      // Return the result.
3697      return $result;
3698    }
3699      
3700    /**
3701     * Checks whether a node matches a node-test.
3702     *
3703     * This method checks whether a node in the document matches a given node-test.
3704     * A node test is something like text(), node(), or an element name.
3705     *
3706     * @param  $contextPath (string)  Full xpath of the node, which should be tested for 
3707     *                                matching the node-test.
3708     * @param  $nodeTest    (string)  String containing the node-test for the node.
3709     * @return              (boolean) This method returns TRUE if the node matches the 
3710     *                                node-test, otherwise FALSE.
3711     * @see    evaluate()
3712     */
3713    function _checkNodeTest($contextPath, $nodeTest) {
3714      // Empty node test means that it must match
3715      if (empty($nodeTest)) return TRUE;
3716  
3717      if ($nodeTest == '*') {
3718        // * matches all element nodes.
3719        return (!preg_match(':/[^/]+\(\)\[\d+\]$:U', $contextPath));
3720      }
3721      elseif (preg_match('/^[\w-:\.]+$/', $nodeTest)) {
3722         // http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
3723         // The real spec for what constitutes whitespace is quite elaborate, and 
3724         // we currently just hope that "\w" catches them all.  In reality it should
3725         // start with a letter too, not a number, but we've just left it simple.
3726         // It's just a node name test.  It should end with "/$nodeTest[x]"
3727         return (preg_match('"/'.$nodeTest.'\[\d+\]$"', $contextPath));
3728      }
3729      elseif (preg_match('/\(/U', $nodeTest)) { // Check whether it's a function.
3730        // Get the type of function to use.
3731        $function = $this->_prestr($nodeTest, '(');
3732        // Check whether the node fits the method.
3733        switch ($function) {
3734          case 'node':   // Add this node to the list of nodes.
3735            return TRUE;
3736          case 'text':   // Check whether the node has some text.
3737            $tmp = implode('', $this->nodeIndex[$contextPath]['textParts']);
3738            if (!empty($tmp)) {
3739              return TRUE; // Add this node to the list of nodes.
3740            }
3741            break;
3742  /******** NOT supported (yet?)          
3743          case 'comment':  // Check whether the node has some comment.
3744            if (!empty($this->nodeIndex[$contextPath]['comment'])) {
3745              return TRUE; // Add this node to the list of nodes.
3746            }
3747            break;
3748          case 'processing-instruction':
3749            $literal = $this->_afterstr($axis['node-test'], '('); // Get the literal argument.
3750            $literal = substr($literal, 0, strlen($literal) - 1); // Cut the literal.
3751            
3752            // Check whether a literal was given.
3753            if (!empty($literal)) {
3754              // Check whether the node's processing instructions are matching the literals given.
3755              if ($this->nodeIndex[$context]['processing-instructions'] == $literal) {
3756                return TRUE; // Add this node to the node-set.
3757              }
3758            } else {
3759              // Check whether the node has processing instructions.
3760              if (!empty($this->nodeIndex[$contextPath]['processing-instructions'])) {
3761                return TRUE; // Add this node to the node-set.
3762              }
3763            }
3764            break;
3765  ***********/            
3766          default:  // Display an error message.
3767            $this->_displayError('While parsing an XPath query there was an undefined function called "' .
3768               str_replace($function, '<b>'.$function.'</b>', $this->currentXpathQuery) .'"', __LINE__, __FILE__);
3769        }
3770      }
3771      else { // Display an error message.
3772        $this->_displayError("While parsing the XPath query \"{$this->currentXpathQuery}\" ".
3773          "an empty and therefore invalid node-test has been found.", __LINE__, __FILE__, FALSE);
3774      }
3775      
3776      return FALSE; // Don't add this context.
3777    }
3778    
3779    //-----------------------------------------------------------------------------------------
3780    // XPath                    ------  XPath AXIS Handlers  ------                            
3781    //-----------------------------------------------------------------------------------------
3782    
3783    /**
3784     * Retrieves axis information from an XPath query step.
3785     *
3786     * This method tries to extract the name of the axis and its node-test
3787     * from a given step of an XPath query at a given node.  If it can't parse
3788     * the step, then we treat it as a PrimaryExpr.
3789     *
3790     * [4]    Step            ::= AxisSpecifier NodeTest Predicate*  
3791     *                            | AbbreviatedStep  
3792     * [5]    AxisSpecifier   ::= AxisName '::'  
3793     *                            | AbbreviatedAxisSpecifier 
3794     * [12]   AbbreviatedStep ::= '.'  
3795     *                            | '..'  
3796     * [13]   AbbreviatedAxisSpecifier    
3797     *                        ::=    '@'? 
3798     * 
3799     * [7]    NodeTest        ::= NameTest  
3800     *                            | NodeType '(' ')'  
3801     *                            | 'processing-instruction' '(' Literal ')'  
3802     * [37]   NameTest        ::= '*'  
3803     *                            | NCName ':' '*'  
3804     *                            | QName  
3805     * [38]   NodeType        ::= 'comment'  
3806     *                            | 'text'  
3807     *                            | 'processing-instruction'  
3808     *                            | 'node' 
3809     *
3810     * @param  $step     (string) String containing a step of an XPath query.
3811     * @return           (array)  Contains information about the axis found in the step, or FALSE
3812     *                            if the string isn't a valid step.
3813     * @see    _evaluateStep()
3814     */
3815    function _getAxis($step) {
3816      // The results of this function are very cachable, as it is completely independant of context.
3817      static $aResultsCache = array();
3818  
3819      // Create an array to save the axis information.
3820      $axis = array(
3821        'axis'      => '',
3822        'node-test' => '',
3823        'predicate' => array()
3824      );
3825  
3826      $cacheKey = $step;
3827      do { // parse block
3828        $parseBlock = 1;
3829  
3830        if (isset($aResultsCache[$cacheKey])) {
3831          return $aResultsCache[$cacheKey];
3832        } else {
3833          // We have some danger of causing recursion here if we refuse to parse a step as having an
3834          // axis, and demand it be treated as a PrimaryExpr.  So if we are going to fail, make sure
3835          // we record what we tried, so that we can catch to see if it comes straight back.
3836          $guess = array(
3837            'axis' => 'child',
3838            'node-test' => $step,
3839            'predicate' => array());
3840          $aResultsCache[$cacheKey] = $guess;
3841        }
3842  
3843        ///////////////////////////////////////////////////
3844        // Spot the steps that won't come with an axis
3845  
3846        // Check whether the step is empty or only self. 
3847        if (empty($step) OR ($step == '.') OR ($step == 'current()')) {
3848          // Set it to the default value.
3849          $step = '.';
3850          $axis['axis']      = 'self';
3851          $axis['node-test'] = '*';
3852          break $parseBlock;
3853        }
3854  
3855        if ($step == '..') {
3856          // Select the parent axis.
3857          $axis['axis']      = 'parent';
3858          $axis['node-test'] = '*';
3859          break $parseBlock;
3860        }
3861  
3862        ///////////////////////////////////////////////////
3863        // Pull off the predicates
3864  
3865        // Check whether there are predicates and add the predicate to the list 
3866        // of predicates without []. Get contents of every [] found.
3867        $groups = $this->_getEndGroups($step);
3868  //print_r($groups);
3869        $groupCount = count($groups);
3870        while (($groupCount > 0) && ($groups[$groupCount - 1][0] == '[')) {
3871          // Remove the [] and add the predicate to the top of the list
3872          $predicate = substr($groups[$groupCount - 1], 1, -1);
3873          array_unshift($axis['predicate'], $predicate);
3874          // Pop a group off the end of the list
3875          array_pop($groups);
3876          $groupCount--;
3877        }
3878  
3879        // Finally stick the rest back together and this is the rest of our step
3880        if ($groupCount > 0) {
3881          $step = implode('', $groups);
3882        }
3883  
3884        ///////////////////////////////////////////////////
3885        // Pull off the axis
3886  
3887        // Check for abbreviated syntax
3888        if ($step[0] == '@') {
3889          // Use the attribute axis and select the attribute.
3890          $axis['axis']      = 'attribute';
3891          $step = substr($step, 1);
3892        } else {
3893          // Check whether the axis is given in plain text.
3894          if (preg_match("/^([^:]*)::(.*)$/", $step, $match)) {
3895            // Split the step to extract axis and node-test.
3896            $axis['axis'] = $match[1];
3897            $step         = $match[2];
3898          } else {
3899            // The default axis is child
3900            $axis['axis'] = 'child';
3901          }
3902        }
3903  
3904        ///////////////////////////////////////////////////
3905        // Process the rest which will either a node test, or else this isn't a step.
3906  
3907        // Check whether is an abbreviated syntax.
3908        if ($step == '*') {
3909          // Use the child axis and select all children.
3910          $axis['node-test'] = '*';
3911          break $parseBlock;
3912        }
3913  
3914        // ### I'm pretty sure our current handling of cdata is a fudge, and we should
3915        // really do this better, but leave this as is for now.
3916        if ($step == "text()") {
3917          // Handle the text node
3918          $axis["node-test"] = "cdata";
3919          break $parseBlock;
3920        }
3921  
3922        // There are a few node tests that we match verbatim.
3923        if ($step == "node()"
3924            || $step == "comment()"
3925            || $step == "text()"
3926            || $step == "processing-instruction") {
3927          $axis["node-test"] = $step;
3928          break $parseBlock;
3929        }
3930  
3931        // processing-instruction() is allowed to take an argument, but if it does, the argument
3932        // is a literal, which we will have parsed out to $[number].
3933        if (preg_match(":processing-instruction\(\$\d*\):", $step)) {
3934          $axis["node-test"] = $step;
3935          break $parseBlock;
3936        }
3937  
3938        // The only remaining way this can be a step, is if the remaining string is a simple name
3939        // or else a :* name.
3940        // http://www.w3.org/TR/xpath#NT-NameTest
3941        // NameTest   ::= '*'  
3942        //                | NCName ':' '*'  
3943        //                | QName 
3944        // QName      ::=  (Prefix ':')? LocalPart 
3945        // Prefix     ::=  NCName 
3946        // LocalPart  ::=  NCName 
3947        //
3948        // ie
3949        // NameTest   ::= '*'  
3950        //                | NCName ':' '*'  
3951        //                | (NCName ':')? NCName
3952        // NCName ::=  (Letter | '_') (NCNameChar)* 
3953        $NCName = "[a-zA-Z_][\w\.\-_]*";
3954        if (preg_match("/^$NCName:$NCName$/", $step)
3955          || preg_match("/^$NCName:*$/", $step)) {
3956          $axis['node-test'] = $step;
3957          if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
3958            // Case in-sensitive
3959            $axis['node-test'] = strtoupper($axis['node-test']);
3960          }
3961          // Not currently recursing
3962          $LastFailedStep = '';
3963          $LastFailedContext = '';
3964          break $parseBlock;
3965        } 
3966  
3967        // It's not a node then, we must treat it as a PrimaryExpr
3968        // Check for recursion
3969        if ($LastFailedStep == $step) {
3970          $this->_displayError('Recursion detected while parsing an XPath query, in the step ' .
3971                str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery)
3972                , __LINE__, __FILE__, FALSE);
3973          $axis['node-test'] = $step;
3974        } else {
3975          $LastFailedStep = $step;
3976          $axis = FALSE;
3977        }
3978        
3979      } while(FALSE); // end parse block
3980      
3981      // Check whether it's a valid axis.
3982      if ($axis !== FALSE) {
3983        if (!in_array($axis['axis'], array_merge($this->axes, array('function')))) {
3984          // Display an error message.
3985          $this->_displayError('While parsing an XPath query, in the step ' .
3986            str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery) .
3987            ' the invalid axis ' . $axis['axis'] . ' was found.', __LINE__, __FILE__, FALSE);
3988        }
3989      }
3990  
3991      // Cache the real axis information
3992      $aResultsCache[$cacheKey] = $axis;
3993  
3994      // Return the axis information.
3995      return $axis;
3996    }
3997     
3998  
3999    /**
4000     * Handles the XPath child axis.
4001     *
4002     * This method handles the XPath child axis.  It essentially filters out the
4003     * children to match the name specified after the '/'.
4004     *
4005     * @param  $axis        (array)  Array containing information about the axis.
4006     * @param  $contextPath (string) xpath to starting node from which the axis should 
4007     *                               be processed.
4008     * @return              (array)  A vector containing all nodes that were found, during 
4009     *                               the evaluation of the axis.
4010     * @see    evaluate()
4011     */
4012    function _handleAxis_child($axis, $contextPath) {
4013      $xPathSet = array(); // Create an empty node-set to hold the results of the child matches
4014      if ($axis["node-test"] == "cdata") {
4015        if (!isSet($this->nodeIndex[$contextPath]['textParts']) ) return '';
4016        $tSize = sizeOf($this->nodeIndex[$contextPath]['textParts']);
4017        for ($i=1; $i<=$tSize; $i++) { 
4018          $xPathSet[] = $contextPath . '/text()['.$i.']';
4019        }
4020      }
4021      else {
4022        // Get a list of all children.
4023        $allChildren = $this->nodeIndex[$contextPath]['childNodes'];
4024        
4025        // Run through all children in the order they where set.
4026        $cSize = sizeOf($allChildren);
4027        for ($i=0; $i<$cSize; $i++) {
4028          $childPath = $contextPath .'/'. $allChildren[$i]['name'] .'['. $allChildren[$i]['contextPos']  .']';
4029          $textChildPath = $contextPath.'/text()['.($i + 1).']';
4030          // Check the text node
4031          if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check
4032            $xPathSet[] = $textChildPath; // Add the child to the node-set.
4033          }
4034          // Check the actual node
4035          if ($this->_checkNodeTest($childPath, $axis['node-test'])) { // node test check
4036            $xPathSet[] = $childPath; // Add the child to the node-set.
4037          }
4038        }
4039  
4040        // Finally there will be one more text node to try
4041       $textChildPath = $contextPath.'/text()['.($cSize + 1).']';
4042       // Check the text node
4043       if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check
4044         $xPathSet[] = $textChildPath; // Add the child to the node-set.
4045       }
4046      }
4047      return $xPathSet; // Return the nodeset.
4048    }
4049    
4050    /**
4051     * Handles the XPath parent axis.
4052     *
4053     * @param  $axis        (array)  Array containing information about the axis.
4054     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4055     * @return              (array)  A vector containing all nodes that were found, during the 
4056     *                               evaluation of the axis.
4057     * @see    evaluate()
4058     */
4059    function _handleAxis_parent($axis, $contextPath) {
4060      $xPathSet = array(); // Create an empty node-set.
4061      
4062      // Check whether the parent matches the node-test.
4063      $parentPath = $this->getParentXPath($contextPath);
4064      if ($this->_checkNodeTest($parentPath, $axis['node-test'])) {
4065        $xPathSet[] = $parentPath; // Add this node to the list of nodes.
4066      }
4067      return $xPathSet; // Return the nodeset.
4068    }
4069    
4070    /**
4071     * Handles the XPath attribute axis.
4072     *
4073     * @param  $axis        (array)  Array containing information about the axis.
4074     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4075     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4076     * @see    evaluate()
4077     */
4078    function _handleAxis_attribute($axis, $contextPath) {
4079      $xPathSet = array(); // Create an empty node-set.
4080      
4081      // Check whether all nodes should be selected.
4082      $nodeAttr = $this->nodeIndex[$contextPath]['attributes'];
4083      if ($axis['node-test'] == '*'  
4084          || $axis['node-test'] == 'node()') {
4085        foreach($nodeAttr as $key=>$dummy) { // Run through the attributes.
4086          $xPathSet[] = $contextPath.'/attribute::'.$key; // Add this node to the node-set.
4087        }
4088      }
4089      elseif (isset($nodeAttr[$axis['node-test']])) {
4090        $xPathSet[] = $contextPath . '/attribute::'. $axis['node-test']; // Add this node to the node-set.
4091      }
4092      return $xPathSet; // Return the nodeset.
4093    }
4094     
4095    /**
4096     * Handles the XPath self axis.
4097     *
4098     * @param  $axis        (array)  Array containing information about the axis.
4099     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4100     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4101     * @see    evaluate()
4102     */
4103    function _handleAxis_self($axis, $contextPath) {
4104      $xPathSet = array(); // Create an empty node-set.
4105      
4106      // Check whether the context match the node-test.
4107      if ($this->_checkNodeTest($contextPath, $axis['node-test'])) {
4108        $xPathSet[] = $contextPath; // Add this node to the node-set.
4109      }
4110      return $xPathSet; // Return the nodeset.
4111    }
4112    
4113    /**
4114     * Handles the XPath descendant axis.
4115     *
4116     * @param  $axis        (array)  Array containing information about the axis.
4117     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4118     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4119     * @see    evaluate()
4120     */
4121    function _handleAxis_descendant($axis, $contextPath) {
4122      $xPathSet = array(); // Create an empty node-set.
4123      
4124      // Get a list of all children.
4125      $allChildren = $this->nodeIndex[$contextPath]['childNodes'];
4126      
4127      // Run through all children in the order they where set.
4128      $cSize = sizeOf($allChildren);
4129      for ($i=0; $i<$cSize; $i++) {
4130        $childPath = $allChildren[$i]['xpath'];
4131        // Check whether the child matches the node-test.
4132        if ($this->_checkNodeTest($childPath, $axis['node-test'])) {
4133          $xPathSet[] = $childPath; // Add the child to the list of nodes.
4134        }
4135        // Recurse to the next level.
4136        $xPathSet = array_merge($xPathSet, $this->_handleAxis_descendant($axis, $childPath));
4137      }
4138      return $xPathSet; // Return the nodeset.
4139    }
4140    
4141    /**
4142     * Handles the XPath ancestor axis.
4143     *
4144     * @param  $axis        (array)  Array containing information about the axis.
4145     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4146     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4147     * @see    evaluate()
4148     */
4149    function _handleAxis_ancestor($axis, $contextPath) {
4150      $xPathSet = array(); // Create an empty node-set.
4151          
4152      $parentPath = $this->getParentXPath($contextPath); // Get the parent of the current node.
4153      
4154      // Check whether the parent isn't super-root.
4155      if (!empty($parentPath)) {
4156        // Check whether the parent matches the node-test.
4157        if ($this->_checkNodeTest($parentPath, $axis['node-test'])) {
4158          $xPathSet[] = $parentPath; // Add the parent to the list of nodes.
4159        }
4160        // Handle all other ancestors.
4161        $xPathSet = array_merge($this->_handleAxis_ancestor($axis, $parentPath), $xPathSet);
4162      }
4163      return $xPathSet; // Return the nodeset.
4164    }
4165    
4166    /**
4167     * Handles the XPath namespace axis.
4168     *
4169     * @param  $axis        (array)  Array containing information about the axis.
4170     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4171     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4172     * @see    evaluate()
4173     */
4174    function _handleAxis_namespace($axis, $contextPath) {
4175      $this->_displayError("The axis 'namespace is not suported'", __LINE__, __FILE__, FALSE);
4176    }
4177    
4178    /**
4179     * Handles the XPath following axis.
4180     *
4181     * @param  $axis        (array)  Array containing information about the axis.
4182     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4183     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4184     * @see    evaluate()
4185     */
4186    function _handleAxis_following($axis, $contextPath) {
4187      $xPathSet = array(); // Create an empty node-set.
4188      
4189      do { // try-block
4190        $node = $this->nodeIndex[$contextPath]; // Get the current node
4191        $position = $node['pos'];               // Get the current tree position.
4192        $parent = $node['parentNode'];
4193        // Check if there is a following sibling at all; if not end.
4194        if ($position >= sizeOf($parent['childNodes'])) break; // try-block
4195        // Build the starting abs. XPath
4196        $startXPath = $parent['childNodes'][$position+1]['xpath'];
4197        // Run through all nodes of the document.
4198        $nodeKeys = array_keys($this->nodeIndex);
4199        $nodeSize = sizeOf($nodeKeys);
4200        for ($k=0; $k<$nodeSize; $k++) {
4201          if ($nodeKeys[$k] == $startXPath) break; // Check whether this is the starting abs. XPath
4202        }
4203        for (; $k<$nodeSize; $k++) {
4204          // Check whether the node fits the node-test.
4205          if ($this->_checkNodeTest($nodeKeys[$k], $axis['node-test'])) {
4206            $xPathSet[] = $nodeKeys[$k]; // Add the node to the list of nodes.
4207          }
4208        }
4209      } while(FALSE);
4210      return $xPathSet; // Return the nodeset.
4211    }
4212    
4213    /**
4214     * Handles the XPath preceding axis.
4215     *
4216     * @param  $axis        (array)  Array containing information about the axis.
4217     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4218     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4219     * @see    evaluate()
4220     */
4221    function _handleAxis_preceding($axis, $contextPath) {
4222      $xPathSet = array(); // Create an empty node-set.
4223      
4224      // Run through all nodes of the document.
4225      foreach ($this->nodeIndex as $xPath=>$dummy) {
4226        if (empty($xPath)) continue; // skip super-Root
4227        
4228        // Check whether this is the context node.
4229        if ($xPath == $contextPath) {
4230          break; // After this we won't look for more nodes.
4231        }
4232        if (!strncmp($xPath, $contextPath, strLen($xPath))) {
4233          continue;
4234        }
4235        // Check whether the node fits the node-test.
4236        if ($this->_checkNodeTest($xPath, $axis['node-test'])) {
4237          $xPathSet[] = $xPath; // Add the node to the list of nodes.
4238        }
4239      }
4240      return $xPathSet; // Return the nodeset.
4241    }
4242    
4243    /**
4244     * Handles the XPath following-sibling axis.
4245     *
4246     * @param  $axis        (array)  Array containing information about the axis.
4247     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4248     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4249     * @see    evaluate()
4250     */
4251    function _handleAxis_following_sibling($axis, $contextPath) {
4252      $xPathSet = array(); // Create an empty node-set.
4253      
4254      // Get all children from the parent.
4255      $siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath));
4256      // Create a flag whether the context node was already found.
4257      $found = FALSE;
4258      
4259      // Run through all siblings.
4260      $size = sizeOf($siblings);
4261      for ($i=0; $i<$size; $i++) {
4262        $sibling = $siblings[$i];
4263        
4264        // Check whether the context node was already found.
4265        if ($found) {
4266          // Check whether the sibling matches the node-test.
4267          if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
4268            $xPathSet[] = $sibling; // Add the sibling to the list of nodes.
4269          }
4270        }
4271        // Check if we reached *this* context node.
4272        if ($sibling == $contextPath) {
4273          $found = TRUE; // Continue looking for other siblings.
4274        }
4275      }
4276      return $xPathSet; // Return the nodeset.
4277    }
4278    
4279    /**
4280     * Handles the XPath preceding-sibling axis.
4281     *
4282     * @param  $axis        (array)  Array containing information about the axis.
4283     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4284     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4285     * @see    evaluate()
4286     */
4287    function _handleAxis_preceding_sibling($axis, $contextPath) {
4288      $xPathSet = array(); // Create an empty node-set.
4289      
4290      // Get all children from the parent.
4291      $siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath));
4292      
4293      // Run through all siblings.
4294      $size = sizeOf($siblings);
4295      for ($i=0; $i<$size; $i++) {
4296        $sibling = $siblings[$i];
4297        // Check whether this is the context node.
4298        if ($sibling == $contextPath) {
4299          break; // Don't continue looking for other siblings.
4300        }
4301        // Check whether the sibling matches the node-test.
4302        if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
4303          $xPathSet[] = $sibling; // Add the sibling to the list of nodes.
4304        }
4305      }
4306      return $xPathSet; // Return the nodeset.
4307    }
4308    
4309    /**
4310     * Handles the XPath descendant-or-self axis.
4311     *
4312     * @param  $axis        (array)  Array containing information about the axis.
4313     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4314     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4315     * @see    evaluate()
4316     */
4317    function _handleAxis_descendant_or_self($axis, $contextPath) {
4318      $xPathSet = array(); // Create an empty node-set.
4319      
4320      // Read the nodes.
4321      $xPathSet = array_merge(
4322                   $this->_handleAxis_self($axis, $contextPath),
4323                   $this->_handleAxis_descendant($axis, $contextPath)
4324                 );
4325      return $xPathSet; // Return the nodeset.
4326    }
4327    
4328    /**
4329     * Handles the XPath ancestor-or-self axis.
4330     *
4331     * This method handles the XPath ancestor-or-self axis.
4332     *
4333     * @param  $axis        (array)  Array containing information about the axis.
4334     * @param  $contextPath (string) xpath to starting node from which the axis should be processed.
4335     * @return              (array)  A vector containing all nodes that were found, during the evaluation of the axis.
4336     * @see    evaluate()
4337     */
4338    function _handleAxis_ancestor_or_self ( $axis, $contextPath) {
4339      $xPathSet = array(); // Create an empty node-set.
4340      
4341      // Read the nodes.
4342      $xPathSet = array_merge(
4343                   $this->_handleAxis_ancestor($axis, $contextPath),
4344                   $this->_handleAxis_self($axis, $contextPath)
4345                 );
4346      return $xPathSet; // Return the nodeset.
4347    }
4348    
4349    
4350    //-----------------------------------------------------------------------------------------
4351    // XPath                  ------  XPath FUNCTION Handlers  ------                          
4352    //-----------------------------------------------------------------------------------------
4353    
4354     /**
4355      * Handles the XPath function last.
4356      *    
4357      * @param  $arguments     (string) String containing the arguments that were passed to the function.
4358      * @param  $context       (array)  The context from which to evaluate the function
4359      * @return                (mixed)  Depending on the type of function being processed
4360      * @see    evaluate()
4361      */
4362    function _handleFunction_last($arguments, $context) {
4363      return $context['size'];
4364    }
4365    
4366    /**
4367     * Handles the XPath function position.
4368     *   
4369     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4370     * @param  $context       (array)  The context from which to evaluate the function
4371     * @return                (mixed)  Depending on the type of function being processed
4372     * @see    evaluate()
4373     */
4374    function _handleFunction_position($arguments, $context) {
4375      return $context['pos'];
4376    }
4377    
4378    /**
4379     * Handles the XPath function count.
4380     *   
4381     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4382     * @param  $context       (array)  The context from which to evaluate the function
4383     * @return                (mixed)  Depending on the type of function being processed
4384     * @see    evaluate()
4385     */
4386    function _handleFunction_count($arguments, $context) {
4387      // Evaluate the argument of the method as an XPath and return the number of results.
4388      return count($this->_evaluateExpr($arguments, $context));
4389    }
4390    
4391    /**
4392     * Handles the XPath function id.
4393     *   
4394     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4395     * @param  $context       (array)  The context from which to evaluate the function
4396     * @return                (mixed)  Depending on the type of function being processed
4397     * @see    evaluate()
4398     */
4399    function _handleFunction_id($arguments, $context) {
4400      $arguments = trim($arguments);         // Trim the arguments.
4401      $arguments = explode(' ', $arguments); // Now split the arguments into an array.
4402      // Create a list of nodes.
4403      $resultXPaths = array();
4404      // Run through all nodes of the document.
4405      $keys = array_keys($this->nodeIndex);
4406      $kSize = $sizeOf($keys);
4407      for ($i=0; $i<$kSize; $i++) {
4408        if (empty($keys[$i])) continue; // skip super-Root
4409        if (in_array($this->nodeIndex[$keys[$i]]['attributes']['id'], $arguments)) {
4410          $resultXPaths[] = $context['nodePath']; // Add this node to the list of nodes.
4411        }
4412      }
4413      return $resultXPaths; // Return the list of nodes.
4414    }
4415    
4416    /**
4417     * Handles the XPath function name.
4418     *   
4419     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4420     * @param  $context       (array)  The context from which to evaluate the function
4421     * @return                (mixed)  Depending on the type of function being processed
4422     * @see    evaluate()
4423     */
4424    function _handleFunction_name($arguments, $context) {
4425      // If the argument it omitted, it defaults to a node-set with the context node as its only member.
4426      if (empty($arguments)) {
4427        return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['name']);
4428      }
4429  
4430      // Evaluate the argument to get a node set.
4431      $nodeSet = $this->_evaluateExpr($arguments, $context);
4432      if (!is_array($nodeSet)) return '';
4433      if (count($nodeSet) < 1) return '';
4434      if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
4435       // Return a reference to the name of the node.
4436      return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['name']);
4437    }
4438    
4439    /**
4440     * Handles the XPath function string.
4441     *
4442     * http://www.w3.org/TR/xpath#section-String-Functions
4443     *   
4444     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4445     * @param  $context       (array)  The context from which to evaluate the function
4446     * @return                (mixed)  Depending on the type of function being processed
4447     * @see    evaluate()
4448     */
4449    function _handleFunction_string($arguments, $context) {
4450      // Check what type of parameter is given
4451      if (is_array($arguments)) {
4452        // Get the value of the first result (which means we want to concat all the text...unless
4453        // a specific text() node has been given, and it will switch off to substringData
4454        if (!count($arguments)) $result = '';
4455        else {
4456          $result = $this->_stringValue($arguments[0]);
4457          if (($literal = $this->_asLiteral($result)) !== FALSE) {
4458            $result = $literal;
4459          }
4460        }
4461      }
4462      // Is it a number string?
4463      elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) OR preg_match('/^\.[0-9]+$/', $arguments)) {
4464        // ### Note no support for NaN and Infinity.
4465        $number = doubleval($arguments); // Convert the digits to a number.
4466        $result = strval($number); // Return the number.
4467      }
4468      elseif (is_bool($arguments)) { // Check whether it's TRUE or FALSE and return as string.
4469        // ### Note that we used to return TRUE and FALSE which was incorrect according to the standard.
4470        if ($arguments === TRUE) {        
4471          $result = 'true'; 
4472        } else {
4473          $result = 'false';
4474        }
4475      }
4476      elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
4477        return $literal;
4478      }
4479      elseif (!empty($arguments)) {
4480        // Spec says:
4481        // "An object of a type other than the four basic types is converted to a string in a way that 
4482        // is dependent on that type."
4483        // Use the argument as an XPath.
4484        $result = $this->_evaluateExpr($arguments, $context);
4485        if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
4486          $this->_displayError("Loop detected in XPath expression.  Probably an internal error :o/.  _handleFunction_string($result)", __LINE__, __FILE__, FALSE);
4487          return '';
4488        } else {
4489          $result = $this->_handleFunction_string($result, $context);
4490        }
4491      }
4492      else {
4493        $result = '';  // Return an empty string.
4494      }
4495      return $result;
4496    }
4497    
4498    /**
4499     * Handles the XPath function concat.
4500     *   
4501     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4502     * @param  $context       (array)  The context from which to evaluate the function
4503     * @return                (mixed)  Depending on the type of function being processed
4504     * @see    evaluate()
4505     */
4506    function _handleFunction_concat($arguments, $context) {
4507      // Split the arguments.
4508      $arguments = explode(',', $arguments);
4509      // Run through each argument and evaluate it.
4510      $size = sizeof($arguments);
4511      for ($i=0; $i<$size; $i++) {
4512        $arguments[$i] = trim($arguments[$i]);  // Trim each argument.
4513        // Evaluate it.
4514        $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
4515      }
4516      $arguments = implode('', $arguments);  // Put the string together and return it.
4517      return $this->_addLiteral($arguments);
4518    }
4519    
4520    /**
4521     * Handles the XPath function starts-with.
4522     *   
4523     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4524     * @param  $context       (array)  The context from which to evaluate the function
4525     * @return                (mixed)  Depending on the type of function being processed
4526     * @see    evaluate()
4527     */
4528    function _handleFunction_starts_with($arguments, $context) {
4529      // Get the arguments.
4530      $first  = trim($this->_prestr($arguments, ','));
4531      $second = trim($this->_afterstr($arguments, ','));
4532      // Evaluate each argument.
4533      $first  = $this->_handleFunction_string($first, $context);
4534      $second = $this->_handleFunction_string($second, $context);
4535      // Check whether the first string starts with the second one.
4536      return  (bool) ereg('^'.$second, $first);
4537    }
4538    
4539    /**
4540     * Handles the XPath function contains.
4541     *   
4542     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4543     * @param  $context       (array)  The context from which to evaluate the function
4544     * @return                (mixed)  Depending on the type of function being processed
4545     * @see    evaluate()
4546     */
4547    function _handleFunction_contains($arguments, $context) {
4548      // Get the arguments.
4549      $first  = trim($this->_prestr($arguments, ','));
4550      $second = trim($this->_afterstr($arguments, ','));
4551      //echo "Predicate: $arguments First: ".$first." Second: ".$second."\n";
4552      // Evaluate each argument.
4553      $first = $this->_handleFunction_string($first, $context);
4554      $second = $this->_handleFunction_string($second, $context);
4555      //echo $second.": ".$first."\n";
4556      // If the search string is null, then the provided there is a value it will contain it as
4557      // it is considered that all strings contain the empty string. ## N.S.
4558      if ($second==='') return TRUE;
4559      // Check whether the first string starts with the second one.
4560      if (strpos($first, $second) === FALSE) {
4561        return FALSE;
4562      } else {
4563        return TRUE;
4564      }
4565    }
4566    
4567    /**
4568     * Handles the XPath function substring-before.
4569     *   
4570     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4571     * @param  $context       (array)  The context from which to evaluate the function
4572     * @return                (mixed)  Depending on the type of function being processed
4573     * @see    evaluate()
4574     */
4575    function _handleFunction_substring_before($arguments, $context) {
4576      // Get the arguments.
4577      $first  = trim($this->_prestr($arguments, ','));
4578      $second = trim($this->_afterstr($arguments, ','));
4579      // Evaluate each argument.
4580      $first  = $this->_handleFunction_string($first, $context);
4581      $second = $this->_handleFunction_string($second, $context);
4582      // Return the substring.
4583      return $this->_addLiteral($this->_prestr(strval($first), strval($second)));
4584    }
4585    
4586    /**
4587     * Handles the XPath function substring-after.
4588     *   
4589     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4590     * @param  $context       (array)  The context from which to evaluate the function
4591     * @return                (mixed)  Depending on the type of function being processed
4592     * @see    evaluate()
4593     */
4594    function _handleFunction_substring_after($arguments, $context) {
4595      // Get the arguments.
4596      $first  = trim($this->_prestr($arguments, ','));
4597      $second = trim($this->_afterstr($arguments, ','));
4598      // Evaluate each argument.
4599      $first  = $this->_handleFunction_string($first, $context);
4600      $second = $this->_handleFunction_string($second, $context);
4601      // Return the substring.
4602      return $this->_addLiteral($this->_afterstr(strval($first), strval($second)));
4603    }
4604    
4605    /**
4606     * Handles the XPath function substring.
4607     *   
4608     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4609     * @param  $context       (array)  The context from which to evaluate the function
4610     * @return                (mixed)  Depending on the type of function being processed
4611     * @see    evaluate()
4612     */
4613    function _handleFunction_substring($arguments, $context) {
4614      // Split the arguments.
4615      $arguments = explode(",", $arguments);
4616      $size = sizeOf($arguments);
4617      for ($i=0; $i<$size; $i++) { // Run through all arguments.
4618        $arguments[$i] = trim($arguments[$i]); // Trim the string.
4619        // Evaluate each argument.
4620        $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
4621      }
4622      // Check whether a third argument was given and return the substring..
4623      if (!empty($arguments[2])) {
4624        return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1, $arguments[2]));
4625      } else {
4626        return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1));
4627      }
4628    }
4629    
4630    /**
4631     * Handles the XPath function string-length.
4632     *   
4633     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4634     * @param  $context       (array)  The context from which to evaluate the function
4635     * @return                (mixed)  Depending on the type of function being processed
4636     * @see    evaluate()
4637     */
4638    function _handleFunction_string_length($arguments, $context) {
4639      $arguments = trim($arguments); // Trim the argument.
4640      // Evaluate the argument.
4641      $arguments = $this->_handleFunction_string($arguments, $context);
4642      return strlen(strval($arguments)); // Return the length of the string.
4643    }
4644  
4645    /**
4646     * Handles the XPath function normalize-space.
4647     *
4648     * The normalize-space function returns the argument string with whitespace
4649     * normalized by stripping leading and trailing whitespace and replacing sequences
4650     * of whitespace characters by a single space.
4651     * If the argument is omitted, it defaults to the context node converted to a string,
4652     * in other words the string-value of the context node
4653     *   
4654     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4655     * @param  $context       (array)  The context from which to evaluate the function
4656     * @return                 (stri)g trimed string
4657     * @see    evaluate()
4658     */
4659    function _handleFunction_normalize_space($arguments, $context) {
4660      if (empty($arguments)) {
4661        $arguments = $this->getParentXPath($context['nodePath']).'/'.$this->nodeIndex[$context['nodePath']]['name'].'['.$this->nodeIndex[$context['nodePath']]['contextPos'].']';
4662      } else {
4663         $arguments = $this->_handleFunction_string($arguments, $context);
4664      }
4665      $arguments = trim(preg_replace (";[[:space:]]+;s",' ',$arguments));
4666      return $this->_addLiteral($arguments);
4667    }
4668  
4669    /**
4670     * Handles the XPath function translate.
4671     *   
4672     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4673     * @param  $context       (array)  The context from which to evaluate the function
4674     * @return                (mixed)  Depending on the type of function being processed
4675     * @see    evaluate()
4676     */
4677    function _handleFunction_translate($arguments, $context) {
4678      $arguments = explode(',', $arguments); // Split the arguments.
4679      $size = sizeOf($arguments);
4680      for ($i=0; $i<$size; $i++) { // Run through all arguments.
4681        $arguments[$i] = trim($arguments[$i]); // Trim the argument.
4682        // Evaluate the argument.
4683        $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
4684      }
4685      // Return the translated string.
4686      return $this->_addLiteral(strtr($arguments[0], $arguments[1], $arguments[2]));
4687    }
4688  
4689    /**
4690     * Handles the XPath function boolean.
4691     *   
4692     * http://www.w3.org/TR/xpath#section-Boolean-Functions
4693     *
4694     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4695     * @param  $context       (array)  The context from which to evaluate the function
4696     * @return                (mixed)  Depending on the type of function being processed
4697     * @see    evaluate()
4698     */
4699    function _handleFunction_boolean($arguments, $context) {
4700      if (empty($arguments)) {
4701        return FALSE; // Sorry, there were no arguments.
4702      }
4703      // a bool is dead obvious
4704      elseif (is_bool($arguments)) {
4705        return $arguments;
4706      }
4707      // a node-set is true if and only if it is non-empty
4708      elseif (is_array($arguments)) {
4709        return (count($arguments) > 0);
4710      }
4711      // a number is true if and only if it is neither positive or negative zero nor NaN 
4712      // (Straight out of the XPath spec.. makes no sense?????)
4713      elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) || preg_match('/^\.[0-9]+$/', $arguments)) {
4714        $number = doubleval($arguments);  // Convert the digits to a number.
4715        // If number zero return FALSE else TRUE.
4716        if ($number == 0) return FALSE; else return TRUE;
4717      }
4718      // a string is true if and only if its length is non-zero
4719      elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
4720        return (strlen($literal) != 0);
4721      }
4722      // an object of a type other than the four basic types is converted to a boolean in a 
4723      // way that is dependent on that type
4724      else {
4725        // Spec says:
4726        // "An object of a type other than the four basic types is converted to a number in a way 
4727        // that is dependent on that type"
4728        // Try to evaluate the argument as an XPath.
4729        $result = $this->_evaluateExpr($arguments, $context);
4730        if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
4731          $this->_displayError("Loop detected in XPath expression.  Probably an internal error :o/.  _handleFunction_boolean($result)", __LINE__, __FILE__, FALSE);
4732          return FALSE;
4733        } else {
4734          return $this->_handleFunction_boolean($result, $context);
4735        }
4736      }
4737    }
4738    
4739    /**
4740     * Handles the XPath function not.
4741     *   
4742     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4743     * @param  $context       (array)  The context from which to evaluate the function
4744     * @return                (mixed)  Depending on the type of function being processed
4745     * @see    evaluate()
4746     */
4747    function _handleFunction_not($arguments, $context) {
4748      // Return the negative value of the content of the brackets.
4749      $bArgResult = $this->_handleFunction_boolean($arguments, $context);
4750  //echo "Before inversion: ".($bArgResult?"TRUE":"FALSE")."\n";
4751      return !$bArgResult;
4752    }
4753    
4754    /**
4755     * Handles the XPath function TRUE.
4756     *   
4757     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4758     * @param  $context       (array)  The context from which to evaluate the function
4759     * @return                (mixed)  Depending on the type of function being processed
4760     * @see    evaluate()
4761     */
4762    function _handleFunction_true($arguments, $context) {
4763      return TRUE; // Return TRUE.
4764    }
4765    
4766    /**
4767     * Handles the XPath function FALSE.
4768     *   
4769     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4770     * @param  $context       (array)  The context from which to evaluate the function
4771     * @return                (mixed)  Depending on the type of function being processed
4772     * @see    evaluate()
4773     */
4774    function _handleFunction_false($arguments, $context) {
4775      return FALSE; // Return FALSE.
4776    }
4777    
4778    /**
4779     * Handles the XPath function lang.
4780     *   
4781     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4782     * @param  $context       (array)  The context from which to evaluate the function
4783     * @return                (mixed)  Depending on the type of function being processed
4784     * @see    evaluate()
4785     */
4786    function _handleFunction_lang($arguments, $context) {
4787      $arguments = trim($arguments); // Trim the arguments.
4788      $currentNode = $this->nodeIndex[$context['nodePath']];
4789      while (!empty($currentNode['name'])) { // Run through the ancestors.
4790        // Check whether the node has an language attribute.
4791        if (isSet($currentNode['attributes']['xml:lang'])) {
4792          // Check whether it's the language, the user asks for; if so return TRUE else FALSE
4793          return eregi('^'.$arguments, $currentNode['attributes']['xml:lang']);
4794        }
4795        $currentNode = $currentNode['parentNode']; // Move up to parent
4796      } // End while
4797      return FALSE;
4798    }
4799    
4800    /**
4801     * Handles the XPath function number.
4802     *   
4803     * http://www.w3.org/TR/xpath#section-Number-Functions
4804     *
4805     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4806     * @param  $context       (array)  The context from which to evaluate the function
4807     * @return                (mixed)  Depending on the type of function being processed
4808     * @see    evaluate()
4809     */
4810    function _handleFunction_number($arguments, $context) {
4811      // Check the type of argument.
4812  
4813      // A string that is a number
4814      if (is_numeric($arguments)) {
4815        return doubleval($arguments); // Return the argument as a number.
4816      }
4817      // A bool
4818      elseif (is_bool($arguments)) {  // Return TRUE/FALSE as a number.
4819        if ($arguments === TRUE) return 1; else return 0;  
4820      }
4821      // A node set
4822      elseif (is_array($arguments)) {
4823        // Is converted to a string then handled like a string
4824        $string = $this->_handleFunction_string($arguments, $context);
4825        if (is_numeric($string))
4826          return doubleval($string);
4827      }
4828      elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
4829        if (is_numeric($literal)) {
4830          return doubleval($literal);
4831        } else {
4832          // If we are to stick strictly to the spec, we should return NaN, but lets just
4833          // leave PHP to see if can do some dynamic conversion.
4834          return $literal;
4835        }
4836      }
4837      else {
4838        // Spec says:
4839        // "An object of a type other than the four basic types is converted to a number in a way 
4840        // that is dependent on that type"
4841        // Try to evaluate the argument as an XPath.
4842        $result = $this->_evaluateExpr($arguments, $context);
4843        if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
4844          $this->_displayError("Loop detected in XPath expression.  Probably an internal error :o/.  _handleFunction_number($result)", __LINE__, __FILE__, FALSE);
4845          return FALSE;
4846        } else {
4847          return $this->_handleFunction_number($result, $context);
4848        }
4849      }
4850    }
4851  
4852    /**
4853     * Handles the XPath function sum.
4854     *   
4855     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4856     * @param  $context       (array)  The context from which to evaluate the function
4857     * @return                (mixed)  Depending on the type of function being processed
4858     * @see    evaluate()
4859     */
4860    function _handleFunction_sum($arguments, $context) {
4861      $arguments = trim($arguments); // Trim the arguments.
4862      // Evaluate the arguments as an XPath query.
4863      $result = $this->_evaluateExpr($arguments, $context);
4864      $sum = 0; // Create a variable to save the sum.
4865      // The sum function expects a node set as an argument.
4866      if (is_array($result)) {
4867        // Run through all results.
4868        $size = sizeOf($result);
4869        for ($i=0; $i<$size; $i++) {
4870          $value = $this->_stringValue($result[$i], $context);
4871          if (($literal = $this->_asLiteral($value)) !== FALSE) {
4872            $value = $literal;
4873          }
4874          $sum += doubleval($value); // Add it to the sum.
4875        }
4876      }
4877      return $sum; // Return the sum.
4878    }
4879  
4880    /**
4881     * Handles the XPath function floor.
4882     *   
4883     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4884     * @param  $context       (array)  The context from which to evaluate the function
4885     * @return                (mixed)  Depending on the type of function being processed
4886     * @see    evaluate()
4887     */
4888    function _handleFunction_floor($arguments, $context) {
4889      if (!is_numeric($arguments)) {
4890        $arguments = $this->_handleFunction_number($arguments, $context);
4891      }
4892      $arguments = doubleval($arguments); // Convert the arguments to a number.
4893      return floor($arguments);           // Return the result
4894    }
4895    
4896    /**
4897     * Handles the XPath function ceiling.
4898     *   
4899     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4900     * @param  $context       (array)  The context from which to evaluate the function
4901     * @return                (mixed)  Depending on the type of function being processed
4902     * @see    evaluate()
4903     */
4904    function _handleFunction_ceiling($arguments, $context) {
4905      if (!is_numeric($arguments)) {
4906        $arguments = $this->_handleFunction_number($arguments, $context);
4907      }
4908      $arguments = doubleval($arguments); // Convert the arguments to a number.
4909      return ceil($arguments);            // Return the result
4910    }
4911    
4912    /**
4913     * Handles the XPath function round.
4914     *   
4915     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4916     * @param  $context       (array)  The context from which to evaluate the function
4917     * @return                (mixed)  Depending on the type of function being processed
4918     * @see    evaluate()
4919     */
4920    function _handleFunction_round($arguments, $context) {
4921      if (!is_numeric($arguments)) {
4922        $arguments = $this->_handleFunction_number($arguments, $context);
4923      }
4924      $arguments = doubleval($arguments); // Convert the arguments to a number.
4925      return round($arguments);           // Return the result
4926    }
4927  
4928    //-----------------------------------------------------------------------------------------
4929    // XPath                  ------  XPath Extension FUNCTION Handlers  ------                          
4930    //-----------------------------------------------------------------------------------------
4931  
4932    /**
4933     * Handles the XPath function x-lower.
4934     *
4935     * lower case a string.
4936     *    string x-lower(string) 
4937     *   
4938     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4939     * @param  $context       (array)  The context from which to evaluate the function
4940     * @return                (mixed)  Depending on the type of function being processed
4941     * @see    evaluate()
4942     */
4943    function _handleFunction_x_lower($arguments, $context) {
4944      // Evaluate the argument.
4945      $string = $this->_handleFunction_string($arguments, $context);
4946       // Return a reference to the lowercased string
4947      return $this->_addLiteral(strtolower(strval($string)));
4948    }
4949  
4950    /**
4951     * Handles the XPath function x-upper.
4952     *
4953     * upper case a string.
4954     *    string x-upper(string) 
4955     *   
4956     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4957     * @param  $context       (array)  The context from which to evaluate the function
4958     * @return                (mixed)  Depending on the type of function being processed
4959     * @see    evaluate()
4960     */
4961    function _handleFunction_x_upper($arguments, $context) {
4962      // Evaluate the argument.
4963      $string = $this->_handleFunction_string($arguments, $context);
4964       // Return a reference to the lowercased string
4965      return $this->_addLiteral(strtoupper(strval($string)));
4966    }
4967  
4968    /**
4969     * Handles the XPath function generate-id.
4970     *
4971     * Produce a unique id for the first node of the node set.
4972     * 
4973     * Example usage, produces an index of all the nodes in an .xml document, where the content of each
4974     * "section" is the exported node as XML.
4975     *
4976     *   $aFunctions = $xPath->match('//');
4977     *   
4978     *   foreach ($aFunctions as $Function) {
4979     *       $id = $xPath->match("generate-id($Function)");
4980     *       echo "<a href='#$id'>$Function</a><br>";
4981     *   }
4982     *   
4983     *   foreach ($aFunctions as $Function) {
4984     *       $id = $xPath->match("generate-id($Function)");
4985     *       echo "<h2 id='$id'>$Function</h2>";
4986     *       echo htmlspecialchars($xPath->exportAsXml($Function));
4987     *   }
4988     * 
4989     * @param  $arguments     (string) String containing the arguments that were passed to the function.
4990     * @param  $context       (array)  The context from which to evaluate the function
4991     * @return                (mixed)  Depending on the type of function being processed
4992     * @author Ricardo Garcia
4993     * @see    evaluate()
4994     */
4995    function _handleFunction_generate_id($arguments, $context) {
4996      // If the argument is omitted, it defaults to a node-set with the context node as its only member.
4997      if (is_string($arguments) && empty($arguments)) {
4998        // We need ids then
4999        $this->_generate_ids();
5000        return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['generated_id']);
5001      }
5002  
5003      // Evaluate the argument to get a node set.
5004      $nodeSet = $this->_evaluateExpr($arguments, $context);
5005  
5006      if (!is_array($nodeSet)) return '';
5007      if (count($nodeSet) < 1) return '';
5008      if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
5009       // Return a reference to the name of the node.
5010      // We need ids then
5011      $this->_generate_ids();
5012      return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['generated_id']);
5013    }
5014  
5015    //-----------------------------------------------------------------------------------------
5016    // XPathEngine                ------  Help Stuff  ------                                   
5017    //-----------------------------------------------------------------------------------------
5018  
5019    /**
5020     * Decodes the character set entities in the given string.
5021     *
5022     * This function is given for convenience, as all text strings or attributes
5023     * are going to come back to you with their entities still encoded.  You can
5024     * use this function to remove these entites.
5025     *
5026     * It makes use of the get_html_translation_table(HTML_ENTITIES) php library 
5027     * call, so is limited in the same ways.  At the time of writing this seemed
5028     * be restricted to iso-8859-1
5029     *
5030     * ### Provide an option that will do this by default.
5031     *
5032     * @param $encodedData (mixed) The string or array that has entities you would like to remove
5033     * @param $reverse     (bool)  If TRUE entities will be encoded rather than decoded, ie
5034     *                             < to &lt; rather than &lt; to <.
5035     * @return             (mixed) The string or array returned with entities decoded.
5036     */
5037    function decodeEntities($encodedData, $reverse=FALSE) {
5038      static $aEncodeTbl;
5039      static $aDecodeTbl;
5040      // Get the translation entities, but we'll cache the result to enhance performance.
5041      if (empty($aDecodeTbl)) {
5042        // Get the translation entities.
5043        $aEncodeTbl = get_html_translation_table(HTML_ENTITIES);
5044        $aDecodeTbl = array_flip($aEncodeTbl);
5045      }
5046  
5047      // If it's just a single string.
5048      if (!is_array($encodedData)) {
5049        if ($reverse) {
5050          return strtr($encodedData, $aEncodeTbl);
5051        } else {
5052          return strtr($encodedData, $aDecodeTbl);
5053        }
5054      }
5055  
5056      $result = array();
5057      foreach($encodedData as $string) {
5058        if ($reverse) {
5059          $result[] = strtr($string, $aEncodeTbl);
5060        } else {
5061          $result[] = strtr($string, $aDecodeTbl);
5062        }
5063      }
5064  
5065      return $result;
5066    }
5067    
5068    /**
5069     * Compare two nodes to see if they are equal (point to the same node in the doc)
5070     *
5071     * 2 nodes are considered equal if the absolute XPath is equal.
5072     * 
5073     * @param  $node1 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array)
5074     * @param  $node2 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array)
5075     * @return        (bool)  TRUE if equal (see text above), FALSE if not (and on error).
5076     */
5077    function equalNodes($node1, $node2) {
5078      $xPath_1 = is_string($node1) ? $node1 : $this->getNodePath($node1);
5079      $xPath_2 = is_string($node2) ? $node2 : $this->getNodePath($node2);
5080      return (strncasecmp ($xPath_1, $xPath_2, strLen($xPath_1)) == 0);
5081    }
5082    
5083    /**
5084     * Get the absolute XPath of a node that is in a document tree.
5085     *
5086     * @param $node (array)  A real tree-node (hash-array)   
5087     * @return      (string) The string path to the node or FALSE on error.
5088     */
5089    function getNodePath($node) {
5090      if (!empty($node['xpath'])) return $node['xpath'];
5091      $pathInfo = array();
5092      do {
5093        if (empty($node['name']) OR empty($node['parentNode'])) break; // End criteria
5094        $pathInfo[] = array('name' => $node['name'], 'contextPos' => $node['contextPos']);
5095        $node = $node['parentNode'];
5096      } while (TRUE);
5097      
5098      $xPath = '';
5099      for ($i=sizeOf($pathInfo)-1; $i>=0; $i--) {
5100        $xPath .= '/' . $pathInfo[$i]['name'] . '[' . $pathInfo[$i]['contextPos'] . ']';
5101      }
5102      if (empty($xPath)) return FALSE;
5103      return $xPath;
5104    }
5105    
5106    /**
5107     * Retrieves the absolute parent XPath query.
5108     *
5109     * The parents stored in the tree are only relative parents...but all the parent
5110     * information is stored in the XPath query itself...so instead we use a function
5111     * to extract the parent from the absolute Xpath query
5112     *
5113     * @param  $childPath (string) String containing an absolute XPath query
5114     * @return            (string) returns the absolute XPath of the parent
5115     */
5116     function getParentXPath($absoluteXPath) {
5117       $lastSlashPos = strrpos($absoluteXPath, '/'); 
5118       if ($lastSlashPos == 0) { // it's already the root path
5119         return ''; // 'super-root'
5120       } else {
5121         return (substr($absoluteXPath, 0, $lastSlashPos));
5122       }
5123     }
5124    
5125    /**
5126     * Returns TRUE if the given node has child nodes below it
5127     *
5128     * @param  $absoluteXPath (string) full path of the potential parent node
5129     * @return                (bool)   TRUE if this node exists and has a child, FALSE otherwise
5130     */
5131    function hasChildNodes($absoluteXPath) {
5132      if ($this->_indexIsDirty) $this->reindexNodeTree();
5133      return (bool) (isSet($this->nodeIndex[$absoluteXPath]) 
5134                     AND sizeOf($this->nodeIndex[$absoluteXPath]['childNodes']));
5135    }
5136    
5137    /**
5138     * Translate all ampersands to it's literal entities '&amp;' and back.
5139     *
5140     * I wasn't aware of this problem at first but it's important to understand why we do this.
5141     * At first you must know:
5142     * a) PHP's XML parser *translates* all entities to the equivalent char E.g. &lt; is returned as '<'
5143     * b) PHP's XML parser (in V 4.1.0) has problems with most *literal* entities! The only one's that are 
5144     *    recognized are &amp;, &lt; &gt; and &quot;. *ALL* others (like &nbsp; &copy; a.s.o.) cause an 
5145     *    XML_ERROR_UNDEFINED_ENTITY error. I reported this as bug at http://bugs.php.net/bug.php?id=15092
5146     *    (It turned out not to be a 'real' bug, but one of those nice W3C-spec things).
5147     * 
5148     * Forget position b) now. It's just for info. Because the way we will solve a) will also solve b) too. 
5149     *
5150     * THE PROBLEM
5151     * To understand the problem, here a sample:
5152     * Given is the following XML:    "<AAA> &lt; &nbsp; &gt; </AAA>"
5153     *   Try to parse it and PHP's XML parser will fail with a XML_ERROR_UNDEFINED_ENTITY becaus of 
5154     *   the unknown litteral-entity '&nbsp;'. (The numeric equivalent '&#160;' would work though). 
5155     * Next try is to use the numeric equivalent 160 for '&nbsp;', thus  "<AAA> &lt; &#160; &gt; </AAA>"
5156     *   The data we receive in the tag <AAA> is  " <   > ". So we get the *translated entities* and 
5157     *   NOT the 3 entities &lt; &#160; &gt. Thus, we will not even notice that there were entities at all!
5158     *   In *most* cases we're not able to tell if the data was given as entity or as 'normal' char.
5159     *   E.g. When receiving a quote or a single space were not able to tell if it was given as 'normal' char
5160     *   or as &nbsp; or &quot;. Thus we loose the entity-information of the XML-data!
5161     * 
5162     * THE SOLUTION
5163     * The better solution is to keep the data 'as is' by replacing the '&' before parsing begins.
5164     * E.g. Taking the original input from above, this would result in "<AAA> &amp;lt; &amp;nbsp; &amp;gt; </AAA>"
5165     * The data we receive now for the tag <AAA> is  " &lt; &nbsp; &gt; ". and that's what we want.
5166     * 
5167     * The bad thing is, that a global replace will also replace data in section that are NOT translated by the 
5168     * PHP XML-parser. That is comments (<!-- -->), IP-sections (stuff between <? ? >) and CDATA-block too.
5169     * So all data comming from those sections must be reversed. This is done during the XML parse phase.
5170     * So:
5171     * a) Replacement of all '&' in the XML-source.
5172     * b) All data that is not char-data or in CDATA-block have to be reversed during the XML-parse phase.
5173     *
5174     * @param  $xmlSource (string) The XML string
5175     * @return            (string) The XML string with translated ampersands.
5176     */
5177    function _translateAmpersand($xmlSource, $reverse=FALSE) {
5178      $PHP5 = (substr(phpversion(), 0, 1) == '5');
5179      if ($PHP5) {
5180        //otherwise we receive  &amp;nbsp;  instead of  &nbsp;
5181        return $xmlSource;
5182      } else {
5183        return ($reverse ? str_replace('&amp;', '&', $xmlSource) : str_replace('&', '&amp;', $xmlSource));
5184      }
5185    }
5186  
5187  } // END OF CLASS XPathEngine
5188  
5189  
5190  /************************************************************************************************
5191  * ===============================================================================================
5192  *                                     X P a t h  -  Class                                        
5193  * ===============================================================================================
5194  ************************************************************************************************/
5195  
5196  define('XPATH_QUERYHIT_ALL'   , 1);
5197  define('XPATH_QUERYHIT_FIRST' , 2);
5198  define('XPATH_QUERYHIT_UNIQUE', 3);
5199  
5200  class XPath extends XPathEngine {
5201      
5202    /**
5203     * Constructor of the class
5204     *
5205     * Optionally you may call this constructor with the XML-filename to parse and the 
5206     * XML option vector. A option vector sample: 
5207     *   $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE, XML_OPTION_SKIP_WHITE => TRUE);
5208     *
5209     * @param  $userXmlOptions (array)  (optional) Vector of (<optionID>=><value>, <optionID>=><value>, ...)
5210     * @param  $fileName       (string) (optional) Filename of XML file to load from.
5211     *                                  It is recommended that you call importFromFile()
5212     *                                  instead as you will get an error code.  If the
5213     *                                  import fails, the object will be set to FALSE.
5214     * @see    parent::XPathEngine()
5215     */
5216    function XPath($fileName='', $userXmlOptions=array()) {
5217      parent::XPathEngine($userXmlOptions);
5218      $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
5219      if ($fileName) {
5220        if (!$this->importFromFile($fileName)) {
5221          // Re-run the base constructor to "reset" the object.  If the user has any sense, then
5222          // they will have created the object, and then explicitly called importFromFile(), giving
5223          // them the chance to catch and handle the error properly.
5224          parent::XPathEngine($userXmlOptions);
5225        }
5226      }
5227    }
5228    
5229    /**
5230     * Resets the object so it's able to take a new xml sting/file
5231     *
5232     * Constructing objects is slow.  If you can, reuse ones that you have used already
5233     * by using this reset() function.
5234     */
5235    function reset() {
5236      parent::reset();
5237      $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
5238    }
5239    
5240    //-----------------------------------------------------------------------------------------
5241    // XPath                    ------  Get / Set Stuff  ------                                
5242    //-----------------------------------------------------------------------------------------
5243    
5244    /**
5245     * Resolves and xPathQuery array depending on the property['modMatch']
5246     *
5247     * Most of the modification functions of XPath will also accept a xPathQuery (instead 
5248     * of an absolute Xpath). The only problem is that the query could match more the one 
5249     * node. The question is, if the none, the fist or all nodes are to be modified.
5250     * The behaver can be set with setModMatch()  
5251     *
5252     * @param $modMatch (int) One of the following:
5253     *                        - XPATH_QUERYHIT_ALL (default) 
5254     *                        - XPATH_QUERYHIT_FIRST
5255     *                        - XPATH_QUERYHIT_UNIQUE // If the query matches more then one node. 
5256     * @see  _resolveXPathQuery()
5257     */
5258    function setModMatch($modMatch = XPATH_QUERYHIT_ALL) {
5259      switch($modMatch) {
5260        case XPATH_QUERYHIT_UNIQUE : $this->properties['modMatch'] =  XPATH_QUERYHIT_UNIQUE; break;
5261        case XPATH_QUERYHIT_FIRST: $this->properties['modMatch'] =  XPATH_QUERYHIT_FIRST; break;
5262        default: $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
5263      }
5264    }
5265    
5266    //-----------------------------------------------------------------------------------------
5267    // XPath                    ------  DOM Like Modification  ------                          
5268    //-----------------------------------------------------------------------------------------
5269    
5270    //-----------------------------------------------------------------------------------------
5271    // XPath                  ------  Child (Node)  Set/Get  ------                           
5272    //-----------------------------------------------------------------------------------------
5273    
5274    /**
5275     * Retrieves the name(s) of a node or a group of document nodes.
5276     *          
5277     * This method retrieves the names of a group of document nodes
5278     * specified in the argument.  So if the argument was '/A[1]/B[2]' then it
5279     * would return 'B' if the node did exist in the tree.
5280     *          
5281     * @param  $xPathQuery (mixed) Array or single full document path(s) of the node(s), 
5282     *                             from which the names should be retrieved.
5283     * @return             (mixed) Array or single string of the names of the specified 
5284     *                             nodes, or just the individual name.  If the node did 
5285     *                             not exist, then returns FALSE.
5286     */
5287    function nodeName($xPathQuery) {
5288      if (is_array($xPathQuery)) {
5289        $xPathSet = $xPathQuery;
5290      } else {
5291        // Check for a valid xPathQuery
5292        $xPathSet = $this->_resolveXPathQuery($xPathQuery,'nodeName');
5293      }
5294      if (count($xPathSet) == 0) return FALSE;
5295      // For each node, get it's name
5296      $result = array();
5297      foreach($xPathSet as $xPath) {
5298        $node = &$this->getNode($xPath);
5299        if (!$node) {
5300          // ### Fatal internal error?? 
5301          continue;
5302        }
5303        $result[] = $node['name'];
5304      }
5305      // If just a single string, return string
5306      if (count($xPathSet) == 1) $result = $result[0];
5307      // Return result.
5308      return $result;
5309    }
5310    
5311    /**
5312     * Removes a node from the XML document.
5313     *
5314     * This method removes a node from the tree of nodes of the XML document. If the node 
5315     * is a document node, all children of the node and its character data will be removed. 
5316     * If the node is an attribute node, only this attribute will be removed, the node to which 
5317     * the attribute belongs as well as its children will remain unmodified.
5318     *
5319     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5320     *       Depending on setModMatch() one, none or multiple nodes are affected.
5321     *
5322     * @param  $xPathQuery  (string) xpath to the node (See note above).
5323     * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the document to reflect 
5324     *                               the changes.  A performance helper.  See reindexNodeTree()
5325     * @return              (bool)   TRUE on success, FALSE on error;
5326     * @see    setModMatch(), reindexNodeTree()
5327     */
5328    function removeChild($xPathQuery, $autoReindex=TRUE) {
5329      $ThisFunctionName = 'removeChild';
5330      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
5331      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
5332      if ($bDebugThisFunction) {
5333        echo "Node: $xPathQuery\n";
5334        echo '<hr>';
5335      }
5336  
5337      $NULL = NULL;
5338      $status = FALSE;
5339      do { // try-block
5340        // Check for a valid xPathQuery
5341        $xPathSet = $this->_resolveXPathQuery($xPathQuery,'removeChild');
5342        if (sizeOf($xPathSet) === 0) {
5343          $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
5344          break; // try-block
5345        }
5346        $mustReindex = FALSE;
5347        // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
5348        for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
5349          $absoluteXPath = $xPathSet[$i];
5350          if (preg_match(';/attribute::;', $absoluteXPath)) { // Handle the case of an attribute node
5351            $xPath = $this->_prestr($absoluteXPath, '/attribute::');       // Get the path to the attribute node's parent.
5352            $attribute = $this->_afterstr($absoluteXPath, '/attribute::'); // Get the name of the attribute.
5353            unSet($this->nodeIndex[$xPath]['attributes'][$attribute]);     // Unset the attribute
5354            if ($bDebugThisFunction) echo "We removed the attribute '$attribute' of node '$xPath'.\n";
5355            continue;
5356          }
5357          // Otherwise remove the node by setting it to NULL. It will be removed on the next reindexNodeTree() call.
5358          $mustReindex = $autoReindex;
5359          // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
5360          $this->_indexIsDirty = TRUE;
5361          
5362          $theNode = $this->nodeIndex[$absoluteXPath];
5363          $theNode['parentNode']['childNodes'][$theNode['pos']] =& $NULL;
5364          if ($bDebugThisFunction) echo "We removed the node '$absoluteXPath'.\n";
5365        }
5366        // Reindex the node tree again
5367        if ($mustReindex) $this->reindexNodeTree();
5368        $status = TRUE;
5369      } while(FALSE);
5370      
5371      $this->_closeDebugFunction($ThisFunctionName, $status, $bDebugThisFunction);
5372  
5373      return $status;
5374    }
5375    
5376    /**
5377     * Replace a node with any data string. The $data is taken 1:1.
5378     *
5379     * This function will delete the node you define by $absoluteXPath (plus it's sub-nodes) and 
5380     * substitute it by the string $text. Often used to push in not well formed HTML.
5381     * WARNING: 
5382     *   The $data is taken 1:1. 
5383     *   You are in charge that the data you enter is valid XML if you intend
5384     *   to export and import the content again.
5385     *
5386     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5387     *       Depending on setModMatch() one, none or multiple nodes are affected.
5388     *
5389     * @param  $xPathQuery  (string) xpath to the node (See note above).
5390     * @param  $data        (string) String containing the content to be set. *READONLY*
5391     * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the document to reflect 
5392     *                               the changes.  A performance helper.  See reindexNodeTree()
5393     * @return              (bool)   TRUE on success, FALSE on error;
5394     * @see    setModMatch(), replaceChild(), reindexNodeTree()
5395     */
5396    function replaceChildByData($xPathQuery, $data, $autoReindex=TRUE) {
5397      $ThisFunctionName = 'replaceChildByData';
5398      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
5399      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
5400      if ($bDebugThisFunction) {
5401        echo "Node: $xPathQuery\n";
5402      }
5403  
5404      $NULL = NULL;
5405      $status = FALSE;
5406      do { // try-block
5407        // Check for a valid xPathQuery
5408        $xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChildByData');
5409        if (sizeOf($xPathSet) === 0) {
5410          $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
5411          break; // try-block
5412        }
5413        $mustReindex = FALSE;
5414        // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
5415        for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
5416          $mustReindex = $autoReindex;
5417          // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
5418          $this->_indexIsDirty = TRUE;
5419          
5420          $absoluteXPath = $xPathSet[$i];
5421          $theNode = $this->nodeIndex[$absoluteXPath];
5422          $pos = $theNode['pos'];
5423          $theNode['parentNode']['textParts'][$pos] .= $data;
5424          $theNode['parentNode']['childNodes'][$pos] =& $NULL;
5425          if ($bDebugThisFunction) echo "We replaced the node '$absoluteXPath' with data.\n";
5426        }
5427        // Reindex the node tree again
5428        if ($mustReindex) $this->reindexNodeTree();
5429        $status = TRUE;
5430      } while(FALSE);
5431      
5432      $this->_closeDebugFunction($ThisFunctionName, ($status) ? 'Success' : '!!! FAILD !!!', $bDebugThisFunction);
5433  
5434      return $status;
5435    }
5436    
5437    /**
5438     * Replace the node(s) that matches the xQuery with the passed node (or passed node-tree)
5439     * 
5440     * If the passed node is a string it's assumed to be XML and replaceChildByXml() 
5441     * will be called.
5442     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5443     *       Depending on setModMatch() one, none or multiple nodes are affected.
5444     *
5445     * @param  $xPathQuery  (string) Xpath to the node being replaced.
5446     * @param  $node        (mixed)  String or Array (Usually a String)
5447     *                               If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
5448     *                               If array:  A Node (can be a whole sub-tree) (See comment in header)
5449     * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the document to reflect 
5450     *                               the changes.  A performance helper.  See reindexNodeTree()
5451     * @return              (array)  The last replaced $node (can be a whole sub-tree)
5452     * @see    reindexNodeTree()
5453     */
5454    function &replaceChild($xPathQuery, $node, $autoReindex=TRUE) {
5455      $NULL = NULL;
5456      if (is_string($node)) {
5457        if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
5458          return array();
5459        } else { 
5460          if (!($node = $this->_xml2Document($node))) return FALSE;
5461        }
5462      }
5463      
5464      // Special case if it's 'super root'. We then have to take the child node == top node
5465      if (empty($node['parentNode'])) $node = $node['childNodes'][0];
5466      
5467      $status = FALSE;
5468      do { // try-block
5469        // Check for a valid xPathQuery
5470        $xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChild');
5471        if (sizeOf($xPathSet) === 0) {
5472          $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
5473          break; // try-block
5474        }
5475        $mustReindex = FALSE;
5476        
5477        // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
5478        for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
5479          $mustReindex = $autoReindex;
5480          // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
5481          $this->_indexIsDirty = TRUE;
5482          
5483          $absoluteXPath = $xPathSet[$i];
5484          $childNode =& $this->nodeIndex[$absoluteXPath];
5485          $parentNode =& $childNode['parentNode'];
5486          $childNode['parentNode'] =& $NULL;
5487          $childPos = $childNode['pos'];
5488          $parentNode['childNodes'][$childPos] =& $this->cloneNode($node);
5489        }
5490        if ($mustReindex) $this->reindexNodeTree();
5491        $status = TRUE;
5492      } while(FALSE);
5493      
5494      if (!$status) return FALSE;
5495      return $childNode;
5496    }
5497    
5498    /**
5499     * Insert passed node (or passed node-tree) at the node(s) that matches the xQuery.
5500     *
5501     * With parameters you can define if the 'hit'-node is shifted to the right or left 
5502     * and if it's placed before of after the text-part.
5503     * Per derfault the 'hit'-node is shifted to the right and the node takes the place 
5504     * the of the 'hit'-node. 
5505     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5506     *       Depending on setModMatch() one, none or multiple nodes are affected.
5507     * 
5508     * E.g. Following is given:           AAA[1]           
5509     *                                  /       \          
5510     *                              ..BBB[1]..BBB[2] ..    
5511     *
5512     * a) insertChild('/AAA[1]/BBB[2]', <node CCC>)
5513     * b) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE)
5514     * c) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE, $afterText=FALSE)
5515     *
5516     * a)                          b)                           c)                        
5517     *          AAA[1]                       AAA[1]                       AAA[1]          
5518     *        /    |   \                   /    |   \                   /    |   \        
5519     *  ..BBB[1]..CCC[1]BBB[2]..     ..BBB[1]..BBB[2]..CCC[1]     ..BBB[1]..BBB[2]CCC[1]..
5520     *
5521     * #### Do a complete review of the "(optional)" tag after several arguments.
5522     *
5523     * @param  $xPathQuery  (string) Xpath to the node to append.
5524     * @param  $node        (mixed)  String or Array (Usually a String)
5525     *                               If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
5526     *                               If array:  A Node (can be a whole sub-tree) (See comment in header)
5527     * @param  $shiftRight  (bool)   (optional, default=TRUE) Shift the target node to the right.
5528     * @param  $afterText   (bool)   (optional, default=TRUE) Insert after the text.
5529     * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the document to reflect 
5530     *                                the changes.  A performance helper.  See reindexNodeTree()
5531     * @return              (mixed)  FALSE on error (or no match). On success we return the path(s) to the newly
5532     *                               appended nodes. That is: Array of paths if more then 1 node was added or
5533     *                               a single path string if only one node was added.
5534     *                               NOTE:  If autoReindex is FALSE, then we can't return the *complete* path
5535     *                               as the exact doc-pos isn't available without reindexing. In that case we leave
5536     *                               out the last [docpos] in the path(s). ie  we'd return /A[3]/B instead of /A[3]/B[2]
5537     * @see    appendChildByXml(), reindexNodeTree()
5538     */
5539    function insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText=TRUE, $autoReindex=TRUE) {
5540      if (is_string($node)) {
5541        if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
5542          return FALSE;
5543        } else { 
5544          if (!($node = $this->_xml2Document($node))) return FALSE;
5545        }
5546      }
5547  
5548      // Special case if it's 'super root'. We then have to take the child node == top node
5549      if (empty($node['parentNode'])) $node = $node['childNodes'][0];
5550      
5551      // Check for a valid xPathQuery
5552      $xPathSet = $this->_resolveXPathQuery($xPathQuery,'insertChild');
5553      if (sizeOf($xPathSet) === 0) {
5554        $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
5555        return FALSE;
5556      }
5557      $mustReindex = FALSE;
5558      $newNodes = array();
5559      $result = array();
5560      // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
5561      for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
5562        $absoluteXPath = $xPathSet[$i];
5563        $childNode =& $this->nodeIndex[$absoluteXPath];
5564        $parentNode =& $childNode['parentNode'];
5565  
5566        // We can't insert at the super root or at the root.
5567        if (empty($absoluteXPath) || (!$parentNode['parentNode'])) {
5568          $this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE);
5569          return FALSE;
5570        }
5571  
5572        $mustReindex = $autoReindex;
5573        // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
5574        $this->_indexIsDirty = TRUE;
5575        
5576        //Special case: It not possible to add siblings to the top node.
5577        if (empty($parentNode['name'])) continue;
5578        $newNode =& $this->cloneNode($node);
5579        $pos = $shiftRight ? $childNode['pos'] : $childNode['pos']+1;
5580        $parentNode['childNodes'] = array_merge(
5581                                      array_slice($parentNode['childNodes'], 0, $pos),
5582                                      array(&$newNode),
5583                                      array_slice($parentNode['childNodes'], $pos)
5584                                    );
5585        $pos += $afterText ? 1 : 0;
5586        $parentNode['textParts'] = array_merge(
5587                                     array_slice($parentNode['textParts'], 0, $pos),
5588                                     array(''),
5589                                     array_slice($parentNode['textParts'], $pos)
5590                                   );
5591        
5592        // We are going from bottom to top, but the user will want results from top to bottom.
5593        if ($mustReindex) {
5594          // We'll have to wait till after the reindex to get the full path to this new node.
5595          $newNodes[] = &$newNode;
5596        } else {
5597          // If we are reindexing the tree later, then we can't return the user any
5598          // useful results, so we just return them the count.
5599          $newNodePath = $parentNode['xpath'].'/'.$newNode['name'];
5600          array_unshift($result, $newNodePath);
5601        }
5602      }
5603      if ($mustReindex) {
5604        $this->reindexNodeTree();
5605        // Now we must fill in the result array.  Because until now we did not
5606        // know what contextpos our newly added entries had, just their pos within
5607        // the siblings.
5608        foreach ($newNodes as $newNode) {
5609          array_unshift($result, $newNode['xpath']);
5610        }
5611      }
5612      if (count($result) == 1) $result = $result[0];
5613      return $result;
5614    }
5615    
5616    /**
5617     * Appends a child to anothers children.
5618     *
5619     * If you intend to do a lot of appending, you should leave autoIndex as FALSE
5620     * and then call reindexNodeTree() when you are finished all the appending.
5621     *
5622     * @param  $xPathQuery  (string) Xpath to the node to append to.
5623     * @param  $node        (mixed)  String or Array (Usually a String)
5624     *                               If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
5625     *                               If array:  A Node (can be a whole sub-tree) (See comment in header)
5626     * @param  $afterText   (bool)   (optional, default=FALSE) Insert after the text.
5627     * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the document to reflect 
5628     *                               the changes.  A performance helper.  See reindexNodeTree()
5629     * @return              (mixed)  FALSE on error (or no match). On success we return the path(s) to the newly
5630     *                               appended nodes. That is: Array of paths if more then 1 node was added or
5631     *                               a single path string if only one node was added.
5632     *                               NOTE:  If autoReindex is FALSE, then we can't return the *complete* path
5633     *                               as the exact doc-pos isn't available without reindexing. In that case we leave
5634     *                               out the last [docpos] in the path(s). ie  we'd return /A[3]/B instead of /A[3]/B[2]
5635     * @see    insertChild(), reindexNodeTree()
5636     */
5637    function appendChild($xPathQuery, $node, $afterText=FALSE, $autoReindex=TRUE) {
5638      if (is_string($node)) {
5639        if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
5640          return FALSE;
5641        } else { 
5642          if (!($node = $this->_xml2Document($node))) return FALSE;
5643        }
5644      }
5645      
5646      // Special case if it's 'super root'. We then have to take the child node == top node
5647      if (empty($node['parentNode'])) $node = $node['childNodes'][0];
5648  
5649      // Check for a valid xPathQuery
5650      $xPathSet = $this->_resolveXPathQueryForNodeMod($xPathQuery, 'appendChild');
5651      if (sizeOf($xPathSet) === 0) return FALSE;
5652  
5653      $mustReindex = FALSE;
5654      $newNodes = array();
5655      $result = array();
5656      // Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
5657      for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
5658        $mustReindex = $autoReindex;
5659        // Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
5660        $this->_indexIsDirty = TRUE;
5661        
5662        $absoluteXPath = $xPathSet[$i];
5663        $parentNode =& $this->nodeIndex[$absoluteXPath];
5664        $newNode =& $this->cloneNode($node);
5665        $parentNode['childNodes'][] =& $newNode;
5666        $pos = count($parentNode['textParts']);
5667        $pos -= $afterText ? 0 : 1;
5668        $parentNode['textParts'] = array_merge(
5669                                     array_slice($parentNode['textParts'], 0, $pos),
5670                                     array(''),
5671                                     array_slice($parentNode['textParts'], $pos)
5672                                   );
5673        // We are going from bottom to top, but the user will want results from top to bottom.
5674        if ($mustReindex) {
5675          // We'll have to wait till after the reindex to get the full path to this new node.
5676          $newNodes[] = &$newNode;
5677        } else {
5678          // If we are reindexing the tree later, then we can't return the user any
5679          // useful results, so we just return them the count.
5680          array_unshift($result, "$absoluteXPath/{$newNode['name']}");
5681        }
5682      }
5683      if ($mustReindex) {
5684        $this->reindexNodeTree();
5685        // Now we must fill in the result array.  Because until now we did not
5686        // know what contextpos our newly added entries had, just their pos within
5687        // the siblings.
5688        foreach ($newNodes as $newNode) {
5689          array_unshift($result, $newNode['xpath']);
5690        }
5691      } 
5692      if (count($result) == 1) $result = $result[0];
5693      return $result;
5694    }
5695    
5696    /**
5697     * Inserts a node before the reference node with the same parent.
5698     *
5699     * If you intend to do a lot of appending, you should leave autoIndex as FALSE
5700     * and then call reindexNodeTree() when you are finished all the appending.
5701     *
5702     * @param  $xPathQuery  (string) Xpath to the node to insert new node before
5703     * @param  $node        (mixed)  String or Array (Usually a String)
5704     *                               If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
5705     *                               If array:  A Node (can be a whole sub-tree) (See comment in header)
5706     * @param  $afterText   (bool)   (optional, default=FLASE) Insert after the text.
5707     * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the document to reflect 
5708     *                               the changes.  A performance helper.  See reindexNodeTree()
5709     * @return              (mixed)  FALSE on error (or no match). On success we return the path(s) to the newly
5710     *                               appended nodes. That is: Array of paths if more then 1 node was added or
5711     *                               a single path string if only one node was added.
5712     *                               NOTE:  If autoReindex is FALSE, then we can't return the *complete* path
5713     *                               as the exact doc-pos isn't available without reindexing. In that case we leave
5714     *                               out the last [docpos] in the path(s). ie  we'd return /A[3]/B instead of /A[3]/B[2]
5715     * @see    reindexNodeTree()
5716     */
5717    function insertBefore($xPathQuery, $node, $afterText=TRUE, $autoReindex=TRUE) {
5718      return $this->insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText, $autoReindex);
5719    }
5720    
5721  
5722    //-----------------------------------------------------------------------------------------
5723    // XPath                     ------  Attribute  Set/Get  ------                            
5724    //-----------------------------------------------------------------------------------------
5725    
5726    /** 
5727     * Retrieves a dedecated attribute value or a hash-array of all attributes of a node.
5728     * 
5729     * The first param $absoluteXPath must be a valid xpath OR a xpath-query that results 
5730     * to *one* xpath. If the second param $attrName is not set, a hash-array of all attributes 
5731     * of that node is returned.
5732     *
5733     * Optionally you may pass an attrubute name in $attrName and the function will return the 
5734     * string value of that attribute.
5735     *
5736     * @param  $absoluteXPath (string) Full xpath OR a xpath-query that results to *one* xpath.
5737     * @param  $attrName      (string) (Optional) The name of the attribute. See above.
5738     * @return                (mixed)  hash-array or a string of attributes depending if the 
5739     *                                 parameter $attrName was set (see above).  FALSE if the 
5740     *                                 node or attribute couldn't be found.
5741     * @see    setAttribute(), removeAttribute()
5742     */
5743    function getAttributes($absoluteXPath, $attrName=NULL) {
5744      // Numpty check
5745      if (!isSet($this->nodeIndex[$absoluteXPath])) {
5746        $xPathSet = $this->_resolveXPathQuery($absoluteXPath,'getAttributes');
5747        if (empty($xPathSet)) return FALSE;
5748        // only use the first entry
5749        $absoluteXPath = $xPathSet[0];
5750      }
5751      if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
5752          // Case in-sensitive
5753          $attrName = strtoupper($attrName);
5754      }
5755      
5756      // Return the complete list or just the desired element
5757      if (is_null($attrName)) {
5758        return $this->nodeIndex[$absoluteXPath]['attributes'];
5759      } elseif (isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attrName])) {
5760        return $this->nodeIndex[$absoluteXPath]['attributes'][$attrName];
5761      }
5762      return FALSE;
5763    }
5764    
5765    /**
5766     * Set attributes of a node(s).
5767     *
5768     * This method sets a number single attributes. An existing attribute is overwritten (default)
5769     * with the new value, but setting the last param to FALSE will prevent overwritten.
5770     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5771     *       Depending on setModMatch() one, none or multiple nodes are affected.
5772     *
5773     * @param  $xPathQuery (string) xpath to the node (See note above).
5774     * @param  $name       (string) Attribute name.
5775     * @param  $value      (string) Attribute value.   
5776     * @param  $overwrite  (bool)   If the attribute is already set we overwrite it (see text above)
5777     * @return             (bool)   TRUE on success, FALSE on failure.
5778     * @see    getAttributes(), removeAttribute()
5779     */
5780    function setAttribute($xPathQuery, $name, $value, $overwrite=TRUE) {
5781      return $this->setAttributes($xPathQuery, array($name => $value), $overwrite);
5782    }
5783    
5784    /**
5785     * Version of setAttribute() that sets multiple attributes to node(s).
5786     *
5787     * This method sets a number of attributes. Existing attributes are overwritten (default)
5788     * with the new values, but setting the last param to FALSE will prevent overwritten.
5789     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5790     *       Depending on setModMatch() one, none or multiple nodes are affected.
5791     *
5792     * @param  $xPathQuery (string) xpath to the node (See note above).
5793     * @param  $attributes (array)  associative array of attributes to set.
5794     * @param  $overwrite  (bool)   If the attributes are already set we overwrite them (see text above)
5795     * @return             (bool)   TRUE on success, FALSE otherwise
5796     * @see    setAttribute(), getAttributes(), removeAttribute()
5797     */
5798    function setAttributes($xPathQuery, $attributes, $overwrite=TRUE) {
5799      $status = FALSE;
5800      do { // try-block
5801        // The attributes parameter should be an associative array.
5802        if (!is_array($attributes)) break;  // try-block
5803        
5804        // Check for a valid xPathQuery
5805        $xPathSet = $this->_resolveXPathQuery($xPathQuery,'setAttributes');
5806        foreach($xPathSet as $absoluteXPath) {
5807          // Add the attributes to the node.
5808          $theNode =& $this->nodeIndex[$absoluteXPath];
5809          if (empty($theNode['attributes'])) {
5810            $this->nodeIndex[$absoluteXPath]['attributes'] = $attributes;
5811          } else {
5812            $theNode['attributes'] = $overwrite ? array_merge($theNode['attributes'],$attributes) : array_merge($attributes, $theNode['attributes']);
5813          }
5814        }
5815        $status = TRUE;
5816      } while(FALSE); // END try-block
5817      
5818      return $status;
5819    }
5820    
5821    /**
5822     * Removes an attribute of a node(s).
5823     *
5824     * This method removes *ALL* attributres per default unless the second parameter $attrList is set.
5825     * $attrList can be either a single attr-name as string OR a vector of attr-names as array.
5826     * E.g. 
5827     *  removeAttribute(<xPath>);                     # will remove *ALL* attributes.
5828     *  removeAttribute(<xPath>, 'A');                # will only remove attributes called 'A'.
5829     *  removeAttribute(<xPath>, array('A_1','A_2')); # will remove attribute 'A_1' and 'A_2'.
5830     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5831     *       Depending on setModMatch() one, none or multiple nodes are affected.
5832     *
5833     * @param   $xPathQuery (string) xpath to the node (See note above).
5834     * @param   $attrList   (mixed)  (optional) if not set will delete *all* (see text above)
5835     * @return              (bool)   TRUE on success, FALSE if the node couldn't be found
5836     * @see     getAttributes(), setAttribute()
5837     */
5838    function removeAttribute($xPathQuery, $attrList=NULL) {
5839      // Check for a valid xPathQuery
5840      $xPathSet = $this->_resolveXPathQuery($xPathQuery, 'removeAttribute');
5841      
5842      if (!empty($attrList) AND is_string($attrList)) $attrList = array($attrList);
5843      if (!is_array($attrList)) return FALSE;
5844      
5845      foreach($xPathSet as $absoluteXPath) {
5846        // If the attribute parameter wasn't set then remove all the attributes
5847        if ($attrList[0] === NULL) {
5848          $this->nodeIndex[$absoluteXPath]['attributes'] = array();
5849          continue; 
5850        }
5851        // Remove all the elements in the array then.
5852        foreach($attrList as $name) {
5853          unset($this->nodeIndex[$absoluteXPath]['attributes'][$name]);
5854        }
5855      }
5856      return TRUE;
5857    }
5858    
5859    //-----------------------------------------------------------------------------------------
5860    // XPath                        ------  Text  Set/Get  ------                              
5861    //-----------------------------------------------------------------------------------------
5862    
5863    /**
5864     * Retrieve all the text from a node as a single string.
5865     *
5866     * Sample  
5867     * Given is: <AA> This <BB\>is <BB\>  some<BB\>text </AA>
5868     * Return of getData('/AA[1]') would be:  " This is   sometext "
5869     * The first param $xPathQuery must be a valid xpath OR a xpath-query that 
5870     * results to *one* xpath. 
5871     *
5872     * @param  $xPathQuery (string) xpath to the node - resolves to *one* xpath.
5873     * @return             (mixed)  The returned string (see above), FALSE if the node 
5874     *                              couldn't be found or is not unique.
5875     * @see getDataParts()
5876     */
5877    function getData($xPathQuery) {
5878      $aDataParts = $this->getDataParts($xPathQuery);
5879      if ($aDataParts === FALSE) return FALSE;
5880      return implode('', $aDataParts);
5881    }
5882    
5883    /**
5884     * Retrieve all the text from a node as a vector of strings
5885     * 
5886     * Where each element of the array was interrupted by a non-text child element.
5887     *
5888     * Sample  
5889     * Given is: <AA> This <BB\>is <BB\>  some<BB\>text </AA>
5890     * Return of getDataParts('/AA[1]') would be:  array([0]=>' This ', [1]=>'is ', [2]=>'  some', [3]=>'text ');
5891     * The first param $absoluteXPath must be a valid xpath OR a xpath-query that results 
5892     * to *one* xpath. 
5893     *
5894     * @param  $xPathQuery (string) xpath to the node - resolves to *one* xpath.
5895     * @return             (mixed)  The returned array (see above), or FALSE if node is not 
5896     *                              found or is not unique.
5897     * @see getData()
5898     */
5899    function getDataParts($xPathQuery) {
5900      // Resolve xPath argument
5901      $xPathSet = $this->_resolveXPathQuery($xPathQuery, 'getDataParts');
5902      if (1 !== ($setSize=count($xPathSet))) {
5903        $this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery) . "Not unique xpath-query, matched {$setSize}-times.", __LINE__, __FILE__, FALSE);
5904        return FALSE;
5905      }
5906      $absoluteXPath = $xPathSet[0];
5907      // Is it an attribute node?
5908      if (preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches)) {
5909        $absoluteXPath = $matches[1];
5910        $attribute = $matches[2];
5911        if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
5912          $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
5913          continue;
5914        }
5915        return array($this->nodeIndex[$absoluteXPath]['attributes'][$attribute]);
5916      } else if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) {
5917        $absoluteXPath = $matches[1];
5918        $textPartNr = $matches[2];      
5919        return array($this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr]);
5920      } else {
5921        return $this->nodeIndex[$absoluteXPath]['textParts'];
5922      }
5923    }
5924    
5925    /**
5926     * Retrieves a sub string of a text-part OR attribute-value.
5927     *
5928     * This method retrieves the sub string of a specific text-part OR (if the 
5929     * $absoluteXPath references an attribute) the the sub string  of the attribute value.
5930     * If no 'direct referencing' is used (Xpath ends with text()[<part-number>]), then 
5931     * the first text-part of the node ist returned (if exsiting).
5932     *
5933     * @param  $absoluteXPath (string) Xpath to the node (See note above).   
5934     * @param  $offset        (int)    (optional, default is 0) Starting offset. (Just like PHP's substr())
5935     * @param  $count         (number) (optional, default is ALL) Character count  (Just like PHP's substr())
5936     * @return                (mixed)  The sub string, FALSE if not found or on error
5937     * @see    XPathEngine::wholeText(), PHP's substr()
5938     */
5939    function substringData($absoluteXPath, $offset = 0, $count = NULL) {
5940      if (!($text = $this->wholeText($absoluteXPath))) return FALSE;
5941      if (is_null($count)) {
5942        return substr($text, $offset);
5943      } else {
5944        return substr($text, $offset, $count);
5945      } 
5946    }
5947    
5948    /**
5949     * Replace a sub string of a text-part OR attribute-value.
5950     *
5951     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5952     *       Depending on setModMatch() one, none or multiple nodes are affected.
5953     *
5954     * @param  $xPathQuery    (string) xpath to the node (See note above).
5955     * @param  $replacement   (string) The string to replace with.
5956     * @param  $offset        (int)    (optional, default is 0) Starting offset. (Just like PHP's substr_replace ())
5957     * @param  $count         (number) (optional, default is 0=ALL) Character count  (Just like PHP's substr_replace())
5958     * @param  $textPartNr    (int)    (optional) (see _getTextSet() )
5959     * @return                (bool)   The new string value on success, FALSE if not found or on error
5960     * @see    substringData()
5961     */
5962    function replaceData($xPathQuery, $replacement, $offset = 0, $count = 0, $textPartNr=1) {
5963      if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
5964      $tSize=sizeOf($textSet);
5965      for ($i=0; $i<$tSize; $i++) {
5966        if ($count) {
5967          $textSet[$i] = substr_replace($textSet[$i], $replacement, $offset, $count);
5968        } else {
5969          $textSet[$i] = substr_replace($textSet[$i], $replacement, $offset);
5970        } 
5971      }
5972      return TRUE;
5973    }
5974    
5975    /**
5976     * Insert a sub string in a text-part OR attribute-value.
5977     *
5978     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5979     *       Depending on setModMatch() one, none or multiple nodes are affected.
5980     *
5981     * @param  $xPathQuery (string) xpath to the node (See note above).
5982     * @param  $data       (string) The string to replace with.
5983     * @param  $offset     (int)    (optional, default is 0) Offset at which to insert the data.
5984     * @return             (bool)   The new string on success, FALSE if not found or on error
5985     * @see    replaceData()
5986     */
5987    function insertData($xPathQuery, $data, $offset=0) {
5988      return $this->replaceData($xPathQuery, $data, $offset, 0);
5989    }
5990    
5991    /**
5992     * Append text data to the end of the text for an attribute OR node text-part.
5993     *
5994     * This method adds content to a node. If it's an attribute node, then
5995     * the value of the attribute will be set, otherwise the passed data will append to 
5996     * character data of the node text-part. Per default the first text-part is taken.
5997     *
5998     * NOTE: When passing a xpath-query instead of an abs. Xpath.
5999     *       Depending on setModMatch() one, none or multiple nodes are affected.
6000     *
6001     * @param   $xPathQuery (string) to the node(s) (See note above).
6002     * @param   $data       (string) String containing the content to be added.
6003     * @param   $textPartNr (int)    (optional, default is 1) (see _getTextSet())
6004     * @return              (bool)   TRUE on success, otherwise FALSE
6005     * @see     _getTextSet()
6006     */
6007    function appendData($xPathQuery, $data, $textPartNr=1) {
6008      if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
6009      $tSize=sizeOf($textSet);
6010      for ($i=0; $i<$tSize; $i++) {
6011        $textSet[$i] .= $data;
6012      }
6013      return TRUE;
6014    }
6015    
6016    /**
6017     * Delete the data of a node.
6018     *
6019     * This method deletes content of a node. If it's an attribute node, then
6020     * the value of the attribute will be removed, otherwise the node text-part. 
6021     * will be deleted.  Per default the first text-part is deleted.
6022     *
6023     * NOTE: When passing a xpath-query instead of an abs. Xpath.
6024     *       Depending on setModMatch() one, none or multiple nodes are affected.
6025     *
6026     * @param  $xPathQuery (string) to the node(s) (See note above).
6027     * @param  $offset     (int)    (optional, default is 0) Starting offset. (Just like PHP's substr_replace())
6028     * @param  $count      (number) (optional, default is 0=ALL) Character count.  (Just like PHP's substr_replace())
6029     * @param  $textPartNr (int)    (optional, default is 0) the text part to delete (see _getTextSet())
6030     * @return             (bool)   TRUE on success, otherwise FALSE
6031     * @see     _getTextSet()
6032     */
6033    function deleteData($xPathQuery, $offset=0, $count=0, $textPartNr=1) {
6034      if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
6035      $tSize=sizeOf($textSet);
6036      for ($i=0; $i<$tSize; $i++) {
6037        if (!$count)
6038          $textSet[$i] = "";
6039        else
6040          $textSet[$i] = substr_replace($textSet[$i],'', $offset, $count);
6041      } 
6042      return TRUE;
6043    }
6044   
6045    //-----------------------------------------------------------------------------------------
6046    // XPath                      ------  Help Stuff  ------                                   
6047    //-----------------------------------------------------------------------------------------
6048     
6049    /**
6050     * Parse the XML to a node-tree. A so called 'document'
6051     *
6052     * @param  $xmlString (string) The string to turn into a document node.
6053     * @return            (&array)  a node-tree
6054     */
6055    function &_xml2Document($xmlString) {
6056      $xmlOptions = array(
6057                      XML_OPTION_CASE_FOLDING => $this->getProperties('caseFolding'), 
6058                      XML_OPTION_SKIP_WHITE   => $this->getProperties('skipWhiteSpaces')
6059                    );
6060      $xmlParser =& new XPathEngine($xmlOptions);
6061      $xmlParser->setVerbose($this->properties['verboseLevel']);
6062      // Parse the XML string
6063      if (!$xmlParser->importFromString($xmlString)) {
6064        $this->_displayError($xmlParser->getLastError(), __LINE__, __FILE__, FALSE);
6065        return FALSE;
6066      }
6067      return $xmlParser->getNode('/');
6068    }
6069    
6070    /**
6071     * Get a reference-list to node text part(s) or node attribute(s).
6072     * 
6073     * If the Xquery references an attribute(s) (Xquery ends with attribute::), 
6074     * then the text value of the node-attribute(s) is/are returned.
6075     * Otherwise the Xquery is referencing to text part(s) of node(s). This can be either a 
6076     * direct reference to text part(s) (Xquery ends with text()[<nr>]) or indirect reference 
6077     * (a simple Xquery to node(s)).
6078     * 1) Direct Reference (Xquery ends with text()[<part-number>]):
6079     *   If the 'part-number' is omitted, the first text-part is assumed; starting by 1.
6080     *   Negative numbers are allowed, where -1 is the last text-part a.s.o.
6081     * 2) Indirect Reference (a simple  Xquery to node(s)):
6082     *   Default is to return the first text part(s). Optionally you may pass a parameter 
6083     *   $textPartNr to define the text-part you want;  starting by 1.
6084     *   Negative numbers are allowed, where -1 is the last text-part a.s.o.
6085     *
6086     * NOTE I : The returned vector is a set of references to the text parts / attributes.
6087     *          This is handy, if you wish to modify the contents.
6088     * NOTE II: text-part numbers out of range will not be in the list
6089     * NOTE III:Instead of an absolute xpath you may also pass a xpath-query.
6090     *          Depending on setModMatch() one, none or multiple nodes are affected.
6091     *
6092     * @param   $xPathQuery (string) xpath to the node (See note above).
6093     * @param   $textPartNr (int)    String containing the content to be set.
6094     * @return              (mixed)  A vector of *references* to the text that match, or 
6095     *                               FALSE on error
6096     * @see XPathEngine::wholeText()
6097     */
6098    function _getTextSet($xPathQuery, $textPartNr=1) {
6099      $ThisFunctionName = '_getTextSet';
6100      $bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
6101      $this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
6102      if ($bDebugThisFunction) {
6103        echo "Node: $xPathQuery\n";
6104        echo "Text Part Number: $textPartNr\n";
6105        echo "<hr>";
6106      }
6107      
6108      $status = FALSE;
6109      $funcName = '_getTextSet';
6110      $textSet = array();
6111      
6112      do { // try-block
6113        // Check if it's a Xpath reference to an attribut(s). Xpath ends with attribute::)
6114        if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $xPathQuery, $matches)) {
6115          $xPathQuery = $matches[1];
6116          $attribute = $matches[3];
6117          // Quick out
6118          if (isSet($this->nodeIndex[$xPathQuery])) {
6119            $xPathSet[] = $xPathQuery;
6120          } else {
6121            // Try to evaluate the absoluteXPath (since it seems to be an Xquery and not an abs. Xpath)
6122            $xPathSet = $this->_resolveXPathQuery("$xPathQuery/attribute::$attribute", $funcName);
6123          }
6124          foreach($xPathSet as $absoluteXPath) {
6125            preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches);
6126            $absoluteXPath = $matches[1];
6127            $attribute = $matches[2];
6128            if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
6129              $this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
6130              continue;
6131            }
6132            $textSet[] =& $this->nodes[$absoluteXPath]['attributes'][$attribute];
6133          }
6134          $status = TRUE;
6135          break; // try-block
6136        }
6137        
6138        // Check if it's a Xpath reference direct to a text-part(s). (xpath ends with text()[<part-number>])
6139        if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) {
6140          $xPathQuery = $matches[1];
6141          // default to the first text node if a text node was not specified
6142          $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
6143          // Quick check
6144          if (isSet($this->nodeIndex[$xPathQuery])) {
6145            $xPathSet[] = $xPathQuery;
6146          } else {
6147            // Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath)
6148            $xPathSet = $this->_resolveXPathQuery("$xPathQuery/text()[$textPartNr]", $funcName);
6149          }
6150        }
6151        else {
6152          // At this point we have been given an xpath with neither a 'text()' or 'attribute::' axis at the end
6153          // So this means to get the text-part of the node. If parameter $textPartNr was not set, use the last
6154          // text-part.
6155          if (isSet($this->nodeIndex[$xPathQuery])) {
6156            $xPathSet[] = $xPathQuery;
6157          } else {
6158            // Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath)
6159            $xPathSet = $this->_resolveXPathQuery($xPathQuery, $funcName);
6160          }
6161        }
6162  
6163        if ($bDebugThisFunction) {
6164          echo "Looking up paths for:\n";
6165          print_r($xPathSet);
6166        }
6167  
6168        // Now fetch all text-parts that match. (May be 0,1 or many)
6169        foreach($xPathSet as $absoluteXPath) {
6170          unset($text);
6171          if ($text =& $this->wholeText($absoluteXPath, $textPartNr)) {
6172            $textSet[] =& $text;
6173          } else {
6174            // The node does not yet have any text, so we have to add a '' string so that
6175            // if we insert or replace to it, then we'll actually have something to op on.
6176            $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1] = '';
6177            $textSet[] =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1];
6178          }
6179        }
6180  
6181        $status = TRUE;
6182      } while (FALSE); // END try-block
6183      
6184      if (!$status) $result = FALSE;
6185      else          $result = $textSet;
6186  
6187      $this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
6188  
6189      return $result;
6190    }
6191    
6192  
6193    /**
6194     * Resolves an xPathQuery vector for a node op for modification
6195     *
6196     * It is possible to create a brand new object, and try to append and insert nodes
6197     * into it, so this is a version of _resolveXPathQuery() that will autocreate the
6198     * super root if it detects that it is not present and the $xPathQuery is empty.
6199     *
6200     * Also it demands that there be at least one node returned, and displays a suitable
6201     * error message if the returned xPathSet does not contain any nodes.
6202     * 
6203     * @param  $xPathQuery (string) An xpath query targeting a single node.  If empty() 
6204     *                              returns the root node and auto creates the root node
6205     *                              if it doesn't exist.
6206     * @param  $function   (string) The function in which this check was called
6207     * @return             (array)  Vector of $absoluteXPath's (May be empty)
6208     * @see    _resolveXPathQuery()
6209     */
6210    function _resolveXPathQueryForNodeMod($xPathQuery, $functionName) {
6211      $xPathSet = array();
6212      if (empty($xPathQuery)) {
6213        // You can append even if the root node doesn't exist.
6214        if (!isset($this->nodeIndex[$xPathQuery])) $this->_createSuperRoot();
6215        $xPathSet[] = '';
6216        // However, you can only append to the super root, if there isn't already a root entry.
6217        $rootNodes = $this->_resolveXPathQuery('/*','appendChild');
6218        if (count($rootNodes) !== 0) {
6219          $this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE);
6220          return array();
6221        }
6222      } else {
6223        $xPathSet = $this->_resolveXPathQuery($xPathQuery,'appendChild');
6224        if (sizeOf($xPathSet) === 0) {
6225          $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
6226          return array();
6227        }
6228      }
6229      return $xPathSet;
6230    }
6231  
6232    /**
6233     * Resolves an xPathQuery vector depending on the property['modMatch']
6234     * 
6235     * To:
6236     *   - all matches, 
6237     *   - the first
6238     *   - none (If the query matches more then one node.)
6239     * see  setModMatch() for details
6240     * 
6241     * @param  $xPathQuery (string) An xpath query targeting a single node.  If empty() 
6242     *                              returns the root node (if it exists).
6243     * @param  $function   (string) The function in which this check was called
6244     * @return             (array)  Vector of $absoluteXPath's (May be empty)
6245     * @see    setModMatch()
6246     */
6247    function _resolveXPathQuery($xPathQuery, $function) {
6248      $xPathSet = array();
6249      do { // try-block
6250        if (isSet($this->nodeIndex[$xPathQuery])) {
6251          $xPathSet[] = $xPathQuery;
6252          break; // try-block
6253        }
6254        if (empty($xPathQuery)) break; // try-block
6255        if (substr($xPathQuery, -1) === '/') break; // If the xPathQuery ends with '/' then it cannot be a good query.
6256        // If this xPathQuery is not absolute then attempt to evaluate it
6257        $xPathSet = $this->match($xPathQuery);
6258        
6259        $resultSize = sizeOf($xPathSet);
6260        switch($this->properties['modMatch']) {
6261          case XPATH_QUERYHIT_UNIQUE : 
6262            if ($resultSize >1) {
6263              $xPathSet = array();
6264              if ($this->properties['verboseLevel']) $this->_displayError("Canceled function '{$function}'. The query '{$xPathQuery}' mached {$resultSize} nodes and 'modMatch' is set to XPATH_QUERYHIT_UNIQUE.", __LINE__, __FILE__, FALSE);
6265            }
6266            break;
6267          case XPATH_QUERYHIT_FIRST : 
6268            if ($resultSize >1) {
6269              $xPathSet = array($xPathSet[0]);
6270              if ($this->properties['verboseLevel']) $this->_displayError("Only modified first node in function '{$function}' because the query '{$xPathQuery}' mached {$resultSize} nodes and 'modMatch' is set to XPATH_QUERYHIT_FIRST.", __LINE__, __FILE__, FALSE);
6271            }
6272            break;
6273          default: ; // DO NOTHING
6274        }
6275      } while (FALSE);
6276      
6277      if ($this->properties['verboseLevel'] >= 2) $this->_displayMessage("'{$xPathQuery}' parameter from '{$function}' returned the following nodes: ".(count($xPathSet)?implode('<br>', $xPathSet):'[none]'), __LINE__, __FILE__);
6278      return $xPathSet;
6279    }
6280  } // END OF CLASS XPath
6281  
6282  // -----------------------------------------------------------------------------------------
6283  // -----------------------------------------------------------------------------------------
6284  // -----------------------------------------------------------------------------------------
6285  // -----------------------------------------------------------------------------------------
6286  
6287  /**************************************************************************************************
6288  // Usage Sample:
6289  // -------------
6290  // Following code will give you an idea how to work with PHP.XPath. It's a working sample
6291  // to help you get started. :o)
6292  // Take the comment tags away and run this file.
6293  **************************************************************************************************/
6294  
6295  /**
6296   * Produces a short title line.
6297   */
6298  function _title($title) { 
6299    echo "<br><hr><b>" . htmlspecialchars($title) . "</b><hr>\n";
6300  }
6301  
6302  $self = isSet($_SERVER) ? $_SERVER['PHP_SELF'] : $PHP_SELF;
6303  if (basename($self) == 'XPath.class.php') {
6304    // The sampe source:
6305    $q = '?';
6306    $xmlSource = <<< EOD
6307    <{$q}Process_Instruction test="&copy;&nbsp;All right reserved" {$q}>
6308      <AAA foo="bar"> ,,1,,
6309        ..1.. <![CDATA[ bla  bla 
6310        newLine blo blo ]]>
6311        <BBB foo="bar">
6312          ..2..
6313        </BBB>..3..<CC/>   ..4..</AAA> 
6314  EOD;
6315    
6316    // The sample code:
6317    $xmlOptions = array(XML_OPTION_CASE_FOLDING => TRUE, XML_OPTION_SKIP_WHITE => TRUE);
6318    $xPath =& new XPath(FALSE, $xmlOptions);
6319    //$xPath->bDebugXmlParse = TRUE;
6320    if (!$xPath->importFromString($xmlSource)) { echo $xPath->getLastError(); exit; }
6321    
6322    _title("Following was imported:");
6323    echo $xPath->exportAsHtml();
6324    
6325    _title("Get some content");
6326    echo "Last text part in &lt;AAA&gt;: '" . $xPath->wholeText('/AAA[1]', -1) ."'<br>\n";
6327    echo "All the text in  &lt;AAA&gt;: '" . $xPath->wholeText('/AAA[1]') ."'<br>\n";
6328    echo "The attibute value  in  &lt;BBB&gt; using getAttributes('/AAA[1]/BBB[1]', 'FOO'): '" . $xPath->getAttributes('/AAA[1]', 'FOO') ."'<br>\n";
6329    echo "The attibute value  in  &lt;BBB&gt; using getData('/AAA[1]/@FOO'): '" . $xPath->getData('/AAA[1]/@FOO') ."'<br>\n";
6330    
6331    _title("Append some additional XML below /AAA/BBB:");
6332    $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 1. Append new node </CCC>', $afterText=FALSE);
6333    $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 2. Append new node </CCC>', $afterText=TRUE);
6334    $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 3. Append new node </CCC>', $afterText=TRUE);
6335    echo $xPath->exportAsHtml();
6336    
6337    _title("Insert some additional XML below <AAA>:");
6338    $xPath->reindexNodeTree();
6339    $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 1. Insert new node </BB>', $shiftRight=TRUE, $afterText=TRUE);
6340    $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 2. Insert new node </BB>', $shiftRight=FALSE, $afterText=TRUE);
6341    $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 3. Insert new node </BB>', $shiftRight=FALSE, $afterText=FALSE);
6342    echo $xPath->exportAsHtml();
6343  
6344    _title("Replace the last <BB> node with new XML data '&lt;DDD&gt; Replaced last BB &lt;/DDD&gt;':");
6345    $xPath->reindexNodeTree();
6346    $xPath->replaceChild('/AAA[1]/BB[last()]', '<DDD> Replaced last BB </DDD>', $afterText=FALSE);
6347    echo $xPath->exportAsHtml();
6348    
6349    _title("Replace second <BB> node with normal text");
6350    $xPath->reindexNodeTree();
6351    $xPath->replaceChildByData('/AAA[1]/BB[2]', '"Some new text"');
6352    echo $xPath->exportAsHtml();
6353  }
6354  
6355  ?>


Généré le : Sun Feb 25 17:20:01 2007 par Balluche grâce à PHPXref 0.7