phpgroupware-cvs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Phpgroupware-cvs] phpsysinfo/includes table_vitals.php, 1.2 table_netwo


From: skwashd
Subject: [Phpgroupware-cvs] phpsysinfo/includes table_vitals.php, 1.2 table_network.php, 1.2 system_functions.php, 1.2 table_filesystems.php, 1.2 table_memory.php, 1.2 table_hardware.php, 1.2 system_footer.php, 1.3 system_header.php, 1.3 common_functions.php, 1.3 XPath.class.php, 1.3 index.html, 1.1
Date: Sat, 19 Nov 2005 06:29:00 +0100

Update of phpsysinfo/includes

Removed Files:
     Branch: MAIN
            table_vitals.php
            table_network.php
            system_functions.php
            table_filesystems.php
            table_memory.php
            table_hardware.php
Modified Files:
     Branch: MAIN
            system_footer.php lines: +63 -44
            system_header.php lines: +25 -14
            common_functions.php lines: +50 -35
            XPath.class.php lines: +2486 -971
Added Files:
     Branch: MAIN
            index.html 

Log Message:
sync with current stable from upstream - version 2.4.1, added some fixes and 
improvements along the way

====================================================
Index: phpsysinfo/includes/system_footer.php
diff -u phpsysinfo/includes/system_footer.php:1.2 
phpsysinfo/includes/system_footer.php:1.3
--- phpsysinfo/includes/system_footer.php:1.2   Sun May 15 12:46:44 2005
+++ phpsysinfo/includes/system_footer.php       Sat Nov 19 05:29:55 2005
@@ -18,62 +18,81 @@
 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 //
 // $Id$
