home *** CD-ROM | disk | FTP | other *** search
/ Chip 2004 April / CMCD0404.ISO / Software / Freeware / Programare / groupoffice-com-2.01 / classes / xpath.class.inc < prev    next >
Text File  |  2004-03-08  |  253KB  |  5,735 lines

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