-
-echo "<center>";
-
-$update_form = "<form method=\"POST\" action=\"$PHP_SELF\">\n"
-             . "\t" . $text['template'] . ":&nbsp;\n"
-             . "\t<select name=\"template\">\n";
-
-$dir = opendir('templates/');
-while (($file = readdir($dir))!=false) {
-    if ($file != 'CVS' && $file != '.' && $file != '..') {
-        $update_form .= "\t\t<option value=\"$file\"";
-        if ($template == $file && !$random) {
-            $update_form .= " SELECTED";
+//
+if (!$hide_picklist) {
+  echo "<center>";
+
+  $update_form = "<form method=\"POST\" action=\"" . $_SERVER['PHP_SELF'] . 
"\">\n" . "\t" . $text['template'] . ":&nbsp;\n" . "\t<select 
name=\"template\">\n";
+
+  $dir = opendir('templates/');
+  while (false !== ($file = readdir($dir))) {
+    if ($file != 'CVS' && $file[0] != '.' && is_dir('templates/' . $file)) {
+      $filelist[] = $file;
+    }
+  }
+  closedir($dir);
+
+  asort($filelist);
+
+  while (list ($key, $val) = each ($filelist)) {
+    if ($_COOKIE['template'] == $val) {
+      $update_form .= "\t\t<option value=\"$val\" SELECTED>$val</option>\n";
+    } else {
+      $update_form .= "\t\t<option value=\"$val\">$val</option>\n";
         }
-        $update_form .= ">$file</option>\n";
     }
-}
-closedir($dir);

-// auto select the random template, if we're set to random
-$update_form .= "\t\t<option value=\"random\"";
-if ($random) {
+  $update_form .= "\t\t<option value=\"xml\">XML</option>\n";
+  // auto select the random template, if we're set to random
+  $update_form .= "\t\t<option value=\"random\"";
+  if ($_COOKIE['template']=='random') {
     $update_form .= " SELECTED";
-}
-$update_form .= ">random</option>\n";
+  }
+  $update_form .= ">random</option>\n";
+
+  $update_form .= "\t</select>\n";

-$update_form .= "\t</select>\n";
+  $update_form .= "\t&nbsp;&nbsp;" . $text['language'] . ":&nbsp;\n" . 
"\t<select name=\"lng\">\n";

+  unset($filelist);

-$update_form .= "\t&nbsp;" . $text['language'] . ":&nbsp;\n"
-             . "\t<select name=\"lng\">\n";
+  $dir = opendir('includes/lang/');
+  while (false !== ($file = readdir($dir))) {
+    if ($file[0] != '.' && is_file('includes/lang/' . $file) && 
eregi("\.php$", $file)) {
+      $filelist[] = eregi_replace('.php', '', $file);
+    }
+  }
+  closedir($dir);

-$dir = opendir('includes/lang/');
-while (($file = readdir($dir)) != false) {
-    if ($file != 'CVS' && $file != '.' && $file != '..') {
-        $file = ereg_replace('.php', '', $file);
-        if ($lng == $file) {
-            $update_form .= "\t\t<option value=\"$file\" 
SELECTED>$file</option>\n";
+  asort($filelist);
+
+  while (list ($key, $val) = each ($filelist)) {
+    if ($_COOKIE['lng'] == $val) {
+      $update_form .= "\t\t<option value=\"$val\" SELECTED>$val</option>\n";
         } else {
-            $update_form .= "\t\t<option value=\"$file\">$file</option>\n";
+      $update_form .= "\t\t<option value=\"$val\">$val</option>\n";
         }
     }
-}
-closedir($dir);

+       $update_form .= "\t\t<option value=\"browser\"";
+  if ($_COOKIE['lng']=='browser') {
+    $update_form .= " SELECTED";
+  }
+  $update_form .= ">browser default</option>\n";
+
+  $update_form .= "\t</select>\n" . "\t<input type=\"submit\" value=\"" . 
$text['submit'] . "\">\n" . "</form>\n";

-$update_form .= "\t</select>\n"
-              . "\t<input type=\"submit\" value=\"" . $text['submit'] . "\">\n"
-              . "</form>\n";
+  echo $update_form;

-print $update_form;
-?>
+  echo "\n\n</center>";
+} else {
+  echo "\n\n<br>";
+}
+
+echo "\n<hr>\n" . $text['created'];

-</center>
+echo '<a href="http://phpsysinfo.sourceforge.net";>&nbsp;phpSysInfo-' . 
$VERSION . '</a> ' . strftime ($text['gen_time'], time());

-<hr>
-<?php echo $text['created']; ?> <a 
href="http://phpsysinfo.sourceforge.net";>phpSysInfo - <?php echo $VERSION ?></a>
-</body>
-</html>
+echo "\n<br>\n</body>\n</html>\n";
+
+?>

====================================================
Index: phpsysinfo/includes/system_header.php
diff -u phpsysinfo/includes/system_header.php:1.2 
phpsysinfo/includes/system_header.php:1.3
--- phpsysinfo/includes/system_header.php:1.2   Sun May 15 12:46:44 2005
+++ phpsysinfo/includes/system_header.php       Sat Nov 19 05:29:55 2005
@@ -1,64 +1,75 @@
 <?php
-//
+
 // phpSysInfo - A PHP System Information Script
 // http://phpsysinfo.sourceforge.net/
-//
+
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
 // as published by the Free Software Foundation; either version 2
 // of the License, or (at your option) any later version.
-//
+
 // This program is distributed in the hope that it will be useful,
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 // GNU General Public License for more details.
-//
+
 // You should have received a copy of the GNU General Public License
 // along with this program; if not, write to the Free Software
 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
-//
-// $Id$

+// $Id$
 header("Cache-Control: no-cache, must-revalidate");
-if (!isset($charset)) { $charset='iso-8859-1'; }
-header('Content-Type: text/html; charset=' . $charset);
+if (!isset($charset)) {
+  $charset = 'iso-8859-1';
+}
+
+setlocale (LC_TIME, $text['locale']);

+header('Content-Type: text/html; charset=' . $charset);
 // our text direction (for hebrew)
 if (!$text_dir) {
     $text_dir = 'ltr';
 }

-// timestamp
-$timestamp = strftime ("%b %d %Y %H:%M:%S", time());
-
 ?>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>

 <?php
 echo created_by();
+
 ?>

 <head>
-    <title><?php echo $text['title']; echo " -- ($timestamp)"; ?></title>
+    <title>
+<?php
+global $XPath;
+echo $text['title'], " -- ", $XPath->getData('/phpsysinfo/Vitals/Hostname'), " 
--\n";

+?>
+    </title>
 <?php
+if (isset($charset) && $charset == 'euc-jp') {
+    echo "    <meta http-equiv=\"content-type\" 
content=\"text/html;charset=$charset\">\n";
+}
 if (isset($refresh) && ($refresh = intval($refresh))) {
     echo "    <meta http-equiv=\"Refresh\" content=\"$refresh\">\n";
 }
-if (file_exists("templates/$template/$template.css")) {
+if (file_exists(APP_ROOT . "/templates/$template/$template.css")) {
     echo '    <link rel="STYLESHEET" type="text/css" href="templates/';
     echo $template . '/' . $template;
     echo ".css\">";
 }
+
 ?>

 </head>

 <?php
-if (file_exists("templates/$template/images/$template" . "_background.gif")) {
+if (file_exists(APP_ROOT . "/templates/$template/images/$template" . 
"_background.gif")) {
   echo '<body background="templates/' . $template . '/images/' . $template . 
'_background.gif" dir="' . $text_dir . '">';
 } else {
   echo "<body dir=$text_dir>";
 }
+
 ?>

====================================================
Index: phpsysinfo/includes/common_functions.php
diff -u phpsysinfo/includes/common_functions.php:1.2 
phpsysinfo/includes/common_functions.php:1.3
--- phpsysinfo/includes/common_functions.php:1.2        Sun May 15 12:46:44 2005
+++ phpsysinfo/includes/common_functions.php    Sat Nov 19 05:29:55 2005
@@ -1,74 +1,67 @@
 <?php
-//
 // phpSysInfo - A PHP System Information Script
 // http://phpsysinfo.sourceforge.net/
-//
 // This program is free software; you can redistribute it and/or
 // modify it under the terms of the GNU General Public License
 // as published by the Free Software Foundation; either version 2
 // of the License, or (at your option) any later version.
-//
 // This program is distributed in the hope that it will be useful,
 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 // GNU General Public License for more details.
-//
 // You should have received a copy of the GNU General Public License
 // along with this program; if not, write to the Free Software
 // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
-//
 // $Id$
-
 // HTML/XML Comment
-function created_by () {
+function created_by ()
+{
     global $VERSION;
     return "<!--\n\tCreated By: phpSysInfo - 
$VERSION\n\thttp://phpsysinfo.sourceforge.net/\n-->\n\n";
 }
-
 // So that stupid warnings do not appear when we stats files that do not exist.
 error_reporting(5);
-
 // print out the bar graph
-function create_bargraph ($percent, $a, $b, $type = "") {
+function create_bargraph ($percent, $a, $b, $type = "")
+{
     if ($percent == 0) {
-        return '<img height="' . BAR_HEIGHT . '" src="templates/' . 
TEMPLATE_SET . '/images/bar_left.gif" alt="">'
-              . '<img src="templates/' . TEMPLATE_SET . 
'/images/bar_middle.gif" height="' . BAR_HEIGHT . '" width="1" alt="">'
-              . '<img src="templates/' . TEMPLATE_SET . 
'/images/bar_right.gif" height="' . BAR_HEIGHT . '" alt="">';
+    return '<img height="' . BAR_HEIGHT . '" src="templates/' . TEMPLATE_SET . 
'/images/bar_left.gif" alt="">' . '<img src="templates/' . TEMPLATE_SET . 
'/images/bar_middle.gif" height="' . BAR_HEIGHT . '" width="1" alt="">' . '<img 
src="templates/' . TEMPLATE_SET . '/images/bar_right.gif" height="' . 
BAR_HEIGHT . '" alt="">';
     } else if (($percent < 90) || ($type == "iso9660")) {
-        return '<img height="' . BAR_HEIGHT . '" src="templates/' . 
TEMPLATE_SET . '/images/bar_left.gif" alt="">'
-              . '<img src="templates/' . TEMPLATE_SET . 
'/images/bar_middle.gif" height="' . BAR_HEIGHT . '" width="' . ($a * $b) . '" 
alt="">'
-              . '<img height="' . BAR_HEIGHT . '" src="templates/' . 
TEMPLATE_SET . '/images/bar_right.gif" alt="">';
+    return '<img height="' . BAR_HEIGHT . '" src="templates/' . TEMPLATE_SET . 
'/images/bar_left.gif" alt="">' . '<img src="templates/' . TEMPLATE_SET . 
'/images/bar_middle.gif" height="' . BAR_HEIGHT . '" width="' . ($a * $b) . '" 
alt="">' . '<img height="' . BAR_HEIGHT . '" src="templates/' . TEMPLATE_SET . 
'/images/bar_right.gif" alt="">';
     } else {
-        return '<img height="' . BAR_HEIGHT . '" src="templates/' . 
TEMPLATE_SET . '/images/redbar_left.gif" alt="">'
-             . '<img src="templates/' . TEMPLATE_SET . 
'/images/redbar_middle.gif" height="' . BAR_HEIGHT . '" width="' . ($a * $b) . 
'" alt="">'
-             . '<img height="' . BAR_HEIGHT . '" src="templates/' . 
TEMPLATE_SET . '/images/redbar_right.gif" alt="">';
+    return '<img height="' . BAR_HEIGHT . '" src="templates/' . TEMPLATE_SET . 
'/images/redbar_left.gif" alt="">' . '<img src="templates/' . TEMPLATE_SET . 
'/images/redbar_middle.gif" height="' . BAR_HEIGHT . '" width="' . ($a * $b) . 
'" alt="">' . '<img height="' . BAR_HEIGHT . '" src="templates/' . TEMPLATE_SET 
. '/images/redbar_right.gif" alt="">';
     }
 }
-
-
 // Find a system program.  Do path checking
-function find_program ($program) {
+function find_program ($program)
+{
     $path = array('/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/local/bin', 
'/usr/local/sbin');
+
+  if (function_exists("is_executable")) {
     while ($this_path = current($path)) {
         if (is_executable("$this_path/$program")) {
             return "$this_path/$program";
         }
         next($path);
     }
+  } else {
+    return strpos($program, '.exe');
+  } ;
+
     return;
 }
-
-
 // Execute a system program. return a trim()'d result.
 // does very crude pipe checking.  you need ' | ' for it to work
 // ie $program = execute_program('netstat', '-anp | grep LIST');
 // NOT $program = execute_program('netstat', '-anp|grep LIST');
-function execute_program ($program, $args = '') {
+function execute_program ($program, $args = '')
+{
     $buffer = '';
     $program = find_program($program);

-    if (!$program) { return; }
-
+  if (!$program) {
+    return;
+  }
     // see if we've gotten a |, if we have we need to do patch checking on the 
cmd
     if ($args) {
         $args_list = split(' ', $args);
@@ -80,7 +73,6 @@
             }
         }
     }
-
     // we've finally got a good cmd line.. execute it
     if ($fp = popen("$program $args", 'r')) {
         while (!feof($fp)) {
@@ -89,25 +81,48 @@
         return trim($buffer);
     }
 }
-
-
 // A helper function, when passed a number representing KB,
 // and optionally the number of decimal places required,
 // it returns a formated number string, with unit identifier.
-function format_bytesize ($kbytes, $dec_places = 2) {
+function format_bytesize ($kbytes, $dec_places = 2)
+{
     global $text;
     $spacer = ' ';
     if ($kbytes > 1048576) {
         $result  = sprintf('%.' . $dec_places . 'f', $kbytes / 1048576);
-        $result .= $spacer.$text['gb'];
+    $result .= $spacer . $text['gb'];
     } elseif ($kbytes > 1024) {
         $result  = sprintf('%.' . $dec_places . 'f', $kbytes / 1024);
-        $result .= $spacer.$text['mb'];
+    $result .= $spacer . $text['mb'];
     } else {
         $result  = sprintf('%.' . $dec_places . 'f', $kbytes);
-        $result .= $spacer.$text['kb'];
+    $result .= $spacer . $text['kb'];
     }
     return $result;
 }
+
+function get_gif_image_height($image)
+{
+  // gives the height of the given GIF image, by reading it's LSD (Logical 
Screen Discriptor)
+  // by Edwin Meester aka MillenniumV3
+  // Header: 3bytes    Discription
+  // 3bytes    Version
+  // LSD:              2bytes  Logical Screen Width
+  // 2bytes    Logical Screen Height
+  // 1bit              Global Color Table Flag
+  // 3bits   Color Resolution
+  // 1bit              Sort Flag
+  // 3bits             Size of Global Color Table
+  // 1byte             Background Color Index
+  // 1byte             Pixel Aspect Ratio
+  // Open Image
+  $fp = fopen($image, 'rb');
+  // read Header + LSD
+  $header_and_lsd = fread($fp, 13);
+  fclose($fp);
+  // calc Height from Logical Screen Height bytes
+  $result = ord($header_and_lsd{8}) + ord($header_and_lsd{9}) * 255;
+  return $result;
+}

 ?>

====================================================
Index: phpsysinfo/includes/XPath.class.php
diff -u phpsysinfo/includes/XPath.class.php:1.2 
phpsysinfo/includes/XPath.class.php:1.3
--- phpsysinfo/includes/XPath.class.php:1.2     Sun May 15 12:46:44 2005
+++ phpsysinfo/includes/XPath.class.php Sat Nov 19 05:29:55 2005
@@ -9,7 +9,7 @@
  * 
+======================================================================================================+
  * | What Is XPath:
  * | --------------
- * | - "What SQL is for a rational database, XPath is for an XML document." -- 
Sam Blum
+ * | - "What SQL is for a relational database, XPath is for an XML document." 
-- Sam Blum
  * | - "The primary purpose of XPath is to address parts of an XML document. 
In support of this
  * |    primary purpose, it also provides basic facilities for manipulting 
it." -- W3C
  * |
@@ -27,7 +27,8 @@
  * | Main Active Authors:
  * | --------------------
  * | Nigel Swinson <address@hidden>
- * |   Started around 2001-07 and creator of V1.N.x branches.
+ * |   Started around 2001-07, saved phpxml from near death and renamed to 
Php.XPath
+ * |   Restructured XPath code to stay in line with XPath spec.
  * | Sam Blum <address@hidden>
  * |   Started around 2001-09 1st major restruct (V2.0) and testbench 
initiator.
  * |   2nd (V3.0) major rewrite in 2002-02
@@ -38,10 +39,10 @@
  * 
+------------------------------------------------------------------------------------------------------+
  * | Code Structure:
  * | --------------_
- * | In V3.0 the code has been split in 3 main objects. To keep usability easy 
all 3
+ * | The class is split into 3 main objects. To keep usability easy all 3
  * | objects are in this file (but may be split in 3 file in future).
  * |   +-------------+
- * |   |  XPathBase  | XPathBase holds general- and debugging-functions.
+ * |   |  XPathBase  | XPathBase holds general and debugging functions.
  * |   +------+------+
  * |          v
  * |   +-------------+ XPathEngine is the implementation of the W3C XPath 
spec. It contains the
@@ -54,7 +55,7 @@
  * 
+------------------------------------------------------------------------------------------------------+
  * | Usage:
  * | ------
- * | Scroll to the end of this file and you will find a little sample code to 
get you started
+ * | Scroll to the end of this php file and you will find a short sample code 
to get you started
  * 
+------------------------------------------------------------------------------------------------------+
  * | Glossary:
  * | ---------
@@ -62,7 +63,7 @@
  * |
  * | Document: (full node tree, XML-tree)
  * |     After a XML-source has been imported and parsed, it's stored as a 
tree of nodes sometimes
- * |     refered as 'document'.
+ * |     refered to as 'document'.
  * |
  * | AbsoluteXPath: (xPath, xPathSet)
  * |     A absolute XPath is a string. It 'points' to *one* node in the 
XML-document. We use the
@@ -78,7 +79,7 @@
  * |
  * | XPathQuery: (xquery, query)
  * |     A xPath-query is a string that is matched against the XML-document. 
The result of the match
- * |     is a xPathSet (vector of xPath's). It's always possable to pass a 
single absolutXPath
+ * |     is a xPathSet (vector of xPath's). It's always possible to pass a 
single absoluteXPath
  * |     instead of a xPath-query. A valid xPathQuery could look like this:
  * |     '//XXX/*[contains(., "foo")]/..' (See the link in 'What Is XPath' to 
learn more).
  * |
@@ -100,7 +101,7 @@
  * |             'depth' 1     BBB[1] CCC[1] BBB[2]               (NOTE: Is 
always size of child nodes+1)
  * | - The Node
  * |   --------
- * | The node itself is an structure desiged mainly to be used in conection 
with the interface of PHP.XPath.
+ * | The node itself is an structure desiged mainly to be used in connection 
with the interface of PHP.XPath.
  * | That means it's possible for functions to return a sub-node-tree that can 
be used as input of an other
  * | PHP.XPath function.
  * |
@@ -117,6 +118,7 @@
  * |     'pos'         => 0,       # Is the zero-based position this node has 
in the parent's 'childNodes'-list.
  * |     'contextPos'  => 1,       # Is the one-based position this node has 
by counting the siblings tags (tags with same name)
  * |     'xpath'       => ''       # Is the abs. XPath to this node.
+ * |     'generated_id'=> ''       # The id returned for this node by 
generate-id() (attribute and text nodes not supported)
  * |
  * | - The NodeIndex
  * |   -------------
@@ -127,8 +129,8 @@
  * | License:
  * | --------
  * | The contents of this file are subject to the Mozilla Public License 
Version 1.1 (the "License");
- * | you may Version 1.1 (the "License"); you may not use this file except in 
compliance with the
- * | License. You may obtain a copy of the License at 
http://www.mozilla.org/MPL/ .
+ * | you may not use this file except in compliance with the License. You may 
obtain a copy of the
+ * | License at http://www.mozilla.org/MPL/
  * |
  * | Software distributed under the License is distributed on an "AS IS" 
basis, WITHOUT WARRANTY
  * | OF ANY KIND, either express or implied. See the License for the specific 
language governing
@@ -139,14 +141,31 @@
  * | The Initial Developer of the Original Code is Michael P. Mehl. Portions 
created by Michael
  * | P. Mehl are Copyright (C) 2001 Michael P. Mehl. All Rights Reserved.
  * |
- * 
+------------------------------------------------------------------------------------------------------+
- * 
+------------------------------------------------------------------------------------------------------+
+ * | Contributor(s): N.Swinson / S.Blum / D.Allen
+ * |
+ * | Alternatively, the contents of this file may be used under the terms of 
either of the GNU
+ * | General Public License Version 2 or later (the "GPL"), or the GNU Lesser 
General Public
+ * | License Version 2.1 or later (the "LGPL"), in which case the provisions 
of the GPL or the
+ * | LGPL License are applicable instead of those above.  If you wish to allow 
use of your version
+ * | of this file only under the terms of the GPL or the LGPL License and not 
to allow others to
+ * | use your version of this file under the MPL, indicate your decision by 
deleting the
+ * | provisions above and replace them with the notice and other provisions 
required by the
+ * | GPL or the LGPL License.  If you do not delete the provisions above, a 
recipient may use
+ * | your version of this file under either the MPL, the GPL or the LGPL 
License.
+ * |
+ * 
+======================================================================================================+
  *
  * @author  S.Blum / N.Swinson / D.Allen / (P.Mehl)
  * @link    http://sourceforge.net/projects/phpxpath/
- * @version 3.0 beta
+ * @version 3.5
+ * @CVS $Id$
  */

+// Include guard, protects file being included twice
+$ConstantName = 'INCLUDED_'.strtoupper(__FILE__);
+if (defined($ConstantName)) return;
+define($ConstantName,1, TRUE);
+
 
/************************************************************************************************
 * 
===============================================================================================
 *                               X P a t h B a s e  -  Class
@@ -158,14 +177,55 @@
   // As debugging of the xml parse is spread across several functions, we need 
to make this a member.
   var $bDebugXmlParse = FALSE;

+  // Used to help navigate through the begin/end debug calls
+  var $iDebugNextLinkNumber = 1;
+  var $aDebugOpenLinks = array();
+  var $aDebugFunctions = array(
+          //'_evaluatePrimaryExpr',
+          //'_evaluateExpr',
+          //'_evaluateStep',
+          //'_checkPredicates',
+          //'_evaluateFunction',
+          //'_evaluateOperator',
+          //'_evaluatePathExpr',
+               );
+
   /**
    * Constructor
    */
   function XPathBase() {
     # $this->bDebugXmlParse = TRUE;
     $this->properties['verboseLevel'] = 1;  // 0=silent, 1 and above produce 
verbose output (an echo to screen).
+
+    if (!isSet($_ENV)) {  // Note: $_ENV introduced in 4.1.0. In earlier 
versions, use $HTTP_ENV_VARS.
+      $_ENV = $GLOBALS['HTTP_ENV_VARS'];
+    }
+
+    // Windows 95/98 do not support file locking. Detecting OS (Operation 
System) and setting the
+    // properties['OS_supports_flock'] to FALSE if win 95/98 is detected.
+    // This will surpress the file locking error reported from win 98 users 
when exportToFile() is called.
+    // May have to add more OS's to the list in future (Macs?).
+    // ### Note that it's only the FAT and NFS file systems that are really a 
problem.  NTFS and
+    // the latest php libs do support flock()
+    $_ENV['OS'] = isSet($_ENV['OS']) ? $_ENV['OS'] : 'Unknown OS';
+    switch ($_ENV['OS']) {
+      case 'Windows_95':
+      case 'Windows_98':
+      case 'Unknown OS':
+        // should catch Mac OS X compatible environment
+        if (!empty($_SERVER['SERVER_SOFTWARE'])
+            && preg_match('/Darwin/',$_SERVER['SERVER_SOFTWARE'])) {
+           // fall-through
+        } else {
+           $this->properties['OS_supports_flock'] = FALSE;
+           break;
+        }
+      default:
+        $this->properties['OS_supports_flock'] = TRUE;
+    }
   }

+
   /**
    * Resets the object so it's able to take a new xml sting/file
    *
@@ -181,11 +241,10 @@
   
//-----------------------------------------------------------------------------------------

   /**
-   * This method checks the right ammount and match of brackets
+   * This method checks the right amount and match of brackets
    *
    * @param     $term (string) String in which is checked.
    * @return          (bool)   TRUE: OK / FALSE: KO
-   * @see       _evaluateStep()
    */
   function _bracketsCheck($term) {
     $leng = strlen($term);
@@ -243,7 +302,6 @@
    * @param     $expression (string) String that should be searched.
    * @return                (int)    This method returns -1 if no string was 
found,
    *                                 otherwise the offset at which the string 
was found.
-   * @see       _evaluateStep()
    */
   function _searchString($term, $expression) {
     $bracketCounter = 0; // Record where we are in the brackets.
@@ -293,7 +351,7 @@
       // Make a substitute separator out of 'unused chars'.
       $substituteSep = str_repeat(chr(2), $sepLeng);

-      // Now determin the first bracket '(' or '['.
+      // Now determine the first bracket '(' or '['.
       $tmp1 = strpos($term, '(');
       $tmp2 = strpos($term, '[');
       if ($tmp1===FALSE) {
@@ -344,6 +402,93 @@
   }

   /**
+   * Split a string at it's groups, ie bracketed expressions
+   *
+   * Returns an array of strings, when concatenated together would produce the 
original
+   * string.  ie a(b)cde(f)(g) would map to:
+   * array ('a', '(b)', cde', '(f)', '(g)')
+   *
+   * @param     $string  (string) The string to process
+   * @param     $open    (string) The substring for the open of a group
+   * @param     $close   (string) The substring for the close of a group
+   * @return             (array)  The parsed string, see above
+   */
+  function _getEndGroups($string, $open='[', $close=']') {
+    // Note that it doesn't make sense for $separator to itself contain (,),[ 
or ],
+    // but as this is a private function we should be ok.
+    $resultArr   = array();
+    do { // BEGIN try block
+      // Check if we have both an open and a close tag
+      if (empty($open) and empty($close)) { // no separator found so end now
+        $resultArr[] = $string;
+        break; // try-block
+      }
+
+      if (empty($string)) {
+        $resultArr[] = $string;
+        break; // try-block
+      }
+
+
+      while (!empty($string)) {
+        // Now determine the first bracket '(' or '['.
+        $openPos = strpos($string, $open);
+        $closePos = strpos($string, $close);
+        if ($openPos===FALSE || $closePos===FALSE) {
+          // Oh, no more groups to be found then.  Quit
+          $resultArr[] = $string;
+          break;
+        }
+
+        // Sanity check
+        if ($openPos > $closePos) {
+          // Malformed string, dump the rest and quit.
+          $resultArr[] = $string;
+          break;
+        }
+
+        // Get prefix string part before the first bracket.
+        $preStr = substr($string, 0, $openPos);
+        // This is the first string that will go in our output
+        if (!empty($preStr))
+          $resultArr[] = $preStr;
+
+        // Skip over what we've proceed, including the open char
+        $string = substr($string, $openPos + 1 - strlen($string));
+
+        // Find the next open char and adjust our close char
+//echo "close: $closePos\nopen: $openPos\n\n";
+        $closePos -= $openPos + 1;
+        $openPos = strpos($string, $open);
+//echo "close: $closePos\nopen: $openPos\n\n";
+
+        // While we have found nesting...
+        while ($openPos && $closePos && ($closePos > $openPos)) {
+          // Find another close pos after the one we are looking at
+          $closePos = strpos($string, $close, $closePos + 1);
+          // And skip our open
+          $openPos = strpos($string, $open, $openPos + 1);
+        }
+//echo "close: $closePos\nopen: $openPos\n\n";
+
+        // If we now have a close pos, then it's the end of the group.
+        if ($closePos === FALSE) {
+          // We didn't... so bail dumping what was left
+          $resultArr[] = $open.$string;
+          break;
+        }
+
+        // We did, so we can extract the group
+        $resultArr[] = $open.substr($string, 0, $closePos + 1);
+        // Skip what we have processed
+        $string = substr($string, $closePos + 1);
+      }
+    } while (FALSE); // End try block
+    // Return the results that we found. May be a array with 1 entry.
+    return $resultArr;
+  }
+
+  /**
    * Retrieves a substring before a delimiter.
    *
    * This method retrieves everything from a string before a given delimiter,
@@ -383,12 +528,13 @@
   
//-----------------------------------------------------------------------------------------

   /**
-   * Turn verbose (error) output ON or OFF
+   * Alter the verbose (error) level reporting.
    *
-   * Pass a bool. TRUE to turn on, FALSE to turn off.
-   * Pass a int >0 to reach higher levels of verbosity (for future use).
+   * Pass an int. >0 to turn on, 0 to turn off.  The higher the number, the
+   * higher the level of verbosity. By default, the class has a verbose level
+   * of 1.
    *
-   * @param $levelOfVerbosity  (mixed) default is 0 = off
+   * @param $levelOfVerbosity (int) default is 1 = on
    */
   function setVerbose($levelOfVerbosity = 1) {
     $level = -1;
@@ -486,9 +632,13 @@
     static $color = array('green','blue','red','lime','fuchsia', 'aqua');
     static $colIndex = -1;
     $colIndex++;
-    $pre = '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; 
padding:5">';
-    $out = '<div align="left"> ' . $pre . "<STRONG>{$fileName} : 
{$functionName}</STRONG><HR>";
-    echo $out;
+    echo '<div style="clear:both" align="left"> ';
+    echo '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; 
padding:5">';
+    echo '<a style="float:right;margin:5px" 
name="'.$this->iDebugNextLinkNumber.'Open" 
href="#'.$this->iDebugNextLinkNumber.'Close">Function Close 
'.$this->iDebugNextLinkNumber.'</a>';
+    echo "<STRONG>{$fileName} : {$functionName}</STRONG>";
+    echo '<hr style="clear:both">';
+    array_push($this->aDebugOpenLinks, $this->iDebugNextLinkNumber);
+    $this->iDebugNextLinkNumber++;
     return microtime();
   }

@@ -505,17 +655,20 @@
    */
   function _closeDebugFunction($aStartTime, $returnValue = "") {
     echo "<hr>";
+    $iOpenLinkNumber = array_pop($this->aDebugOpenLinks);
+    echo '<a style="float:right" name="'.$iOpenLinkNumber.'Close" 
href="#'.$iOpenLinkNumber.'Open">Function Open '.$iOpenLinkNumber.'</a>';
     if (isSet($returnValue)) {
       if (is_array($returnValue))
         echo "Return Value: ".print_r($returnValue)."\n";
       else if (is_numeric($returnValue))
-        echo "Return Value: '$return_value'\n";
+        echo "Return Value: ".(string)$returnValue."\n";
       else if (is_bool($returnValue))
         echo "Return Value: ".($returnValue ? "TRUE" : "FALSE")."\n";
       else
         echo "Return Value: \"".htmlspecialchars($returnValue)."\"\n";
     }
     $this->_profileFunction($aStartTime, "Function took");
+    echo '<br style="clear:both">';
     echo " \n</pre></div>";
   }

@@ -534,6 +687,15 @@
   }

   /**
+   * Echo an XPath context for diagnostic purposes
+   *
+   * @param $context   (array)   An XPath context
+   */
+  function _printContext($context) {
+    echo "{$context['nodePath']}({$context['pos']}/{$context['size']})";
+  }
+
+  /**
    * This is a debug helper function. It dumps the node-tree as HTML
    *
    * *QUICK AND DIRTY*. Needs some polishing.
@@ -591,11 +753,11 @@
   //   -or-     =>     _or_
   //
   // This array contains a list of all valid axes that can be evaluated in an
-  // XPath expression.
-  var $axes = array ( 'child', 'descendant', 'parent', 'ancestor',
-    'following_sibling', 'preceding_sibling', 'following', 'preceding',
-    'attribute', 'text', 'namespace', 'self', 'descendant_or_self',
-    'ancestor_or_self' );
+  // XPath query.
+  var $axes = array ( 'ancestor', 'ancestor_or_self', 'attribute', 'child', 
'descendant',
+                        'descendant_or_self', 'following', 'following_sibling',
+                        'namespace', 'parent', 'preceding', 
'preceding_sibling', 'self'
+     );

   // List of supported XPath functions.
   // What a stupid idea from W3C to take function name containing a '-' (dash)
@@ -607,20 +769,23 @@
   //   string-length    => string_length
   //
   // This array contains a list of all valid functions that can be evaluated
-  // in an XPath expression.
+  // in an XPath query.
   var $functions = array ( 'last', 'position', 'count', 'id', 'name',
     'string', 'concat', 'starts_with', 'contains', 'substring_before',
     'substring_after', 'substring', 'string_length', 'normalize_space', 
'translate',
     'boolean', 'not', 'true', 'false', 'lang', 'number', 'sum', 'floor',
-    'ceiling', 'round' );
+    'ceiling', 'round', 'x_lower', 'x_upper', 'generate_id' );

   // List of supported XPath operators.
   //
   // This array contains a list of all valid operators that can be evaluated
-  // in a predicate of an XPath expression. The list is ordered by the
+  // in a predicate of an XPath query. The list is ordered by the
   // precedence of the operators (lowest precedence first).
   var $operators = array( ' or ', ' and ', '=', '!=', '<=', '<', '>=', '>',
-    '+', '-', '*', ' div ', ' mod ' );
+    '+', '-', '*', ' div ', ' mod ', ' | ');
+
+  // List of literals from the xPath string.
+  var $axPathLiterals = array();

   // The index and tree that is created during the analysis of an XML source.
   var $nodeIndex = array();
@@ -637,6 +802,7 @@
                      'contextPos'  => 1,  // Is the one-based position this 
node has by counting the siblings tags (tags with same name)
                      'xpath'       => ''  // Is the abs. XPath to this node.
                    );
+  var $_indexIsDirty = FALSE;


   // These variable used during the parse XML source
@@ -652,7 +818,8 @@
   // This is the array of error strings, to keep consistency.
   var $errorStrings = array(
     'AbsoluteXPathRequired' => "The supplied xPath '%s' does not *uniquely* 
describe a node in the xml document.",
-    'NoNodeMatch'           => "The supplied xPath-query '%s' does not match 
*any* node in the xml document."
+    'NoNodeMatch'           => "The supplied xPath-query '%s' does not match 
*any* node in the xml document.",
+    'RootNodeAlreadyExists' => "An xml document may have only one root node."
     );

   /**
@@ -670,7 +837,7 @@
    *                                 <optionID>=><value>, ...).  See PHP's
    *                                 xml_parser_set_option() docu for a list 
of possible
    *                                 options.
-   * @see   importFromFile(), importFromString(), setXmlOption()
+   * @see   importFromFile(), importFromString(), setXmlOptions()
    */
   function XPathEngine($userXmlOptions=array()) {
     parent::XPathBase();
@@ -701,6 +868,8 @@
     $this->nodeIndex     = array();
     $this->nodeRoot      = array();
     $this->nodeStack     = array();
+    $this->aLiterals     = array();
+    $this->_indexIsDirty = FALSE;
   }


@@ -731,7 +900,7 @@
   }

   /**
-   * xml_parser_set_option -- set options in an XML parser.
+   * Set an xml_parser_set_option()
    *
    * @param $optionID (int) The option ID (e.g. XML_OPTION_SKIP_WHITE)
    * @param $value    (int) The option value.
@@ -743,7 +912,22 @@
   }

   /**
-   * Controls whether case-folding is enabled for this XML parser.
+   * Sets a number of xml_parser_set_option()s
+   *
+   * @param  $userXmlOptions (array) An array of parser options.
+   * @see setXmlOption
+   */
+  function setXmlOptions($userXmlOptions=array()) {
+    if (!is_array($userXmlOptions)) return;
+    foreach($userXmlOptions as $key => $val) {
+      $this->setXmlOption($key, $val);
+    }
+  }
+
+  /**
+   * Alternative way to control whether case-folding is enabled for this XML 
parser.
+   *
+   * Short cut to setXmlOptions(XML_OPTION_CASE_FOLDING, TRUE/FALSE)
    *
    * When it comes to XML, case-folding simply means uppercasing all tag-
    * and attribute-names (NOT the content) if set to TRUE.  Note if you
@@ -758,11 +942,18 @@
   }

   /**
-   * Controls whether skip-white-spaces is enabled for this XML parser.
+   * Alternative way to control whether skip-white-spaces is enabled for this 
XML parser.
+   *
+   * Short cut to setXmlOptions(XML_OPTION_SKIP_WHITE, TRUE/FALSE)
    *
    * When it comes to XML, skip-white-spaces will trim the tag content.
-   * This will speed up performance, but will make your data less human
-   * readable when you come to write it out.
+   * An XML file with no whitespace will be faster to process, but will make
+   * your data less human readable when you come to write it out.
+   *
+   * Running with this option on will slow the class down, so if you want to
+   * speed up your XML, then run it through once skipping white-spaces, then
+   * write out the new version of your XML without whitespace, then use the
+   * new XML file with skip whitespaces turned off.
    *
    * @param $onOff (bool) (default TRUE)
    * @see XML parser functions in PHP doc
@@ -780,13 +971,14 @@
   function &getNode($absoluteXPath='') {
     if ($absoluteXPath==='/') $absoluteXPath = '';
     if (!isSet($this->nodeIndex[$absoluteXPath])) return FALSE;
+    if ($this->_indexIsDirty) $this->reindexNodeTree();
     return $this->nodeIndex[$absoluteXPath];
   }

   /**
    * Get a the content of a node text part or node attribute.
    *
-   * If the absolute Xpath references an attribute (Xpath ends with 
attribute::),
+   * If the absolute Xpath references an attribute (Xpath ends with @ or 
attribute::),
    * then the text value of that node-attribute is returned.
    * Otherwise the Xpath is referencing a text part of the node. This can be 
either a
    * direct reference to a text part (Xpath ends with text()[<nr>]) or 
indirect reference
@@ -815,12 +1007,13 @@
   function &wholeText($absoluteXPath, $textPartNr=NULL) {
     $status = FALSE;
     $text   = NULL;
+    if ($this->_indexIsDirty) $this->reindexNodeTree();

     do { // try-block
-      if ( preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $absoluteXPath, 
$matches) ) {
+      if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $absoluteXPath, 
$matches)) {
         $absoluteXPath = $matches[1];
         $attribute = $matches[3];
-        if ( 
!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute]) ) {
+        if 
(!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
           $this->_displayError("The $absoluteXPath/attribute::$attribute value 
isn't a node in this document.", __LINE__, __FILE__, FALSE);
           break; // try-block
         }
@@ -829,7 +1022,11 @@
         break; // try-block
       }

-      if ( !isSet($this->nodeIndex[$absoluteXPath]) ) {
+      // Xpath contains a 'text()'-function, thus goes right to a text node. 
If so interpret the Xpath.
+      if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $absoluteXPath, 
$matches)) {
+        $absoluteXPath = $matches[1];
+
+        if (!isSet($this->nodeIndex[$absoluteXPath])) {
         $this->_displayError("The $absoluteXPath value isn't a node in this 
document.", __LINE__, __FILE__, FALSE);
         break; // try-block
       }
@@ -837,15 +1034,13 @@
       // Get the amount of the text parts in the node.
      $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);

-      // Xpath contains a 'text()'-function, thus goes right to a text node. 
If so interpete the Xpath.
-      if ( preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $absoluteXPath, 
$matches) ) {
-        $absoluteXPath = $matches[1];
         // default to the first text node if a text node was not specified
         $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
+
         // Support negative indexes like -1 === last a.s.o.
         if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
         if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
-          $this->_displayError("The $absoluteXPath/text()[$textPartNr] value 
isn't a node in this document.", __LINE__, __FILE__, FALSE);
+          $this->_displayError("The $absoluteXPath/text()[$textPartNr] value 
isn't a NODE in this document.", __LINE__, __FILE__, FALSE);
           break; // try-block
         }
         $text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr - 
1];
@@ -856,6 +1051,14 @@
       // At this point we have been given an xpath with neither a 'text()' nor 
'attribute::' axis at the end
       // So we assume a get to text is wanted and use the optioanl fallback 
parameters $textPartNr

+      if (!isSet($this->nodeIndex[$absoluteXPath])) {
+          $this->_displayError("The $absoluteXPath value isn't a node in this 
document.", __LINE__, __FILE__, FALSE);
+          break; // try-block
+      }
+
+      // Get the amount of the text parts in the node.
+      $textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
+
       // If $textPartNr == NULL we return a *copy* of the whole concated 
text-parts
       if (is_null($textPartNr)) {
         unset($text);
@@ -878,6 +1081,24 @@
     return $text;
   }

+  /**
+   * Obtain the string value of an object
+   *
+   * http://www.w3.org/TR/xpath#dt-string-value
+   *
+   * "For every type of node, there is a way of determining a string-value for 
a node of that type.
+   * For some types of node, the string-value is part of the node; for other 
types of node, the
+   * string-value is computed from the string-value of descendant nodes."
+   *
+   * @param $node   (node)   The node we have to convert
+   * @return        (string) The string value of the node.  "" if the object 
has no evaluatable
+   *                         string value
+   */
+  function _stringValue($node) {
+    // Decode the entitites and then add the resulting literal string into our 
array.
+    return $this->_addLiteral($this->decodeEntities($this->wholeText($node)));
+  }
+
   
//-----------------------------------------------------------------------------------------
   // XPathEngine           ------ Export the XML Document ------
   
//-----------------------------------------------------------------------------------------
@@ -925,12 +1146,15 @@
    *
    * @param     $fileName       (string)
    * @param     $absoluteXPath  (string) The path to the parent node you 
want(see text above)
-   * @param     $xmlHeader      (string) default is '< ? xml version="1.0" ? >'
+   * @param     $xmlHeader      (array)  The string that you would like to 
appear before
+   *                                     the XML content.  ie before the 
<root></root>.  If you
+   *                                     do not specify this argument, the 
xmlHeader that was
+   *                                     found in the parsed xml file will be 
used instead.
    * @return                    (string) The returned string contains 
well-formed XML data
    *                                     or FALSE on error.
    * @see       exportAsXml(), exportAsHtml()
    */
-  function exportToFile($fileName, $absoluteXPath='', $xmlHeader='<?xml 
version="1.0"?>') {
+  function exportToFile($fileName, $absoluteXPath='', $xmlHeader=NULL) {
     $status = FALSE;
     do { // try-block
       if (!($hFile = fopen($fileName, "wb"))) {   // Did we open the file ok?
@@ -938,17 +1162,19 @@
         break; // try-block
       }

-      if (!flock($hFile, LOCK_EX)) {  // Lock the file
+      if ($this->properties['OS_supports_flock']) {
+        if (!flock($hFile, LOCK_EX + LOCK_NB)) {  // Lock the file
         $errStr = "Couldn't get an exclusive lock on the $fileName file.";
         break; // try-block
       }
-
+      }
       if (!($xmlOut = $this->_export($absoluteXPath, $xmlHeader))) {
         $errStr = "Export failed";
         break; // try-block
       }

-      if (!fwrite($hFile, $xmlOut)) {
+      $iBytesWritten = fwrite($hFile, $xmlOut);
+      if ($iBytesWritten != strlen($xmlOut)) {
         $errStr = "Write error when writing back the $fileName file.";
         break; // try-block
       }
@@ -960,6 +1186,12 @@

     @flock($hFile, LOCK_UN);
     @fclose($hFile);
+    // Sanity check the produced file.
+    clearstatcache();
+    if (filesize($fileName) < strlen($xmlOut)) {
+      $errStr = "Write error when writing back the $fileName file.";
+      $status = FALSE;
+    }

     if (!$status)  $this->_displayError($errStr, __LINE__, __FILE__, FALSE);
     return $status;
@@ -974,8 +1206,10 @@
    * document. Default is '', meaning to extract the whole document.
    *
    * You also may pass a 'xmlHeader' (usually something like <?xml 
version="1.0"? > that will
-   * overwrite any other 'xmlHeader', if there was one in the original source.
-   * Finaly, when exproting to HTML, you may pass a vector xPaths you want to 
hi-light.
+   * overwrite any other 'xmlHeader', if there was one in the original source. 
 If there
+   * wasn't one in the original source, and you still don't specify one, then 
it will
+   * use a default of <?xml version="1.0"? >
+   * Finaly, when exporting to HTML, you may pass a vector xPaths you want to 
hi-light.
    * The hi-lighted tags and attributes will receive a nice color.
    *
    * NOTE I : The output can have 2 formats:
@@ -995,6 +1229,7 @@
     // Check whether a root node is given.
     if (empty($absoluteXpath)) $absoluteXpath = '';
     if ($absoluteXpath == '/') $absoluteXpath = '';
+    if ($this->_indexIsDirty) $this->reindexNodeTree();
     if (!isSet($this->nodeIndex[$absoluteXpath])) {
       // If the $absoluteXpath was '' and it didn't exist, then the document 
is empty
       // and we can safely return ''.
@@ -1023,7 +1258,12 @@
       // If they didn't specify an xml header, use the one in the object
       if (is_null($xmlHeader)) {
         $xmlHeader = $this->parseSkipWhiteCache ? 
trim($superRoot['textParts'][0]) : $superRoot['textParts'][0];
+        // If we still don't have an XML header, then use a suitable default
+        if (empty($xmlHeader)) {
+            $xmlHeader = '<?xml version="1.0"?>';
       }
+      }
+
       if (isSet($superRoot['childNodes'][0])) $startNode = 
$superRoot['childNodes'][0];
     } else {
       $startNode = $this->nodeIndex[$absoluteXPath];
@@ -1074,7 +1314,6 @@

     // The output starts as empty.
     $xmlOut = '';
-
     // This loop will output the text before the current child of a parent 
then the
     // current child.  Where the child is a short tag we output the child, 
then move
     // onto the next child.  Where the child is not a short tag, we output the 
open tag,
@@ -1119,9 +1358,12 @@
     // We start at 0.
     $nodeStackIndex = 0;

-    // We have not to output text before/after our node, so blank it.  (As the 
nodeStack[]
-    // holds copies of nodes, we can do this ok without affecting the 
document.)
-    $nodeStack['Parent'][0]['textParts'][$node['pos']] = '';
+    // We have not to output text before/after our node, so blank it.  We will 
recover it
+    // later
+    $OldPreceedingStringValue = 
$nodeStack['Parent'][0]['textParts'][$node['pos']];
+    $OldPreceedingStringRef =& 
$nodeStack['Parent'][0]['textParts'][$node['pos']];
+    $OldPreceedingStringRef = "";
+    $currentXpath = "";

     // While we still have data on our stack
     while ($nodeStackIndex >= 0) {
@@ -1139,11 +1381,22 @@
       // Add the text before our child.

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

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

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

@@ -1258,6 +1525,9 @@

     $result = $xmlOut;

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

     if ($bDebugThisFunction) {
@@ -1292,7 +1562,7 @@
         break; // try-block
       }
       // The the source is an url try to fetch it.
-      if ( preg_match(';^http(s)?://;', $fileName) ) {
+      if (preg_match(';^http(s)?://;', $fileName)) {
         // Read the content of the url...this is really prone to errors, and 
we don't really
         // check for too many here...for now, suppressing both possible 
warnings...we need
         // to check if we get a none xml page or something of that nature in 
the future
@@ -1319,7 +1589,7 @@
         $errStr = "Failed to open '{$fileName}' for read.";
         break; // try-block
       }
-      $xmlString = fread ($fp, filesize($fileName));
+      $xmlString = fread($fp, filesize($fileName));
       @fclose($fp);

       $status = TRUE;
@@ -1369,7 +1639,7 @@
         $status = TRUE;
         // If we were importing to root, build a blank root.
         if (empty($absoluteParentPath)) {
-          $this->nodeRoot = array();
+          $this->_createSuperRoot();
         }
         $this->reindexNodeTree();
 //        $errStr = 'This xml document (string) was empty';
@@ -1393,9 +1663,7 @@
         $this->nodeStack[0] =& $nodeIndex[$absoluteParentPath];
       } else {
         // Build a 'super-root'
-        $this->nodeRoot = $this->emptyNode;
-        $this->nodeRoot['name']      = '';
-        $this->nodeRoot['parentNode'] = NULL;
+        $this->_createSuperRoot();
         // Put it in as the start of our node stack.
         $this->nodeStack[0] =& $this->nodeRoot;
       }
@@ -1416,7 +1684,7 @@
       }

       // Set the object and the element handlers for the XML parser.
-      xml_set_object($parser, &$this);
+      xml_set_object($parser, $this);
       xml_set_element_handler($parser, '_handleStartElement', 
'_handleEndElement');
       xml_set_character_data_handler($parser, '_handleCharacterData');
       xml_set_default_handler($parser, '_handleDefaultData');
@@ -1444,6 +1712,10 @@

       $this->reindexNodeTree();

+      if ($bDebugThisFunction) {
+        print_r(array_keys($this->nodeIndex));
+      }
+
       if ($bDebugThisFunction)
        $this->_profileFunction($aStartTime, "Reindex Object");

@@ -1592,6 +1864,7 @@
    * @see       _handleStartElement(), _handleEndElement()
    */
   function _handleCharacterData($parser, $text) {
+
     if ($this->parsInCData >0) $text = $this->_translateAmpersand($text, 
$reverse=TRUE);

     if ($this->bDebugXmlParse) echo "Handling character data: 
'".htmlspecialchars($text)."'<br>";
@@ -1652,6 +1925,7 @@
    * @see       PHP's manual "xml_set_processing_instruction_handler"
    */
   function _handlePI($parser, $target, $data) {
+    //echo("pi data=".$data."end"); exit;
     $data = $this->_translateAmpersand($data, $reverse=TRUE);
     $this->parsedTextLocation .= "<?{$target} {$data}?>";
     return TRUE;
@@ -1662,6 +1936,17 @@
   
//-----------------------------------------------------------------------------------------

   /**
+   * Creates a super root node.
+   */
+  function _createSuperRoot() {
+    // Build a 'super-root'
+    $this->nodeRoot = $this->emptyNode;
+    $this->nodeRoot['name']      = '';
+    $this->nodeRoot['parentNode'] = NULL;
+    $this->nodeIndex[''] =& $this->nodeRoot;
+  }
+
+  /**
    * Adds a new node to the XML document tree during xml parsing.
    *
    * This method adds a new node to the tree of nodes of the XML document
@@ -1777,6 +2062,7 @@
    */
   function reindexNodeTree() {
     //return;
+    $this->_indexIsDirty = FALSE;
     $this->nodeIndex = array();
     $this->nodeIndex[''] =& $this->nodeRoot;
     // Quick out for when the tree has no data.
@@ -1784,6 +2070,30 @@
     return $this->_recursiveReindexNodeTree('');
   }

+
+  /**
+   * Create the ids that are accessable through the generate-id() function
+   */
+  function _generate_ids() {
+    // If we have generated them already, then bail.
+    if (isset($this->nodeIndex['']['generate_id'])) return;
+
+    // keys generated are the string 'id0' . hexatridecimal-based (0..9,a-z) 
index
+    $aNodeIndexes = array_keys($this->nodeIndex);
+    $idNumber = 0;
+    foreach($aNodeIndexes as $index => $key) {
+//      $this->nodeIndex[$key]['generated_id'] = 'id' . 
base_convert($index,10,36);
+      // Skip attribute and text nodes.
+      // ### Currently don't support attribute and text nodes.
+      if (strstr($key, 'text()') !== FALSE) continue;
+      if (strstr($key, 'attribute::') !== FALSE) continue;
+      $this->nodeIndex[$key]['generated_id'] = 'idPhpXPath' . $idNumber;
+
+      // Make the id's sequential so that we can test predictively.
+      $idNumber++;
+    }
+  }
+
   /**
    * Here's where the work is done for reindexing (see reindexNodeTree)
    *
@@ -1795,42 +2105,80 @@
     $parentNode =& $this->nodeIndex[$absoluteParentPath];

     // Check for any 'dead' child nodes first and concate the text parts if 
found.
-    for ($i=sizeOf($parentNode['childNodes'])-1; $i>=0; $i--) {
+    for ($iChildIndex=sizeOf($parentNode['childNodes'])-1; $iChildIndex>=0; 
$iChildIndex--) {
       // Check if the child node still exits (it may have been removed).
-      if (!empty($parentNode['childNodes'][$i])) continue;
+      if (!empty($parentNode['childNodes'][$iChildIndex])) continue;
       // Child node was removed. We got to merge the text parts then.
-      $parentNode['textParts'][$i] .= $parentNode['textParts'][$i+1];
-      array_splice($parentNode['textParts'], $i+1, 1);
-      array_splice($parentNode['childNodes'], $i, 1);
+      $parentNode['textParts'][$iChildIndex] .= 
$parentNode['textParts'][$iChildIndex+1];
+      array_splice($parentNode['textParts'], $iChildIndex+1, 1);
+      array_splice($parentNode['childNodes'], $iChildIndex, 1);
     }

     // Now start a reindex.
     $contextHash = array();
     $childSize = sizeOf($parentNode['childNodes']);
-    for ($i=0; $i<$childSize; $i++) {
-      $childNode =& $parentNode['childNodes'][$i];
-      // Make sure that theire is a text-part infornt of every node. (May be 
empty)
-      if (!isSet($parentNode['textParts'][$i])) $parentNode['textParts'][$i] = 
'';
-      // Count the nodes with same name (to determin theire context position)
+
+    // If there are no children, we have to treat this specially:
+    if ($childSize == 0) {
+      // Add a dummy text node.
+      $this->nodeIndex[$absoluteParentPath.'/text()[1]'] =& $parentNode;
+    } else {
+      for ($iChildIndex=0; $iChildIndex<$childSize; $iChildIndex++) {
+        $childNode =& $parentNode['childNodes'][$iChildIndex];
+        // Make sure that there is a text-part in front of every node. (May be 
empty)
+        if (!isSet($parentNode['textParts'][$iChildIndex])) 
$parentNode['textParts'][$iChildIndex] = '';
+        // Count the nodes with same name (to determine their context position)
       $childName = $childNode['name'];
       if (empty($contextHash[$childName])) {
-        $contextHash[$childName] = 1;
+          $contextPos = $contextHash[$childName] = 1;
       } else {
-        $contextHash[$childName]++;
+          $contextPos = ++$contextHash[$childName];
       }
       // Make the node-index hash
-      $newPath = $absoluteParentPath . '/' . $childName . 
'['.$contextHash[$childName].']';
+        $newPath = $absoluteParentPath . '/' . $childName . 
'['.$contextPos.']';
+
+        // ### Note ultimately we will end up supporting text nodes as actual 
nodes.
+
+        // Preceed with a dummy entry for the text node.
+        
$this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+1).']'] =& 
$childNode;
+        // Then the node itself
       $this->nodeIndex[$newPath] =& $childNode;
+
+        // Now some dummy nodes for each of the attribute nodes.
+        $iAttributeCount = sizeOf($childNode['attributes']);
+        if ($iAttributeCount > 0) {
+          $aAttributesNames = array_keys($childNode['attributes']);
+          for ($iAttributeIndex = 0; $iAttributeIndex < $iAttributeCount; 
$iAttributeIndex++) {
+            $attribute = $aAttributesNames[$iAttributeIndex];
+            $newAttributeNode = $this->emptyNode;
+            $newAttributeNode['name'] = $attribute;
+            $newAttributeNode['textParts'] = 
array($childNode['attributes'][$attribute]);
+            $newAttributeNode['contextPos'] = $iAttributeIndex;
+            $newAttributeNode['xpath'] = "$newPath/attribute::$attribute";
+            $newAttributeNode['parentNode'] =& $childNode;
+            $newAttributeNode['depth'] =& $parentNode['depth'] + 2;
+            // Insert the node as a master node, not a reference, otherwise 
there will be
+            // variable "bleeding".
+            $this->nodeIndex["$newPath/attribute::$attribute"] = 
$newAttributeNode;
+          }
+        }
+
       // Update the node info (optimisation)
       $childNode['parentNode'] =& $parentNode;
-      $childNode['depth'] = $parentNode['depth'] +1;
-      $childNode['pos'] = $i;
+        $childNode['depth'] = $parentNode['depth'] + 1;
+        $childNode['pos'] = $iChildIndex;
       $childNode['contextPos'] = $contextHash[$childName];
       $childNode['xpath'] = $newPath;
       $this->_recursiveReindexNodeTree($newPath);
+
+        // Follow with a dummy entry for the text node.
+        
$this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+2).']'] =& 
$childNode;
+      }
+
+      // Make sure that their is a text-part after the last node.
+      if (!isSet($parentNode['textParts'][$iChildIndex])) 
$parentNode['textParts'][$iChildIndex] = '';
     }
-    // Make sure that theire is a text-part after the last node.
-    if (!isSet($parentNode['textParts'][$i])) $parentNode['textParts'][$i] = 
'';
+
     return TRUE;
   }

@@ -1846,17 +2194,29 @@
    *                         the current doc
    * @return        (&array) A node and it's child nodes.
    */
-  function &cloneNode($node) {
+  function &cloneNode($node, $recursive=FALSE) {
     if (is_string($node) AND isSet($this->nodeIndex[$node])) {
       $node = $this->nodeIndex[$node];
     }
+    // Copy the text-parts ()
+    $textParts = $node['textParts'];
+    $node['textParts'] = array();
+    foreach ($textParts as $key => $val) {
+      $node['textParts'][] = $val;
+    }

     $childSize = sizeOf($node['childNodes']);
     for ($i=0; $i<$childSize; $i++) {
-      $childNode =& $this->cloneNode($node['childNodes'][$i]);  // copy child
+      $childNode =& $this->cloneNode($node['childNodes'][$i], TRUE);  // copy 
child
       $node['childNodes'][$i] =& $childNode; // reference the copy
       $childNode['parentNode'] =& $node;      // child references the parent.
     }
+
+    if (!$recursive) {
+      //$node['childNodes'][0]['parentNode'] = null;
+      //print "<pre>";
+      //var_dump($node);
+    }
     return $node;
   }

@@ -1895,23 +2255,27 @@
   
//-----------------------------------------------------------------------------------------

   /**
-   * Matches (evaluates) an XPath expression.
+   * Matches (evaluates) an XPath query
    *
-   * This method tries to evaluate an XPath expression by parsing it. A XML 
source must
+   * This method tries to evaluate an XPath query by parsing it. A XML source 
must
    * have been imported before this method is able to work.
    *
-   * @param     $xPathQuery  (string) XPath expression to be evaluated.
-   * @param     $baseXPath   (string) (default is super-root) Full path of a 
document node,
-   *                                  from which the XPath expression should  
start evaluating.
-   * @return                 (array)  The returned vector contains a list of 
the full document
-   *                                  Xpaths of all nodes that match the 
evaluated XPath
-   *                                  expression.  Returns FALSE on error.
+   * @param     $xPathQuery  (string) XPath query to be evaluated.
+   * @param     $baseXPath   (string) (default is super-root) XPath query to a 
single document node,
+   *                                  from which the XPath query should  start 
evaluating.
+   * @return                 (mixed)  The result of the XPath expression.  
Either:
+   *                                    node-set (an ordered collection of 
absolute references to nodes without duplicates)
+   *                                    boolean (true or false)
+   *                                    number (a floating-point number)
+   *                                    string (a sequence of UCS characters)
    */
   function match($xPathQuery, $baseXPath='') {
+    if ($this->_indexIsDirty) $this->reindexNodeTree();
+
     // Replace a double slashes, because they'll cause problems otherwise.
     static $slashes2descendant = array(
-        '//@' => '/descendant::*/attribute::',
-        '//'  => '/descendant::',
+        '//@' => '/descendant_or_self::*/attribute::',
+        '//'  => '/descendant_or_self::node()/',
         '/@'  => '/attribute::');
     // Stupid idea from W3C to take axes name containing a '-' (dash) !!!
     // We replace the '-' with '_' to avoid the conflict with the minus 
operator.
@@ -1922,7 +2286,10 @@
         'substring-before' => 'substring_before',
         'substring-after'  => 'substring_after',
         'string-length'    => 'string_length',
-        'normalize-space'  => 'normalize_space');
+        'normalize-space'  => 'normalize_space',
+        'x-lower'          => 'x_lower',
+        'x-upper'          => 'x_upper',
+        'generate-id'      => 'generate_id');

     if (empty($xPathQuery)) return array();

@@ -1930,15 +2297,35 @@
     if (empty($this->nodeRoot)) return array();

     if (!isSet($this->nodeIndex[$baseXPath])) {
-      
$this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'],$xPathQuery),
 __LINE__, __FILE__, FALSE);
+            $xPathSet = $this->_resolveXPathQuery($baseXPath,'match');
+            if (sizeOf($xPathSet) !== 1) {
+                
$this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), 
__LINE__, __FILE__, FALSE);
       return FALSE;
     }
+            $baseXPath = $xPathSet[0];
+    }
+
+    // We should possibly do a proper syntactical parse, but instead we will 
cheat and just
+    // remove any literals that could make things very difficult for us, and 
replace them with
+    // special tags.  Then we can treat the xPathQuery much more easily as 
JUST "syntax".  Provided
+    // there are no literals in the string, then we can guarentee that most of 
the operators and
+    // syntactical elements are indeed elements and not just part of a literal 
string.
+    $processedxPathQuery = $this->_removeLiterals($xPathQuery);

     // Replace a double slashes, and '-' (dash) in axes names.
-    $xPathQuery = strtr($xPathQuery, $slashes2descendant);
-    $xPathQuery = strtr($xPathQuery, $dash2underscoreHash);
-
-    return $this->_internalEvaluate($xPathQuery, $baseXPath);
+    $processedxPathQuery = strtr($processedxPathQuery, $slashes2descendant);
+    $processedxPathQuery = strtr($processedxPathQuery, $dash2underscoreHash);
+
+    // Build the context
+    $context = array('nodePath' => $baseXPath, 'pos' => 1, 'size' => 1);
+
+    // The primary syntactic construct in XPath is the expression.
+    $result = $this->_evaluateExpr($processedxPathQuery, $context);
+
+    // We might have been returned a string.. If so convert back to a literal
+    $literalString = $this->_asLiteral($result);
+    if ($literalString != FALSE) return $literalString;
+    else return $result;
   }

   /**
@@ -1951,310 +2338,1023 @@
   }

   /**
-   * Internal recursive evaluate an-XPath-expression function.
-   *
-   * $this->evaluate() is the entry point and does some inits, while this
-   * function is called recursive internaly for every sub-xPath expresion we 
find.
+   * Parse out the literals of an XPath expression.
    *
-   * @param  $xPathQuery  (string) XPath expression to be evaluated.
-   * @param  $contextPath (mixed) (string or array) Full path of a document 
node, starting
-   *                              from which the XPath expression should be 
evaluated.
-   * @return              (array) Vector of absolute XPath's, or FALSE on 
error.
-   * @see    evaluate()
-   */
-  function _internalEvaluate($xPathQuery, $contextPath='') {
-    // If you are having difficulty using this function.  Then set this to 
TRUE and
-    // you'll get diagnostic info displayed to the output.
-    $bDebugThisFunction = FALSE;
-
-    if ($bDebugThisFunction) {
-      $aStartTime = $this->_beginDebugFunction("evaluate");
-      echo "Path: $xPathQuery\n Context: $contextPath\n";
-    }
-
-    // Numpty check
-    if (empty($xPathQuery)) {
-      $this->_displayError("The $xPathQuery argument must have a value.", 
__LINE__, __FILE__);
-      return FALSE;
-    }
-
-    // Split the paths that are sparated by '|' into distinct xPath expresions.
-    $xpathQueryList = (strpos($xPathQuery, '|') === FALSE) ? 
array($xPathQuery) : $this->_bracketExplode('|', $xPathQuery);
-    if ($bDebugThisFunction) { echo "<hr>Split the paths that are sparated by 
'|'\n"; print_r($xpathQueryList); }
-
-    // Create an empty set to save the result.
-    $result = array();
-
-    // Run through all paths.
-    foreach ($xpathQueryList as $xQuery) {
-      // mini syntax check
-      if (!$this->_bracketsCheck($xQuery)) {
-        $this->_displayError('While parsing an XPath expression, in the 
predicate ' .
-        str_replace($xQuery, '<b>'.$xQuery.'</b>', $xPathQuery) .
-        ', there was an invalid number of brackets or a bracket mismatch.', 
__LINE__, __FILE__);
-      }
-      // Save the current path.
-      $this->currentXpathQuery = $xQuery;
-      // Split the path at every slash *outside* a bracket.
-      $steps = $this->_bracketExplode('/', $xQuery);
-      if ($bDebugThisFunction) { echo "<hr>Split the path '$xQuery' at every 
slash *outside* a bracket.\n "; print_r($steps); }
-      // Check whether the first element is empty.
-      if (empty($steps[0])) {
-        // Remove the first and empty element. It's a starting  '//'.
-        array_shift($steps);
-      }
-      // Start to evaluate the steps.
-      $nodes = $this->_evaluateStep($contextPath, $steps);
-      // Remove duplicated nodes.
-      $nodes = array_unique($nodes);
-      // Add the nodes to the result set.
-      $result = array_merge($result, $nodes);
-    }
-    //////////////////////////////////////////////
-    if ($bDebugThisFunction) {
-      $this->_closeDebugFunction($aStartTime, $result);
-    }
-    // Return the result.
-    return $result;
-  }
-
-  /**
-   * Evaluate a step from a XPathQuery expression at a specific contextPath.
-   *
-   * Steps are the arguments of a XPathQuery when divided by a '/'. A 
contextPath is a
-   * absolute XPath (or vector of XPaths) to a starting node(s) from which the 
step should
-   * be evaluated.
-   *
-   * @param  $contextPath  (mixed) String or vector.  A absolute XPath OR 
vector of XPaths
-   *                               (see above)
-   * @param  $steps        (array) Vector containing the remaining steps of 
the current
-   *                               XPathQuery expression.
-   * @return               (array) Vector of absolute XPath's as a result of 
the step
-   *                               evaluation.
-   * @see    evaluate()
-   */
-  function _evaluateStep($contextPath, $steps) {
-    // If you are having difficulty using this function.  Then set this to 
TRUE and
-    // you'll get diagnostic info displayed to the output.
-    $bDebugThisFunction = FALSE;
-    if ($bDebugThisFunction) {
-      $aStartTime = 
$this->_beginDebugFunction(__LINE__.":_evaluateStep(contextPath:[$contextPath], 
steps:[$steps])");
-      if (is_array($contextPath)) {
-        echo "Context:\n";
-        print_r($contextPath);
-      } else {
-        echo "Context: $contextPath\n";
-      }
-      echo "Steps: ";
-      print_r($steps);
-      echo "<hr>\n";
-    }
-    $xPathSet = array(); // Create an empty array for saving the abs. XPath's 
found.
-    // We may have an "array" of one context.  If so convert it from
-    // array to single string.  Often, this function will be called with
-    // a /Path1[1]/Path[3]/Path[2] sytle predicate.
-    if (is_array($contextPath) && (count($contextPath) == 1)) $contextPath = 
$contextPath[0];
-    // Check whether the context is an array of contexts.
-    if (is_array($contextPath)) {
-      // Run through the array.
-      $size = sizeOf($contextPath);
-      for ($i=0; $i<$size; $i++) {
-        if ($bDebugThisFunction) echo __LINE__.":Evaluating step for the 
{$contextPath[$i]} context...\n";
-        // Call this method for this single path.
-        $xPathSet = array_merge($xPathSet, 
$this->_evaluateStep($contextPath[$i], $steps));
-      }
-    } else {
-      $contextPaths = array();   // Create an array to save the new contexts.
-      $step = trim(array_shift($steps)); // Get this step.
-      if ($bDebugThisFunction) echo __LINE__.":Evaluating step $step\n";
-
-      $axis = $this->_getAxis($step, $contextPath); // Get the axis of the 
current step.
-      if ($bDebugThisFunction) { echo __LINE__.":Axis of step is:\n"; 
print_r($axis); echo "\n";}
-
-      // Check whether it's a function.
-      if ($axis['axis'] == 'function') {
-        // Check whether an array was return by the function.
-        if (is_array($axis['node-test'])) {
-          $contextPaths = array_merge($contextPaths, $axis['node-test']);  // 
Add the results to the list of contexts.
-        } else {
-          $contextPaths[] = $axis['node-test']; // Add the result to the list 
of contexts.
+   * Instead of doing a full lexical parse, we parse out the literal strings, 
and then
+   * Treat the sections of the string either as parts of XPath or literal 
strings.  So
+   * this function replaces each literal it finds with a literal reference, 
and then inserts
+   * the reference into an array of strings that we can access.  The literals 
can be accessed
+   * later from the literals associative array.
+   *
+   * Example:
+   *  XPathExpr = /address@hidden = "hello"]/BBB[DDD = 'world']
+   *  =>  literals: array("hello", "world")
+   *      return value: /address@hidden = $1]/BBB[DDD = $2]
+   *
+   * Note: This does not interfere with the VariableReference syntactical 
element, as these
+   * elements must not start with a number.
+   *
+   * @param  $xPathQuery  (string) XPath expression to be processed
+   * @return              (string) The XPath expression without the literals.
+   *
+   */
+  function _removeLiterals($xPathQuery) {
+    // What comes first?  A " or a '?
+    if (!preg_match(":^([^\"']*)([\"'].*)$:", $xPathQuery, $aMatches)) {
+      // No " or ' means no more literals.
+      return $xPathQuery;
+    }
+
+    $result = $aMatches[1];
+    $remainder = $aMatches[2];
+    // What kind of literal?
+    if (preg_match(':^"([^"]*)"(.*)$:', $remainder, $aMatches)) {
+      // A "" literal.
+      $literal = $aMatches[1];
+      $remainder = $aMatches[2];
+    } else if (preg_match(":^'([^']*)'(.*)$:", $remainder, $aMatches)) {
+      // A '' literal.
+      $literal = $aMatches[1];
+      $remainder = $aMatches[2];
+    } else {
+      $this->_displayError("The '$xPathQuery' argument began a literal, but 
did not close it.", __LINE__, __FILE__);
+    }
+
+    // Store the literal
+    $literalNumber = count($this->axPathLiterals);
+    $this->axPathLiterals[$literalNumber] = $literal;
+    $result .= '$'.$literalNumber;
+    return $result.$this->_removeLiterals($remainder);
+    }
+
+  /**
+   * Returns the given string as a literal reference.
+   *
+   * @param $string (string) The string that we are processing
+   * @return        (mixed)  The literal string.  FALSE if the string isn't a 
literal reference.
+   */
+  function _asLiteral($string) {
+    if (empty($string)) return FALSE;
+    if (empty($string[0])) return FALSE;
+    if ($string[0] == '$') {
+      $remainder = substr($string, 1);
+      if (is_numeric($remainder)) {
+        // We have a string reference then.
+        $stringNumber = (int)$remainder;
+        if ($stringNumber >= count($this->axPathLiterals)) {
+            $this->_displayError("Internal error.  Found a string reference 
that we didn't set in xPathQuery: '$xPathQuery'.", __LINE__, __FILE__);
+            return FALSE;
+      }
+        return $this->axPathLiterals[$stringNumber];
+    }
+    }
+
+    // It's not a reference then.
+    return FALSE;
+  }
+
+  /**
+   * Adds a literal to our array of literals
+   *
+   * In order to make sure we don't interpret literal strings as XPath 
expressions, we have to
+   * encode literal strings so that we know that they are not XPaths.
+   *
+   * @param $string (string) The literal string that we need to store for 
future access
+   * @return        (mixed)  A reference string to this literal.
+   */
+  function _addLiteral($string) {
+    // Store the literal
+    $literalNumber = count($this->axPathLiterals);
+    $this->axPathLiterals[$literalNumber] = $string;
+    $result = '$'.$literalNumber;
+    return $result;
+      }
+
+  /**
+   * Look for operators in the expression
+   *
+   * Parses through the given expression looking for operators.  If found 
returns
+   * the operands and the operator in the resulting array.
+   *
+   * @param  $xPathQuery  (string) XPath query to be evaluated.
+   * @return              (array)  If an operator is found, it returns an 
array containing
+   *                               information about the operator.  If no 
operator is found
+   *                               then it returns an empty array.  If an 
operator is found,
+   *                               but has invalid operands, it returns FALSE.
+   *                               The resulting array has the following 
entries:
+   *                                'operator' => The string version of 
operator that was found,
+   *                                              trimmed for whitespace
+   *                                'left operand' => The left operand, or 
empty if there was no
+   *                                              left operand for this 
operator.
+   *                                'right operand' => The right operand, or 
empty if there was no
+   *                                              right operand for this 
operator.
+   */
+  function _GetOperator($xPathQuery) {
+    $position = 0;
+    $operator = '';
+
+    // The results of this function can easily be cached.
+    static $aResultsCache = array();
+    if (isset($aResultsCache[$xPathQuery])) {
+      return $aResultsCache[$xPathQuery];
         }
-      } else {
-        $method = '_handleAxis_' . $axis['axis']; // Create the name of the 
method.
-
-        // Check whether the axis handler is defined. If not display an error 
message.
-        if (!method_exists(&$this, $method)) {
-          $this->_displayError('While parsing an XPath expression, the axis ' .
-          $axis['axis'] . ' could not be handled, because this version does 
not support this axis.', __LINE__, __FILE__);
-        }
-        if ($bDebugThisFunction) echo __LINE__.":Calling user method 
$method\n";
-
-        // Perform an axis action.
-        $contextPaths = $this->$method($axis, $contextPath);
-        if ($bDebugThisFunction) { echo __LINE__.":We found these contexts 
from this step:\n"; print_r( $contextPaths ); echo "\n";}

-        // Check whether there are predicates.
-        if (count($axis['predicate']) > 0) {
-          if ($bDebugThisFunction) echo __LINE__.":Filtering contexts by 
predicate...\n";
+    // Run through all operators and try to find one.
+    $opSize = sizeOf($this->operators);
+    for ($i=0; $i<$opSize; $i++) {
+      // Pick an operator to try.
+      $operator = $this->operators[$i];
+      // Quickcheck. If not present don't wast time searching 'the hard way'
+      if (strpos($xPathQuery, $operator)===FALSE) continue;
+      // Special check
+      $position = $this->_searchString($xPathQuery, $operator);
+      // Check whether a operator was found.
+      if ($position <= 0 ) continue;

-          // Check whether each node fits the predicates.
-          $contextPaths = $this->_checkPredicates($contextPaths, 
$axis['predicate'], $axis['node-test']);
+      // Check whether it's the equal operator.
+      if ($operator == '=') {
+        // Also look for other operators containing the equal sign.
+        switch ($xPathQuery[$position-1]) {
+          case '<' :
+            $position--;
+            $operator = '<=';
+            break;
+          case '>' :
+            $position--;
+            $operator = '>=';
+            break;
+          case '!' :
+            $position--;
+            $operator = '!=';
+            break;
+          default:
+            // It's a pure = operator then.
         }
+        break;
       }

-      // Check whether there are more steps left.
-      if (count($steps) > 0) {
-        if ($bDebugThisFunction) echo __LINE__.":Evaluating next step given 
the context of the first step...\n";
-        // Continue the evaluation of the next steps.
-        $xPathSet = $this->_evaluateStep($contextPaths, $steps);
+      if ($operator == '*') {
+        // http://www.w3.org/TR/xpath#exprlex:
+        // "If there is a preceding token and the preceding token is not one 
of @, ::, (, [,
+        // or an Operator, then a * must be recognized as a MultiplyOperator 
and an NCName must
+        // be recognized as an OperatorName."
+
+        // Get some substrings.
+        $character = substr($xPathQuery, $position - 1, 1);
+
+        // Check whether it's a multiply operator or a name test.
+        if (strchr('/@:([', $character) != FALSE) {
+          // Don't use the operator.
+            $position = -1;
+          continue;
       } else {
-        $xPathSet = $contextPaths; // Save the found contexts.
-      }
-    }
-
-    if ($bDebugThisFunction) $this->_closeDebugFunction($aStartTime, 
$xPathSet);
-
+          // The operator is good.  Lets use it.
+          break;
+        }
+      }
+
+      // Extremely annoyingly, we could have a node name like "for-each" and 
we should not
+      // parse this as a "-" operator.  So if the first char of the right 
operator is alphabetic,
+      // then this is NOT an interger operator.
+      if (strchr('-+*', $operator) != FALSE) {
+        $rightOperand = trim(substr($xPathQuery, $position + 
strlen($operator)));
+        if (strlen($rightOperand) > 1) {
+          if (preg_match(':^\D$:', $rightOperand[0])) {
+            // Don't use the operator.
+            $position = -1;
+            continue;
+          } else {
+            // The operator is good.  Lets use it.
+            break;
+          }
+        }
+      }
+
+      // The operator must be good then :o)
+      break;
+
+    } // end while each($this->operators)
+
+    // Did we find an operator?
+    if ($position == -1) {
+      $aResultsCache[$xPathQuery] = array();
+      return array();
+    }
+
+    /////////////////////////////////////////////
+    // Get the operands
+
+    // Get the left and the right part of the expression.
+    $leftOperand  = trim(substr($xPathQuery, 0, $position));
+    $rightOperand = trim(substr($xPathQuery, $position + strlen($operator)));
+
+    // Remove whitespaces.
+    $leftOperand  = trim($leftOperand);
+    $rightOperand = trim($rightOperand);
+
+    /////////////////////////////////////////////
+    // Check the operands.
+
+    if ($leftOperand == '') {
+      $aResultsCache[$xPathQuery] = FALSE;
+      return FALSE;
+    }
+
+    if ($rightOperand == '') {
+      $aResultsCache[$xPathQuery] = FALSE;
+      return FALSE;
+      }
+
+    // Package up and return what we found.
+    $aResult = array('operator' => $operator,
+                'left operand' => $leftOperand,
+                'right operand' => $rightOperand);
+
+    $aResultsCache[$xPathQuery] = $aResult;
+
+    return $aResult;
+  }
+
+  /**
+   * Evaluates an XPath PrimaryExpr
+   *
+   * http://www.w3.org/TR/xpath#section-Basics
+   *
+   *  [15]    PrimaryExpr    ::= VariableReference
+   *                             | '(' Expr ')'
+   *                             | Literal
+   *                             | Number
+   *                             | FunctionCall
+   *
+   * @param  $xPathQuery  (string)   XPath query to be evaluated.
+   * @param  $context     (array)    The context from which to evaluate
+   * @param  $results     (mixed)    If the expression could be parsed and 
evaluated as one of these
+   *                                 syntactical elements, then this will be 
either:
+   *                                    - node-set (an ordered collection of 
nodes without duplicates)
+   *                                    - boolean (true or false)
+   *                                    - number (a floating-point number)
+   *                                    - string (a sequence of UCS characters)
+   * @return              (string)    An empty string if the query was 
successfully parsed and
+   *                                  evaluated, else a string containing the 
reason for failing.
+   * @see    evaluate()
+   */
+  function _evaluatePrimaryExpr($xPathQuery, $context, &$result) {
+    // If you are having difficulty using this function.  Then set this to 
TRUE and
+    // you'll get diagnostic info displayed to the output.
+    $bDebugThisFunction = in_array('_evaluatePrimaryExpr', 
$this->aDebugFunctions);
+
+    if ($bDebugThisFunction) {
+      $aStartTime = $this->_beginDebugFunction("_evaluatePrimaryExpr");
+      echo "Path: $xPathQuery\n";
+      echo "Context:";
+      $this->_printContext($context);
+      echo "\n";
+    }
+
+    // Certain expressions will never be PrimaryExpr, so to speed up 
processing, cache the
+    // results we do find from this function.
+    static $aResultsCache = array();
+
+    // Do while false loop
+    $error = "";
+    // If the result is independant of context, then we can cache the result 
and speed this function
+    // up on future calls.
+    $bCacheableResult = FALSE;
+    do {
+      if (isset($aResultsCache[$xPathQuery])) {
+        $error = $aResultsCache[$xPathQuery]['Error'];
+        $result = $aResultsCache[$xPathQuery]['Result'];
+        break;
+      }
+
+      // VariableReference
+      // ### Not supported.
+
+      // Is it a number?
+      // | Number
+      if (is_numeric($xPathQuery)) {
+        $result = doubleval($xPathQuery);
+        $bCacheableResult = TRUE;
+        break;
+    }
+
+      // If it starts with $, and the remainder is a number, then it's a 
string.
+      // | Literal
+      $literal = $this->_asLiteral($xPathQuery);
+      if ($literal !== FALSE) {
+        $result = $xPathQuery;
+        $bCacheableResult = TRUE;
+        break;
+      }
+
+      // Is it a function?
+      // | FunctionCall
+      {
+        // Check whether it's all wrapped in a function.  will be like 
count(.*) where .* is anything
+        // text() will try to be matched here, so just explicitly ignore it
+        $regex = ":^([^\(\)\[\]/]*)\s*\((.*)\)$:U";
+        if (preg_match($regex, $xPathQuery, $aMatch) && $xPathQuery != 
"text()") {
+          $function = $aMatch[1];
+          $data     = $aMatch[2];
+          // It is possible that we will get "a() or b()" which will match as 
function "a" with
+          // arguments ") or b(" which is clearly wrong... _bracketsCheck() 
should catch this.
+          if ($this->_bracketsCheck($data)) {
+            if (in_array($function, $this->functions)) {
+              if ($bDebugThisFunction) echo "XPathExpr: $xPathQuery is a 
$function() function call:\n";
+              $result = $this->_evaluateFunction($function, $data, $context);
+              break;
+            }
+          }
+        }
+      }
+
+      // Is it a bracketed expression?
+      // | '(' Expr ')'
+      // If it is surrounded by () then trim the brackets
+      $bBrackets = FALSE;
+      if (preg_match(":^\((.*)\):", $xPathQuery, $aMatches)) {
+        // Do not keep trimming off the () as we could have "(() and ())"
+        $bBrackets = TRUE;
+        $xPathQuery = $aMatches[1];
+      }
+
+      if ($bBrackets) {
+        // Must be a Expr then.
+        $result = $this->_evaluateExpr($xPathQuery, $context);
+        break;
+      }
+
+      // Can't be a PrimaryExpr then.
+      $error = "Expression is not a PrimaryExpr";
+      $bCacheableResult = TRUE;
+    } while (FALSE);
+    //////////////////////////////////////////////
+
+    // If possible, cache the result.
+    if ($bCacheableResult) {
+        $aResultsCache[$xPathQuery]['Error'] = $error;
+        $aResultsCache[$xPathQuery]['Result'] = $result;
+    }
+
+    if ($bDebugThisFunction) {
+      $this->_closeDebugFunction($aStartTime, array('result' => $result, 
'error' => $error));
+    }
     // Return the result.
-    return $xPathSet;
+    return $error;
   }

   /**
-   * Checks whether a node matches predicates.
+   * Evaluates an XPath Expr
    *
-   * This method checks whether a list of nodes passed to this method match
-   * a given list of predicates.
-   *
-   * @param  $xPathSet   (array)  Array of full paths of all nodes to be 
tested.
-   * @param  $predicates (array)  Array of predicates to use.
-   * @param  $nodeTest   (string) The node test used to filter the node set.  
Passed
-   *                              to evaluatePredicate()
-   * @return             (array)  Vector of absolute XPath's that match the 
given predicates.
-   * @see    _evaluateStep()
+   * $this->evaluate() is the entry point and does some inits, while this
+   * function is called recursive internaly for every sub-xPath expresion we 
find.
+   * It handles the following syntax, and calls evaluatePathExpr if it finds 
that none
+   * of this grammer applies.
+   *
+   * http://www.w3.org/TR/xpath#section-Basics
+   *
+   * [14]    Expr               ::= OrExpr
+   * [21]    OrExpr             ::= AndExpr
+   *                                | OrExpr 'or' AndExpr
+   * [22]    AndExpr            ::= EqualityExpr
+   *                                | AndExpr 'and' EqualityExpr
+   * [23]    EqualityExpr       ::= RelationalExpr
+   *                                | EqualityExpr '=' RelationalExpr
+   *                                | EqualityExpr '!=' RelationalExpr
+   * [24]    RelationalExpr     ::= AdditiveExpr
+   *                                | RelationalExpr '<' AdditiveExpr
+   *                                | RelationalExpr '>' AdditiveExpr
+   *                                | RelationalExpr '<=' AdditiveExpr
+   *                                | RelationalExpr '>=' AdditiveExpr
+   * [25]    AdditiveExpr       ::= MultiplicativeExpr
+   *                                | AdditiveExpr '+' MultiplicativeExpr
+   *                                | AdditiveExpr '-' MultiplicativeExpr
+   * [26]    MultiplicativeExpr ::= UnaryExpr
+   *                                | MultiplicativeExpr MultiplyOperator 
UnaryExpr
+   *                                | MultiplicativeExpr 'div' UnaryExpr
+   *                                | MultiplicativeExpr 'mod' UnaryExpr
+   * [27]    UnaryExpr          ::= UnionExpr
+   *                                | '-' UnaryExpr
+   * [18]    UnionExpr          ::= PathExpr
+   *                                | UnionExpr '|' PathExpr
+   *
+   * NOTE: The effect of the above grammar is that the order of precedence is
+   * (lowest precedence first):
+   * 1) or
+   * 2) and
+   * 3) =, !=
+   * 4) <=, <, >=, >
+   * 5) +, -
+   * 6) *, div, mod
+   * 7) - (negate)
+   * 8) |
+   *
+   * @param  $xPathQuery  (string)   XPath query to be evaluated.
+   * @param  $context     (array)    An associative array the describes the 
context from which
+   *                                 to evaluate the XPath Expr.  Contains 
three members:
+   *                                  'nodePath' => The absolute XPath 
expression to the context node
+   *                                  'size' => The context size
+   *                                  'pos' => The context position
+   * @return              (mixed)    The result of the XPath expression.  
Either:
+   *                                 node-set (an ordered collection of nodes 
without duplicates)
+   *                                 boolean (true or false)
+   *                                 number (a floating-point number)
+   *                                 string (a sequence of UCS characters)
+   * @see    evaluate()
    */
-  function _checkPredicates($xPathSet, $predicates, $nodeTest) {
+  function _evaluateExpr($xPathQuery, $context) {
     // If you are having difficulty using this function.  Then set this to 
TRUE and
     // you'll get diagnostic info displayed to the output.
-    $bDebugThisFunction = FALSE;
+    $bDebugThisFunction = in_array('_evaluateExpr', $this->aDebugFunctions);
+
     if ($bDebugThisFunction) {
-      $aStartTime = 
$this->_beginDebugFunction("_checkPredicates(Nodes:[$xPathSet], 
Predicates:[$predicates])");
-      echo "XPathSet:";
-      print_r($xPathSet);
-      echo "Predicates:";
-      print_r($predicates);
-      echo "<hr>";
-    }
+      $aStartTime = $this->_beginDebugFunction("_evaluateExpr");
+      echo "Path: $xPathQuery\n";
+      echo "Context:";
+      $this->_printContext($context);
+      echo "\n";
+    }
+
+    // Numpty check
+    if (!isset($xPathQuery) || ($xPathQuery == '')) {
+      $this->_displayError("The \$xPathQuery argument must have a value.", 
__LINE__, __FILE__);
+      return FALSE;
+    }
+
+    // At the top level we deal with booleans.  Only if the Expr is just an 
AdditiveExpr will
+    // the result not be a boolean.
+    //
+    //
+    // Between these syntactical elements we get PathExprs.
+
+    // Do while false loop
+    do {
+      static $aKnownPathExprCache = array();
+
+      if (isset($aKnownPathExprCache[$xPathQuery])) {
+        if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
+        $result = $this->_evaluatePathExpr($xPathQuery, $context);
+        break;
+      }
+
+      // Check for operators first, as we could have "() op ()" and the 
PrimaryExpr will try to
+      // say that that is an Expr called ") op ("
+      // Set the default position and the type of the operator.
+      $aOperatorInfo = $this->_GetOperator($xPathQuery);
+
+      // An expression can be one of these, and we should catch these "first" 
as they are most common
+      if (empty($aOperatorInfo)) {
+        $error = $this->_evaluatePrimaryExpr($xPathQuery, $context, $result);
+        if (empty($error)) {
+          // It could be parsed as a PrimaryExpr, so look no further :o)
+          break;
+        }
+    }
+
+      // Check whether an operator was found.
+      if (empty($aOperatorInfo)) {
+        if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
+        $aKnownPathExprCache[$xPathQuery] = TRUE;
+        // No operator.  Means we have a PathExpr then.  Go to the next level.
+        $result = $this->_evaluatePathExpr($xPathQuery, $context);
+        break;
+      }
+
+      if ($bDebugThisFunction) { echo "\nFound and operator:"; 
print_r($aOperatorInfo); }//LEFT:[$leftOperand]  oper:[$operator]  
RIGHT:[$rightOperand]";
+
+      $operator = $aOperatorInfo['operator'];
+
+      /////////////////////////////////////////////
+      // Recursively process the operator
+
+      // Check the kind of operator.
+      switch ($operator) {
+        case ' or ':
+        case ' and ':
+          $operatorType = 'Boolean';
+          break;
+        case '+':
+        case '-':
+        case '*':
+        case ' div ':
+        case ' mod ':
+          $operatorType = 'Integer';
+          break;
+        case ' | ':
+          $operatorType = 'NodeSet';
+          break;
+        case '<=':
+        case '<':
+        case '>=':
+        case '>':
+        case '=':
+        case '!=':
+          $operatorType = 'Multi';
+          break;
+        default:
+            $this->_displayError("Internal error.  Default case of switch 
statement reached.", __LINE__, __FILE__);
+      }
+
+      if ($bDebugThisFunction) echo "\nOperator is a [$operator]($operatorType 
operator)";
+
+      /////////////////////////////////////////////
+      // Evaluate the operands
+
+      // Evaluate the left part.
+      if ($bDebugThisFunction) echo "\nEvaluating LEFT:[{$aOperatorInfo['left 
operand']}]\n";
+      $left = $this->_evaluateExpr($aOperatorInfo['left operand'], $context);
+      if ($bDebugThisFunction) {echo "{$aOperatorInfo['left operand']} evals 
as:\n"; print_r($left); }
+
+      // If it is a boolean operator, it's possible we don't need to evaluate 
the right part.
+
+      // Only evaluate the right part if we need to.
+      $right = '';
+      if ($operatorType == 'Boolean') {
+        // Is the left part false?
+        $left = $this->_handleFunction_boolean($left, $context);
+        if (!$left and ($operator == ' and ')) {
+          $result = FALSE;
+          break;
+        } else if ($left and ($operator == ' or ')) {
+          $result = TRUE;
+          break;
+        }
+      }
+
+      // Evaluate the right part
+      if ($bDebugThisFunction) echo "\nEvaluating 
RIGHT:[{$aOperatorInfo['right operand']}]\n";
+      $right = $this->_evaluateExpr($aOperatorInfo['right operand'], $context);
+      if ($bDebugThisFunction) {echo "{$aOperatorInfo['right operand']} evals 
as:\n"; print_r($right); echo "\n";}
+
+      /////////////////////////////////////////////
+      // Combine the operands
+
+      // If necessary, work out how to treat the multi operators
+      if ($operatorType != 'Multi') {
+        $result = $this->_evaluateOperator($left, $operator, $right, 
$operatorType, $context);
+      } else {
+        // http://www.w3.org/TR/xpath#booleans
+        // If both objects to be compared are node-sets, then the comparison 
will be true if and
+        // only if there is a node in the first node-set and a node in the 
second node-set such
+        // that the result of performing the comparison on the string-values 
of the two nodes is
+        // true.
+        //
+        // If one object to be compared is a node-set and the other is a 
number, then the
+        // comparison will be true if and only if there is a node in the 
node-set such that the
+        // result of performing the comparison on the number to be compared 
and on the result of
+        // converting the string-value of that node to a number using the 
number function is true.
+        //
+        // If one object to be compared is a node-set and the other is a 
string, then the comparison
+        // will be true if and only if there is a node in the node-set such 
that the result of performing
+        // the comparison on the string-value of the node and the other string 
is true.
+        //
+        // If one object to be compared is a node-set and the other is a 
boolean, then the comparison
+        // will be true if and only if the result of performing the comparison 
on the boolean and on
+        // the result of converting the node-set to a boolean using the 
boolean function is true.
+        if (is_array($left) || is_array($right)) {
+          if ($bDebugThisFunction) echo "As one of the operands is an array, 
we will need to loop\n";
+          if (is_array($left) && is_array($right)) {
+            $operatorType = 'String';
+          } elseif (is_numeric($left) || is_numeric($right)) {
+            $operatorType = 'Integer';
+          } elseif (is_bool($left)) {
+            $operatorType = 'Boolean';
+            $right = $this->_handleFunction_boolean($right, $context);
+          } elseif (is_bool($right)) {
+            $operatorType = 'Boolean';
+            $left = $this->_handleFunction_boolean($left, $context);
+          } else {
+            $operatorType = 'String';
+          }
+          if ($bDebugThisFunction) echo "Equals operator is a $operatorType 
operator\n";
+          // Turn both operands into arrays to simplify logic
+          $aLeft = $left;
+          $aRight = $right;
+          if (!is_array($aLeft)) $aLeft = array($aLeft);
+          if (!is_array($aRight)) $aRight = array($aRight);
+          $result = FALSE;
+          if (!empty($aLeft)) {
+            foreach ($aLeft as $leftItem) {
+              if (empty($aRight)) break;
+              // If the item is from a node set, we should evaluate it's 
string-value
+              if (is_array($left)) {
+                if ($bDebugThisFunction) echo "\tObtaining string-value of 
LHS:$leftItem as it's from a nodeset\n";
+                $leftItem = $this->_stringValue($leftItem);
+              }
+              foreach ($aRight as $rightItem) {
+                // If the item is from a node set, we should evaluate it's 
string-value
+                if (is_array($right)) {
+                  if ($bDebugThisFunction) echo "\tObtaining string-value of 
RHS:$rightItem as it's from a nodeset\n";
+                  $rightItem = $this->_stringValue($rightItem);
+                }
+
+                if ($bDebugThisFunction) echo "\tEvaluating $leftItem 
$operator $rightItem\n";
+                $result = $this->_evaluateOperator($leftItem, $operator, 
$rightItem, $operatorType, $context);
+                if ($result === TRUE) break;
+              }
+              if ($result === TRUE) break;
+            }
+          }
+        }
+        // When neither object to be compared is a node-set and the operator 
is = or !=, then the
+        // objects are compared by converting them to a common type as follows 
and then comparing
+        // them.
+        //
+        // If at least one object to be compared is a boolean, then each 
object to be compared
+        // is converted to a boolean as if by applying the boolean function.
+        //
+        // Otherwise, if at least one object to be compared is a number, then 
each object to be
+        // compared is converted to a number as if by applying the number 
function.
+        //
+        // Otherwise, both objects to be compared are converted to strings as 
if by applying
+        // the string function.
+        //
+        // The = comparison will be true if and only if the objects are equal; 
the != comparison
+        // will be true if and only if the objects are not equal. Numbers are 
compared for equality
+        // according to IEEE 754 [IEEE 754]. Two booleans are equal if either 
both are true or
+        // both are false. Two strings are equal if and only if they consist 
of the same sequence
+        // of UCS characters.
+        else {
+          if (is_bool($left) || is_bool($right)) {
+            $operatorType = 'Boolean';
+          } elseif (is_numeric($left) || is_numeric($right)) {
+            $operatorType = 'Integer';
+          } else {
+            $operatorType = 'String';
+          }
+          if ($bDebugThisFunction) echo "Equals operator is a $operatorType 
operator\n";
+          $result = $this->_evaluateOperator($left, $operator, $right, 
$operatorType, $context);
+        }
+      }
+
+    } while (FALSE);
     //////////////////////////////////////////////
-    // Create an empty set of nodes.
-    $result = array();

-    // Run through all nodes.
-    $nSize = sizeOf($xPathSet);
-    for ($i=0; $i<$nSize; $i++) {
-      $xPath = $xPathSet[$i];
-      // Create a variable whether to add this node to the node-set.
-      $add = TRUE;
-
-      // Run through all predicates.
-      $pSize = sizeOf($predicates);
-      for ($j=0; $j<$pSize; $j++) {
-        $predicate = $predicates[$j];
-        if ($bDebugThisFunction) echo "Evaluating predicate \"$predicate\"\n";
-        // Check whether the predicate is just an number.
-        if (preg_match('/^\d+$/', $predicate)) {
-          if ($bDebugThisFunction) echo "Taking short cut and calling 
_handleFunction_position() directly.\n";
-          // Take a short cut.  If it is just a position, then call
-          // _handleFunction_position() directly.  70% of the
-          // time this will be the case. ## N.S
-          $check = (bool) ($predicate == 
$this->_handleFunction_position($xPath, '', $nodeTest));
-          // Enhance the predicate.
-          //                    $predicate .= "=position()";
-        } else {
-          // Else do the predicate check the long and thorough way.
-          $check = $this->_evaluatePredicate($xPath, $predicate, $nodeTest);
-        }
-        // Check whether it's a string.
-        if (is_string($check) && ( ( $check == '' ) || ( $check == $predicate 
) )) {
-          $check = FALSE; // Set the result to FALSE.
-        }
-        else if (is_bool($check) )  {
-          // 0 and 1 are both bools and ints.  We need to capture the bools
-          // as they might have been the intended result                    ## 
N.S
+    if ($bDebugThisFunction) {
+      $this->_closeDebugFunction($aStartTime, $result);
+    }
+    // Return the result.
+    return $result;
+  }
+
+  /**
+   * Evaluate the result of an operator whose operands have been evaluated
+   *
+   * If the operator type is not "NodeSet", then neither the left or right 
operators
+   * will be node sets, as the processing when one or other is an array is 
complex,
+   * and should be handled by the caller.
+   *
+   * @param  $left          (mixed)   The left operand
+   * @param  $right         (mixed)   The right operand
+   * @param  $operator      (string)  The operator to use to combine the 
operands
+   * @param  $operatorType  (string)  The type of the operator.  Either 
'Boolean',
+   *                                  'Integer', 'String', or 'NodeSet'
+   * @param  $context     (array)    The context from which to evaluate
+   * @return              (mixed)    The result of the XPath expression.  
Either:
+   *                                 node-set (an ordered collection of nodes 
without duplicates)
+   *                                 boolean (true or false)
+   *                                 number (a floating-point number)
+   *                                 string (a sequence of UCS characters)
+   */
+  function _evaluateOperator($left, $operator, $right, $operatorType, 
$context) {
+    // If you are having difficulty using this function.  Then set this to 
TRUE and
+    // you'll get diagnostic info displayed to the output.
+    $bDebugThisFunction = in_array('_evaluateOperator', 
$this->aDebugFunctions);
+
+    if ($bDebugThisFunction) {
+      $aStartTime = $this->_beginDebugFunction("_evaluateOperator");
+      echo "left: $left\n";
+      echo "right: $right\n";
+      echo "operator: $operator\n";
+      echo "operator type: $operatorType\n";
+    }
+
+    // Do while false loop
+    do {
+      // Handle the operator depending on the operator type.
+      switch ($operatorType) {
+        case 'Boolean':
+          {
+            // Boolify the arguments.  (The left arg is already a bool)
+            $right = $this->_handleFunction_boolean($right, $context);
+            switch ($operator) {
+              case '=': // Compare the two results.
+                $result = (bool)($left == $right);
+                break;
+              case ' or ': // Return the two results connected by an 'or'.
+                $result = (bool)( $left or $right );
+                break;
+              case ' and ': // Return the two results connected by an 'and'.
+                $result = (bool)( $left and $right );
+                break;
+              case '!=': // Check whether the two results are not equal.
+                $result = (bool)( $left != $right );
+                break;
+              default:
+                $this->_displayError("Internal error.  Default case of switch 
statement reached.", __LINE__, __FILE__);
+            }
+          }
+          break;
+        case 'Integer':
+          {
+            // Convert both left and right operands into numbers.
+            if (empty($left) && ($operator == '-')) {
+              // There may be no left operator if the op is '-'
+              $left = 0;
         } else {
-          if (is_int($check)) { // Check whether it's an integer.
-            // Check whether it's the current position.
-            $check = (bool) ($check == $this->_handleFunction_position($xPath, 
'', $nodeTest));
+              $left = $this->_handleFunction_number($left, $context);
           }
+            $right = $this->_handleFunction_number($right, $context);
+            if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
+            switch ($operator) {
+              case '=': // Compare the two results.
+                $result = (bool)($left == $right);
+                break;
+              case '!=': // Compare the two results.
+                $result = (bool)($left != $right);
+                break;
+              case '+': // Return the result by adding one result to the other.
+                $result = $left + $right;
+                break;
+              case '-': // Return the result by decrease one result by the 
other.
+                $result = $left - $right;
+                break;
+              case '*': // Return a multiplication of the two results.
+                $result =  $left * $right;
+                break;
+              case ' div ': // Return a division of the two results.
+                $result = $left / $right;
+                break;
+              case ' mod ': // Return a modulo division of the two results.
+                $result = $left % $right;
+                break;
+              case '<=': // Compare the two results.
+                $result = (bool)( $left <= $right );
+                break;
+              case '<': // Compare the two results.
+                $result = (bool)( $left < $right );
+                break;
+              case '>=': // Compare the two results.
+                $result = (bool)( $left >= $right );
+                break;
+              case '>': // Compare the two results.
+                $result = (bool)( $left > $right );
+                break;
+              default:
+                $this->_displayError("Internal error.  Default case of switch 
statement reached.", __LINE__, __FILE__);
         }
-        if ($bDebugThisFunction) echo "Node $xPath matches predicate 
$predicate: " . (($check) ? "TRUE" : "FALSE") ."\n";
-        // Check whether the predicate is OK for this node.
-        $add = $add && $check;
       }
+          break;
+        case 'NodeSet':
+          // Add the nodes to the result set
+          $result = array_merge($left, $right);
+          // Remove duplicated nodes.
+          $result = array_unique($result);

-      // Check whether to add this node to the node-set.
-      if ($add) {
-        $result[] = $xPath; // Add the node to the node-set.
+          // Preserve doc order if there was more than one query.
+          if (count($result) > 1) {
+            $result = $this->_sortByDocOrder($result);
       }
-      if ($bDebugThisFunction) echo "Node $xPath matches: " . (($add) ? "TRUE" 
: "FALSE") ."\n\n";
-    }
+          break;
+        case 'String':
+            $left = $this->_handleFunction_string($left, $context);
+            $right = $this->_handleFunction_string($right, $context);
+            if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
+            switch ($operator) {
+              case '=': // Compare the two results.
+                $result = (bool)($left == $right);
+                break;
+              case '!=': // Compare the two results.
+                $result = (bool)($left != $right);
+                break;
+              default:
+                $this->_displayError("Internal error.  Default case of switch 
statement reached.", __LINE__, __FILE__);
+    }
+          break;
+        default:
+          $this->_displayError("Internal error.  Default case of switch 
statement reached.", __LINE__, __FILE__);
+      }
+    } while (FALSE);
+
     //////////////////////////////////////////////
+
     if ($bDebugThisFunction) {
       $this->_closeDebugFunction($aStartTime, $result);
     }
-    // Return the array of nodes.
+    // Return the result.
     return $result;
   }

   /**
-   * Evaluates an XPath function
+   * Evaluates an XPath PathExpr
    *
-   * This method evaluates a given XPath function with its arguments on a
-   * specific node of the document.
+   * It handles the following syntax:
    *
-   * @param  $function      (string) Name of the function to be evaluated.
-   * @param  $arguments     (string) String containing the arguments being
-   *                                 passed to the function.
-   * @param  $absoluteXPath (string) Full path to the document node on which 
the
-   *                                 function should be evaluated.
-   * @return                (mixed)  This method returns the result of the 
evaluation of
-   *                                 the function. Depending on the function 
the type of the
-   *                                 return value can be different.
+   * http://www.w3.org/TR/xpath#node-sets
+   * http://www.w3.org/TR/xpath#NT-LocationPath
+   * http://www.w3.org/TR/xpath#path-abbrev
+   * http://www.w3.org/TR/xpath#NT-Step
+   *
+   * [19]   PathExpr              ::= LocationPath
+   *                                  | FilterExpr
+   *                                  | FilterExpr '/' RelativeLocationPath
+   *                                  | FilterExpr '//' RelativeLocationPath
+   * [20]   FilterExpr            ::= PrimaryExpr
+   *                                  | FilterExpr Predicate
+   * [1]    LocationPath          ::= RelativeLocationPath
+   *                                  | AbsoluteLocationPath
+   * [2]    AbsoluteLocationPath  ::= '/' RelativeLocationPath?
+   *                                  | AbbreviatedAbsoluteLocationPath
+   * [3]    RelativeLocationPath  ::= Step
+   *                                  | RelativeLocationPath '/' Step
+   *                                  | AbbreviatedRelativeLocationPath
+   * [4]    Step                  ::= AxisSpecifier NodeTest Predicate*
+   *                                  | AbbreviatedStep
+   * [5]    AxisSpecifier         ::= AxisName '::'
+   *                                  | AbbreviatedAxisSpecifier
+   * [10]   AbbreviatedAbsoluteLocationPath
+   *                              ::= '//' RelativeLocationPath
+   * [11]   AbbreviatedRelativeLocationPath
+   *                              ::= RelativeLocationPath '//' Step
+   * [12]   AbbreviatedStep       ::= '.'
+   *                                  | '..'
+   * [13]   AbbreviatedAxisSpecifier
+   *                              ::= '@'?
+   *
+   * If you expand all the abbreviated versions, then the grammer simplifies 
to:
+   *
+   * [19]   PathExpr              ::= RelativeLocationPath
+   *                                  | '/' RelativeLocationPath?
+   *                                  | FilterExpr
+   *                                  | FilterExpr '/' RelativeLocationPath
+   * [20]   FilterExpr            ::= PrimaryExpr
+   *                                  | FilterExpr Predicate
+   * [3]    RelativeLocationPath  ::= Step
+   *                                  | RelativeLocationPath '/' Step
+   * [4]    Step                  ::= AxisName '::' NodeTest Predicate*
+   *
+   * Conceptually you can say that we should split by '/' and try to treat the 
parts
+   * as steps, and if that fails then try to treat it as a PrimaryExpr.
+   *
+   * @param  $PathExpr   (string) PathExpr syntactical element
+   * @param  $context    (array)  The context from which to evaluate
+   * @return             (mixed)  The result of the XPath expression.  Either:
+   *                               node-set (an ordered collection of nodes 
without duplicates)
+   *                               boolean (true or false)
+   *                               number (a floating-point number)
+   *                               string (a sequence of UCS characters)
    * @see    evaluate()
    */
-  function _evaluateFunction($function, $arguments, $absoluteXPath, 
$nodeTest='') {
+  function _evaluatePathExpr($PathExpr, $context) {
     // If you are having difficulty using this function.  Then set this to 
TRUE and
     // you'll get diagnostic info displayed to the output.
-    $bDebugThisFunction = FALSE;
+    $bDebugThisFunction = in_array('_evaluatePathExpr', 
$this->aDebugFunctions);
+
     if ($bDebugThisFunction) {
-      $aStartTime = 
$this->_beginDebugFunction("_evaluateFunction(Function:[$function], 
Arguments:[$arguments], node:[$absoluteXPath], nodeTest:[$nodeTest])");
-      if (is_array($arguments)) {
-        echo "Arguments:\n";
-        print_r($arguments);
+      $aStartTime = $this->_beginDebugFunction("_evaluatePathExpr");
+      echo "PathExpr: $PathExpr\n";
+      echo "Context:";
+      $this->_printContext($context);
+      echo "\n";
+    }
+
+    // Numpty check
+    if (empty($PathExpr)) {
+      $this->_displayError("The \$PathExpr argument must have a value.", 
__LINE__, __FILE__);
+      return FALSE;
+    }
+    //////////////////////////////////////////////
+
+    // Parsing the expression into steps is a cachable operation as it doesn't 
depend on the context
+    static $aResultsCache = array();
+
+    if (isset($aResultsCache[$PathExpr])) {
+      $steps = $aResultsCache[$PathExpr];
       } else {
-        echo "Arguments: $arguments\n";
-      }
+      // Note that we have used $this->slashes2descendant to simplify this 
logic, so the
+      // "Abbreviated" paths basically never exist as '//' never exists.
+
+      // mini syntax check
+      if (!$this->_bracketsCheck($PathExpr)) {
+        $this->_displayError('While parsing an XPath query, in the PathExpr "' 
.
+        $PathExpr.
+        '", there was an invalid number of brackets or a bracket mismatch.', 
__LINE__, __FILE__);
+      }
+      // Save the current path.
+      $this->currentXpathQuery = $PathExpr;
+      // Split the path at every slash *outside* a bracket.
+      $steps = $this->_bracketExplode('/', $PathExpr);
+      if ($bDebugThisFunction) { echo "<hr>Split the path '$PathExpr' at every 
slash *outside* a bracket.\n "; print_r($steps); }
+      // Check whether the first element is empty.
+      if (empty($steps[0])) {
+        // Remove the first and empty element. It's a starting  '//'.
+        array_shift($steps);
+      }
+      $aResultsCache[$PathExpr] = $steps;
+    }
+
+    // Start to evaluate the steps.
+    // ### Consider implementing an evaluateSteps() function that removes 
recursion from
+    // evaluateStep()
+    $result = $this->_evaluateStep($steps, $context);
+
+    // Preserve doc order if there was more than one result
+    if (count($result) > 1) {
+      $result = $this->_sortByDocOrder($result);
+    }
+    //////////////////////////////////////////////
+    if ($bDebugThisFunction) {
+      $this->_closeDebugFunction($aStartTime, $result);
+    }
+    // Return the result.
+    return $result;
+      }
+
+  /**
+   * Sort an xPathSet by doc order.
+   *
+   * @param  $xPathSet (array) Array of full paths to nodes that need to be 
sorted
+   * @return           (array) Array containing the same contents as 
$xPathSet, but
+   *                           with the contents in doc order
+   */
+  function _sortByDocOrder($xPathSet) {
+    // If you are having difficulty using this function.  Then set this to 
TRUE and
+    // you'll get diagnostic info displayed to the output.
+    $bDebugThisFunction = in_array('_sortByDocOrder', $this->aDebugFunctions);
+
+    if ($bDebugThisFunction) {
+      $aStartTime = 
$this->_beginDebugFunction(__LINE__.":_sortByDocOrder(xPathSet:[".count($xPathSet)."])");
+      echo "xPathSet:\n";
+      print_r($xPathSet);
       echo "<hr>\n";
     }
-    /////////////////////////////////////
-    // Remove whitespaces.
-    $function  = trim($function);
-    $arguments = trim($arguments);
-    // Create the name of the function handling function.
-    $method = '_handleFunction_'. $function;
+    //////////////////////////////////////////////

-    // Check whether the function handling function is available.
-    if (!method_exists(&$this, $method)) {
-      // Display an error message.
-      $this->_displayError("While parsing an XPath expression, ".
-        "the function \"$function\" could not be handled, because this ".
-        "version does not support this function.", __LINE__, __FILE__);
-    }
-    if ($bDebugThisFunction) echo "Calling function $method($absoluteXPath, 
$arguments)\n";
-
-    // Return the result of the function.
-    $result = $this->$method($absoluteXPath, $arguments, $nodeTest);
+    $aResult = array();
+
+    // Spot some common shortcuts.
+    if (count($xPathSet) < 1) {
+      $aResult = $xPathSet;
+    } else {
+      // Build an array of doc-pos indexes.
+      $aDocPos = array();
+      $nodeCount = count($this->nodeIndex);
+      $aPaths = array_keys($this->nodeIndex);
+      if ($bDebugThisFunction) {
+        echo "searching for path indices in array_keys(this->nodeIndex)...\n";
+        //print_r($aPaths);
+    }
+
+      // The last index we found.  In general the elements will be in groups
+      // that are themselves in order.
+      $iLastIndex = 0;
+      foreach ($xPathSet as $path) {
+        // Cycle round the nodes, starting at the last index, looking for the 
path.
+        $foundNode = FALSE;
+        for ($iIndex = $iLastIndex; $iIndex < $nodeCount + $iLastIndex; 
$iIndex++) {
+          $iThisIndex = $iIndex % $nodeCount;
+          if (!strcmp($aPaths[$iThisIndex],$path)) {
+            // we have found the doc-position index of the path
+            $aDocPos[] = $iThisIndex;
+            $iLastIndex = $iThisIndex;
+            $foundNode = TRUE;
+            break;
+          }
+        }
+        if ($bDebugThisFunction) {
+          if (!$foundNode)
+            echo "Error: $path not found in \$this->nodeIndex\n";
+          else
+            echo "Found node after ".($iIndex - $iLastIndex)." iterations\n";
+        }
+      }
+      // Now count the number of doc pos we have and the number of results and
+      // confirm that we have the same number of each.
+      $iDocPosCount = count($aDocPos);
+      $iResultCount = count($xPathSet);
+      if ($iDocPosCount != $iResultCount) {
+        if ($bDebugThisFunction) {
+          echo "count(\$aDocPos)=$iDocPosCount; 
count(\$result)=$iResultCount\n";
+          print_r(array_keys($this->nodeIndex));
+        }
+        $this->_displayError('Results from _InternalEvaluate() are corrupt.  '.
+                                      'Do you need to call 
reindexNodeTree()?', __LINE__, __FILE__);
+      }
+
+      // Now sort the indexes.
+      sort($aDocPos);
+
+      // And now convert back to paths.
+      $iPathCount = count($aDocPos);
+      for ($iIndex = 0; $iIndex < $iPathCount; $iIndex++) {
+        $aResult[] = $aPaths[$aDocPos[$iIndex]];
+      }
+    }
+
+    // Our result from the function is this array.
+    $result = $aResult;

     //////////////////////////////////////////////
-    // Return the nodes found.
     if ($bDebugThisFunction) {
       $this->_closeDebugFunction($aStartTime, $result);
     }
@@ -2263,298 +3363,265 @@
   }

   /**
-   * Evaluates a predicate on a node.
+   * Evaluate a step from a XPathQuery expression at a specific contextPath.
    *
-   * This method tries to evaluate a predicate on a given node.
-   *
-   * @param  $absoluteXPath (string) Full path of the node on which the 
predicate
-   *                                 should be evaluated.
-   * @param  $predicate     (string) String containing the predicate expression
-   *                                 to be evaluated.
-   * @param  $nodeTest      (string) The node test used to filter the node set.
-   * @return                (mixed)  This method is called recursively. The 
first call
-   *                                 should return a boolean value, whether 
the node
-   *                                 matches the predicateor not. Any call to 
the
-   *                                 method being made during the recursion
-   *                                 may also return other types for further 
processing.
-   * @see    evaluate()
+   * Steps are the arguments of a XPathQuery when divided by a '/'. A 
contextPath is a
+   * absolute XPath (or vector of XPaths) to a starting node(s) from which the 
step should
+   * be evaluated.
+   *
+   * @param  $steps        (array) Vector containing the remaining steps of 
the current
+   *                               XPathQuery expression.
+   * @param  $context      (array) The context from which to evaluate
+   * @return               (array) Vector of absolute XPath's as a result of 
the step
+   *                               evaluation.  The results will not 
necessarily be in doc order
+   * @see    _evaluatePathExpr()
    */
-  function _evaluatePredicate($absoluteXPath, $predicate, $nodeTest) {
+  function _evaluateStep($steps, $context) {
     // If you are having difficulty using this function.  Then set this to 
TRUE and
     // you'll get diagnostic info displayed to the output.
-    $bDebugThisFunction = FALSE;
+    $bDebugThisFunction = in_array('_evaluateStep', $this->aDebugFunctions);
+
     if ($bDebugThisFunction) {
-      $aStartTime = $this->_beginDebugFunction("_evaluatePredicate");
-      echo "Node: [$absoluteXPath]\n";
-      echo "Predicate: [$predicate]\n";
-      echo "Node Test: [$nodeTest]\n";
-      echo "<hr>";
-    }
-
-    do { // try-block
-      // Numpty check
-      if (!is_string($predicate)) {
-        // Display an error message.
-        $this->_displayError("While parsing an XPath expression ".
-          "there was an error in the following predicate, ".
-          "because it was not a string. It was a '".$predicate."'", __LINE__, 
__FILE__);
-        $result = FALSE;
-        break; // try-block
-      }
-
-      $predicate = trim($predicate);
-      // Numpty check.  If they give us an empty string, then this is an 
error. ## N.S
-      if ($predicate === '') {
-        // Display an error message.
-        $this->_displayError("While parsing an XPath expression ".
-          "there was an error in the predicate " .
-          "because it was the null string.  If you wish to seach ".
-          "for the empty string, you must use ''.", __LINE__, __FILE__);
-        $result = FALSE;
-        break; // try-block
-      }
-
-      /////////////////////////////////////////////
-      // Quick ways out.
-      // If it is a literal string, then we return the literal string.  ## 
N.S. --sb
-      $stringDelimiterMismatsh = 0;
-      if (preg_match(':^"(.*)"$:', $predicate, $regs)) {
-        $result = $regs[1];
-        $stringDelimiterMismatsh = strpos(' ' . $result, '"');
-        if ($bDebugThisFunction) echo "Predicate is literal: \"{$result}\"\n";
-      } elseif (preg_match(":^'(.*)'$:", $predicate, $regs)) {
-        $result = $regs[1];
-        $stringDelimiterMismatsh = strpos(' ' . $result, "'");
-        if ($bDebugThisFunction) echo "Predicate is literal '{$result}'\n";
-      }
-      if (isSet($result)) break; // try-block
-
-      if ($stringDelimiterMismatsh > 0) {
-        $this->_displayError("While parsing an XPath expression ".
-              "there was an string delimiter miss match at pos 
[{$stringDelimiterMismatsh}] in the predicate string '{$predicate}'.", 
__LINE__, __FILE__);
-        $result = FALSE;
-        break; // try-block
-      }
-
-      // Check whether the predicate is just a digit.
-      if (is_numeric($predicate)) {
-        // Return the value of the digit.
-        $result = doubleval($predicate);
-        if ($bDebugThisFunction) echo "Predicate is double: '{$result}'\n";
-        break; // try-block
-      }
-
-      /////////////////////////////////////////////
-      // Check for operators.
-      // Set the default position and the type of the operator.
-      $position = 0;
-      $operator = '';
-
-      // Run through all operators and try to find one.
-      $opSize = sizeOf($this->operators);
-      for ($i=0; $i<$opSize; $i++) {
-        if ($position >0) break;
-        $operator = $this->operators[$i];
-        // Quickcheck. If not present don't wast time searching 'the hard way'
-        if (strpos($predicate, $operator)===FALSE) continue;
-        // Special check
-        $position = $this->_searchString($predicate, $operator);
-        // Check whether a operator was found.
-        if ($position <= 0 ) continue;
-        // Check whether it's the equal operator.
-        if ($operator == '=') {
-          // Also look for other operators containing the equal sign.
-          switch ($predicate[$position-1]) {
-            case '<' :
-              $position--;
-              $operator = '<=';
-              break;
-            case '>' :
-              $position--;
-              $operator = '>=';
-              break;
-            case '!' :
-              $position--;
-              $operator = '!=';
-              break;
-            default:
+      $aStartTime = $this->_beginDebugFunction(__LINE__.":_evaluateStep");
+      echo "Context:";
+      $this->_printContext($context);
+      echo "\n";
+      echo "Steps: ";
+      print_r($steps);
+      echo "<hr>\n";
+    }
+    //////////////////////////////////////////////
+
+    $result = array(); // Create an empty array for saving the abs. XPath's 
found.
+
+    $contextPaths = array();   // Create an array to save the new contexts.
+    $step = trim(array_shift($steps)); // Get this step.
+    if ($bDebugThisFunction) echo __LINE__.":Evaluating step $step\n";
+
+    $axis = $this->_getAxis($step); // Get the axis of the current step.
+
+    // If there was no axis, then it must be a PrimaryExpr
+    if ($axis == FALSE) {
+      if ($bDebugThisFunction) echo __LINE__.":Step is not an axis but a 
PrimaryExpr\n";
+      // ### This isn't correct, as the result of this function might not be a 
node set.
+      $error = $this->_evaluatePrimaryExpr($step, $context, $contextPaths);
+      if (!empty($error)) {
+        $this->_displayError("Expression failed to parse as PrimaryExpr 
because: $error"
+                , __LINE__, __FILE__, FALSE);
+      }
+    } else {
+      if ($bDebugThisFunction) { echo __LINE__.":Axis of step is:\n"; 
print_r($axis); echo "\n";}
+      $method = '_handleAxis_' . $axis['axis']; // Create the name of the 
method.
+
+      // Check whether the axis handler is defined. If not display an error 
message.
+      if (!method_exists($this, $method)) {
+        $this->_displayError('While parsing an XPath query, the axis ' .
+        $axis['axis'] . ' could not be handled, because this version does not 
support this axis.', __LINE__, __FILE__);
+      }
+      if ($bDebugThisFunction) echo __LINE__.":Calling user method $method\n";
+
+      // Perform an axis action.
+      $contextPaths = $this->$method($axis, $context['nodePath']);
+      if ($bDebugThisFunction) { echo __LINE__.":We found these contexts from 
this step:\n"; print_r( $contextPaths ); echo "\n";}
+      }
+
+    // Check whether there are predicates.
+    if (count($contextPaths) > 0 && count($axis['predicate']) > 0) {
+      if ($bDebugThisFunction) echo __LINE__.":Filtering contexts by 
predicate...\n";
+
+      // Check whether each node fits the predicates.
+      $contextPaths = $this->_checkPredicates($contextPaths, 
$axis['predicate']);
+      }
+
+    // Check whether there are more steps left.
+    if (count($steps) > 0) {
+      if ($bDebugThisFunction) echo __LINE__.":Evaluating next step given the 
context of the first step...\n";
+
+      // Continue the evaluation of the next steps.
+
+      // Run through the array.
+      $size = sizeOf($contextPaths);
+      for ($pos=0; $pos<$size; $pos++) {
+        // Build new context
+        $newContext = array('nodePath' => $contextPaths[$pos], 'size' => 
$size, 'pos' => $pos + 1);
+        if ($bDebugThisFunction) echo __LINE__.":Evaluating step for the 
{$contextPaths[$pos]} context...\n";
+        // Call this method for this single path.
+        $xPathSetNew = $this->_evaluateStep($steps, $newContext);
+        if ($bDebugThisFunction) {echo "New results for this context:\n"; 
print_r($xPathSetNew);}
+        $result = array_merge($result, $xPathSetNew);
           }
+
+      // Remove duplicated nodes.
+      $result = array_unique($result);
+    } else {
+      $result = $contextPaths; // Save the found contexts.
         }
-        if ($operator == '*') {
-          // Get some substrings.
-          $character = substr($predicate, $position - 1, 1);
-          $attribute = substr($predicate, $position - 11, 11);

-          // Check whether it's an attribute selection.
-          if (( $character == '@' ) || ( $attribute == 'attribute::' )) {
-            // Don't use the operator.
-            $operator = '';
-            $position = -1;
-          }
+    //////////////////////////////////////////////
+    if ($bDebugThisFunction) $this->_closeDebugFunction($aStartTime, $result);
+
+    // Return the result.
+    return $result;
+  }
+
+  /**
+   * Checks whether a node matches predicates.
+   *
+   * This method checks whether a list of nodes passed to this method match
+   * a given list of predicates.
+   *
+   * @param  $xPathSet   (array)  Array of full paths of all nodes to be 
tested.
+   * @param  $predicates (array)  Array of predicates to use.
+   * @return             (array)  Vector of absolute XPath's that match the 
given predicates.
+   * @see    _evaluateStep()
+   */
+  function _checkPredicates($xPathSet, $predicates) {
+    // If you are having difficulty using this function.  Then set this to 
TRUE and
+    // you'll get diagnostic info displayed to the output.
+    $bDebugThisFunction = in_array('_checkPredicates', $this->aDebugFunctions);
+
+    if ($bDebugThisFunction) {
+      $aStartTime = 
$this->_beginDebugFunction("_checkPredicates(Nodes:[$xPathSet], 
Predicates:[$predicates])");
+      echo "XPathSet:";
+      print_r($xPathSet);
+      echo "Predicates:";
+      print_r($predicates);
+      echo "<hr>";
         }
-      } // end while each($this->operators)
-
-      // Check whether an operator was found.
-      if ($position > 0) {
-        if ($bDebugThisFunction) echo "\nPredicate operator is a [$operator] 
at pos '$position'";
-        // Get the left and the right part of the expression.
-        $left_predicate  = trim(substr($predicate, 0, $position));
-        $right_predicate = trim(substr($predicate, $position + 
strlen($operator)));
-        if ($bDebugThisFunction) echo "\nLEFT:[$left_predicate]  
oper:[$operator]  RIGHT:[$right_predicate]";
-
-        // Remove whitespaces.
-        $left_predicate  = trim($left_predicate);
-        $right_predicate = trim($right_predicate);
-        // Evaluate the left and the right part.
-        if ($bDebugThisFunction) echo "\nEvaluating LEFT:[$left_predicate]";
-        $left = $this->_evaluatePredicate($absoluteXPath, $left_predicate, 
$nodeTest);
-        if ($bDebugThisFunction) echo "$left_predicate evals as: $left - ";
-        // Only evaluate the right part if we need to.
-        $right = FALSE;
-        if (!$left and ($operator == ' and ')) {
-          if ($bDebugThisFunction) echo "\nNo point in evaluating the right 
predicate: [$right_predicate]";
-        } else {
-          if ($bDebugThisFunction) echo "\nEvaluating 
RIGHT:[$right_predicate]";
-          $right = $this->_evaluatePredicate($absoluteXPath, $right_predicate, 
$nodeTest);
-          if ($bDebugThisFunction) echo "$right_predicate evals as: $right \n";
-        }
-        // Check the kind of operator.
-        $b_result = FALSE;
-        switch ($operator) {
-          case ' or ': // Return the two results connected by an 'or'.
-            $b_result = (bool)( $left or $right );
-            break;
-          case ' and ': // Return the two results connected by an 'and'.
-            $b_result = (bool)( $left and $right );
-            break;
-          case '=': // Compare the two results.
-            $b_result = (bool)( $left == $right );
-            break;
-          case '!=': // Check whether the two results are not equal.
-            $b_result = (bool)( $left != $right );
-            break;
-          case '<=': // Compare the two results.
-            $b_result = (bool)( $left <= $right );
-            break;
-          case '<': // Compare the two results.
-            $b_result = (bool)( $left < $right );
-            break;
-          case '>=': // Compare the two results.
-            $b_result = (bool)( $left >= $right );
-            break;
-          case '>': // Compare the two results.
-            $b_result = (bool)( $left > $right );
-            break;
-          case '+': // Return the result by adding one result to the other.
-            $b_result = $left + $right;
-            break;
-          case '-': // Return the result by decrease one result by the other.
-            $b_result = $left - $right;
-            break;
-          case '*': // Return a multiplication of the two results.
-            $b_result =  $left * $right;
-            break;
-          case ' div ': // Return a division of the two results.
-            if ($right == 0) {
-              // Display an error message.
-              $this->_displayError('While parsing an XPath '.
-                'predicate, a error due a division by zero '.
-                'occured.', __LINE__, __FILE__);
+    //////////////////////////////////////////////
+    // Create an empty set of nodes.
+    $result = array();
+
+    // Run through all predicates.
+    $pSize = sizeOf($predicates);
+    for ($j=0; $j<$pSize; $j++) {
+      $predicate = $predicates[$j];
+      if ($bDebugThisFunction) echo "Evaluating predicate \"$predicate\"\n";
+
+      // This will contain all the nodes that match this predicate
+      $aNewSet = array();
+
+      // Run through all nodes.
+      $contextSize = count($xPathSet);
+      for ($contextPos=0; $contextPos<$contextSize; $contextPos++) {
+        $xPath = $xPathSet[$contextPos];
+
+        // Build the context for this predicate
+        $context = array('nodePath' => $xPath, 'size' => $contextSize, 'pos' 
=> $contextPos + 1);
+
+        // Check whether the predicate is just an number.
+        if (preg_match('/^\d+$/', $predicate)) {
+          if ($bDebugThisFunction) echo "Taking short cut and calling 
_handleFunction_position() directly.\n";
+          // Take a short cut.  If it is just a position, then call
+          // _handleFunction_position() directly.  70% of the
+          // time this will be the case. ## N.S
+//          $check = (bool) ($predicate == $context['pos']);
+          $check = (bool) ($predicate == $this->_handleFunction_position('', 
$context));
             } else {
-              // Return the result of the division.
-              $b_result = $left / $right;
+          // Else do the predicate check the long and through way.
+          $check = $this->_evaluateExpr($predicate, $context);
             }
-            break;
-          case ' mod ': // Return a modulo of the two results.
-            $b_result = $left % $right;
-            break;
+        if ($bDebugThisFunction) {
+          echo "Evaluating the predicate returned ";
+          var_dump($check);
+          echo "\n";
         }
-        $result = $b_result;
+
+        if (is_int($check)) { // Check whether it's an integer.
+          // Check whether it's the current position.
+          $check = (bool) ($check == $this->_handleFunction_position('', 
$context));
+        } else {
+          $check = (bool) ($this->_handleFunction_boolean($check, $context));
+//          if ($bDebugThisFunction) {echo 
$this->_handleFunction_string($check, $context);}
       }
-      if (isSet($result)) break; // try-block

-      /////////////////////////////////////////////
-      // Check for functions.
-      // Check whether the predicate is a function.
-      // do not catch the text() node, which looks like a function in its 
pattern
-      if (preg_match(':\(:U', $predicate) && 
!preg_match(":text\(\)(\[\d*\])?$:",$predicate) ) {
-        // Get the position of the first bracket.
-        $start = strpos($predicate, '(');
-        // If we search for the right bracket from the end of the string, we 
can support nested function calls.
-        // Fix by Andrei Zmievski
-        $end   = strrpos($predicate, ')');
-
-        // Get everything before, between and after the brackets.
-        $before  = substr($predicate, 0, $start);
-        $between = substr($predicate, $start + 1, $end - $start - 1);
-        $after   = substr($predicate, $end + 1);
-
-        // Trim each string.
-        $before  = trim($before);
-        $between = trim($between);
-        $after   = trim($after);
-
-        if ($bDebugThisFunction) echo "\nPredicate is function \"$before\"";
-        // Check whether there's something after the bracket.
-        if (!empty($after)) {
-          // Display an error message.
-          $this->_displayError('While parsing an XPath expression there was an 
error in the predicate ' .
-            str_replace($predicate,'<b>'.$predicate.'</b>', 
$this->currentXpathQuery) .
-            '. After a closing bracket there was something unknown: "'. $after 
.'"', __LINE__, __FILE__);
+        if ($bDebugThisFunction) echo "Node $xPath matches predicate 
$predicate: " . (($check) ? "TRUE" : "FALSE") ."\n";
+
+        // Do we add it?
+        if ($check) $aNewSet[] = $xPath;
         }

-        // Check whether it's a function.
-        if (empty($before) && empty($after)) {
-          // Evaluate the content of the brackets.
-          $result = $this->_evaluatePredicate($absoluteXPath, $between, 
$nodeTest);
-        }
-        elseif (in_array($before, $this->functions)) {
-          // Return the evaluated function.
-          $result = $this->_evaluateFunction($before, $between, 
$absoluteXPath, $nodeTest);
+      // Use the newly filtered list.
+      $xPathSet = $aNewSet;
+
+      if ($bDebugThisFunction) {echo "Node set now contains : "; 
print_r($xPathSet); }
         }
-        else {
-          // Display an error message.
-          $this->_displayError('While parsing a predicate in an XPath 
expression, a function '.
-            str_replace($before, '<b>'.$before.'</b>', 
$this->currentXpathQuery) .
-            ' was found, which is not yet supported by the parser.', __LINE__, 
__FILE__);
+
+    $result = $xPathSet;
+
+    //////////////////////////////////////////////
+    if ($bDebugThisFunction) {
+      $this->_closeDebugFunction($aStartTime, $result);
         }
+    // Return the array of nodes.
+    return $result;
       }
-      if (isSet($result)) break; // try-block

-      /////////////////////////////////////////////
-      // Else it must just be an XPath expression.
-      // Check whether it's an XPath expression.
-      if ($bDebugThisFunction) echo "\nPredicate is XPath expression that is 
to be evaluated.";
-      $tmpXpathSet = $this->_internalEvaluate($predicate, $absoluteXPath);
-      if ($bDebugThisFunction) { echo "\nResult of XPath expression"; 
print_r($tmpXpathSet); }
-      if (count($tmpXpathSet) > 0) {
-        // Convert the array.
-        $tmpXpathSet = explode("|", implode("|", $tmpXpathSet));
-        // Get the value of the first result (which means we want to concat 
all the text...unless
-        // a specific text() node has been given, and it will switch off to 
substringData
-        $result = $this->wholeText($tmpXpathSet[0]);
-      }
+  /**
+   * Evaluates an XPath function
+   *
+   * This method evaluates a given XPath function with its arguments on a
+   * specific node of the document.
+   *
+   * @param  $function      (string) Name of the function to be evaluated.
+   * @param  $arguments     (string) String containing the arguments being
+   *                                 passed to the function.
+   * @param  $context       (array)  The context from which to evaluate
+   * @return                (mixed)  This method returns the result of the 
evaluation of
+   *                                 the function. Depending on the function 
the type of the
+   *                                 return value can be different.
+   * @see    evaluate()
+   */
+  function _evaluateFunction($function, $arguments, $context) {
+    // If you are having difficulty using this function.  Then set this to 
TRUE and
+    // you'll get diagnostic info displayed to the output.
+    $bDebugThisFunction = in_array('_evaluateFunction', 
$this->aDebugFunctions);
+    if ($bDebugThisFunction) {
+      $aStartTime = $this->_beginDebugFunction("_evaluateFunction");
+      if (is_array($arguments)) {
+        echo "Arguments:\n";
+        print_r($arguments);
+      } else {
+        echo "Arguments: $arguments\n";
+      }
+      echo "Context:";
+      $this->_printContext($context);
+      echo "\n";
+      echo "<hr>\n";
+    }
+    /////////////////////////////////////
+    // Remove whitespaces.
+    $function  = trim($function);
+    $arguments = trim($arguments);
+    // Create the name of the function handling function.
+    $method = '_handleFunction_'. $function;

-    } while(FALSE);  // END try-block
+    // Check whether the function handling function is available.
+    if (!method_exists($this, $method)) {
+      // Display an error message.
+      $this->_displayError("While parsing an XPath query, ".
+        "the function \"$function\" could not be handled, because this ".
+        "version does not support this function.", __LINE__, __FILE__);
+    }
+    if ($bDebugThisFunction) echo "Calling function $method($arguments)\n";

-    // Else no content so return the empty string.  ## N.S
-    if (!isSet($result)) $result = '';
+    // Return the result of the function.
+    $result = $this->$method($arguments, $context);

+    //////////////////////////////////////////////
+    // Return the nodes found.
     if ($bDebugThisFunction) {
-      echo "<pre>";
-      var_dump($result);
-      echo "</pre>";
       $this->_closeDebugFunction($aStartTime, $result);
     }
-    // Return the array of nodes.
+    // Return the result.
     return $result;
   }

-
   /**
    * Checks whether a node matches a node-test.
    *
    * This method checks whether a node in the document matches a given 
node-test.
+   * A node test is something like text(), node(), or an element name.
    *
    * @param  $contextPath (string)  Full xpath of the node, which should be 
tested for
    *                                matching the node-test.
@@ -2564,8 +3631,20 @@
    * @see    evaluate()
    */
   function _checkNodeTest($contextPath, $nodeTest) {
+    // Empty node test means that it must match
+    if (empty($nodeTest)) return TRUE;
+
     if ($nodeTest == '*') {
-      return TRUE; // Add this node to the node-set.
+      // * matches all element nodes.
+      return (!preg_match(':/[^/]+\(\)\[\d+\]$:U', $contextPath));
+    }
+    elseif (preg_match('/^[\w-:\.]+$/', $nodeTest)) {
+       // http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
+       // The real spec for what constitutes whitespace is quite elaborate, and
+       // we currently just hope that "\w" catches them all.  In reality it 
should
+       // start with a letter too, not a number, but we've just left it simple.
+       // It's just a node name test.  It should end with "/$nodeTest[x]"
+       return (preg_match('"/'.$nodeTest.'\[\d+\]$"', $contextPath));
     }
     elseif (preg_match('/\(/U', $nodeTest)) { // Check whether it's a function.
       // Get the type of function to use.
@@ -2605,19 +3684,13 @@
           break;
 ***********/
         default:  // Display an error message.
-          $this->_displayError('While parsing an XPath expression there was an 
undefined function called "' .
+          $this->_displayError('While parsing an XPath query there was an 
undefined function called "' .
              str_replace($function, '<b>'.$function.'</b>', 
$this->currentXpathQuery) .'"', __LINE__, __FILE__);
       }
     }
-    elseif (preg_match('/^[a-zA-Z0-9\-_]+/', $nodeTest)) {
-      // Check whether the node-test can be fulfilled.
-      if (!strcmp($this->nodeIndex[$contextPath]['name'],$nodeTest)) {
-        return TRUE; // Add this node to the node-set.
-      }
-    }
     else { // Display an error message.
-      $this->_displayError("While parsing the XPath expression 
\"{$this->currentXpathQuery}\" ".
-        "an empty and therefore invalid node-test has been found.", __LINE__, 
__FILE__);
+      $this->_displayError("While parsing the XPath query 
\"{$this->currentXpathQuery}\" ".
+        "an empty and therefore invalid node-test has been found.", __LINE__, 
__FILE__, FALSE);
     }

     return FALSE; // Don't add this context.
@@ -2628,17 +3701,41 @@
   
//-----------------------------------------------------------------------------------------

   /**
-   * Retrieves axis information from an XPath expression step.
+   * Retrieves axis information from an XPath query step.
    *
    * This method tries to extract the name of the axis and its node-test
-   * from a given step of an XPath expression at a given node.
+   * from a given step of an XPath query at a given node.  If it can't parse
+   * the step, then we treat it as a PrimaryExpr.
    *
-   * @param  $step     (string) String containing a step of an XPath 
expression.
-   * @param  $nodePath (string) Full document path of the node on which the 
step is executed.
-   * @return           (array)  Contains information about the axis found in 
the step.
+   * [4]    Step            ::= AxisSpecifier NodeTest Predicate*
+   *                            | AbbreviatedStep
+   * [5]    AxisSpecifier   ::= AxisName '::'
+   *                            | AbbreviatedAxisSpecifier
+   * [12]   AbbreviatedStep ::= '.'
+   *                            | '..'
+   * [13]   AbbreviatedAxisSpecifier
+   *                        ::=    '@'?
+   *
+   * [7]    NodeTest        ::= NameTest
+   *                            | NodeType '(' ')'
+   *                            | 'processing-instruction' '(' Literal ')'
+   * [37]   NameTest        ::= '*'
+   *                            | NCName ':' '*'
+   *                            | QName
+   * [38]   NodeType        ::= 'comment'
+   *                            | 'text'
+   *                            | 'processing-instruction'
+   *                            | 'node'
+   *
+   * @param  $step     (string) String containing a step of an XPath query.
+   * @return           (array)  Contains information about the axis found in 
the step, or FALSE
+   *                            if the string isn't a valid step.
    * @see    _evaluateStep()
    */
-  function _getAxis($step, $nodePath) {
+  function _getAxis($step) {
+    // The results of this function are very cachable, as it is completely 
independant of context.
+    static $aResultsCache = array();
+
     // Create an array to save the axis information.
     $axis = array(
       'axis'      => '',
@@ -2646,11 +3743,28 @@
       'predicate' => array()
     );

+    $cacheKey = $step;
     do { // parse block
       $parseBlock = 1;

+      if (isset($aResultsCache[$cacheKey])) {
+        return $aResultsCache[$cacheKey];
+      } else {
+        // We have some danger of causing recursion here if we refuse to parse 
a step as having an
+        // axis, and demand it be treated as a PrimaryExpr.  So if we are 
going to fail, make sure
+        // we record what we tried, so that we can catch to see if it comes 
straight back.
+        $guess = array(
+          'axis' => 'child',
+          'node-test' => $step,
+          'predicate' => array());
+        $aResultsCache[$cacheKey] = $guess;
+      }
+
+      ///////////////////////////////////////////////////
+      // Spot the steps that won't come with an axis
+
       // Check whether the step is empty or only self.
-      if ( empty($step) OR ($step == '.') OR ($step == 'current()') ) {
+      if (empty($step) OR ($step == '.') OR ($step == 'current()')) {
         // Set it to the default value.
         $step = '.';
         $axis['axis']      = 'self';
@@ -2658,119 +3772,149 @@
         break $parseBlock;
       }

-      // Check whether is an abbreviated syntax.
-      if ($step == '*') {
-        // Use the child axis and select all children.
-        $axis['axis']      = 'child';
+      if ($step == '..') {
+        // Select the parent axis.
+        $axis['axis']      = 'parent';
         $axis['node-test'] = '*';
         break $parseBlock;
       }

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

       // Check whether there are predicates and add the predicate to the list
       // of predicates without []. Get contents of every [] found.
-      $regex = '/\[(.*)\]/';
-      preg_match_all($regex, $step, $regs);
-      if (!empty($regs[1])) {
-        $axis['predicate'] = $regs[1];
-        // Reduce the step.
-        $step = preg_replace($regex,"",$step); //$this->_prestr($step, '[');
-      }
-
+      $groups = $this->_getEndGroups($step);
+//print_r($groups);
+      $groupCount = count($groups);
+      while (($groupCount > 0) && ($groups[$groupCount - 1][0] == '[')) {
+        // Remove the [] and add the predicate to the top of the list
+        $predicate = substr($groups[$groupCount - 1], 1, -1);
+        array_unshift($axis['predicate'], $predicate);
+        // Pop a group off the end of the list
+        array_pop($groups);
+        $groupCount--;
+      }
+
+      // Finally stick the rest back together and this is the rest of our step
+      if ($groupCount > 0) {
+        $step = implode('', $groups);
+      }
+
+      ///////////////////////////////////////////////////
+      // Pull off the axis
+
+      // Check for abbreviated syntax
+      if ($step[0] == '@') {
+        // Use the attribute axis and select the attribute.
+        $axis['axis']      = 'attribute';
+        $step = substr($step, 1);
+      } else {
       // Check whether the axis is given in plain text.
-      if ($this->_searchString($step, '::') > -1) {
+        if (preg_match("/^([^:]*)::(.*)$/", $step, $match)) {
         // Split the step to extract axis and node-test.
-        $axis['axis']      = $this->_prestr($step, '::');
-        $axis['node-test'] = $this->_afterstr($step, '::');
-        if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
-          // Case in-sensitive
-          $axis['node-test'] = strtoupper($axis['node-test']);
+          $axis['axis'] = $match[1];
+          $step         = $match[2];
+        } else {
+          // The default axis is child
+          $axis['axis'] = 'child';
         }
-        break $parseBlock;
       }

-      if ($step[0] == '@') {
-        // Use the attribute axis and select the attribute.
-        $axis['axis']      = 'attribute';
-        $axis['node-test'] = substr($step, 1);
-        if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
-          // Case in-sensitive
-          $axis['node-test'] = strtoupper($axis['node-test']);
+      ///////////////////////////////////////////////////
+      // Process the rest which will either a node test, or else this isn't a 
step.
+
+      // Check whether is an abbreviated syntax.
+      if ($step == '*') {
+        // Use the child axis and select all children.
+        $axis['node-test'] = '*';
+        break $parseBlock;
         }
+
+      // ### I'm pretty sure our current handling of cdata is a fudge, and we 
should
+      // really do this better, but leave this as is for now.
+      if ($step == "text()") {
+        // Handle the text node
+        $axis["node-test"] = "cdata";
         break $parseBlock;
       }

-      if (eregi('\]$', $step)) {
-        // Use the child axis and select a position.
-        $axis['axis']      = 'child';
-        $axis['node-test'] = substr($step, strpos($step, '['));
+      // There are a few node tests that we match verbatim.
+      if ($step == "node()"
+          || $step == "comment()"
+          || $step == "text()"
+          || $step == "processing-instruction") {
+        $axis["node-test"] = $step;
         break $parseBlock;
       }

-      if ($step == '..') {
-        // Select the parent axis.
-        $axis['axis']      = 'parent';
-        $axis['node-test'] = '*';
+      // processing-instruction() is allowed to take an argument, but if it 
does, the argument
+      // is a literal, which we will have parsed out to $[number].
+      if (preg_match(":processing-instruction\(\$\d*\):", $step)) {
+        $axis["node-test"] = $step;
         break $parseBlock;
       }

-      if (preg_match('/^[a-zA-Z0-9\-_]+$/', $step)) {
-        // Select the child axis and the child.
-        $axis['axis']      = 'child';
+      // The only remaining way this can be a step, is if the remaining string 
is a simple name
+      // or else a :* name.
+      // http://www.w3.org/TR/xpath#NT-NameTest
+      // NameTest   ::= '*'
+      //                | NCName ':' '*'
+      //                | QName
+      // QName      ::=  (Prefix ':')? LocalPart
+      // Prefix     ::=  NCName
+      // LocalPart  ::=  NCName
+      //
+      // ie
+      // NameTest   ::= '*'
+      //                | NCName ':' '*'
+      //                | (NCName ':')? NCName
+      $NCName = "[a-zA-Z][\w\.\-_]*";
+      if (preg_match("/^$NCName:$NCName$/", $step)
+        || preg_match("/^$NCName:*$/", $step)) {
         $axis['node-test'] = $step;
         if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
           // Case in-sensitive
           $axis['node-test'] = strtoupper($axis['node-test']);
         }
+        // Not currently recursing
+        $LastFailedStep = '';
+        $LastFailedContext = '';
         break $parseBlock;
       }

-      if ( $step == "text()" ) {
-        // Handle the text node
-        $axis["axis"]      = "child";
-        $axis["node-test"] = "cdata";
-        break $parseBlock;
-      }
-
-      // Default will be to fall back to using the child axis and a name.
-      $axis['axis']      = 'child';
+      // It's not a node then, we must treat it as a PrimaryExpr
+      // Check for recursion
+      if ($LastFailedStep == $step) {
+        $this->_displayError('Recursion detected while parsing an XPath query, 
in the step ' .
+              str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery)
+              , __LINE__, __FILE__, FALSE);
       $axis['node-test'] = $step;
-      if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
-        // Case in-sensitive
-        $axis['node-test'] = strtoupper($axis['node-test']);
+      } else {
+        $LastFailedStep = $step;
+        $axis = FALSE;
       }

     } while(FALSE); // end parse block

     // Check whether it's a valid axis.
+    if ($axis !== FALSE) {
     if (!in_array($axis['axis'], array_merge($this->axes, array('function')))) 
{
       // Display an error message.
-      $this->_displayError('While parsing an XPath expression, in the step ' .
+        $this->_displayError('While parsing an XPath query, in the step ' .
         str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery) .
         ' the invalid axis ' . $axis['axis'] . ' was found.', __LINE__, 
__FILE__, FALSE);
     }
+    }
+
+    // Cache the real axis information
+    $aResultsCache[$cacheKey] = $axis;
+
     // Return the axis information.
     return $axis;
   }

+
   /**
    * Handles the XPath child axis.
    *
@@ -2786,8 +3930,8 @@
    */
   function _handleAxis_child($axis, $contextPath) {
     $xPathSet = array(); // Create an empty node-set to hold the results of 
the child matches
-    if ( $axis["node-test"] == "cdata" ) {
-      if ( !isSet($this->nodeIndex[$contextPath]['textParts']) ) return '';
+    if ($axis["node-test"] == "cdata") {
+      if (!isSet($this->nodeIndex[$contextPath]['textParts']) ) return '';
       $tSize = sizeOf($this->nodeIndex[$contextPath]['textParts']);
       for ($i=1; $i<=$tSize; $i++) {
         $xPathSet[] = $contextPath . '/text()['.$i.']';
@@ -2801,10 +3945,23 @@
       $cSize = sizeOf($allChildren);
       for ($i=0; $i<$cSize; $i++) {
         $childPath = $contextPath .'/'. $allChildren[$i]['name'] .'['. 
$allChildren[$i]['contextPos']  .']';
+        $textChildPath = $contextPath.'/text()['.($i + 1).']';
+        // Check the text node
+        if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // 
node test check
+          $xPathSet[] = $textChildPath; // Add the child to the node-set.
+        }
+        // Check the actual node
         if ($this->_checkNodeTest($childPath, $axis['node-test'])) { // node 
test check
           $xPathSet[] = $childPath; // Add the child to the node-set.
         }
       }
+
+      // Finally there will be one more text node to try
+     $textChildPath = $contextPath.'/text()['.($cSize + 1).']';
+     // Check the text node
+     if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node 
test check
+       $xPathSet[] = $textChildPath; // Add the child to the node-set.
+     }
     }
     return $xPathSet; // Return the nodeset.
   }
@@ -2842,12 +3999,13 @@

     // Check whether all nodes should be selected.
     $nodeAttr = $this->nodeIndex[$contextPath]['attributes'];
-    if ($axis['node-test'] == '*') {
+    if ($axis['node-test'] == '*'
+        || $axis['node-test'] == 'node()') {
       foreach($nodeAttr as $key=>$dummy) { // Run through the attributes.
         $xPathSet[] = $contextPath.'/attribute::'.$key; // Add this node to 
the node-set.
       }
     }
-    elseif (!empty($nodeAttr[$axis['node-test']])) {
+    elseif (isset($nodeAttr[$axis['node-test']])) {
       $xPathSet[] = $contextPath . '/attribute::'. $axis['node-test']; // Add 
this node to the node-set.
     }
     return $xPathSet; // Return the nodeset.
@@ -2919,7 +4077,7 @@
         $xPathSet[] = $parentPath; // Add the parent to the list of nodes.
       }
       // Handle all other ancestors.
-      $xPathSet = array_merge($xPathSet, $this->_handleAxis_ancestor($axis, 
$parentPath));
+      $xPathSet = array_merge($this->_handleAxis_ancestor($axis, $parentPath), 
$xPathSet);
     }
     return $xPathSet; // Return the nodeset.
   }
@@ -3101,8 +4259,8 @@

     // Read the nodes.
     $xPathSet = array_merge(
-                 $this->_handleAxis_self($axis, $contextPath),
-                 $this->_handleAxis_ancestor($axis, $contextPath)
+                 $this->_handleAxis_ancestor($axis, $contextPath),
+                 $this->_handleAxis_self($axis, $contextPath)
                );
     return $xPathSet; // Return the nodeset.
   }
@@ -3115,76 +4273,49 @@
    /**
     * Handles the XPath function last.
     *
-    * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
     * @param  $arguments     (string) String containing the arguments that 
were passed to the function.
+    * @param  $context       (array)  The context from which to evaluate the 
function
     * @return                (mixed)  Depending on the type of function being 
processed
     * @see    evaluate()
     */
-  function _handleFunction_last($absoluteXPath, $arguments, $nodeTest) {
-    // Calculate the size of the context.
-    $parentNode = $this->nodeIndex[$absoluteXPath]['parentNode'];
-    if ($nodeTest == "*") {
-      $contextPos = sizeOf($parentNode['childNodes']);
-    }
-    elseif ($nodeTest == "cdata") {
-      $absoluteXPath = 
substr($absoluteXPath,0,strrpos($absoluteXPath,"/text()"));
-      $contextPos = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
-    }
-    else {
-      $contextPos = 0;
-      $name = $this->nodeIndex[$absoluteXPath]['name'];
-      foreach($parentNode['childNodes'] as $childNode) {
-        $contextPos += ($childNode['name'] === $name) ? 1 : 0;
-      }
-    }
-    return $contextPos; // Return the size.
+  function _handleFunction_last($arguments, $context) {
+    return $context['size'];
   }

   /**
    * Handles the XPath function position.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_position($absoluteXPath, $arguments, $nodeTest) {
-    // return the context-position.
-    if ($nodeTest == "*") { // if we are matching all children, then we need 
to find the position regardless of name
-      // 'pos' is zero-based, not one based.
-      $contextPos = $this->nodeIndex[$absoluteXPath]['pos']+1;
-    }
-    elseif ($nodeTest == "cdata") {  // if we are looking for text nodes, we 
go about it a bit differently
-      $contextPos = substr($absoluteXPath,strrpos($absoluteXPath,"[")+1,-1);
-    }
-    else {
-      $contextPos = $this->nodeIndex[$absoluteXPath]['contextPos'];
-    }
-    return $contextPos;
+  function _handleFunction_position($arguments, $context) {
+    return $context['pos'];
   }

   /**
    * Handles the XPath function count.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_count($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_count($arguments, $context) {
     // Evaluate the argument of the method as an XPath and return the number 
of results.
-    return count($this->_internalEvaluate($arguments, $absoluteXPath));
+    return count($this->_evaluateExpr($arguments, $context));
   }

   /**
    * Handles the XPath function id.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_id($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_id($arguments, $context) {
     $arguments = trim($arguments);         // Trim the arguments.
     $arguments = explode(' ', $arguments); // Now split the arguments into an 
array.
     // Create a list of nodes.
@@ -3195,7 +4326,7 @@
     for ($i=0; $i<$kSize; $i++) {
       if (empty($keys[$i])) continue; // skip super-Root
       if (in_array($this->nodeIndex[$keys[$i]]['attributes']['id'], 
$arguments)) {
-        $resultXPaths[] = $absoluteXPath; // Add this node to the list of 
nodes.
+        $resultXPaths[] = $context['nodePath']; // Add this node to the list 
of nodes.
       }
     }
     return $resultXPaths; // Return the list of nodes.
@@ -3204,55 +4335,94 @@
   /**
    * Handles the XPath function name.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_name($absoluteXPath, $arguments, $nodeTest) {
-    return $this->nodeIndex[$absoluteXPath]['name']; // Return the name of the 
node.
+  function _handleFunction_name($arguments, $context) {
+    // If the argument it omitted, it defaults to a node-set with the context 
node as its only member.
+    if (empty($arguments)) {
+      return 
$this->_addLiteral($this->nodeIndex[$context['nodePath']]['name']);
+    }
+
+    // Evaluate the argument to get a node set.
+    $nodeSet = $this->_evaluateExpr($arguments, $context);
+    if (!is_array($nodeSet)) return '';
+    if (count($nodeSet) < 1) return '';
+    if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
+     // Return a reference to the name of the node.
+    return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['name']);
   }

   /**
    * Handles the XPath function string.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
+   * http://www.w3.org/TR/xpath#section-String-Functions
+   *
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_string($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_string($arguments, $context) {
     // Check what type of parameter is given
-    if (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) OR 
preg_match('/^\.[0-9]+$/', $arguments)) {
+    if (is_array($arguments)) {
+      // Get the value of the first result (which means we want to concat all 
the text...unless
+      // a specific text() node has been given, and it will switch off to 
substringData
+      if (!count($arguments)) $result = '';
+      else {
+        $result = $this->_stringValue($arguments[0]);
+        if (($literal = $this->_asLiteral($result)) !== FALSE) {
+          $result = $literal;
+        }
+      }
+    }
+    // Is it a number string?
+    elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) OR 
preg_match('/^\.[0-9]+$/', $arguments)) {
+      // ### Note no support for NaN and Infinity.
       $number = doubleval($arguments); // Convert the digits to a number.
-      return strval($number); // Return the number.
+      $result = strval($number); // Return the number.
     }
     elseif (is_bool($arguments)) { // Check whether it's TRUE or FALSE and 
return as string.
-      if ($arguments === TRUE)  return 'TRUE'; else return 'FALSE';
+      // ### Note that we used to return TRUE and FALSE which was incorrect 
according to the standard.
+      if ($arguments === TRUE) {
+        $result = 'true';
+      } else {
+        $result = 'false';
+      }
+    }
+    elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
+      return $literal;
     }
     elseif (!empty($arguments)) {
+      // Spec says:
+      // "An object of a type other than the four basic types is converted to 
a string in a way that
+      // is dependent on that type."
       // Use the argument as an XPath.
-      $result = $this->_internalEvaluate($arguments, $absoluteXPath);
-      $result = explode('|', implode('|', $result)); // Get the first argument.
-      return $result[0];          // Return the first result as a string.
+      $result = $this->_evaluateExpr($arguments, $context);
+      if (is_string($result) && is_string($arguments) && (!strcmp($result, 
$arguments))) {
+        $this->_displayError("Loop detected in XPath expression.  Probably an 
internal error :o/.  _handleFunction_string($result)", __LINE__, __FILE__, 
FALSE);
+        return '';
+      } else {
+        $result = $this->_handleFunction_string($result, $context);
     }
-    elseif (empty($arguments)) {
-      return $absoluteXPath;      // Return the current node.
     }
     else {
-      return '';  // Return an empty string.
+      $result = '';  // Return an empty string.
     }
+    return $result;
   }

   /**
    * Handles the XPath function concat.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_concat($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_concat($arguments, $context) {
     // Split the arguments.
     $arguments = explode(',', $arguments);
     // Run through each argument and evaluate it.
@@ -3260,27 +4430,27 @@
     for ($i=0; $i<$size; $i++) {
       $arguments[$i] = trim($arguments[$i]);  // Trim each argument.
       // Evaluate it.
-      $arguments[$i] = $this->_evaluatePredicate($absoluteXPath, 
$arguments[$i], $nodeTest);
+      $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
     }
     $arguments = implode('', $arguments);  // Put the string together and 
return it.
-    return $arguments;
+    return $this->_addLiteral($arguments);
   }

   /**
    * Handles the XPath function starts-with.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_starts_with ($absoluteXPath, $arguments, $nodeTest) 
{
+  function _handleFunction_starts_with($arguments, $context) {
     // Get the arguments.
     $first  = trim($this->_prestr($arguments, ','));
     $second = trim($this->_afterstr($arguments, ','));
     // Evaluate each argument.
-    $first  = $this->_evaluatePredicate($absoluteXPath, $first, $nodeTest);
-    $second = $this->_evaluatePredicate($absoluteXPath, $second, $nodeTest);
+    $first  = $this->_handleFunction_string($first, $context);
+    $second = $this->_handleFunction_string($second, $context);
     // Check whether the first string starts with the second one.
     return  (bool) ereg('^'.$second, $first);
   }
@@ -3288,19 +4458,19 @@
   /**
    * Handles the XPath function contains.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_contains($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_contains($arguments, $context) {
     // Get the arguments.
     $first  = trim($this->_prestr($arguments, ','));
     $second = trim($this->_afterstr($arguments, ','));
     //echo "Predicate: $arguments First: ".$first." Second: ".$second."\n";
     // Evaluate each argument.
-    $first = $this->_evaluatePredicate($absoluteXPath, $first, $nodeTest);
-    $second = $this->_evaluatePredicate($absoluteXPath, $second, $nodeTest);
+    $first = $this->_handleFunction_string($first, $context);
+    $second = $this->_handleFunction_string($second, $context);
     //echo $second.": ".$first."\n";
     // If the search string is null, then the provided there is a value it 
will contain it as
     // it is considered that all strings contain the empty string. ## N.S.
@@ -3316,78 +4486,78 @@
   /**
    * Handles the XPath function substring-before.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_substring_before($absoluteXPath, $arguments, 
$nodeTest) {
+  function _handleFunction_substring_before($arguments, $context) {
     // Get the arguments.
     $first  = trim($this->_prestr($arguments, ','));
     $second = trim($this->_afterstr($arguments, ','));
     // Evaluate each argument.
-    $first  = $this->_evaluatePredicate($absoluteXPath, $first, $nodeTest);
-    $second = $this->_evaluatePredicate($absoluteXPath, $second, $nodeTest);
+    $first  = $this->_handleFunction_string($first, $context);
+    $second = $this->_handleFunction_string($second, $context);
     // Return the substring.
-    return $this->_prestr(strval($first), strval($second));
+    return $this->_addLiteral($this->_prestr(strval($first), strval($second)));
   }

   /**
    * Handles the XPath function substring-after.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_substring_after($absoluteXPath, $arguments, 
$nodeTest) {
+  function _handleFunction_substring_after($arguments, $context) {
     // Get the arguments.
     $first  = trim($this->_prestr($arguments, ','));
     $second = trim($this->_afterstr($arguments, ','));
     // Evaluate each argument.
-    $first  = $this->_evaluatePredicate($absoluteXPath, $first, $nodeTest);
-    $second = $this->_evaluatePredicate($absoluteXPath, $second, $nodeTest);
+    $first  = $this->_handleFunction_string($first, $context);
+    $second = $this->_handleFunction_string($second, $context);
     // Return the substring.
-    return $this->_afterstr(strval($first), strval($second));
+    return $this->_addLiteral($this->_afterstr(strval($first), 
strval($second)));
   }

   /**
    * Handles the XPath function substring.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_substring($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_substring($arguments, $context) {
     // Split the arguments.
     $arguments = explode(",", $arguments);
     $size = sizeOf($arguments);
     for ($i=0; $i<$size; $i++) { // Run through all arguments.
       $arguments[$i] = trim($arguments[$i]); // Trim the string.
       // Evaluate each argument.
-      $arguments[$i] = $this->_evaluatePredicate($absoluteXPath, 
$arguments[$i], $nodeTest);
+      $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
     }
     // Check whether a third argument was given and return the substring..
     if (!empty($arguments[2])) {
-      return substr(strval($arguments[0]), $arguments[1] - 1, $arguments[2]);
+      return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 
1, $arguments[2]));
     } else {
-      return substr(strval($arguments[0]), $arguments[1] - 1);
+      return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 
1));
     }
   }

   /**
    * Handles the XPath function string-length.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_string_length($absoluteXPath, $arguments, 
$nodeTest) {
+  function _handleFunction_string_length($arguments, $context) {
     $arguments = trim($arguments); // Trim the argument.
     // Evaluate the argument.
-    $arguments = $this->_evaluatePredicate($absoluteXPath, $arguments, 
$nodeTest);
+    $arguments = $this->_handleFunction_string($arguments, $context);
     return strlen(strval($arguments)); // Return the length of the string.
   }

@@ -3400,116 +4570,141 @@
    * If the argument is omitted, it defaults to the context node converted to 
a string,
    * in other words the string-value of the context node
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                 (stri)g trimed string
    * @see    evaluate()
    */
-  function _handleFunction_normalize_space($absoluteXPath, $arguments, 
$nodeTest) {
+  function _handleFunction_normalize_space($arguments, $context) {
     if (empty($arguments)) {
-      $arguments = 
$this->getParentXPath($absoluteXPath).'/'.$this->nodeIndex[$absoluteXPath]['name'].'['.$this->nodeIndex[$absoluteXPath]['contextPos'].']';
+      $arguments = 
$this->getParentXPath($context['nodePath']).'/'.$this->nodeIndex[$context['nodePath']]['name'].'['.$this->nodeIndex[$context['nodePath']]['contextPos'].']';
     } else {
-       $arguments = $this->_evaluatePredicate($absoluteXPath, $arguments, 
$nodeTest);
+       $arguments = $this->_handleFunction_string($arguments, $context);
     }
     $arguments = trim(preg_replace (";[[:space:]]+;s",' ',$arguments));
-    return $arguments;
+    return $this->_addLiteral($arguments);
   }

   /**
    * Handles the XPath function translate.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_translate($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_translate($arguments, $context) {
     $arguments = explode(',', $arguments); // Split the arguments.
     $size = sizeOf($arguments);
     for ($i=0; $i<$size; $i++) { // Run through all arguments.
       $arguments[$i] = trim($arguments[$i]); // Trim the argument.
       // Evaluate the argument.
-      $arguments[$i] = $this->_evaluatePredicate($absoluteXPath, 
$arguments[$i], $nodeTest);
+      $arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
     }
-    return strtr($arguments[0], $arguments[1], $arguments[2]);  // Return the 
translated string.
+    // Return the translated string.
+    return $this->_addLiteral(strtr($arguments[0], $arguments[1], 
$arguments[2]));
   }

   /**
    * Handles the XPath function boolean.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
+   * http://www.w3.org/TR/xpath#section-Boolean-Functions
+   *
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_boolean($absoluteXPath, $arguments, $nodeTest) {
-    $arguments = trim($arguments); // Trim the arguments.
-    // Check what type of parameter is given
-    if (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) || 
preg_match('/^\.[0-9]+$/', $arguments)) {
+  function _handleFunction_boolean($arguments, $context) {
+    if (empty($arguments)) {
+      return FALSE; // Sorry, there were no arguments.
+    }
+    // a bool is dead obvious
+    elseif (is_bool($arguments)) {
+      return $arguments;
+    }
+    // a node-set is true if and only if it is non-empty
+    elseif (is_array($arguments)) {
+      return (count($arguments) > 0);
+    }
+    // a number is true if and only if it is neither positive or negative zero 
nor NaN
+    // (Straight out of the XPath spec.. makes no sense?????)
+    elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) || 
preg_match('/^\.[0-9]+$/', $arguments)) {
       $number = doubleval($arguments);  // Convert the digits to a number.
       // If number zero return FALSE else TRUE.
       if ($number == 0) return FALSE; else return TRUE;
     }
-    elseif (empty($arguments)) {
-      return FALSE; // Sorry, there were no arguments.
+    // a string is true if and only if its length is non-zero
+    elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
+      return (strlen($literal) != 0);
     }
+    // an object of a type other than the four basic types is converted to a 
boolean in a
+    // way that is dependent on that type
     else {
+      // Spec says:
+      // "An object of a type other than the four basic types is converted to 
a number in a way
+      // that is dependent on that type"
       // Try to evaluate the argument as an XPath.
-      $result = $this->_internalEvaluate($arguments, $absoluteXPath);
-      // If we found something return TRUE else FALSE.
-      if (count($result) > 0) return FALSE; else return TRUE;
+      $result = $this->_evaluateExpr($arguments, $context);
+      if (is_string($result) && is_string($arguments) && (!strcmp($result, 
$arguments))) {
+        $this->_displayError("Loop detected in XPath expression.  Probably an 
internal error :o/.  _handleFunction_boolean($result)", __LINE__, __FILE__, 
FALSE);
+        return FALSE;
+      } else {
+        return $this->_handleFunction_boolean($result, $context);
+      }
     }
   }

   /**
    * Handles the XPath function not.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_not($absoluteXPath, $arguments, $nodeTest) {
-    $arguments = trim($arguments); // Trim the arguments.
+  function _handleFunction_not($arguments, $context) {
     // Return the negative value of the content of the brackets.
-    return !$this->_evaluatePredicate($absoluteXPath, $arguments, $nodeTest);
+    $bArgResult = $this->_handleFunction_boolean($arguments, $context);
+//echo "Before inversion: ".($bArgResult?"TRUE":"FALSE")."\n";
+    return !$bArgResult;
   }

   /**
    * Handles the XPath function TRUE.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_true($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_true($arguments, $context) {
     return TRUE; // Return TRUE.
   }

   /**
    * Handles the XPath function FALSE.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_false($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_false($arguments, $context) {
     return FALSE; // Return FALSE.
   }

   /**
    * Handles the XPath function lang.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_lang($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_lang($arguments, $context) {
     $arguments = trim($arguments); // Trim the arguments.
-    $currentNode = $this->nodeIndex[$absoluteXPath];
+    $currentNode = $this->nodeIndex[$context['nodePath']];
     while (!empty($currentNode['name'])) { // Run through the ancestors.
       // Check whether the node has an language attribute.
       if (isSet($currentNode['attributes']['xml:lang'])) {
@@ -3524,57 +4719,94 @@
   /**
    * Handles the XPath function number.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
+   * http://www.w3.org/TR/xpath#section-Number-Functions
+   *
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_number($absoluteXPath, $arguments, $nodeTest) {
-    if (!is_numeric($arguments)) {
-      $arguments = $this->_evaluatePredicate($absoluteXPath, $arguments, 
$nodeTest);
-    }
+  function _handleFunction_number($arguments, $context) {
     // Check the type of argument.
+
+    // A string that is a number
     if (is_numeric($arguments)) {
       return doubleval($arguments); // Return the argument as a number.
     }
+    // A bool
     elseif (is_bool($arguments)) {  // Return TRUE/FALSE as a number.
       if ($arguments === TRUE) return 1; else return 0;
     }
+    // A node set
+    elseif (is_array($arguments)) {
+      // Is converted to a string then handled like a string
+      $string = $this->_handleFunction_string($arguments, $context);
+      if (is_numeric($string))
+        return doubleval($string);
+    }
+    elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
+      if (is_numeric($literal)) {
+        return doubleval($literal);
+      } else {
+        // If we are to stick strictly to the spec, we should return NaN, but 
lets just
+        // leave PHP to see if can do some dynamic conversion.
+        return $literal;
+      }
+    }
+    else {
+      // Spec says:
+      // "An object of a type other than the four basic types is converted to 
a number in a way
+      // that is dependent on that type"
+      // Try to evaluate the argument as an XPath.
+      $result = $this->_evaluateExpr($arguments, $context);
+      if (is_string($result) && is_string($arguments) && (!strcmp($result, 
$arguments))) {
+        $this->_displayError("Loop detected in XPath expression.  Probably an 
internal error :o/.  _handleFunction_number($result)", __LINE__, __FILE__, 
FALSE);
+        return FALSE;
+      } else {
+        return $this->_handleFunction_number($result, $context);
+      }
+    }
   }

   /**
    * Handles the XPath function sum.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_sum($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_sum($arguments, $context) {
     $arguments = trim($arguments); // Trim the arguments.
-    // Evaluate the arguments as an XPath expression.
-    $result = $this->_internalEvaluate($arguments, $absoluteXPath);
+    // Evaluate the arguments as an XPath query.
+    $result = $this->_evaluateExpr($arguments, $context);
     $sum = 0; // Create a variable to save the sum.
+    // The sum function expects a node set as an argument.
+    if (is_array($result)) {
     // Run through all results.
     $size = sizeOf($result);
     for ($i=0; $i<$size; $i++) {
-      $value = $this->substringData($result[$i]); // Get the value of the node.
+        $value = $this->_stringValue($result[$i], $context);
+        if (($literal = $this->_asLiteral($value)) !== FALSE) {
+          $value = $literal;
+        }
       $sum += doubleval($value); // Add it to the sum.
     }
+    }
     return $sum; // Return the sum.
   }

   /**
    * Handles the XPath function floor.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_floor($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_floor($arguments, $context) {
     if (!is_numeric($arguments)) {
-      $arguments = $this->_evaluatePredicate($absoluteXPath, $arguments, 
$nodeTest);
+      $arguments = $this->_handleFunction_number($arguments, $context);
     }
     $arguments = doubleval($arguments); // Convert the arguments to a number.
     return floor($arguments);           // Return the result
@@ -3583,14 +4815,14 @@
   /**
    * Handles the XPath function ceiling.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_ceiling($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_ceiling($arguments, $context) {
     if (!is_numeric($arguments)) {
-      $arguments = $this->_evaluatePredicate($absoluteXPath, $arguments, 
$nodeTest);
+      $arguments = $this->_handleFunction_number($arguments, $context);
     }
     $arguments = doubleval($arguments); // Convert the arguments to a number.
     return ceil($arguments);            // Return the result
@@ -3599,30 +4831,166 @@
   /**
    * Handles the XPath function round.
    *
-   * @param  $absoluteXPath (string) Full xpath of the node on which the 
function should be processed.
    * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
    * @return                (mixed)  Depending on the type of function being 
processed
    * @see    evaluate()
    */
-  function _handleFunction_round($absoluteXPath, $arguments, $nodeTest) {
+  function _handleFunction_round($arguments, $context) {
     if (!is_numeric($arguments)) {
-      $arguments = $this->_evaluatePredicate($absoluteXPath, $arguments, 
$nodeTest);
+      $arguments = $this->_handleFunction_number($arguments, $context);
     }
     $arguments = doubleval($arguments); // Convert the arguments to a number.
     return round($arguments);           // Return the result
   }

   
//-----------------------------------------------------------------------------------------
+  // XPath                  ------  XPath Extension FUNCTION Handlers  ------
+  
//-----------------------------------------------------------------------------------------
+
+  /**
+   * Handles the XPath function x-lower.
+   *
+   * lower case a string.
+   *    string x-lower(string)
+   *
+   * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
+   * @return                (mixed)  Depending on the type of function being 
processed
+   * @see    evaluate()
+   */
+  function _handleFunction_x_lower($arguments, $context) {
+    // Evaluate the argument.
+    $string = $this->_handleFunction_string($arguments, $context);
+     // Return a reference to the lowercased string
+    return $this->_addLiteral(strtolower(strval($string)));
+  }
+
+  /**
+   * Handles the XPath function x-upper.
+   *
+   * upper case a string.
+   *    string x-upper(string)
+   *
+   * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
+   * @return                (mixed)  Depending on the type of function being 
processed
+   * @see    evaluate()
+   */
+  function _handleFunction_x_upper($arguments, $context) {
+    // Evaluate the argument.
+    $string = $this->_handleFunction_string($arguments, $context);
+     // Return a reference to the lowercased string
+    return $this->_addLiteral(strtoupper(strval($string)));
+  }
+
+  /**
+   * Handles the XPath function generate-id.
+   *
+   * Produce a unique id for the first node of the node set.
+   *
+   * Example usage, produces an index of all the nodes in an .xml document, 
where the content of each
+   * "section" is the exported node as XML.
+   *
+   *   $aFunctions = $xPath->match('//');
+   *
+   *   foreach ($aFunctions as $Function) {
+   *       $id = $xPath->match("generate-id($Function)");
+   *       echo "<a href='#$id'>$Function</a><br>";
+   *   }
+   *
+   *   foreach ($aFunctions as $Function) {
+   *       $id = $xPath->match("generate-id($Function)");
+   *       echo "<h2 id='$id'>$Function</h2>";
+   *       echo htmlspecialchars($xPath->exportAsXml($Function));
+   *   }
+   *
+   * @param  $arguments     (string) String containing the arguments that were 
passed to the function.
+   * @param  $context       (array)  The context from which to evaluate the 
function
+   * @return                (mixed)  Depending on the type of function being 
processed
+   * @author Ricardo Garcia
+   * @see    evaluate()
+   */
+  function _handleFunction_generate_id($arguments, $context) {
+    // If the argument is omitted, it defaults to a node-set with the context 
node as its only member.
+    if (is_string($arguments) && empty($arguments)) {
+      // We need ids then
+      $this->_generate_ids();
+      return 
$this->_addLiteral($this->nodeIndex[$context['nodePath']]['generated_id']);
+    }
+
+    // Evaluate the argument to get a node set.
+    $nodeSet = $this->_evaluateExpr($arguments, $context);
+
+    if (!is_array($nodeSet)) return '';
+    if (count($nodeSet) < 1) return '';
+    if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
+     // Return a reference to the name of the node.
+    // We need ids then
+    $this->_generate_ids();
+    return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['generated_id']);
+  }
+
+  
//-----------------------------------------------------------------------------------------
   // XPathEngine                ------  Help Stuff  ------
   
//-----------------------------------------------------------------------------------------

   /**
-   * Compare to nodes if they are equal
+   * Decodes the character set entities in the given string.
+   *
+   * This function is given for convenience, as all text strings or attributes
+   * are going to come back to you with their entities still encoded.  You can
+   * use this function to remove these entites.
+   *
+   * It makes use of the get_html_translation_table(HTML_ENTITIES) php library
+   * call, so is limited in the same ways.  At the time of writing this seemed
+   * be restricted to iso-8859-1
+   *
+   * ### Provide an option that will do this by default.
+   *
+   * @param $encodedData (mixed) The string or array that has entities you 
would like to remove
+   * @param $reverse     (bool)  If TRUE entities will be encoded rather than 
decoded, ie
+   *                             < to &lt; rather than &lt; to <.
+   * @return             (mixed) The string or array returned with entities 
decoded.
+   */
+  function decodeEntities($encodedData, $reverse=FALSE) {
+    static $aEncodeTbl;
+    static $aDecodeTbl;
+    // Get the translation entities, but we'll cache the result to enhance 
performance.
+    if (empty($aDecodeTbl)) {
+      // Get the translation entities.
+      $aEncodeTbl = get_html_translation_table(HTML_ENTITIES);
+      $aDecodeTbl = array_flip($aEncodeTbl);
+    }
+
+    // If it's just a single string.
+    if (!is_array($encodedData)) {
+      if ($reverse) {
+        return strtr($encodedData, $aEncodeTbl);
+      } else {
+        return strtr($encodedData, $aDecodeTbl);
+      }
+    }
+
+    $result = array();
+    foreach($encodedData as $string) {
+      if ($reverse) {
+        $result[] = strtr($string, $aEncodeTbl);
+      } else {
+        $result[] = strtr($string, $aDecodeTbl);
+      }
+    }
+
+    return $result;
+  }
+
+  /**
+   * Compare two nodes to see if they are equal (point to the same node in the 
doc)
    *
-   * 2 nodes are considered equal if the abs. xpath is equal.
+   * 2 nodes are considered equal if the absolute XPath is equal.
    *
-   * @param  $node1 (mixed) Either a xpath string to an node OR a real 
tree-node (hash-array)
-   * @param  $node2 (mixed) Either a xpath string to an node OR a real 
tree-node (hash-array)
+   * @param  $node1 (mixed) Either an absolute XPath to an node OR a real 
tree-node (hash-array)
+   * @param  $node2 (mixed) Either an absolute XPath to an node OR a real 
tree-node (hash-array)
    * @return        (bool)  TRUE if equal (see text above), FALSE if not (and 
on error).
    */
   function equalNodes($node1, $node2) {
@@ -3632,7 +5000,7 @@
   }

   /**
-   * Get the Xpath string of a node that is in a document tree.
+   * Get the absolute XPath of a node that is in a document tree.
    *
    * @param $node (array)  A real tree-node (hash-array)
    * @return      (string) The string path to the node or FALSE on error.
@@ -3655,13 +5023,13 @@
   }

   /**
-   * Retrieves the absolute parent XPath expression.
+   * Retrieves the absolute parent XPath query.
    *
    * The parents stored in the tree are only relative parents...but all the 
parent
-   * information is stored in the xPath expression itself...so instead we use 
a function
-   * to extract the parent from the absolute xpath expression
+   * information is stored in the XPath query itself...so instead we use a 
function
+   * to extract the parent from the absolute Xpath query
    *
-   * @param  $childPath (string) String containing an absolute XPath expression
+   * @param  $childPath (string) String containing an absolute XPath query
    * @return            (string) returns the absolute XPath of the parent
    */
    function getParentXPath($absoluteXPath) {
@@ -3680,6 +5048,7 @@
    * @return                (bool)   TRUE if this node exists and has a child, 
FALSE otherwise
    */
   function hasChildNodes($absoluteXPath) {
+    if ($this->_indexIsDirty) $this->reindexNodeTree();
     return (bool) (isSet($this->nodeIndex[$absoluteXPath])
                    AND sizeOf($this->nodeIndex[$absoluteXPath]['childNodes']));
   }
@@ -3725,8 +5094,14 @@
    * @return            (string) The XML string with translated ampersands.
    */
   function _translateAmpersand($xmlSource, $reverse=FALSE) {
+    $PHP5 = (substr(phpversion(), 0, 1) == '5');
+    if ($PHP5) {
+      //otherwise we receive  &amp;nbsp;  instead of  &nbsp;
+      return $xmlSource;
+    } else {
     return ($reverse ? str_replace('&amp;', '&', $xmlSource) : 
str_replace('&', '&amp;', $xmlSource));
   }
+  }

 } // END OF CLASS XPathEngine

@@ -3762,7 +5137,10 @@
     $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
     if ($fileName) {
       if (!$this->importFromFile($fileName)) {
-        $this = FALSE;
+        // Re-run the base constructor to "reset" the object.  If the user has 
any sense, then
+        // they will have created the object, and then explicitly called 
importFromFile(), giving
+        // them the chance to catch and handle the error properly.
+        parent::XPathEngine($userXmlOptions);
       }
     }
   }
@@ -3775,7 +5153,6 @@
    */
   function reset() {
     parent::reset();
-    $this->xpath   = '';
     $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
   }

@@ -3827,8 +5204,12 @@
    *                             not exist, then returns FALSE.
    */
   function nodeName($xPathQuery) {
+    if (is_array($xPathQuery)) {
+      $xPathSet = $xPathQuery;
+    } else {
     // Check for a valid xPathQuery
     $xPathSet = $this->_resolveXPathQuery($xPathQuery,'nodeName');
+    }
     if (count($xPathSet) == 0) return FALSE;
     // For each node, get it's name
     $result = array();
@@ -3892,6 +5273,9 @@
         }
         // Otherwise remove the node by setting it to NULL. It will be removed 
on the next reindexNodeTree() call.
         $mustReindex = $autoReindex;
+        // Flag the index as dirty; it's not uptodate. A reindex will be 
forced (if dirty) when exporting the XML doc
+        $this->_indexIsDirty = TRUE;
+
         $theNode = $this->nodeIndex[$absoluteXPath];
         $theNode['parentNode']['childNodes'][$theNode['pos']] =& $NULL;
         if ($bDebugThisFunction) echo "We removed the node 
'$absoluteXPath'.\n";
@@ -3943,8 +5327,11 @@
       $mustReindex = FALSE;
       // Make chages from 'bottom-up'. In this manner the modifications will 
not affect itself.
       for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
-        $absoluteXPath = $xPathSet[$i];
         $mustReindex = $autoReindex;
+        // Flag the index as dirty; it's not uptodate. A reindex will be 
forced (if dirty) when exporting the XML doc
+        $this->_indexIsDirty = TRUE;
+
+        $absoluteXPath = $xPathSet[$i];
         $theNode = $this->nodeIndex[$absoluteXPath];
         $pos = $theNode['pos'];
         $theNode['parentNode']['textParts'][$pos] .= $data;
@@ -3969,7 +5356,9 @@
    *       Depending on setModMatch() one, none or multiple nodes are affected.
    *
    * @param  $xPathQuery  (string) Xpath to the node being replaced.
-   * @param  $node        (array)  A doc node.
+   * @param  $node        (mixed)  String or Array (Usually a String)
+   *                               If string: Vaild XML. E.g. "<A/>" or "<A> 
foo <B/> bar <A/>"
+   *                               If array:  A Node (can be a whole sub-tree) 
(See comment in header)
    * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the 
document to reflect
    *                               the changes.  A performance helper.  See 
reindexNodeTree()
    * @return              (array)  The last replaced $node (can be a whole 
sub-tree)
@@ -3978,10 +5367,15 @@
   function &replaceChild($xPathQuery, $node, $autoReindex=TRUE) {
     $NULL = NULL;
     if (is_string($node)) {
+      if (empty($node)) { //--sam. Not sure how to react on an empty string - 
think it's an error.
+        return array();
+      } else {
       if (!($node = $this->_xml2Document($node))) return FALSE;
     }
+    }
+
     // Special case if it's 'super root'. We then have to take the child node 
== top node
-    if (empty($node['name'])) $node = $node['childNodes'][0];
+    if (empty($node['parentNode'])) $node = $node['childNodes'][0];

     $status = FALSE;
     do { // try-block
@@ -3992,10 +5386,14 @@
         break; // try-block
       }
       $mustReindex = FALSE;
+
       // Make chages from 'bottom-up'. In this manner the modifications will 
not affect itself.
       for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
-        $absoluteXPath = $xPathSet[$i];
         $mustReindex = $autoReindex;
+        // Flag the index as dirty; it's not uptodate. A reindex will be 
forced (if dirty) when exporting the XML doc
+        $this->_indexIsDirty = TRUE;
+
+        $absoluteXPath = $xPathSet[$i];
         $childNode =& $this->nodeIndex[$absoluteXPath];
         $parentNode =& $childNode['parentNode'];
         $childNode['parentNode'] =& $NULL;
@@ -4024,9 +5422,9 @@
    *                                  /       \
    *                              ..BBB[1]..BBB[2] ..
    *
-   * a) insertChild('/AAA[1]/BBB[1]', <node CCC>)
-   * b) insertChild('/AAA[1]/BBB[1]', <node CCC>, $shiftRight=FALSE)
-   * c) insertChild('/AAA[1]/BBB[1]', <node CCC>, $shiftRight=FALSE, 
$afterText=FALSE)
+   * a) insertChild('/AAA[1]/BBB[2]', <node CCC>)
+   * b) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE)
+   * c) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE, 
$afterText=FALSE)
    *
    * a)                          b)                           c)
    *          AAA[1]                       AAA[1]                       AAA[1]
@@ -4036,42 +5434,65 @@
    * #### Do a complete review of the "(optional)" tag after several arguments.
    *
    * @param  $xPathQuery  (string) Xpath to the node to append.
-   * @param  $node        (array)  A doc node.
+   * @param  $node        (mixed)  String or Array (Usually a String)
+   *                               If string: Vaild XML. E.g. "<A/>" or "<A> 
foo <B/> bar <A/>"
+   *                               If array:  A Node (can be a whole sub-tree) 
(See comment in header)
    * @param  $shiftRight  (bool)   (optional, default=TRUE) Shift the target 
node to the right.
    * @param  $afterText   (bool)   (optional, default=TRUE) Insert after the 
text.
    * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the 
document to reflect
    *                                the changes.  A performance helper.  See 
reindexNodeTree()
-   * @return              (bool)   TRUE on success, FALSE on error.
-   * @see    _xml2Document(), appendChildByXml(), reindexNodeTree()
+   * @return              (mixed)  FALSE on error (or no match). On success we 
return the path(s) to the newly
+   *                               appended nodes. That is: Array of paths if 
more then 1 node was added or
+   *                               a single path string if only one node was 
added.
+   *                               NOTE:  If autoReindex is FALSE, then we 
can't return the *complete* path
+   *                               as the exact doc-pos isn't available 
without reindexing. In that case we leave
+   *                               out the last [docpos] in the path(s). ie  
we'd return /A[3]/B instead of /A[3]/B[2]
+   * @see    appendChildByXml(), reindexNodeTree()
    */
   function insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText=TRUE, 
$autoReindex=TRUE) {
     if (is_string($node)) {
+      if (empty($node)) { //--sam. Not sure how to react on an empty string - 
think it's an error.
+        return FALSE;
+      } else {
       if (!($node = $this->_xml2Document($node))) return FALSE;
     }
+    }
+
     // Special case if it's 'super root'. We then have to take the child node 
== top node
-    if (empty($node['name'])) $node = $node['childNodes'][0];
+    if (empty($node['parentNode'])) $node = $node['childNodes'][0];

     // Check for a valid xPathQuery
-    $xPathSet = $this->_resolveXPathQuery($xPathQuery,'appendChild');
+    $xPathSet = $this->_resolveXPathQuery($xPathQuery,'insertChild');
     if (sizeOf($xPathSet) === 0) {
       $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], 
$xPathQuery), __LINE__, __FILE__, FALSE);
       return FALSE;
     }
-
     $mustReindex = FALSE;
+    $newNodes = array();
+    $result = array();
     // Make chages from 'bottom-up'. In this manner the modifications will not 
affect itself.
     for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
       $absoluteXPath = $xPathSet[$i];
-      $mustReindex = $autoReindex;
       $childNode =& $this->nodeIndex[$absoluteXPath];
       $parentNode =& $childNode['parentNode'];
+
+      // We can't insert at the super root or at the root.
+      if (empty($absoluteXPath) || (!$parentNode['parentNode'])) {
+        
$this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), 
__LINE__, __FILE__, FALSE);
+        return FALSE;
+      }
+
+      $mustReindex = $autoReindex;
+      // Flag the index as dirty; it's not uptodate. A reindex will be forced 
(if dirty) when exporting the XML doc
+      $this->_indexIsDirty = TRUE;
+
       //Special case: It not possible to add siblings to the top node.
       if (empty($parentNode['name'])) continue;
       $newNode =& $this->cloneNode($node);
       $pos = $shiftRight ? $childNode['pos'] : $childNode['pos']+1;
       $parentNode['childNodes'] = array_merge(
                                     array_slice($parentNode['childNodes'], 0, 
$pos),
-                                    array($newNode),
+                                    array(&$newNode),
                                     array_slice($parentNode['childNodes'], 
$pos)
                                   );
       $pos += $afterText ? 1 : 0;
@@ -4080,9 +5501,29 @@
                                    '',
                                    array_slice($parentNode['textParts'], $pos)
                                  );
-    }
-    if ($mustReindex) $this->reindexNodeTree();
-    return TRUE;
+
+      // We are going from bottom to top, but the user will want results from 
top to bottom.
+      if ($mustReindex) {
+        // We'll have to wait till after the reindex to get the full path to 
this new node.
+        $newNodes[] = &$newNode;
+      } else {
+        // If we are reindexing the tree later, then we can't return the user 
any
+        // useful results, so we just return them the count.
+        $newNodePath = $parentNode['xpath'].'/'.$newNode['name'];
+        array_unshift($result, $newNodePath);
+      }
+    }
+    if ($mustReindex) {
+      $this->reindexNodeTree();
+      // Now we must fill in the result array.  Because until now we did not
+      // know what contextpos our newly added entries had, just their pos 
within
+      // the siblings.
+      foreach ($newNodes as $newNode) {
+        array_unshift($result, $newNode['xpath']);
+    }
+    }
+    if (count($result) == 1) $result = $result[0];
+    return $result;
   }

   /**
@@ -4092,46 +5533,75 @@
    * and then call reindexNodeTree() when you are finished all the appending.
    *
    * @param  $xPathQuery  (string) Xpath to the node to append to.
-   * @param  $node        (array)  A doc node.
+   * @param  $node        (mixed)  String or Array (Usually a String)
+   *                               If string: Vaild XML. E.g. "<A/>" or "<A> 
foo <B/> bar <A/>"
+   *                               If array:  A Node (can be a whole sub-tree) 
(See comment in header)
    * @param  $afterText   (bool)   (optional, default=FALSE) Insert after the 
text.
    * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the 
document to reflect
    *                               the changes.  A performance helper.  See 
reindexNodeTree()
-   * @return              (bool)   TRUE on success, FALSE on error.
-   * @see    reindexNodeTree()
+   * @return              (mixed)  FALSE on error (or no match). On success we 
return the path(s) to the newly
+   *                               appended nodes. That is: Array of paths if 
more then 1 node was added or
+   *                               a single path string if only one node was 
added.
+   *                               NOTE:  If autoReindex is FALSE, then we 
can't return the *complete* path
+   *                               as the exact doc-pos isn't available 
without reindexing. In that case we leave
+   *                               out the last [docpos] in the path(s). ie  
we'd return /A[3]/B instead of /A[3]/B[2]
+   * @see    insertChild(), reindexNodeTree()
    */
   function appendChild($xPathQuery, $node, $afterText=FALSE, 
$autoReindex=TRUE) {
     if (is_string($node)) {
+      if (empty($node)) { //--sam. Not sure how to react on an empty string - 
think it's an error.
+        return FALSE;
+      } else {
       if (!($node = $this->_xml2Document($node))) return FALSE;
     }
+    }
+
     // Special case if it's 'super root'. We then have to take the child node 
== top node
-    if (empty($node['name'])) $node = $node['childNodes'][0];
+    if (empty($node['parentNode'])) $node = $node['childNodes'][0];

     // Check for a valid xPathQuery
-    $xPathSet = $this->_resolveXPathQuery($xPathQuery,'appendChild');
-    if (sizeOf($xPathSet) === 0) {
-      $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], 
$xPathQuery), __LINE__, __FILE__, FALSE);
-      return FALSE;
-    }
+    $xPathSet = $this->_resolveXPathQueryForNodeMod($xPathQuery, 
'appendChild');
+    if (sizeOf($xPathSet) === 0) return FALSE;

     $mustReindex = FALSE;
-    $result = FALSE;
+    $newNodes = array();
+    $result = array();
     // Make chages from 'bottom-up'. In this manner the modifications will not 
affect itself.
     for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
-      $absoluteXPath = $xPathSet[$i];
       $mustReindex = $autoReindex;
+      // Flag the index as dirty; it's not uptodate. A reindex will be forced 
(if dirty) when exporting the XML doc
+      $this->_indexIsDirty = TRUE;
+
+      $absoluteXPath = $xPathSet[$i];
       $parentNode =& $this->nodeIndex[$absoluteXPath];
       $newNode =& $this->cloneNode($node);
-      $pos = count($parentNode['childNodes']);
       $parentNode['childNodes'][] =& $newNode;
+      $pos = count($parentNode['textParts']);
       $pos -= $afterText ? 0 : 1;
       $parentNode['textParts'] = array_merge(
                                    array_slice($parentNode['textParts'], 0, 
$pos),
                                    '',
                                    array_slice($parentNode['textParts'], $pos)
                                  );
-      $result[] = "$absoluteXPath/{$newNode['name']}";
+      // We are going from bottom to top, but the user will want results from 
top to bottom.
+      if ($mustReindex) {
+        // We'll have to wait till after the reindex to get the full path to 
this new node.
+        $newNodes[] = &$newNode;
+      } else {
+        // If we are reindexing the tree later, then we can't return the user 
any
+        // useful results, so we just return them the count.
+        array_unshift($result, "$absoluteXPath/{$newNode['name']}");
+      }
+    }
+    if ($mustReindex) {
+      $this->reindexNodeTree();
+      // Now we must fill in the result array.  Because until now we did not
+      // know what contextpos our newly added entries had, just their pos 
within
+      // the siblings.
+      foreach ($newNodes as $newNode) {
+        array_unshift($result, $newNode['xpath']);
+      }
     }
-    if ($mustReindex) $this->reindexNodeTree();
     if (count($result) == 1) $result = $result[0];
     return $result;
   }
@@ -4143,11 +5613,18 @@
    * and then call reindexNodeTree() when you are finished all the appending.
    *
    * @param  $xPathQuery  (string) Xpath to the node to insert new node before
-   * @param  $node        (array)  A doc node.
+   * @param  $node        (mixed)  String or Array (Usually a String)
+   *                               If string: Vaild XML. E.g. "<A/>" or "<A> 
foo <B/> bar <A/>"
+   *                               If array:  A Node (can be a whole sub-tree) 
(See comment in header)
    * @param  $afterText   (bool)   (optional, default=FLASE) Insert after the 
text.
    * @param  $autoReindex (bool)   (optional, default=TRUE) Reindex the 
document to reflect
    *                               the changes.  A performance helper.  See 
reindexNodeTree()
-   * @return              (bool)   TRUE on success, FALSE on error.
+   * @return              (mixed)  FALSE on error (or no match). On success we 
return the path(s) to the newly
+   *                               appended nodes. That is: Array of paths if 
more then 1 node was added or
+   *                               a single path string if only one node was 
added.
+   *                               NOTE:  If autoReindex is FALSE, then we 
can't return the *complete* path
+   *                               as the exact doc-pos isn't available 
without reindexing. In that case we leave
+   *                               out the last [docpos] in the path(s). ie  
we'd return /A[3]/B instead of /A[3]/B[2]
    * @see    reindexNodeTree()
    */
   function insertBefore($xPathQuery, $node, $afterText=TRUE, 
$autoReindex=TRUE) {
@@ -4179,7 +5656,7 @@
   function getAttributes($absoluteXPath, $attrName=NULL) {
     // Numpty check
     if (!isSet($this->nodeIndex[$absoluteXPath])) {
-      $xPathSet = $this->_resolveXPathQuery($absoluteXPath,'setAttributes');
+      $xPathSet = $this->_resolveXPathQuery($absoluteXPath,'getAttributes');
       if (empty($xPathSet)) return FALSE;
       // only use the first entry
       $absoluteXPath = $xPathSet[0];
@@ -4298,16 +5775,16 @@
    * Sample
    * Given is: <AA> This <BB\>is <BB\>  some<BB\>text </AA>
    * Return of getData('/AA[1]') would be:  " This is   sometext "
-   * The first param $absoluteXPath must be a valid xpath OR a xpath-query 
that results
-   * to *one* xpath.
+   * The first param $xPathQuery must be a valid xpath OR a xpath-query that
+   * results to *one* xpath.
    *
-   * @param  $absoluteXPath (string) Full xpath OR a xpath-query that results 
to *one* xpath.
+   * @param  $xPathQuery (string) xpath to the node - resolves to *one* xpath.
    * @return                (mixed)  The returned string (see above), FALSE if 
the node
    *                                 couldn't be found or is not unique.
    * @see getDataParts()
    */
-  function getData($absoluteXPath) {
-    $aDataParts = $this->getDataParts($absoluteXPath);
+  function getData($xPathQuery) {
+    $aDataParts = $this->getDataParts($xPathQuery);
     if ($aDataParts === FALSE) return FALSE;
     return implode('', $aDataParts);
   }
@@ -4323,22 +5800,36 @@
    * The first param $absoluteXPath must be a valid xpath OR a xpath-query 
that results
    * to *one* xpath.
    *
-   * @param  $absoluteXPath   (string) Full xpath OR a xpath-query that 
results to *one* xpath.
+   * @param  $xPathQuery (string) xpath to the node - resolves to *one* xpath.
    * @return                  (mixed)  The returned array (see above), or 
FALSE if node is not
    *                                   found or is not unique.
    * @see getData()
    */
-  function getDataParts($absoluteXPath) {
+  function getDataParts($xPathQuery) {
     // Resolve xPath argument
-    $xPathSet = $this->_resolveXPathQuery($absoluteXPath, 'getDataParts');
-    if (count($xPathSet) != 1) {
-      
$this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], 
$absoluteXPath), __LINE__, __FILE__, FALSE);
+    $xPathSet = $this->_resolveXPathQuery($xPathQuery, 'getDataParts');
+    if (1 !== ($setSize=count($xPathSet))) {
+      
$this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], 
$xPathQuery) . "Not unique xpath-query, matched {$setSize}-times.", __LINE__, 
__FILE__, FALSE);
       return FALSE;
     }
     $absoluteXPath = $xPathSet[0];
-
+    // Is it an attribute node?
+    if (preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches)) {
+      $absoluteXPath = $matches[1];
+      $attribute = $matches[2];
+      if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
+        $this->_displayError("The $absoluteXPath/attribute::$attribute value 
isn't a node in this document.", __LINE__, __FILE__, FALSE);
+        continue;
+      }
+      return array($this->nodeIndex[$absoluteXPath]['attributes'][$attribute]);
+    } else if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, 
$matches)) {
+      $absoluteXPath = $matches[1];
+      $textPartNr = $matches[2];
+      return array($this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr]);
+    } else {
     return $this->nodeIndex[$absoluteXPath]['textParts'];
   }
+  }

   /**
    * Retrieves a sub string of a text-part OR attribute-value.
@@ -4366,7 +5857,10 @@
   /**
    * Replace a sub string of a text-part OR attribute-value.
    *
-   * @param  $absoluteXPath (string) Xpath to the node.
+   * NOTE: When passing a xpath-query instead of an abs. Xpath.
+   *       Depending on setModMatch() one, none or multiple nodes are affected.
+   *
+   * @param  $xPathQuery    (string) xpath to the node (See note above).
    * @param  $replacement   (string) The string to replace with.
    * @param  $offset        (int)    (optional, default is 0) Starting offset. 
(Just like PHP's substr_replace ())
    * @param  $count         (number) (optional, default is 0=ALL) Character 
count  (Just like PHP's substr_replace())
@@ -4462,51 +5956,6 @@
   
//-----------------------------------------------------------------------------------------

   /**
-   * Decodes the character set entities in the given string.
-   *
-   * This function is given for convenience, as all text strings or attributes
-   * are going to come back to you with their entities still encoded.  You can
-   * use this function to remove these entites.
-   *
-   * ### Provide an option that will do this by default.
-   *
-   * @param $encodedData (mixed) The string or array that has entities you 
would like to remove
-   * @param $reverse     (bool)  If TRUE entities will be encoded rather than 
decoded, ie
-   *                             < to &lt; rather than &lt; to <.
-   * @return             (mixed) The string or array returned with entities 
decoded.
-   */
-  function decodeEntities($encodedData, $reverse=FALSE) {
-    static $aEncodeTbl;
-    static $aDecodeTbl;
-    // Get the translation entities, but we'll cache the result to enhance 
performance.
-    if (empty($aDecodeTbl)) {
-      // Get the translation entities.
-      $aEncodeTbl = get_html_translation_table(HTML_ENTITIES);
-      $aDecodeTbl = array_flip($aEncodeTbl);
-    }
-
-    // If it's just a single string.
-    if (!is_array($encodedData)) {
-      if ($reverse) {
-        return strtr($encodedData, $aEncodeTbl);
-      } else {
-        return strtr($encodedData, $aDecodeTbl);
-      }
-    }
-
-    $result = array();
-    foreach($encodedData as $string) {
-      if ($reverse) {
-        $result[] = strtr($string, $aEncodeTbl);
-      } else {
-        $result[] = strtr($string, $aDecodeTbl);
-      }
-    }
-
-    return $result;
-  }
-
-  /**
    * Parse the XML to a node-tree. A so called 'document'
    *
    * @param  $xmlString (string) The string to turn into a document node.
@@ -4518,7 +5967,7 @@
                     XML_OPTION_SKIP_WHITE   => 
$this->getProperties('skipWhiteSpaces')
                   );
     $xmlParser =& new XPathEngine($xmlOptions);
-    $xmlParser->setVerbose(FALSE);
+    $xmlParser->setVerbose($this->properties['verboseLevel']);
     // Parse the XML string
     if (!$xmlParser->importFromString($xmlString)) {
       $this->_displayError($xmlParser->getLastError(), __LINE__, __FILE__, 
FALSE);
@@ -4557,16 +6006,25 @@
    */
   function _getTextSet($xPathQuery, $textPartNr=1) {
     $status = FALSE;
+
+    $bDebugThisFunction = FALSE;  // Get diagnostic output for this function
+    if ($bDebugThisFunction) {
+      $aStartTime = $this->_beginDebugFunction('_getTextSet');
+      echo "Node: $xPathQuery\n";
+      echo "Text Part Number: $textPartNr\n";
+      echo "<hr>";
+    }
+
     $funcName = '_getTextSet';
     $textSet = array();

     do { // try-block
       // Check if it's a Xpath reference to an attribut(s). Xpath ends with 
attribute::)
-      if ( preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $xPathQuery, 
$matches) ) {
+      if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $xPathQuery, 
$matches)) {
         $xPathQuery = $matches[1];
         $attribute = $matches[3];
         // Quick out
-        if (isSet($this->nodeIndex[$xPathQuery]) ) {
+        if (isSet($this->nodeIndex[$xPathQuery])) {
           $xPathSet[] = $xPathQuery;
         } else {
           // Try to evaluate the absoluteXPath (since it seems to be an Xquery 
and not an abs. Xpath)
@@ -4576,7 +6034,7 @@
           preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches);
           $absoluteXPath = $matches[1];
           $attribute = $matches[2];
-          if ( 
!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute]) ) {
+          if 
(!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
             $this->_displayError("The $absoluteXPath/attribute::$attribute 
value isn't a node in this document.", __LINE__, __FILE__, FALSE);
             continue;
           }
@@ -4587,12 +6045,12 @@
       }

       // Check if it's a Xpath reference direct to a text-part(s). (xpath ends 
with text()[<part-number>])
-      if ( preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches) ) 
{
+      if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) {
         $xPathQuery = $matches[1];
         // default to the first text node if a text node was not specified
         $textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
         // Quick check
-        if (isSet($this->nodeIndex[$xPathQuery]) ) {
+        if (isSet($this->nodeIndex[$xPathQuery])) {
           $xPathSet[] = $xPathQuery;
         } else {
           // Try to evaluate the absoluteXPath (since it seams to be an Xquery 
and not an abs. Xpath)
@@ -4603,7 +6061,7 @@
         // At this point we have been given an xpath with neither a 'text()' 
or 'attribute::' axis at the end
         // So this means to get the text-part of the node. If parameter 
$textPartNr was not set, use the last
         // text-part.
-        if (isSet($this->nodeIndex[$xPathQuery]) ) {
+        if (isSet($this->nodeIndex[$xPathQuery])) {
           $xPathSet[] = $xPathQuery;
         } else {
           // Try to evaluate the absoluteXPath (since it seams to be an Xquery 
and not an abs. Xpath)
@@ -4611,19 +6069,73 @@
         }
       }

+      if ($bDebugThisFunction) {
+        echo "Looking up paths for:\n";
+        print_r($xPathSet);
+      }
+
       // Now fetch all text-parts that match. (May be 0,1 or many)
       foreach($xPathSet as $absoluteXPath) {
         unset($text);
         if ($text =& $this->wholeText($absoluteXPath, $textPartNr)) {
           $textSet[] =& $text;
+        } else {
+          // The node does not yet have any text, so we have to add a '' 
string so that
+          // if we insert or replace to it, then we'll actually have something 
to op on.
+          $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1] = '';
+          $textSet[] =& 
$this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1];
         }
       }

       $status = TRUE;
     } while (FALSE); // END try-block

-    if (!$status) return FALSE;
-    return $textSet;
+    if (!$status) $result = FALSE;
+    else          $result = $textSet;
+
+    if ($bDebugThisFunction) $this->_closeDebugFunction($aStartTime, $result);
+
+    return $result;
+  }
+
+
+  /**
+   * Resolves an xPathQuery vector for a node op for modification
+   *
+   * It is possible to create a brand new object, and try to append and insert 
nodes
+   * into it, so this is a version of _resolveXPathQuery() that will 
autocreate the
+   * super root if it detects that it is not present and the $xPathQuery is 
empty.
+   *
+   * Also it demands that there be at least one node returned, and displays a 
suitable
+   * error message if the returned xPathSet does not contain any nodes.
+   *
+   * @param  $xPathQuery (string) An xpath query targeting a single node.  If 
empty()
+   *                              returns the root node and auto creates the 
root node
+   *                              if it doesn't exist.
+   * @param  $function   (string) The function in which this check was called
+   * @return             (array)  Vector of $absoluteXPath's (May be empty)
+   * @see    _resolveXPathQuery()
+   */
+  function _resolveXPathQueryForNodeMod($xPathQuery, $functionName) {
+    $xPathSet = array();
+    if (empty($xPathQuery)) {
+      // You can append even if the root node doesn't exist.
+      if (!isset($this->nodeIndex[$xPathQuery])) $this->_createSuperRoot();
+      $xPathSet[] = '';
+      // However, you can only append to the super root, if there isn't 
already a root entry.
+      $rootNodes = $this->_resolveXPathQuery('/*','appendChild');
+      if (count($rootNodes) !== 0) {
+        
$this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), 
__LINE__, __FILE__, FALSE);
+        return array();
+      }
+    } else {
+      $xPathSet = $this->_resolveXPathQuery($xPathQuery,'appendChild');
+      if (sizeOf($xPathSet) === 0) {
+        $this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], 
$xPathQuery), __LINE__, __FILE__, FALSE);
+        return array();
+      }
+    }
+    return $xPathSet;
   }

   /**
@@ -4635,7 +6147,8 @@
    *   - none (If the query matches more then one node.)
    * see  setModMatch() for details
    *
-   * @param  $xPathQuery (string) An xpath query targeting a single node
+   * @param  $xPathQuery (string) An xpath query targeting a single node.  If 
empty()
+   *                              returns the root node (if it exists).
    * @param  $function   (string) The function in which this check was called
    * @return             (array)  Vector of $absoluteXPath's (May be empty)
    * @see    setModMatch()
@@ -4695,14 +6208,15 @@
   echo "<br><hr><b>" . htmlspecialchars($title) . "</b><hr>\n";
 }

-if (basename($PHP_SELF) == 'XPath.class.php') {
+$self = isSet($_SERVER) ? $_SERVER['PHP_SELF'] : $PHP_SELF;
+if (basename($self) == 'XPath.class.php') {
   // The sampe source:
   $q = '?';
   $xmlSource = <<< EOD
   <{$q}Process_Instruction test="&copy;&nbsp;All right reserved" {$q}>
-    <AAA> ,,1,,
-      ..1.. <![CDATA[ bal  bla
-      kkk ]]>
+    <AAA foo="bar"> ,,1,,
+      ..1.. <![CDATA[ bla  bla
+      newLine blo blo ]]>
       <BBB foo="bar">
         ..2..
       </BBB>..3..<CC/>   ..4..</AAA>
@@ -4710,7 +6224,7 @@

   // The sample code:
   $xmlOptions = array(XML_OPTION_CASE_FOLDING => TRUE, XML_OPTION_SKIP_WHITE 
=> TRUE);
-  $xPath =& new XPath($xmlOptions);
+  $xPath =& new XPath(FALSE, $xmlOptions);
   //$xPath->bDebugXmlParse = TRUE;
   if (!$xPath->importFromString($xmlSource)) { echo $xPath->getLastError(); 
exit; }

@@ -4720,12 +6234,13 @@
   _title("Get some content");
   echo "Last text part in &lt;AAA&gt;: '" . $xPath->wholeText('/AAA[1]', -1) 
."'<br>\n";
   echo "All the text in  &lt;AAA&gt;: '" . $xPath->wholeText('/AAA[1]') 
."'<br>\n";
-  echo "The attibute value  in  &lt;BBB&gt;: '" . 
$xPath->getAttributes('/AAA[1]/BBB[1]', 'FOO') ."'<br>\n";
+  echo "The attibute value  in  &lt;BBB&gt; using 
getAttributes('/AAA[1]/BBB[1]', 'FOO'): '" . $xPath->getAttributes('/AAA[1]', 
'FOO') ."'<br>\n";
+  echo "The attibute value  in  &lt;BBB&gt; using getData('/AAA[1]/@FOO'): '" 
. $xPath->getData('/AAA[1]/@FOO') ."'<br>\n";

   _title("Append some additional XML below /AAA/BBB:");
-  $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 1. Append new node 
</CCC>', $afterText=TRUE);
-  $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 2. Append new node 
</CCC>', $afterText=FALSE);
-  $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 3. Append new node 
</CCC>', $afterText=FALSE);
+  $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 1. Append new node 
</CCC>', $afterText=FALSE);
+  $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 2. Append new node 
</CCC>', $afterText=TRUE);
+  $xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 3. Append new node 
</CCC>', $afterText=TRUE);
   echo $xPath->exportAsHtml();

   _title("Insert some additional XML below <AAA>:");
@@ -4735,14 +6250,14 @@
   $xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 3. Insert new node </BB>', 
$shiftRight=FALSE, $afterText=FALSE);
   echo $xPath->exportAsHtml();

-  _title("Replace the last <BB> node with new XML:");
+  _title("Replace the last <BB> node with new XML data '&lt;DDD&gt; Replaced 
last BB &lt;/DDD&gt;':");
   $xPath->reindexNodeTree();
   $xPath->replaceChild('/AAA[1]/BB[last()]', '<DDD> Replaced last BB </DDD>', 
$afterText=FALSE);
   echo $xPath->exportAsHtml();

   _title("Replace second <BB> node with normal text");
   $xPath->reindexNodeTree();
-  $xPath->replaceChildByData('/AAA[1]/BB[2]', '"Some new text"', 
$afterText=FALSE);
+  $xPath->replaceChildByData('/AAA[1]/BB[2]', '"Some new text"');
   echo $xPath->exportAsHtml();
 }


====================================================
Index: index.html






reply via email to

[Prev in Thread] Current Thread [Next in Thread]