325 |
richard |
1 |
<?php
|
|
|
2 |
/**
|
|
|
3 |
* Php.XPath
|
|
|
4 |
*
|
|
|
5 |
* +======================================================================================================+
|
|
|
6 |
* | A php class for searching an XML document using XPath, and making modifications using a DOM
|
|
|
7 |
* | style API. Does not require the DOM XML PHP library.
|
|
|
8 |
* |
|
|
|
9 |
* +======================================================================================================+
|
|
|
10 |
* | What Is XPath:
|
|
|
11 |
* | --------------
|
|
|
12 |
* | - "What SQL is for a relational database, XPath is for an XML document." -- Sam Blum
|
|
|
13 |
* | - "The primary purpose of XPath is to address parts of an XML document. In support of this
|
|
|
14 |
* | primary purpose, it also provides basic facilities for manipulting it." -- W3C
|
|
|
15 |
* |
|
|
|
16 |
* | XPath in action and a very nice intro is under:
|
|
|
17 |
* | http://www.zvon.org/xxl/XPathTutorial/General/examples.html
|
|
|
18 |
* | Specs Can be found under:
|
|
|
19 |
* | http://www.w3.org/TR/xpath W3C XPath Recommendation
|
|
|
20 |
* | http://www.w3.org/TR/xpath20 W3C XPath Recommendation
|
|
|
21 |
* |
|
|
|
22 |
* | NOTE: Most of the XPath-spec has been realized, but not all. Usually this should not be
|
|
|
23 |
* | problem as the missing part is either rarely used or it's simpler to do with PHP itself.
|
|
|
24 |
* +------------------------------------------------------------------------------------------------------+
|
|
|
25 |
* | Requires PHP version 4.0.5 and up
|
|
|
26 |
* +------------------------------------------------------------------------------------------------------+
|
|
|
27 |
* | Main Active Authors:
|
|
|
28 |
* | --------------------
|
|
|
29 |
* | Nigel Swinson <nigelswinson@users.sourceforge.net>
|
|
|
30 |
* | Started around 2001-07, saved phpxml from near death and renamed to Php.XPath
|
|
|
31 |
* | Restructured XPath code to stay in line with XPath spec.
|
|
|
32 |
* | Sam Blum <bs_php@infeer.com>
|
|
|
33 |
* | Started around 2001-09 1st major restruct (V2.0) and testbench initiator.
|
|
|
34 |
* | 2nd (V3.0) major rewrite in 2002-02
|
|
|
35 |
* | Daniel Allen <bigredlinux@yahoo.com>
|
|
|
36 |
* | Started around 2001-10 working to make Php.XPath adhere to specs
|
|
|
37 |
* | Main Former Author: Michael P. Mehl <mpm@phpxml.org>
|
|
|
38 |
* | Inital creator of V 1.0. Stoped activities around 2001-03
|
|
|
39 |
* +------------------------------------------------------------------------------------------------------+
|
|
|
40 |
* | Code Structure:
|
|
|
41 |
* | --------------_
|
|
|
42 |
* | The class is split into 3 main objects. To keep usability easy all 3
|
|
|
43 |
* | objects are in this file (but may be split in 3 file in future).
|
|
|
44 |
* | +-------------+
|
|
|
45 |
* | | XPathBase | XPathBase holds general and debugging functions.
|
|
|
46 |
* | +------+------+
|
|
|
47 |
* | v
|
|
|
48 |
* | +-------------+ XPathEngine is the implementation of the W3C XPath spec. It contains the
|
|
|
49 |
* | | XPathEngine | XML-import (parser), -export and can handle xPathQueries. It's a fully
|
|
|
50 |
* | +------+------+ functional class but has no functions to modify the XML-document (see following).
|
|
|
51 |
* | v
|
|
|
52 |
* | +-------------+
|
|
|
53 |
* | | XPath | XPath extends the functionality with actions to modify the XML-document.
|
|
|
54 |
* | +-------------+ We tryed to implement a DOM - like interface.
|
|
|
55 |
* +------------------------------------------------------------------------------------------------------+
|
|
|
56 |
* | Usage:
|
|
|
57 |
* | ------
|
|
|
58 |
* | Scroll to the end of this php file and you will find a short sample code to get you started
|
|
|
59 |
* +------------------------------------------------------------------------------------------------------+
|
|
|
60 |
* | Glossary:
|
|
|
61 |
* | ---------
|
|
|
62 |
* | To understand how to use the functions and to pass the right parameters, read following:
|
|
|
63 |
* |
|
|
|
64 |
* | Document: (full node tree, XML-tree)
|
|
|
65 |
* | After a XML-source has been imported and parsed, it's stored as a tree of nodes sometimes
|
|
|
66 |
* | refered to as 'document'.
|
|
|
67 |
* |
|
|
|
68 |
* | AbsoluteXPath: (xPath, xPathSet)
|
|
|
69 |
* | A absolute XPath is a string. It 'points' to *one* node in the XML-document. We use the
|
|
|
70 |
* | term 'absolute' to emphasise that it is not an xPath-query (see xPathQuery). A valid xPath
|
|
|
71 |
* | has the form like '/AAA[1]/BBB[2]/CCC[1]'. Usually functions that require a node (see Node)
|
|
|
72 |
* | will also accept an abs. XPath.
|
|
|
73 |
* |
|
|
|
74 |
* | Node: (node, nodeSet, node-tree)
|
|
|
75 |
* | Some funtions require or return a node (or a whole node-tree). Nodes are only used with the
|
|
|
76 |
* | XPath-interface and have an internal structure. Every node in a XML document has a unique
|
|
|
77 |
* | corresponding abs. xPath. That's why public functions that accept a node, will usually also
|
|
|
78 |
* | accept a abs. xPath (a string) 'pointing' to an existing node (see absolutXPath).
|
|
|
79 |
* |
|
|
|
80 |
* | XPathQuery: (xquery, query)
|
|
|
81 |
* | A xPath-query is a string that is matched against the XML-document. The result of the match
|
|
|
82 |
* | is a xPathSet (vector of xPath's). It's always possible to pass a single absoluteXPath
|
|
|
83 |
* | instead of a xPath-query. A valid xPathQuery could look like this:
|
|
|
84 |
* | '//XXX/*[contains(., "foo")]/..' (See the link in 'What Is XPath' to learn more).
|
|
|
85 |
* |
|
|
|
86 |
* |
|
|
|
87 |
* +------------------------------------------------------------------------------------------------------+
|
|
|
88 |
* | Internals:
|
|
|
89 |
* | ----------
|
|
|
90 |
* | - The Node Tree
|
|
|
91 |
* | -------------
|
|
|
92 |
* | A central role of the package is how the XML-data is stored. The whole data is in a node-tree.
|
|
|
93 |
* | A node can be seen as the equvalent to a tag in the XML soure with some extra info.
|
|
|
94 |
* | For instance the following XML
|
|
|
95 |
* | <AAA foo="x">***<BBB/><CCC/>**<BBB/>*</AAA>
|
|
|
96 |
* | Would produce folowing node-tree:
|
|
|
97 |
* | 'super-root' <-- $nodeRoot (Very handy)
|
|
|
98 |
* | |
|
|
|
99 |
* | 'depth' 0 AAA[1] <-- top node. The 'textParts' of this node would be
|
|
|
100 |
* | / | \ 'textParts' => array('***','','**','*')
|
|
|
101 |
* | 'depth' 1 BBB[1] CCC[1] BBB[2] (NOTE: Is always size of child nodes+1)
|
|
|
102 |
* | - The Node
|
|
|
103 |
* | --------
|
|
|
104 |
* | The node itself is an structure desiged mainly to be used in connection with the interface of PHP.XPath.
|
|
|
105 |
* | That means it's possible for functions to return a sub-node-tree that can be used as input of an other
|
|
|
106 |
* | PHP.XPath function.
|
|
|
107 |
* |
|
|
|
108 |
* | The main structure of a node is:
|
|
|
109 |
* | $node = array(
|
|
|
110 |
* | 'name' => '', # The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
|
|
|
111 |
* | 'attributes' => array(), # The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
|
|
|
112 |
* | 'textParts' => array(), # Array of text parts surrounding the children E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd')
|
|
|
113 |
* | 'childNodes' => array(), # Array of refences (pointers) to child nodes.
|
|
|
114 |
* |
|
|
|
115 |
* | For optimisation reasions some additional data is stored in the node too:
|
|
|
116 |
* | 'parentNode' => NULL # Reference (pointer) to the parent node (or NULL if it's 'super root')
|
|
|
117 |
* | 'depth' => 0, # The tag depth (or tree level) starting with the root tag at 0.
|
|
|
118 |
* | 'pos' => 0, # Is the zero-based position this node has in the parent's 'childNodes'-list.
|
|
|
119 |
* | 'contextPos' => 1, # Is the one-based position this node has by counting the siblings tags (tags with same name)
|
|
|
120 |
* | 'xpath' => '' # Is the abs. XPath to this node.
|
|
|
121 |
* | 'generated_id'=> '' # The id returned for this node by generate-id() (attribute and text nodes not supported)
|
|
|
122 |
* |
|
|
|
123 |
* | - The NodeIndex
|
|
|
124 |
* | -------------
|
|
|
125 |
* | Every node in the tree has an absolute XPath. E.g '/AAA[1]/BBB[2]' the $nodeIndex is a hash array
|
|
|
126 |
* | to all the nodes in the node-tree. The key used is the absolute XPath (a string).
|
|
|
127 |
* |
|
|
|
128 |
* +------------------------------------------------------------------------------------------------------+
|
|
|
129 |
* | License:
|
|
|
130 |
* | --------
|
|
|
131 |
* | The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License");
|
|
|
132 |
* | you may not use this file except in compliance with the License. You may obtain a copy of the
|
|
|
133 |
* | License at http://www.mozilla.org/MPL/
|
|
|
134 |
* |
|
|
|
135 |
* | Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY
|
|
|
136 |
* | OF ANY KIND, either express or implied. See the License for the specific language governing
|
|
|
137 |
* | rights and limitations under the License.
|
|
|
138 |
* |
|
|
|
139 |
* | The Original Code is <phpXML/>.
|
|
|
140 |
* |
|
|
|
141 |
* | The Initial Developer of the Original Code is Michael P. Mehl. Portions created by Michael
|
|
|
142 |
* | P. Mehl are Copyright (C) 2001 Michael P. Mehl. All Rights Reserved.
|
|
|
143 |
* |
|
|
|
144 |
* | Contributor(s): N.Swinson / S.Blum / D.Allen
|
|
|
145 |
* |
|
|
|
146 |
* | Alternatively, the contents of this file may be used under the terms of either of the GNU
|
|
|
147 |
* | General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public
|
|
|
148 |
* | License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the
|
|
|
149 |
* | LGPL License are applicable instead of those above. If you wish to allow use of your version
|
|
|
150 |
* | of this file only under the terms of the GPL or the LGPL License and not to allow others to
|
|
|
151 |
* | use your version of this file under the MPL, indicate your decision by deleting the
|
|
|
152 |
* | provisions above and replace them with the notice and other provisions required by the
|
|
|
153 |
* | GPL or the LGPL License. If you do not delete the provisions above, a recipient may use
|
|
|
154 |
* | your version of this file under either the MPL, the GPL or the LGPL License.
|
|
|
155 |
* |
|
|
|
156 |
* +======================================================================================================+
|
|
|
157 |
*
|
|
|
158 |
* @author S.Blum / N.Swinson / D.Allen / (P.Mehl)
|
|
|
159 |
* @link http://sourceforge.net/projects/phpxpath/
|
|
|
160 |
* @version 3.5
|
|
|
161 |
* @CVS $Id: XPath.class.php,v 1.9 2005/11/16 17:26:05 bigmichi1 Exp $
|
|
|
162 |
*/
|
|
|
163 |
|
|
|
164 |
// Include guard, protects file being included twice
|
|
|
165 |
$ConstantName = 'INCLUDED_'.strtoupper(__FILE__);
|
|
|
166 |
if (defined($ConstantName)) return;
|
|
|
167 |
define($ConstantName,1, TRUE);
|
|
|
168 |
|
|
|
169 |
/************************************************************************************************
|
|
|
170 |
* ===============================================================================================
|
|
|
171 |
* X P a t h B a s e - Class
|
|
|
172 |
* ===============================================================================================
|
|
|
173 |
************************************************************************************************/
|
|
|
174 |
class XPathBase {
|
|
|
175 |
var $_lastError;
|
|
|
176 |
|
|
|
177 |
// As debugging of the xml parse is spread across several functions, we need to make this a member.
|
|
|
178 |
var $bDebugXmlParse = FALSE;
|
|
|
179 |
|
|
|
180 |
// do we want to do profiling?
|
|
|
181 |
var $bClassProfiling = FALSE;
|
|
|
182 |
|
|
|
183 |
// Used to help navigate through the begin/end debug calls
|
|
|
184 |
var $iDebugNextLinkNumber = 1;
|
|
|
185 |
var $aDebugOpenLinks = array();
|
|
|
186 |
var $aDebugFunctions = array(
|
|
|
187 |
//'_evaluatePrimaryExpr',
|
|
|
188 |
//'_evaluateExpr',
|
|
|
189 |
//'_evaluateStep',
|
|
|
190 |
//'_checkPredicates',
|
|
|
191 |
//'_evaluateFunction',
|
|
|
192 |
//'_evaluateOperator',
|
|
|
193 |
//'_evaluatePathExpr',
|
|
|
194 |
);
|
|
|
195 |
|
|
|
196 |
/**
|
|
|
197 |
* Constructor
|
|
|
198 |
*/
|
|
|
199 |
function XPathBase() {
|
|
|
200 |
# $this->bDebugXmlParse = TRUE;
|
|
|
201 |
$this->properties['verboseLevel'] = 1; // 0=silent, 1 and above produce verbose output (an echo to screen).
|
|
|
202 |
|
|
|
203 |
if (!isSet($_ENV)) { // Note: $_ENV introduced in 4.1.0. In earlier versions, use $HTTP_ENV_VARS.
|
|
|
204 |
$_ENV = $GLOBALS['HTTP_ENV_VARS'];
|
|
|
205 |
}
|
|
|
206 |
|
|
|
207 |
// Windows 95/98 do not support file locking. Detecting OS (Operation System) and setting the
|
|
|
208 |
// properties['OS_supports_flock'] to FALSE if win 95/98 is detected.
|
|
|
209 |
// This will surpress the file locking error reported from win 98 users when exportToFile() is called.
|
|
|
210 |
// May have to add more OS's to the list in future (Macs?).
|
|
|
211 |
// ### Note that it's only the FAT and NFS file systems that are really a problem. NTFS and
|
|
|
212 |
// the latest php libs do support flock()
|
|
|
213 |
$_ENV['OS'] = isSet($_ENV['OS']) ? $_ENV['OS'] : 'Unknown OS';
|
|
|
214 |
switch ($_ENV['OS']) {
|
|
|
215 |
case 'Windows_95':
|
|
|
216 |
case 'Windows_98':
|
|
|
217 |
case 'Unknown OS':
|
|
|
218 |
// should catch Mac OS X compatible environment
|
|
|
219 |
if (!empty($_SERVER['SERVER_SOFTWARE'])
|
|
|
220 |
&& preg_match('/Darwin/',$_SERVER['SERVER_SOFTWARE'])) {
|
|
|
221 |
// fall-through
|
|
|
222 |
} else {
|
|
|
223 |
$this->properties['OS_supports_flock'] = FALSE;
|
|
|
224 |
break;
|
|
|
225 |
}
|
|
|
226 |
default:
|
|
|
227 |
$this->properties['OS_supports_flock'] = TRUE;
|
|
|
228 |
}
|
|
|
229 |
}
|
|
|
230 |
|
|
|
231 |
|
|
|
232 |
/**
|
|
|
233 |
* Resets the object so it's able to take a new xml sting/file
|
|
|
234 |
*
|
|
|
235 |
* Constructing objects is slow. If you can, reuse ones that you have used already
|
|
|
236 |
* by using this reset() function.
|
|
|
237 |
*/
|
|
|
238 |
function reset() {
|
|
|
239 |
$this->_lastError = '';
|
|
|
240 |
}
|
|
|
241 |
|
|
|
242 |
//-----------------------------------------------------------------------------------------
|
|
|
243 |
// XPathBase ------ Helpers ------
|
|
|
244 |
//-----------------------------------------------------------------------------------------
|
|
|
245 |
|
|
|
246 |
/**
|
|
|
247 |
* This method checks the right amount and match of brackets
|
|
|
248 |
*
|
|
|
249 |
* @param $term (string) String in which is checked.
|
|
|
250 |
* @return (bool) TRUE: OK / FALSE: KO
|
|
|
251 |
*/
|
|
|
252 |
function _bracketsCheck($term) {
|
|
|
253 |
$leng = strlen($term);
|
|
|
254 |
$brackets = 0;
|
|
|
255 |
$bracketMisscount = $bracketMissmatsh = FALSE;
|
|
|
256 |
$stack = array();
|
|
|
257 |
for ($i=0; $i<$leng; $i++) {
|
|
|
258 |
switch ($term[$i]) {
|
|
|
259 |
case '(' :
|
|
|
260 |
case '[' :
|
|
|
261 |
$stack[$brackets] = $term[$i];
|
|
|
262 |
$brackets++;
|
|
|
263 |
break;
|
|
|
264 |
case ')':
|
|
|
265 |
$brackets--;
|
|
|
266 |
if ($brackets<0) {
|
|
|
267 |
$bracketMisscount = TRUE;
|
|
|
268 |
break 2;
|
|
|
269 |
}
|
|
|
270 |
if ($stack[$brackets] != '(') {
|
|
|
271 |
$bracketMissmatsh = TRUE;
|
|
|
272 |
break 2;
|
|
|
273 |
}
|
|
|
274 |
break;
|
|
|
275 |
case ']' :
|
|
|
276 |
$brackets--;
|
|
|
277 |
if ($brackets<0) {
|
|
|
278 |
$bracketMisscount = TRUE;
|
|
|
279 |
break 2;
|
|
|
280 |
}
|
|
|
281 |
if ($stack[$brackets] != '[') {
|
|
|
282 |
$bracketMissmatsh = TRUE;
|
|
|
283 |
break 2;
|
|
|
284 |
}
|
|
|
285 |
break;
|
|
|
286 |
}
|
|
|
287 |
}
|
|
|
288 |
// Check whether we had a valid number of brackets.
|
|
|
289 |
if ($brackets != 0) $bracketMisscount = TRUE;
|
|
|
290 |
if ($bracketMisscount || $bracketMissmatsh) {
|
|
|
291 |
return FALSE;
|
|
|
292 |
}
|
|
|
293 |
return TRUE;
|
|
|
294 |
}
|
|
|
295 |
|
|
|
296 |
/**
|
|
|
297 |
* Looks for a string within another string -- BUT the search-string must be located *outside* of any brackets.
|
|
|
298 |
*
|
|
|
299 |
* This method looks for a string within another string. Brackets in the
|
|
|
300 |
* string the method is looking through will be respected, which means that
|
|
|
301 |
* only if the string the method is looking for is located outside of
|
|
|
302 |
* brackets, the search will be successful.
|
|
|
303 |
*
|
|
|
304 |
* @param $term (string) String in which the search shall take place.
|
|
|
305 |
* @param $expression (string) String that should be searched.
|
|
|
306 |
* @return (int) This method returns -1 if no string was found,
|
|
|
307 |
* otherwise the offset at which the string was found.
|
|
|
308 |
*/
|
|
|
309 |
function _searchString($term, $expression) {
|
|
|
310 |
$bracketCounter = 0; // Record where we are in the brackets.
|
|
|
311 |
$leng = strlen($term);
|
|
|
312 |
$exprLeng = strlen($expression);
|
|
|
313 |
for ($i=0; $i<$leng; $i++) {
|
|
|
314 |
$char = $term[$i];
|
|
|
315 |
if ($char=='(' || $char=='[') {
|
|
|
316 |
$bracketCounter++;
|
|
|
317 |
continue;
|
|
|
318 |
}
|
|
|
319 |
elseif ($char==')' || $char==']') {
|
|
|
320 |
$bracketCounter--;
|
|
|
321 |
}
|
|
|
322 |
if ($bracketCounter == 0) {
|
|
|
323 |
// Check whether we can find the expression at this index.
|
|
|
324 |
if (substr($term, $i, $exprLeng) == $expression) return $i;
|
|
|
325 |
}
|
|
|
326 |
}
|
|
|
327 |
// Nothing was found.
|
|
|
328 |
return (-1);
|
|
|
329 |
}
|
|
|
330 |
|
|
|
331 |
/**
|
|
|
332 |
* Split a string by a searator-string -- BUT the separator-string must be located *outside* of any brackets.
|
|
|
333 |
*
|
|
|
334 |
* Returns an array of strings, each of which is a substring of string formed
|
|
|
335 |
* by splitting it on boundaries formed by the string separator.
|
|
|
336 |
*
|
|
|
337 |
* @param $separator (string) String that should be searched.
|
|
|
338 |
* @param $term (string) String in which the search shall take place.
|
|
|
339 |
* @return (array) see above
|
|
|
340 |
*/
|
|
|
341 |
function _bracketExplode($separator, $term) {
|
|
|
342 |
// Note that it doesn't make sense for $separator to itself contain (,),[ or ],
|
|
|
343 |
// but as this is a private function we should be ok.
|
|
|
344 |
$resultArr = array();
|
|
|
345 |
$bracketCounter = 0; // Record where we are in the brackets.
|
|
|
346 |
do { // BEGIN try block
|
|
|
347 |
// Check if any separator is in the term
|
|
|
348 |
$sepLeng = strlen($separator);
|
|
|
349 |
if (strpos($term, $separator)===FALSE) { // no separator found so end now
|
|
|
350 |
$resultArr[] = $term;
|
|
|
351 |
break; // try-block
|
|
|
352 |
}
|
|
|
353 |
|
|
|
354 |
// Make a substitute separator out of 'unused chars'.
|
|
|
355 |
$substituteSep = str_repeat(chr(2), $sepLeng);
|
|
|
356 |
|
|
|
357 |
// Now determine the first bracket '(' or '['.
|
|
|
358 |
$tmp1 = strpos($term, '(');
|
|
|
359 |
$tmp2 = strpos($term, '[');
|
|
|
360 |
if ($tmp1===FALSE) {
|
|
|
361 |
$startAt = (int)$tmp2;
|
|
|
362 |
} elseif ($tmp2===FALSE) {
|
|
|
363 |
$startAt = (int)$tmp1;
|
|
|
364 |
} else {
|
|
|
365 |
$startAt = min($tmp1, $tmp2);
|
|
|
366 |
}
|
|
|
367 |
|
|
|
368 |
// Get prefix string part before the first bracket.
|
|
|
369 |
$preStr = substr($term, 0, $startAt);
|
|
|
370 |
// Substitute separator in prefix string.
|
|
|
371 |
$preStr = str_replace($separator, $substituteSep, $preStr);
|
|
|
372 |
|
|
|
373 |
// Now get the rest-string (postfix string)
|
|
|
374 |
$postStr = substr($term, $startAt);
|
|
|
375 |
// Go all the way through the rest-string.
|
|
|
376 |
$strLeng = strlen($postStr);
|
|
|
377 |
for ($i=0; $i < $strLeng; $i++) {
|
|
|
378 |
$char = $postStr[$i];
|
|
|
379 |
// Spot (,),[,] and modify our bracket counter. Note there is an
|
|
|
380 |
// assumption here that you don't have a string(with[mis)matched]brackets.
|
|
|
381 |
// This should be ok as the dodgy string will be detected elsewhere.
|
|
|
382 |
if ($char=='(' || $char=='[') {
|
|
|
383 |
$bracketCounter++;
|
|
|
384 |
continue;
|
|
|
385 |
}
|
|
|
386 |
elseif ($char==')' || $char==']') {
|
|
|
387 |
$bracketCounter--;
|
|
|
388 |
}
|
|
|
389 |
// If no brackets surround us check for separator
|
|
|
390 |
if ($bracketCounter == 0) {
|
|
|
391 |
// Check whether we can find the expression starting at this index.
|
|
|
392 |
if ((substr($postStr, $i, $sepLeng) == $separator)) {
|
|
|
393 |
// Substitute the found separator
|
|
|
394 |
for ($j=0; $j<$sepLeng; $j++) {
|
|
|
395 |
$postStr[$i+$j] = $substituteSep[$j];
|
|
|
396 |
}
|
|
|
397 |
}
|
|
|
398 |
}
|
|
|
399 |
}
|
|
|
400 |
// Now explod using the substitute separator as key.
|
|
|
401 |
$resultArr = explode($substituteSep, $preStr . $postStr);
|
|
|
402 |
} while (FALSE); // End try block
|
|
|
403 |
// Return the results that we found. May be a array with 1 entry.
|
|
|
404 |
return $resultArr;
|
|
|
405 |
}
|
|
|
406 |
|
|
|
407 |
/**
|
|
|
408 |
* Split a string at it's groups, ie bracketed expressions
|
|
|
409 |
*
|
|
|
410 |
* Returns an array of strings, when concatenated together would produce the original
|
|
|
411 |
* string. ie a(b)cde(f)(g) would map to:
|
|
|
412 |
* array ('a', '(b)', cde', '(f)', '(g)')
|
|
|
413 |
*
|
|
|
414 |
* @param $string (string) The string to process
|
|
|
415 |
* @param $open (string) The substring for the open of a group
|
|
|
416 |
* @param $close (string) The substring for the close of a group
|
|
|
417 |
* @return (array) The parsed string, see above
|
|
|
418 |
*/
|
|
|
419 |
function _getEndGroups($string, $open='[', $close=']') {
|
|
|
420 |
// Note that it doesn't make sense for $separator to itself contain (,),[ or ],
|
|
|
421 |
// but as this is a private function we should be ok.
|
|
|
422 |
$resultArr = array();
|
|
|
423 |
do { // BEGIN try block
|
|
|
424 |
// Check if we have both an open and a close tag
|
|
|
425 |
if (empty($open) and empty($close)) { // no separator found so end now
|
|
|
426 |
$resultArr[] = $string;
|
|
|
427 |
break; // try-block
|
|
|
428 |
}
|
|
|
429 |
|
|
|
430 |
if (empty($string)) {
|
|
|
431 |
$resultArr[] = $string;
|
|
|
432 |
break; // try-block
|
|
|
433 |
}
|
|
|
434 |
|
|
|
435 |
|
|
|
436 |
while (!empty($string)) {
|
|
|
437 |
// Now determine the first bracket '(' or '['.
|
|
|
438 |
$openPos = strpos($string, $open);
|
|
|
439 |
$closePos = strpos($string, $close);
|
|
|
440 |
if ($openPos===FALSE || $closePos===FALSE) {
|
|
|
441 |
// Oh, no more groups to be found then. Quit
|
|
|
442 |
$resultArr[] = $string;
|
|
|
443 |
break;
|
|
|
444 |
}
|
|
|
445 |
|
|
|
446 |
// Sanity check
|
|
|
447 |
if ($openPos > $closePos) {
|
|
|
448 |
// Malformed string, dump the rest and quit.
|
|
|
449 |
$resultArr[] = $string;
|
|
|
450 |
break;
|
|
|
451 |
}
|
|
|
452 |
|
|
|
453 |
// Get prefix string part before the first bracket.
|
|
|
454 |
$preStr = substr($string, 0, $openPos);
|
|
|
455 |
// This is the first string that will go in our output
|
|
|
456 |
if (!empty($preStr))
|
|
|
457 |
$resultArr[] = $preStr;
|
|
|
458 |
|
|
|
459 |
// Skip over what we've proceed, including the open char
|
|
|
460 |
$string = substr($string, $openPos + 1 - strlen($string));
|
|
|
461 |
|
|
|
462 |
// Find the next open char and adjust our close char
|
|
|
463 |
//echo "close: $closePos\nopen: $openPos\n\n";
|
|
|
464 |
$closePos -= $openPos + 1;
|
|
|
465 |
$openPos = strpos($string, $open);
|
|
|
466 |
//echo "close: $closePos\nopen: $openPos\n\n";
|
|
|
467 |
|
|
|
468 |
// While we have found nesting...
|
|
|
469 |
while ($openPos && $closePos && ($closePos > $openPos)) {
|
|
|
470 |
// Find another close pos after the one we are looking at
|
|
|
471 |
$closePos = strpos($string, $close, $closePos + 1);
|
|
|
472 |
// And skip our open
|
|
|
473 |
$openPos = strpos($string, $open, $openPos + 1);
|
|
|
474 |
}
|
|
|
475 |
//echo "close: $closePos\nopen: $openPos\n\n";
|
|
|
476 |
|
|
|
477 |
// If we now have a close pos, then it's the end of the group.
|
|
|
478 |
if ($closePos === FALSE) {
|
|
|
479 |
// We didn't... so bail dumping what was left
|
|
|
480 |
$resultArr[] = $open.$string;
|
|
|
481 |
break;
|
|
|
482 |
}
|
|
|
483 |
|
|
|
484 |
// We did, so we can extract the group
|
|
|
485 |
$resultArr[] = $open.substr($string, 0, $closePos + 1);
|
|
|
486 |
// Skip what we have processed
|
|
|
487 |
$string = substr($string, $closePos + 1);
|
|
|
488 |
}
|
|
|
489 |
} while (FALSE); // End try block
|
|
|
490 |
// Return the results that we found. May be a array with 1 entry.
|
|
|
491 |
return $resultArr;
|
|
|
492 |
}
|
|
|
493 |
|
|
|
494 |
/**
|
|
|
495 |
* Retrieves a substring before a delimiter.
|
|
|
496 |
*
|
|
|
497 |
* This method retrieves everything from a string before a given delimiter,
|
|
|
498 |
* not including the delimiter.
|
|
|
499 |
*
|
|
|
500 |
* @param $string (string) String, from which the substring should be extracted.
|
|
|
501 |
* @param $delimiter (string) String containing the delimiter to use.
|
|
|
502 |
* @return (string) Substring from the original string before the delimiter.
|
|
|
503 |
* @see _afterstr()
|
|
|
504 |
*/
|
|
|
505 |
function _prestr(&$string, $delimiter, $offset=0) {
|
|
|
506 |
// Return the substring.
|
|
|
507 |
$offset = ($offset<0) ? 0 : $offset;
|
|
|
508 |
$pos = strpos($string, $delimiter, $offset);
|
|
|
509 |
if ($pos===FALSE) return $string; else return substr($string, 0, $pos);
|
|
|
510 |
}
|
|
|
511 |
|
|
|
512 |
/**
|
|
|
513 |
* Retrieves a substring after a delimiter.
|
|
|
514 |
*
|
|
|
515 |
* This method retrieves everything from a string after a given delimiter,
|
|
|
516 |
* not including the delimiter.
|
|
|
517 |
*
|
|
|
518 |
* @param $string (string) String, from which the substring should be extracted.
|
|
|
519 |
* @param $delimiter (string) String containing the delimiter to use.
|
|
|
520 |
* @return (string) Substring from the original string after the delimiter.
|
|
|
521 |
* @see _prestr()
|
|
|
522 |
*/
|
|
|
523 |
function _afterstr($string, $delimiter, $offset=0) {
|
|
|
524 |
$offset = ($offset<0) ? 0 : $offset;
|
|
|
525 |
// Return the substring.
|
|
|
526 |
return substr($string, strpos($string, $delimiter, $offset) + strlen($delimiter));
|
|
|
527 |
}
|
|
|
528 |
|
|
|
529 |
//-----------------------------------------------------------------------------------------
|
|
|
530 |
// XPathBase ------ Debug Stuff ------
|
|
|
531 |
//-----------------------------------------------------------------------------------------
|
|
|
532 |
|
|
|
533 |
/**
|
|
|
534 |
* Alter the verbose (error) level reporting.
|
|
|
535 |
*
|
|
|
536 |
* Pass an int. >0 to turn on, 0 to turn off. The higher the number, the
|
|
|
537 |
* higher the level of verbosity. By default, the class has a verbose level
|
|
|
538 |
* of 1.
|
|
|
539 |
*
|
|
|
540 |
* @param $levelOfVerbosity (int) default is 1 = on
|
|
|
541 |
*/
|
|
|
542 |
function setVerbose($levelOfVerbosity = 1) {
|
|
|
543 |
$level = -1;
|
|
|
544 |
if ($levelOfVerbosity === TRUE) {
|
|
|
545 |
$level = 1;
|
|
|
546 |
} elseif ($levelOfVerbosity === FALSE) {
|
|
|
547 |
$level = 0;
|
|
|
548 |
} elseif (is_numeric($levelOfVerbosity)) {
|
|
|
549 |
$level = $levelOfVerbosity;
|
|
|
550 |
}
|
|
|
551 |
if ($level >= 0) $this->properties['verboseLevel'] = $levelOfVerbosity;
|
|
|
552 |
}
|
|
|
553 |
|
|
|
554 |
/**
|
|
|
555 |
* Returns the last occured error message.
|
|
|
556 |
*
|
|
|
557 |
* @access public
|
|
|
558 |
* @return string (may be empty if there was no error at all)
|
|
|
559 |
* @see _setLastError(), _lastError
|
|
|
560 |
*/
|
|
|
561 |
function getLastError() {
|
|
|
562 |
return $this->_lastError;
|
|
|
563 |
}
|
|
|
564 |
|
|
|
565 |
/**
|
|
|
566 |
* Creates a textual error message and sets it.
|
|
|
567 |
*
|
|
|
568 |
* example: 'XPath error in THIS_FILE_NAME:LINE. Message: YOUR_MESSAGE';
|
|
|
569 |
*
|
|
|
570 |
* I don't think the message should include any markup because not everyone wants to debug
|
|
|
571 |
* into the browser window.
|
|
|
572 |
*
|
|
|
573 |
* You should call _displayError() rather than _setLastError() if you would like the message,
|
|
|
574 |
* dependant on their verbose settings, echoed to the screen.
|
|
|
575 |
*
|
|
|
576 |
* @param $message (string) a textual error message default is ''
|
|
|
577 |
* @param $line (int) the line number where the error occured, use __LINE__
|
|
|
578 |
* @see getLastError()
|
|
|
579 |
*/
|
|
|
580 |
function _setLastError($message='', $line='-', $file='-') {
|
|
|
581 |
$this->_lastError = 'XPath error in ' . basename($file) . ':' . $line . '. Message: ' . $message;
|
|
|
582 |
}
|
|
|
583 |
|
|
|
584 |
/**
|
|
|
585 |
* Displays an error message.
|
|
|
586 |
*
|
|
|
587 |
* This method displays an error messages depending on the users verbose settings
|
|
|
588 |
* and sets the last error message.
|
|
|
589 |
*
|
|
|
590 |
* If also possibly stops the execution of the script.
|
|
|
591 |
* ### Terminate should not be allowed --fab. Should it?? N.S.
|
|
|
592 |
*
|
|
|
593 |
* @param $message (string) Error message to be displayed.
|
|
|
594 |
* @param $lineNumber (int) line number given by __LINE__
|
|
|
595 |
* @param $terminate (bool) (default TURE) End the execution of this script.
|
|
|
596 |
*/
|
|
|
597 |
function _displayError($message, $lineNumber='-', $file='-', $terminate=TRUE) {
|
|
|
598 |
// Display the error message.
|
|
|
599 |
$err = '<b>XPath error in '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
|
|
|
600 |
$this->_setLastError($message, $lineNumber, $file);
|
|
|
601 |
if (($this->properties['verboseLevel'] > 0) OR ($terminate)) echo $err;
|
|
|
602 |
// End the execution of this script.
|
|
|
603 |
if ($terminate) exit;
|
|
|
604 |
}
|
|
|
605 |
|
|
|
606 |
/**
|
|
|
607 |
* Displays a diagnostic message
|
|
|
608 |
*
|
|
|
609 |
* This method displays an error messages
|
|
|
610 |
*
|
|
|
611 |
* @param $message (string) Error message to be displayed.
|
|
|
612 |
* @param $lineNumber (int) line number given by __LINE__
|
|
|
613 |
*/
|
|
|
614 |
function _displayMessage($message, $lineNumber='-', $file='-') {
|
|
|
615 |
// Display the error message.
|
|
|
616 |
$err = '<b>XPath message from '.basename($file).':'.$lineNumber.'</b> '.$message."<br \>\n";
|
|
|
617 |
if ($this->properties['verboseLevel'] > 0) echo $err;
|
|
|
618 |
}
|
|
|
619 |
|
|
|
620 |
/**
|
|
|
621 |
* Called to begin the debug run of a function.
|
|
|
622 |
*
|
|
|
623 |
* This method starts a <DIV><PRE> tag so that the entry to this function
|
|
|
624 |
* is clear to the debugging user. Call _closeDebugFunction() at the
|
|
|
625 |
* end of the function to create a clean box round the function call.
|
|
|
626 |
*
|
|
|
627 |
* @author Nigel Swinson <nigelswinson@users.sourceforge.net>
|
|
|
628 |
* @author Sam Blum <bs_php@infeer.com>
|
|
|
629 |
* @param $functionName (string) the name of the function we are beginning to debug
|
|
|
630 |
* @param $bDebugFlag (bool) TRUE if we are to draw a call stack, FALSE otherwise
|
|
|
631 |
* @return (array) the output from the microtime() function.
|
|
|
632 |
* @see _closeDebugFunction()
|
|
|
633 |
*/
|
|
|
634 |
function _beginDebugFunction($functionName, $bDebugFlag) {
|
|
|
635 |
if ($bDebugFlag) {
|
|
|
636 |
$fileName = basename(__FILE__);
|
|
|
637 |
static $color = array('green','blue','red','lime','fuchsia', 'aqua');
|
|
|
638 |
static $colIndex = -1;
|
|
|
639 |
$colIndex++;
|
|
|
640 |
echo '<div style="clear:both" align="left"> ';
|
|
|
641 |
echo '<pre STYLE="border:solid thin '. $color[$colIndex % 6] . '; padding:5">';
|
|
|
642 |
echo '<a style="float:right;margin:5px" name="'.$this->iDebugNextLinkNumber.'Open" href="#'.$this->iDebugNextLinkNumber.'Close">Function Close '.$this->iDebugNextLinkNumber.'</a>';
|
|
|
643 |
echo "<STRONG>{$fileName} : {$functionName}</STRONG>";
|
|
|
644 |
echo '<hr style="clear:both">';
|
|
|
645 |
array_push($this->aDebugOpenLinks, $this->iDebugNextLinkNumber);
|
|
|
646 |
$this->iDebugNextLinkNumber++;
|
|
|
647 |
}
|
|
|
648 |
|
|
|
649 |
if ($this->bClassProfiling)
|
|
|
650 |
$this->_ProfBegin($FunctionName);
|
|
|
651 |
|
|
|
652 |
return TRUE;
|
|
|
653 |
}
|
|
|
654 |
|
|
|
655 |
/**
|
|
|
656 |
* Called to end the debug run of a function.
|
|
|
657 |
*
|
|
|
658 |
* This method ends a <DIV><PRE> block and reports the time since $aStartTime
|
|
|
659 |
* is clear to the debugging user.
|
|
|
660 |
*
|
|
|
661 |
* @author Nigel Swinson <nigelswinson@users.sourceforge.net>
|
|
|
662 |
* @param $functionName (string) the name of the function we are beginning to debug
|
|
|
663 |
* @param $return_value (mixed) the return value from the function call that
|
|
|
664 |
* we are debugging
|
|
|
665 |
* @param $bDebugFlag (bool) TRUE if we are to draw a call stack, FALSE otherwise
|
|
|
666 |
*/
|
|
|
667 |
function _closeDebugFunction($functionName, $returnValue = "", $bDebugFlag) {
|
|
|
668 |
if ($bDebugFlag) {
|
|
|
669 |
echo "<hr>";
|
|
|
670 |
$iOpenLinkNumber = array_pop($this->aDebugOpenLinks);
|
|
|
671 |
echo '<a style="float:right" name="'.$iOpenLinkNumber.'Close" href="#'.$iOpenLinkNumber.'Open">Function Open '.$iOpenLinkNumber.'</a>';
|
|
|
672 |
if (isSet($returnValue)) {
|
|
|
673 |
if (is_array($returnValue))
|
|
|
674 |
echo "Return Value: ".print_r($returnValue)."\n";
|
|
|
675 |
else if (is_numeric($returnValue))
|
|
|
676 |
echo "Return Value: ".(string)$returnValue."\n";
|
|
|
677 |
else if (is_bool($returnValue))
|
|
|
678 |
echo "Return Value: ".($returnValue ? "TRUE" : "FALSE")."\n";
|
|
|
679 |
else
|
|
|
680 |
echo "Return Value: \"".htmlspecialchars($returnValue)."\"\n";
|
|
|
681 |
}
|
|
|
682 |
echo '<br style="clear:both">';
|
|
|
683 |
echo " \n</pre></div>";
|
|
|
684 |
}
|
|
|
685 |
|
|
|
686 |
if ($this->bClassProfiling)
|
|
|
687 |
$this->_ProfEnd($FunctionName);
|
|
|
688 |
|
|
|
689 |
return TRUE;
|
|
|
690 |
}
|
|
|
691 |
|
|
|
692 |
/**
|
|
|
693 |
* Profile begin call
|
|
|
694 |
*/
|
|
|
695 |
function _ProfBegin($sonFuncName) {
|
|
|
696 |
static $entryTmpl = array ( 'start' => array(),
|
|
|
697 |
'recursiveCount' => 0,
|
|
|
698 |
'totTime' => 0,
|
|
|
699 |
'callCount' => 0 );
|
|
|
700 |
$now = explode(' ', microtime());
|
|
|
701 |
|
|
|
702 |
if (empty($this->callStack)) {
|
|
|
703 |
$fatherFuncName = '';
|
|
|
704 |
}
|
|
|
705 |
else {
|
|
|
706 |
$fatherFuncName = $this->callStack[sizeOf($this->callStack)-1];
|
|
|
707 |
$fatherEntry = &$this->profile[$fatherFuncName];
|
|
|
708 |
}
|
|
|
709 |
$this->callStack[] = $sonFuncName;
|
|
|
710 |
|
|
|
711 |
if (!isSet($this->profile[$sonFuncName])) {
|
|
|
712 |
$this->profile[$sonFuncName] = $entryTmpl;
|
|
|
713 |
}
|
|
|
714 |
|
|
|
715 |
$sonEntry = &$this->profile[$sonFuncName];
|
|
|
716 |
$sonEntry['callCount']++;
|
|
|
717 |
// if we call the t's the same function let the time run, otherwise sum up
|
|
|
718 |
if ($fatherFuncName == $sonFuncName) {
|
|
|
719 |
$sonEntry['recursiveCount']++;
|
|
|
720 |
}
|
|
|
721 |
if (!empty($fatherFuncName)) {
|
|
|
722 |
$last = $fatherEntry['start'];
|
|
|
723 |
$fatherEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 );
|
|
|
724 |
$fatherEntry['start'] = 0;
|
|
|
725 |
}
|
|
|
726 |
$sonEntry['start'] = explode(' ', microtime());
|
|
|
727 |
}
|
|
|
728 |
|
|
|
729 |
/**
|
|
|
730 |
* Profile end call
|
|
|
731 |
*/
|
|
|
732 |
function _ProfEnd($sonFuncName) {
|
|
|
733 |
$now = explode(' ', microtime());
|
|
|
734 |
|
|
|
735 |
array_pop($this->callStack);
|
|
|
736 |
if (empty($this->callStack)) {
|
|
|
737 |
$fatherFuncName = '';
|
|
|
738 |
}
|
|
|
739 |
else {
|
|
|
740 |
$fatherFuncName = $this->callStack[sizeOf($this->callStack)-1];
|
|
|
741 |
$fatherEntry = &$this->profile[$fatherFuncName];
|
|
|
742 |
}
|
|
|
743 |
$sonEntry = &$this->profile[$sonFuncName];
|
|
|
744 |
if (empty($sonEntry)) {
|
|
|
745 |
echo "ERROR in profEnd(): '$funcNam' not in list. Seams it was never started ;o)";
|
|
|
746 |
}
|
|
|
747 |
|
|
|
748 |
$last = $sonEntry['start'];
|
|
|
749 |
$sonEntry['totTime'] += round( (($now[1] - $last[1]) + ($now[0] - $last[0]))*10000 );
|
|
|
750 |
$sonEntry['start'] = 0;
|
|
|
751 |
if (!empty($fatherEntry)) $fatherEntry['start'] = explode(' ', microtime());
|
|
|
752 |
}
|
|
|
753 |
|
|
|
754 |
/**
|
|
|
755 |
* Show profile gathered so far as HTML table
|
|
|
756 |
*/
|
|
|
757 |
function _ProfileToHtml() {
|
|
|
758 |
$sortArr = array();
|
|
|
759 |
if (empty($this->profile)) return '';
|
|
|
760 |
reset($this->profile);
|
|
|
761 |
while (list($funcName) = each($this->profile)) {
|
|
|
762 |
$sortArrKey[] = $this->profile[$funcName]['totTime'];
|
|
|
763 |
$sortArrVal[] = $funcName;
|
|
|
764 |
}
|
|
|
765 |
//echo '<pre>';var_dump($sortArrVal);echo '</pre>';
|
|
|
766 |
array_multisort ($sortArrKey, SORT_DESC, $sortArrVal );
|
|
|
767 |
//echo '<pre>';var_dump($sortArrVal);echo '</pre>';
|
|
|
768 |
|
|
|
769 |
$totTime = 0;
|
|
|
770 |
$size = sizeOf($sortArrVal);
|
|
|
771 |
for ($i=0; $i<$size; $i++) {
|
|
|
772 |
$funcName = &$sortArrVal[$i];
|
|
|
773 |
$totTime += $this->profile[$funcName]['totTime'];
|
|
|
774 |
}
|
|
|
775 |
$out = '<table border="1">';
|
|
|
776 |
$out .='<tr align="center" bgcolor="#bcd6f1"><th>Function</th><th> % </th><th>Total [ms]</th><th># Call</th><th>[ms] per Call</th><th># Recursive</th></tr>';
|
|
|
777 |
for ($i=0; $i<$size; $i++) {
|
|
|
778 |
$funcName = &$sortArrVal[$i];
|
|
|
779 |
$row = &$this->profile[$funcName];
|
|
|
780 |
$procent = round($row['totTime']*100/$totTime);
|
|
|
781 |
if ($procent>20) $bgc = '#ff8080';
|
|
|
782 |
elseif ($procent>15) $bgc = '#ff9999';
|
|
|
783 |
elseif ($procent>10) $bgc = '#ffcccc';
|
|
|
784 |
elseif ($procent>5) $bgc = '#ffffcc';
|
|
|
785 |
else $bgc = '#66ff99';
|
|
|
786 |
|
|
|
787 |
$out .="<tr align='center' bgcolor='{$bgc}'>";
|
|
|
788 |
$out .='<td>'. $funcName .'</td><td>'. $procent .'% '.'</td><td>'. $row['totTime']/10 .'</td><td>'. $row['callCount'] .'</td><td>'. round($row['totTime']/10/$row['callCount'],2) .'</td><td>'. $row['recursiveCount'].'</td>';
|
|
|
789 |
$out .='</tr>';
|
|
|
790 |
}
|
|
|
791 |
$out .= '</table> Total Time [' . $totTime/10 .'ms]' ;
|
|
|
792 |
|
|
|
793 |
echo $out;
|
|
|
794 |
return TRUE;
|
|
|
795 |
}
|
|
|
796 |
|
|
|
797 |
/**
|
|
|
798 |
* Echo an XPath context for diagnostic purposes
|
|
|
799 |
*
|
|
|
800 |
* @param $context (array) An XPath context
|
|
|
801 |
*/
|
|
|
802 |
function _printContext($context) {
|
|
|
803 |
echo "{$context['nodePath']}({$context['pos']}/{$context['size']})";
|
|
|
804 |
}
|
|
|
805 |
|
|
|
806 |
/**
|
|
|
807 |
* This is a debug helper function. It dumps the node-tree as HTML
|
|
|
808 |
*
|
|
|
809 |
* *QUICK AND DIRTY*. Needs some polishing.
|
|
|
810 |
*
|
|
|
811 |
* @param $node (array) A node
|
|
|
812 |
* @param $indent (string) (optional, default=''). For internal recursive calls.
|
|
|
813 |
*/
|
|
|
814 |
function _treeDump($node, $indent = '') {
|
|
|
815 |
$out = '';
|
|
|
816 |
|
|
|
817 |
// Get rid of recursion
|
|
|
818 |
$parentName = empty($node['parentNode']) ? "SUPER ROOT" : $node['parentNode']['name'];
|
|
|
819 |
unset($node['parentNode']);
|
|
|
820 |
$node['parentNode'] = $parentName ;
|
|
|
821 |
|
|
|
822 |
$out .= "NODE[{$node['name']}]\n";
|
|
|
823 |
|
|
|
824 |
foreach($node as $key => $val) {
|
|
|
825 |
if ($key === 'childNodes') continue;
|
|
|
826 |
if (is_Array($val)) {
|
|
|
827 |
$out .= $indent . " [{$key}]\n" . arrayToStr($val, $indent . ' ');
|
|
|
828 |
} else {
|
|
|
829 |
$out .= $indent . " [{$key}] => '{$val}' \n";
|
|
|
830 |
}
|
|
|
831 |
}
|
|
|
832 |
|
|
|
833 |
if (!empty($node['childNodes'])) {
|
|
|
834 |
$out .= $indent . " ['childNodes'] (Size = ".sizeOf($node['childNodes']).")\n";
|
|
|
835 |
foreach($node['childNodes'] as $key => $childNode) {
|
|
|
836 |
$out .= $indent . " [$key] => " . $this->_treeDump($childNode, $indent . ' ') . "\n";
|
|
|
837 |
}
|
|
|
838 |
}
|
|
|
839 |
|
|
|
840 |
if (empty($indent)) {
|
|
|
841 |
return "<pre>" . htmlspecialchars($out) . "</pre>";
|
|
|
842 |
}
|
|
|
843 |
return $out;
|
|
|
844 |
}
|
|
|
845 |
} // END OF CLASS XPathBase
|
|
|
846 |
|
|
|
847 |
|
|
|
848 |
/************************************************************************************************
|
|
|
849 |
* ===============================================================================================
|
|
|
850 |
* X P a t h E n g i n e - Class
|
|
|
851 |
* ===============================================================================================
|
|
|
852 |
************************************************************************************************/
|
|
|
853 |
|
|
|
854 |
class XPathEngine extends XPathBase {
|
|
|
855 |
|
|
|
856 |
// List of supported XPath axes.
|
|
|
857 |
// What a stupid idea from W3C to take axes name containing a '-' (dash)
|
|
|
858 |
// NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
|
|
|
859 |
// We will then do the same on the users Xpath querys
|
|
|
860 |
// -sibling => _sibling
|
|
|
861 |
// -or- => _or_
|
|
|
862 |
//
|
|
|
863 |
// This array contains a list of all valid axes that can be evaluated in an
|
|
|
864 |
// XPath query.
|
|
|
865 |
var $axes = array ( 'ancestor', 'ancestor_or_self', 'attribute', 'child', 'descendant',
|
|
|
866 |
'descendant_or_self', 'following', 'following_sibling',
|
|
|
867 |
'namespace', 'parent', 'preceding', 'preceding_sibling', 'self'
|
|
|
868 |
);
|
|
|
869 |
|
|
|
870 |
// List of supported XPath functions.
|
|
|
871 |
// What a stupid idea from W3C to take function name containing a '-' (dash)
|
|
|
872 |
// NOTE: We replace the '-' with '_' to avoid the conflict with the minus operator.
|
|
|
873 |
// We will then do the same on the users Xpath querys
|
|
|
874 |
// starts-with => starts_with
|
|
|
875 |
// substring-before => substring_before
|
|
|
876 |
// substring-after => substring_after
|
|
|
877 |
// string-length => string_length
|
|
|
878 |
//
|
|
|
879 |
// This array contains a list of all valid functions that can be evaluated
|
|
|
880 |
// in an XPath query.
|
|
|
881 |
var $functions = array ( 'last', 'position', 'count', 'id', 'name',
|
|
|
882 |
'string', 'concat', 'starts_with', 'contains', 'substring_before',
|
|
|
883 |
'substring_after', 'substring', 'string_length', 'normalize_space', 'translate',
|
|
|
884 |
'boolean', 'not', 'true', 'false', 'lang', 'number', 'sum', 'floor',
|
|
|
885 |
'ceiling', 'round', 'x_lower', 'x_upper', 'generate_id' );
|
|
|
886 |
|
|
|
887 |
// List of supported XPath operators.
|
|
|
888 |
//
|
|
|
889 |
// This array contains a list of all valid operators that can be evaluated
|
|
|
890 |
// in a predicate of an XPath query. The list is ordered by the
|
|
|
891 |
// precedence of the operators (lowest precedence first).
|
|
|
892 |
var $operators = array( ' or ', ' and ', '=', '!=', '<=', '<', '>=', '>',
|
|
|
893 |
'+', '-', '*', ' div ', ' mod ', ' | ');
|
|
|
894 |
|
|
|
895 |
// List of literals from the xPath string.
|
|
|
896 |
var $axPathLiterals = array();
|
|
|
897 |
|
|
|
898 |
// The index and tree that is created during the analysis of an XML source.
|
|
|
899 |
var $nodeIndex = array();
|
|
|
900 |
var $nodeRoot = array();
|
|
|
901 |
var $emptyNode = array(
|
|
|
902 |
'name' => '', // The tag name. E.g. In <FOO bar="aaa"/> it would be 'FOO'
|
|
|
903 |
'attributes' => array(), // The attributes of the tag E.g. In <FOO bar="aaa"/> it would be array('bar'=>'aaa')
|
|
|
904 |
'childNodes' => array(), // Array of pointers to child nodes.
|
|
|
905 |
'textParts' => array(), // Array of text parts between the cilderen E.g. <FOO>aa<A>bb<B/>cc</A>dd</FOO> -> array('aa','bb','cc','dd')
|
|
|
906 |
'parentNode' => NULL, // Pointer to parent node or NULL if this node is the 'super root'
|
|
|
907 |
//-- *!* Following vars are set by the indexer and is for optimisation only *!*
|
|
|
908 |
'depth' => 0, // The tag depth (or tree level) starting with the root tag at 0.
|
|
|
909 |
'pos' => 0, // Is the zero-based position this node has in the parents 'childNodes'-list.
|
|
|
910 |
'contextPos' => 1, // Is the one-based position this node has by counting the siblings tags (tags with same name)
|
|
|
911 |
'xpath' => '' // Is the abs. XPath to this node.
|
|
|
912 |
);
|
|
|
913 |
var $_indexIsDirty = FALSE;
|
|
|
914 |
|
|
|
915 |
|
|
|
916 |
// These variable used during the parse XML source
|
|
|
917 |
var $nodeStack = array(); // The elements that we have still to close.
|
|
|
918 |
var $parseStackIndex = 0; // The current element of the nodeStack[] that we are adding to while
|
|
|
919 |
// parsing an XML source. Corresponds to the depth of the xml node.
|
|
|
920 |
// in our input data.
|
|
|
921 |
var $parseOptions = array(); // Used to set the PHP's XML parser options (see xml_parser_set_option)
|
|
|
922 |
var $parsedTextLocation = ''; // A reference to where we have to put char data collected during XML parsing
|
|
|
923 |
var $parsInCData = 0 ; // Is >0 when we are inside a CDATA section.
|
|
|
924 |
var $parseSkipWhiteCache = 0; // A cache of the skip whitespace parse option to speed up the parse.
|
|
|
925 |
|
|
|
926 |
// This is the array of error strings, to keep consistency.
|
|
|
927 |
var $errorStrings = array(
|
|
|
928 |
'AbsoluteXPathRequired' => "The supplied xPath '%s' does not *uniquely* describe a node in the xml document.",
|
|
|
929 |
'NoNodeMatch' => "The supplied xPath-query '%s' does not match *any* node in the xml document.",
|
|
|
930 |
'RootNodeAlreadyExists' => "An xml document may have only one root node."
|
|
|
931 |
);
|
|
|
932 |
|
|
|
933 |
/**
|
|
|
934 |
* Constructor
|
|
|
935 |
*
|
|
|
936 |
* Optionally you may call this constructor with the XML-filename to parse and the
|
|
|
937 |
* XML option vector. Each of the entries in the option vector will be passed to
|
|
|
938 |
* xml_parser_set_option().
|
|
|
939 |
*
|
|
|
940 |
* A option vector sample:
|
|
|
941 |
* $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE,
|
|
|
942 |
* XML_OPTION_SKIP_WHITE => TRUE);
|
|
|
943 |
*
|
|
|
944 |
* @param $userXmlOptions (array) (optional) Vector of (<optionID>=><value>,
|
|
|
945 |
* <optionID>=><value>, ...). See PHP's
|
|
|
946 |
* xml_parser_set_option() docu for a list of possible
|
|
|
947 |
* options.
|
|
|
948 |
* @see importFromFile(), importFromString(), setXmlOptions()
|
|
|
949 |
*/
|
|
|
950 |
function XPathEngine($userXmlOptions=array()) {
|
|
|
951 |
parent::XPathBase();
|
|
|
952 |
// Default to not folding case
|
|
|
953 |
$this->parseOptions[XML_OPTION_CASE_FOLDING] = FALSE;
|
|
|
954 |
// And not skipping whitespace
|
|
|
955 |
$this->parseOptions[XML_OPTION_SKIP_WHITE] = FALSE;
|
|
|
956 |
|
|
|
957 |
// Now merge in the overrides.
|
|
|
958 |
// Don't use PHP's array_merge!
|
|
|
959 |
if (is_array($userXmlOptions)) {
|
|
|
960 |
foreach($userXmlOptions as $key => $val) $this->parseOptions[$key] = $val;
|
|
|
961 |
}
|
|
|
962 |
}
|
|
|
963 |
|
|
|
964 |
/**
|
|
|
965 |
* Resets the object so it's able to take a new xml sting/file
|
|
|
966 |
*
|
|
|
967 |
* Constructing objects is slow. If you can, reuse ones that you have used already
|
|
|
968 |
* by using this reset() function.
|
|
|
969 |
*/
|
|
|
970 |
function reset() {
|
|
|
971 |
parent::reset();
|
|
|
972 |
$this->properties['xmlFile'] = '';
|
|
|
973 |
$this->parseStackIndex = 0;
|
|
|
974 |
$this->parsedTextLocation = '';
|
|
|
975 |
$this->parsInCData = 0;
|
|
|
976 |
$this->nodeIndex = array();
|
|
|
977 |
$this->nodeRoot = array();
|
|
|
978 |
$this->nodeStack = array();
|
|
|
979 |
$this->aLiterals = array();
|
|
|
980 |
$this->_indexIsDirty = FALSE;
|
|
|
981 |
}
|
|
|
982 |
|
|
|
983 |
|
|
|
984 |
//-----------------------------------------------------------------------------------------
|
|
|
985 |
// XPathEngine ------ Get / Set Stuff ------
|
|
|
986 |
//-----------------------------------------------------------------------------------------
|
|
|
987 |
|
|
|
988 |
/**
|
|
|
989 |
* Returns the property/ies you want.
|
|
|
990 |
*
|
|
|
991 |
* if $param is not given, all properties will be returned in a hash.
|
|
|
992 |
*
|
|
|
993 |
* @param $param (string) the property you want the value of, or NULL for all the properties
|
|
|
994 |
* @return (mixed) string OR hash of all params, or NULL on an unknown parameter.
|
|
|
995 |
*/
|
|
|
996 |
function getProperties($param=NULL) {
|
|
|
997 |
$this->properties['hasContent'] = !empty($this->nodeRoot);
|
|
|
998 |
$this->properties['caseFolding'] = $this->parseOptions[XML_OPTION_CASE_FOLDING];
|
|
|
999 |
$this->properties['skipWhiteSpaces'] = $this->parseOptions[XML_OPTION_SKIP_WHITE];
|
|
|
1000 |
|
|
|
1001 |
if (empty($param)) return $this->properties;
|
|
|
1002 |
|
|
|
1003 |
if (isSet($this->properties[$param])) {
|
|
|
1004 |
return $this->properties[$param];
|
|
|
1005 |
} else {
|
|
|
1006 |
return NULL;
|
|
|
1007 |
}
|
|
|
1008 |
}
|
|
|
1009 |
|
|
|
1010 |
/**
|
|
|
1011 |
* Set an xml_parser_set_option()
|
|
|
1012 |
*
|
|
|
1013 |
* @param $optionID (int) The option ID (e.g. XML_OPTION_SKIP_WHITE)
|
|
|
1014 |
* @param $value (int) The option value.
|
|
|
1015 |
* @see XML parser functions in PHP doc
|
|
|
1016 |
*/
|
|
|
1017 |
function setXmlOption($optionID, $value) {
|
|
|
1018 |
if (!is_numeric($optionID)) return;
|
|
|
1019 |
$this->parseOptions[$optionID] = $value;
|
|
|
1020 |
}
|
|
|
1021 |
|
|
|
1022 |
/**
|
|
|
1023 |
* Sets a number of xml_parser_set_option()s
|
|
|
1024 |
*
|
|
|
1025 |
* @param $userXmlOptions (array) An array of parser options.
|
|
|
1026 |
* @see setXmlOption
|
|
|
1027 |
*/
|
|
|
1028 |
function setXmlOptions($userXmlOptions=array()) {
|
|
|
1029 |
if (!is_array($userXmlOptions)) return;
|
|
|
1030 |
foreach($userXmlOptions as $key => $val) {
|
|
|
1031 |
$this->setXmlOption($key, $val);
|
|
|
1032 |
}
|
|
|
1033 |
}
|
|
|
1034 |
|
|
|
1035 |
/**
|
|
|
1036 |
* Alternative way to control whether case-folding is enabled for this XML parser.
|
|
|
1037 |
*
|
|
|
1038 |
* Short cut to setXmlOptions(XML_OPTION_CASE_FOLDING, TRUE/FALSE)
|
|
|
1039 |
*
|
|
|
1040 |
* When it comes to XML, case-folding simply means uppercasing all tag-
|
|
|
1041 |
* and attribute-names (NOT the content) if set to TRUE. Note if you
|
|
|
1042 |
* have this option set, then your XPath queries will also be case folded
|
|
|
1043 |
* for you.
|
|
|
1044 |
*
|
|
|
1045 |
* @param $onOff (bool) (default TRUE)
|
|
|
1046 |
* @see XML parser functions in PHP doc
|
|
|
1047 |
*/
|
|
|
1048 |
function setCaseFolding($onOff=TRUE) {
|
|
|
1049 |
$this->parseOptions[XML_OPTION_CASE_FOLDING] = $onOff;
|
|
|
1050 |
}
|
|
|
1051 |
|
|
|
1052 |
/**
|
|
|
1053 |
* Alternative way to control whether skip-white-spaces is enabled for this XML parser.
|
|
|
1054 |
*
|
|
|
1055 |
* Short cut to setXmlOptions(XML_OPTION_SKIP_WHITE, TRUE/FALSE)
|
|
|
1056 |
*
|
|
|
1057 |
* When it comes to XML, skip-white-spaces will trim the tag content.
|
|
|
1058 |
* An XML file with no whitespace will be faster to process, but will make
|
|
|
1059 |
* your data less human readable when you come to write it out.
|
|
|
1060 |
*
|
|
|
1061 |
* Running with this option on will slow the class down, so if you want to
|
|
|
1062 |
* speed up your XML, then run it through once skipping white-spaces, then
|
|
|
1063 |
* write out the new version of your XML without whitespace, then use the
|
|
|
1064 |
* new XML file with skip whitespaces turned off.
|
|
|
1065 |
*
|
|
|
1066 |
* @param $onOff (bool) (default TRUE)
|
|
|
1067 |
* @see XML parser functions in PHP doc
|
|
|
1068 |
*/
|
|
|
1069 |
function setSkipWhiteSpaces($onOff=TRUE) {
|
|
|
1070 |
$this->parseOptions[XML_OPTION_SKIP_WHITE] = $onOff;
|
|
|
1071 |
}
|
|
|
1072 |
|
|
|
1073 |
/**
|
|
|
1074 |
* Get the node defined by the $absoluteXPath.
|
|
|
1075 |
*
|
|
|
1076 |
* @param $absoluteXPath (string) (optional, default is 'super-root') xpath to the node.
|
|
|
1077 |
* @return (array) The node, or FALSE if the node wasn't found.
|
|
|
1078 |
*/
|
|
|
1079 |
function &getNode($absoluteXPath='') {
|
|
|
1080 |
if ($absoluteXPath==='/') $absoluteXPath = '';
|
|
|
1081 |
if (!isSet($this->nodeIndex[$absoluteXPath])) return FALSE;
|
|
|
1082 |
if ($this->_indexIsDirty) $this->reindexNodeTree();
|
|
|
1083 |
return $this->nodeIndex[$absoluteXPath];
|
|
|
1084 |
}
|
|
|
1085 |
|
|
|
1086 |
/**
|
|
|
1087 |
* Get a the content of a node text part or node attribute.
|
|
|
1088 |
*
|
|
|
1089 |
* If the absolute Xpath references an attribute (Xpath ends with @ or attribute::),
|
|
|
1090 |
* then the text value of that node-attribute is returned.
|
|
|
1091 |
* Otherwise the Xpath is referencing a text part of the node. This can be either a
|
|
|
1092 |
* direct reference to a text part (Xpath ends with text()[<nr>]) or indirect reference
|
|
|
1093 |
* (a simple abs. Xpath to a node).
|
|
|
1094 |
* 1) Direct Reference (xpath ends with text()[<part-number>]):
|
|
|
1095 |
* If the 'part-number' is omitted, the first text-part is assumed; starting by 1.
|
|
|
1096 |
* Negative numbers are allowed, where -1 is the last text-part a.s.o.
|
|
|
1097 |
* 2) Indirect Reference (a simple abs. Xpath to a node):
|
|
|
1098 |
* Default is to return the *whole text*; that is the concated text-parts of the matching
|
|
|
1099 |
* node. (NOTE that only in this case you'll only get a copy and changes to the returned
|
|
|
1100 |
* value wounld have no effect). Optionally you may pass a parameter
|
|
|
1101 |
* $textPartNr to define the text-part you want; starting by 1.
|
|
|
1102 |
* Negative numbers are allowed, where -1 is the last text-part a.s.o.
|
|
|
1103 |
*
|
|
|
1104 |
* NOTE I : The returned value can be fetched by reference
|
|
|
1105 |
* E.g. $text =& wholeText(). If you wish to modify the text.
|
|
|
1106 |
* NOTE II: text-part numbers out of range will return FALSE
|
|
|
1107 |
* SIDENOTE:The function name is a suggestion from W3C in the XPath specification level 3.
|
|
|
1108 |
*
|
|
|
1109 |
* @param $absoluteXPath (string) xpath to the node (See above).
|
|
|
1110 |
* @param $textPartNr (int) If referring to a node, specifies which text part
|
|
|
1111 |
* to query.
|
|
|
1112 |
* @return (&string) A *reference* to the text if the node that the other
|
|
|
1113 |
* parameters describe or FALSE if the node is not found.
|
|
|
1114 |
*/
|
|
|
1115 |
function &wholeText($absoluteXPath, $textPartNr=NULL) {
|
|
|
1116 |
$status = FALSE;
|
|
|
1117 |
$text = NULL;
|
|
|
1118 |
if ($this->_indexIsDirty) $this->reindexNodeTree();
|
|
|
1119 |
|
|
|
1120 |
do { // try-block
|
|
|
1121 |
if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $absoluteXPath, $matches)) {
|
|
|
1122 |
$absoluteXPath = $matches[1];
|
|
|
1123 |
$attribute = $matches[3];
|
|
|
1124 |
if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
|
|
|
1125 |
$this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
|
|
|
1126 |
break; // try-block
|
|
|
1127 |
}
|
|
|
1128 |
$text =& $this->nodeIndex[$absoluteXPath]['attributes'][$attribute];
|
|
|
1129 |
$status = TRUE;
|
|
|
1130 |
break; // try-block
|
|
|
1131 |
}
|
|
|
1132 |
|
|
|
1133 |
// Xpath contains a 'text()'-function, thus goes right to a text node. If so interpret the Xpath.
|
|
|
1134 |
if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $absoluteXPath, $matches)) {
|
|
|
1135 |
$absoluteXPath = $matches[1];
|
|
|
1136 |
|
|
|
1137 |
if (!isSet($this->nodeIndex[$absoluteXPath])) {
|
|
|
1138 |
$this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
|
|
|
1139 |
break; // try-block
|
|
|
1140 |
}
|
|
|
1141 |
|
|
|
1142 |
// Get the amount of the text parts in the node.
|
|
|
1143 |
$textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
|
|
|
1144 |
|
|
|
1145 |
// default to the first text node if a text node was not specified
|
|
|
1146 |
$textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
|
|
|
1147 |
|
|
|
1148 |
// Support negative indexes like -1 === last a.s.o.
|
|
|
1149 |
if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
|
|
|
1150 |
if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
|
|
|
1151 |
$this->_displayError("The $absoluteXPath/text()[$textPartNr] value isn't a NODE in this document.", __LINE__, __FILE__, FALSE);
|
|
|
1152 |
break; // try-block
|
|
|
1153 |
}
|
|
|
1154 |
$text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr - 1];
|
|
|
1155 |
$status = TRUE;
|
|
|
1156 |
break; // try-block
|
|
|
1157 |
}
|
|
|
1158 |
|
|
|
1159 |
// At this point we have been given an xpath with neither a 'text()' nor 'attribute::' axis at the end
|
|
|
1160 |
// So we assume a get to text is wanted and use the optioanl fallback parameters $textPartNr
|
|
|
1161 |
|
|
|
1162 |
if (!isSet($this->nodeIndex[$absoluteXPath])) {
|
|
|
1163 |
$this->_displayError("The $absoluteXPath value isn't a node in this document.", __LINE__, __FILE__, FALSE);
|
|
|
1164 |
break; // try-block
|
|
|
1165 |
}
|
|
|
1166 |
|
|
|
1167 |
// Get the amount of the text parts in the node.
|
|
|
1168 |
$textPartSize = sizeOf($this->nodeIndex[$absoluteXPath]['textParts']);
|
|
|
1169 |
|
|
|
1170 |
// If $textPartNr == NULL we return a *copy* of the whole concated text-parts
|
|
|
1171 |
if (is_null($textPartNr)) {
|
|
|
1172 |
unset($text);
|
|
|
1173 |
$text = implode('', $this->nodeIndex[$absoluteXPath]['textParts']);
|
|
|
1174 |
$status = TRUE;
|
|
|
1175 |
break; // try-block
|
|
|
1176 |
}
|
|
|
1177 |
|
|
|
1178 |
// Support negative indexes like -1 === last a.s.o.
|
|
|
1179 |
if ($textPartNr < 0) $textPartNr = $textPartSize + $textPartNr +1;
|
|
|
1180 |
if (($textPartNr <= 0) OR ($textPartNr > $textPartSize)) {
|
|
|
1181 |
$this->_displayError("The $absoluteXPath has no text part at pos [$textPartNr] (Note: text parts start with 1).", __LINE__, __FILE__, FALSE);
|
|
|
1182 |
break; // try-block
|
|
|
1183 |
}
|
|
|
1184 |
$text =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr -1];
|
|
|
1185 |
$status = TRUE;
|
|
|
1186 |
} while (FALSE); // END try-block
|
|
|
1187 |
|
|
|
1188 |
if (!$status) return FALSE;
|
|
|
1189 |
return $text;
|
|
|
1190 |
}
|
|
|
1191 |
|
|
|
1192 |
/**
|
|
|
1193 |
* Obtain the string value of an object
|
|
|
1194 |
*
|
|
|
1195 |
* http://www.w3.org/TR/xpath#dt-string-value
|
|
|
1196 |
*
|
|
|
1197 |
* "For every type of node, there is a way of determining a string-value for a node of that type.
|
|
|
1198 |
* For some types of node, the string-value is part of the node; for other types of node, the
|
|
|
1199 |
* string-value is computed from the string-value of descendant nodes."
|
|
|
1200 |
*
|
|
|
1201 |
* @param $node (node) The node we have to convert
|
|
|
1202 |
* @return (string) The string value of the node. "" if the object has no evaluatable
|
|
|
1203 |
* string value
|
|
|
1204 |
*/
|
|
|
1205 |
function _stringValue($node) {
|
|
|
1206 |
// Decode the entitites and then add the resulting literal string into our array.
|
|
|
1207 |
return $this->_addLiteral($this->decodeEntities($this->wholeText($node)));
|
|
|
1208 |
}
|
|
|
1209 |
|
|
|
1210 |
//-----------------------------------------------------------------------------------------
|
|
|
1211 |
// XPathEngine ------ Export the XML Document ------
|
|
|
1212 |
//-----------------------------------------------------------------------------------------
|
|
|
1213 |
|
|
|
1214 |
/**
|
|
|
1215 |
* Returns the containing XML as marked up HTML with specified nodes hi-lighted
|
|
|
1216 |
*
|
|
|
1217 |
* @param $absoluteXPath (string) The address of the node you would like to export.
|
|
|
1218 |
* If empty the whole document will be exported.
|
|
|
1219 |
* @param $hilighXpathList (array) A list of nodes that you would like to highlight
|
|
|
1220 |
* @return (mixed) The Xml document marked up as HTML so that it can
|
|
|
1221 |
* be viewed in a browser, including any XML headers.
|
|
|
1222 |
* FALSE on error.
|
|
|
1223 |
* @see _export()
|
|
|
1224 |
*/
|
|
|
1225 |
function exportAsHtml($absoluteXPath='', $hilightXpathList=array()) {
|
|
|
1226 |
$htmlString = $this->_export($absoluteXPath, $xmlHeader=NULL, $hilightXpathList);
|
|
|
1227 |
if (!$htmlString) return FALSE;
|
|
|
1228 |
return "<pre>\n" . $htmlString . "\n</pre>";
|
|
|
1229 |
}
|
|
|
1230 |
|
|
|
1231 |
/**
|
|
|
1232 |
* Given a context this function returns the containing XML
|
|
|
1233 |
*
|
|
|
1234 |
* @param $absoluteXPath (string) The address of the node you would like to export.
|
|
|
1235 |
* If empty the whole document will be exported.
|
|
|
1236 |
* @param $xmlHeader (array) The string that you would like to appear before
|
|
|
1237 |
* the XML content. ie before the <root></root>. If you
|
|
|
1238 |
* do not specify this argument, the xmlHeader that was
|
|
|
1239 |
* found in the parsed xml file will be used instead.
|
|
|
1240 |
* @return (mixed) The Xml fragment/document, suitable for writing
|
|
|
1241 |
* out to an .xml file or as part of a larger xml file, or
|
|
|
1242 |
* FALSE on error.
|
|
|
1243 |
* @see _export()
|
|
|
1244 |
*/
|
|
|
1245 |
function exportAsXml($absoluteXPath='', $xmlHeader=NULL) {
|
|
|
1246 |
$this->hilightXpathList = NULL;
|
|
|
1247 |
return $this->_export($absoluteXPath, $xmlHeader);
|
|
|
1248 |
}
|
|
|
1249 |
|
|
|
1250 |
/**
|
|
|
1251 |
* Generates a XML string with the content of the current document and writes it to a file.
|
|
|
1252 |
*
|
|
|
1253 |
* Per default includes a <?xml ...> tag at the start of the data too.
|
|
|
1254 |
*
|
|
|
1255 |
* @param $fileName (string)
|
|
|
1256 |
* @param $absoluteXPath (string) The path to the parent node you want(see text above)
|
|
|
1257 |
* @param $xmlHeader (array) The string that you would like to appear before
|
|
|
1258 |
* the XML content. ie before the <root></root>. If you
|
|
|
1259 |
* do not specify this argument, the xmlHeader that was
|
|
|
1260 |
* found in the parsed xml file will be used instead.
|
|
|
1261 |
* @return (string) The returned string contains well-formed XML data
|
|
|
1262 |
* or FALSE on error.
|
|
|
1263 |
* @see exportAsXml(), exportAsHtml()
|
|
|
1264 |
*/
|
|
|
1265 |
function exportToFile($fileName, $absoluteXPath='', $xmlHeader=NULL) {
|
|
|
1266 |
$status = FALSE;
|
|
|
1267 |
do { // try-block
|
|
|
1268 |
if (!($hFile = fopen($fileName, "wb"))) { // Did we open the file ok?
|
|
|
1269 |
$errStr = "Failed to open the $fileName xml file.";
|
|
|
1270 |
break; // try-block
|
|
|
1271 |
}
|
|
|
1272 |
|
|
|
1273 |
if ($this->properties['OS_supports_flock']) {
|
|
|
1274 |
if (!flock($hFile, LOCK_EX + LOCK_NB)) { // Lock the file
|
|
|
1275 |
$errStr = "Couldn't get an exclusive lock on the $fileName file.";
|
|
|
1276 |
break; // try-block
|
|
|
1277 |
}
|
|
|
1278 |
}
|
|
|
1279 |
if (!($xmlOut = $this->_export($absoluteXPath, $xmlHeader))) {
|
|
|
1280 |
$errStr = "Export failed";
|
|
|
1281 |
break; // try-block
|
|
|
1282 |
}
|
|
|
1283 |
|
|
|
1284 |
$iBytesWritten = fwrite($hFile, $xmlOut);
|
|
|
1285 |
if ($iBytesWritten != strlen($xmlOut)) {
|
|
|
1286 |
$errStr = "Write error when writing back the $fileName file.";
|
|
|
1287 |
break; // try-block
|
|
|
1288 |
}
|
|
|
1289 |
|
|
|
1290 |
// Flush and unlock the file
|
|
|
1291 |
@fflush($hFile);
|
|
|
1292 |
$status = TRUE;
|
|
|
1293 |
} while(FALSE);
|
|
|
1294 |
|
|
|
1295 |
@flock($hFile, LOCK_UN);
|
|
|
1296 |
@fclose($hFile);
|
|
|
1297 |
// Sanity check the produced file.
|
|
|
1298 |
clearstatcache();
|
|
|
1299 |
if (filesize($fileName) < strlen($xmlOut)) {
|
|
|
1300 |
$errStr = "Write error when writing back the $fileName file.";
|
|
|
1301 |
$status = FALSE;
|
|
|
1302 |
}
|
|
|
1303 |
|
|
|
1304 |
if (!$status) $this->_displayError($errStr, __LINE__, __FILE__, FALSE);
|
|
|
1305 |
return $status;
|
|
|
1306 |
}
|
|
|
1307 |
|
|
|
1308 |
/**
|
|
|
1309 |
* Generates a XML string with the content of the current document.
|
|
|
1310 |
*
|
|
|
1311 |
* This is the start for extracting the XML-data from the node-tree. We do some preperations
|
|
|
1312 |
* and then call _InternalExport() to fetch the main XML-data. You optionally may pass
|
|
|
1313 |
* xpath to any node that will then be used as top node, to extract XML-parts of the
|
|
|
1314 |
* document. Default is '', meaning to extract the whole document.
|
|
|
1315 |
*
|
|
|
1316 |
* You also may pass a 'xmlHeader' (usually something like <?xml version="1.0"? > that will
|
|
|
1317 |
* overwrite any other 'xmlHeader', if there was one in the original source. If there
|
|
|
1318 |
* wasn't one in the original source, and you still don't specify one, then it will
|
|
|
1319 |
* use a default of <?xml version="1.0"? >
|
|
|
1320 |
* Finaly, when exporting to HTML, you may pass a vector xPaths you want to hi-light.
|
|
|
1321 |
* The hi-lighted tags and attributes will receive a nice color.
|
|
|
1322 |
*
|
|
|
1323 |
* NOTE I : The output can have 2 formats:
|
|
|
1324 |
* a) If "skip white spaces" is/was set. (Not Recommended - slower)
|
|
|
1325 |
* The output is formatted by adding indenting and carriage returns.
|
|
|
1326 |
* b) If "skip white spaces" is/was *NOT* set.
|
|
|
1327 |
* 'as is'. No formatting is done. The output should the same as the
|
|
|
1328 |
* the original parsed XML source.
|
|
|
1329 |
*
|
|
|
1330 |
* @param $absoluteXPath (string) (optional, default is root) The node we choose as top-node
|
|
|
1331 |
* @param $xmlHeader (string) (optional) content before <root/> (see text above)
|
|
|
1332 |
* @param $hilightXpath (array) (optional) a vector of xPaths to nodes we wat to
|
|
|
1333 |
* hi-light (see text above)
|
|
|
1334 |
* @return (mixed) The xml string, or FALSE on error.
|
|
|
1335 |
*/
|
|
|
1336 |
function _export($absoluteXPath='', $xmlHeader=NULL, $hilightXpathList='') {
|
|
|
1337 |
// Check whether a root node is given.
|
|
|
1338 |
if (empty($absoluteXpath)) $absoluteXpath = '';
|
|
|
1339 |
if ($absoluteXpath == '/') $absoluteXpath = '';
|
|
|
1340 |
if ($this->_indexIsDirty) $this->reindexNodeTree();
|
|
|
1341 |
if (!isSet($this->nodeIndex[$absoluteXpath])) {
|
|
|
1342 |
// If the $absoluteXpath was '' and it didn't exist, then the document is empty
|
|
|
1343 |
// and we can safely return ''.
|
|
|
1344 |
if ($absoluteXpath == '') return '';
|
|
|
1345 |
$this->_displayError("The given xpath '{$absoluteXpath}' isn't a node in this document.", __LINE__, __FILE__, FALSE);
|
|
|
1346 |
return FALSE;
|
|
|
1347 |
}
|
|
|
1348 |
|
|
|
1349 |
$this->hilightXpathList = $hilightXpathList;
|
|
|
1350 |
$this->indentStep = ' ';
|
|
|
1351 |
$hilightIsActive = is_array($hilightXpathList);
|
|
|
1352 |
if ($hilightIsActive) {
|
|
|
1353 |
$this->indentStep = ' ';
|
|
|
1354 |
}
|
|
|
1355 |
|
|
|
1356 |
// Cache this now
|
|
|
1357 |
$this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;
|
|
|
1358 |
|
|
|
1359 |
///////////////////////////////////////
|
|
|
1360 |
// Get the starting node and begin with the header
|
|
|
1361 |
|
|
|
1362 |
// Get the start node. The super root is a special case.
|
|
|
1363 |
$startNode = NULL;
|
|
|
1364 |
if (empty($absoluteXPath)) {
|
|
|
1365 |
$superRoot = $this->nodeIndex[''];
|
|
|
1366 |
// If they didn't specify an xml header, use the one in the object
|
|
|
1367 |
if (is_null($xmlHeader)) {
|
|
|
1368 |
$xmlHeader = $this->parseSkipWhiteCache ? trim($superRoot['textParts'][0]) : $superRoot['textParts'][0];
|
|
|
1369 |
// If we still don't have an XML header, then use a suitable default
|
|
|
1370 |
if (empty($xmlHeader)) {
|
|
|
1371 |
$xmlHeader = '<?xml version="1.0"?>';
|
|
|
1372 |
}
|
|
|
1373 |
}
|
|
|
1374 |
|
|
|
1375 |
if (isSet($superRoot['childNodes'][0])) $startNode = $superRoot['childNodes'][0];
|
|
|
1376 |
} else {
|
|
|
1377 |
$startNode = $this->nodeIndex[$absoluteXPath];
|
|
|
1378 |
}
|
|
|
1379 |
|
|
|
1380 |
if (!empty($xmlHeader)) {
|
|
|
1381 |
$xmlOut = $this->parseSkipWhiteCache ? $xmlHeader."\n" : $xmlHeader;
|
|
|
1382 |
} else {
|
|
|
1383 |
$xmlOut = '';
|
|
|
1384 |
}
|
|
|
1385 |
|
|
|
1386 |
///////////////////////////////////////
|
|
|
1387 |
// Output the document.
|
|
|
1388 |
|
|
|
1389 |
if (($xmlOut .= $this->_InternalExport($startNode)) === FALSE) {
|
|
|
1390 |
return FALSE;
|
|
|
1391 |
}
|
|
|
1392 |
|
|
|
1393 |
///////////////////////////////////////
|
|
|
1394 |
|
|
|
1395 |
// Convert our markers to hi-lights.
|
|
|
1396 |
if ($hilightIsActive) {
|
|
|
1397 |
$from = array('<', '>', chr(2), chr(3));
|
|
|
1398 |
$to = array('<', '>', '<font color="#FF0000"><b>', '</b></font>');
|
|
|
1399 |
$xmlOut = str_replace($from, $to, $xmlOut);
|
|
|
1400 |
}
|
|
|
1401 |
return $xmlOut;
|
|
|
1402 |
}
|
|
|
1403 |
|
|
|
1404 |
/**
|
|
|
1405 |
* Export the xml document starting at the named node.
|
|
|
1406 |
*
|
|
|
1407 |
* @param $node (node) The node we have to start exporting from
|
|
|
1408 |
* @return (string) The string representation of the node.
|
|
|
1409 |
*/
|
|
|
1410 |
function _InternalExport($node) {
|
|
|
1411 |
$ThisFunctionName = '_InternalExport';
|
|
|
1412 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
1413 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
1414 |
if ($bDebugThisFunction) {
|
|
|
1415 |
echo "Exporting node: ".$node['xpath']."<br>\n";
|
|
|
1416 |
}
|
|
|
1417 |
|
|
|
1418 |
////////////////////////////////
|
|
|
1419 |
|
|
|
1420 |
// Quick out.
|
|
|
1421 |
if (empty($node)) return '';
|
|
|
1422 |
|
|
|
1423 |
// The output starts as empty.
|
|
|
1424 |
$xmlOut = '';
|
|
|
1425 |
// This loop will output the text before the current child of a parent then the
|
|
|
1426 |
// current child. Where the child is a short tag we output the child, then move
|
|
|
1427 |
// onto the next child. Where the child is not a short tag, we output the open tag,
|
|
|
1428 |
// then queue up on currentParentStack[] the child.
|
|
|
1429 |
//
|
|
|
1430 |
// When we run out of children, we then output the last text part, and close the
|
|
|
1431 |
// 'parent' tag before popping the stack and carrying on.
|
|
|
1432 |
//
|
|
|
1433 |
// To illustrate, the numbers in this xml file indicate what is output on each
|
|
|
1434 |
// pass of the while loop:
|
|
|
1435 |
//
|
|
|
1436 |
// 1
|
|
|
1437 |
// <1>2
|
|
|
1438 |
// <2>3
|
|
|
1439 |
// <3/>4
|
|
|
1440 |
// </4>5
|
|
|
1441 |
// <5/>6
|
|
|
1442 |
// </6>
|
|
|
1443 |
|
|
|
1444 |
// Although this is neater done using recursion, there's a 33% performance saving
|
|
|
1445 |
// to be gained by using this stack mechanism.
|
|
|
1446 |
|
|
|
1447 |
// Only add CR's if "skip white spaces" was set. Otherwise leave as is.
|
|
|
1448 |
$CR = ($this->parseSkipWhiteCache) ? "\n" : '';
|
|
|
1449 |
$currentIndent = '';
|
|
|
1450 |
$hilightIsActive = is_array($this->hilightXpathList);
|
|
|
1451 |
|
|
|
1452 |
// To keep track of where we are in the document we use a node stack. The node
|
|
|
1453 |
// stack has the following parallel entries:
|
|
|
1454 |
// 'Parent' => (array) A copy of the parent node that who's children we are
|
|
|
1455 |
// exporting
|
|
|
1456 |
// 'ChildIndex' => (array) The child index of the corresponding parent that we
|
|
|
1457 |
// are currently exporting.
|
|
|
1458 |
// 'Highlighted'=> (bool) If we are highlighting this node. Only relevant if
|
|
|
1459 |
// the hilight is active.
|
|
|
1460 |
|
|
|
1461 |
// Setup our node stack. The loop is designed to output children of a parent,
|
|
|
1462 |
// not the parent itself, so we must put the parent on as the starting point.
|
|
|
1463 |
$nodeStack['Parent'] = array($node['parentNode']);
|
|
|
1464 |
// And add the childpos of our node in it's parent to our "child index stack".
|
|
|
1465 |
$nodeStack['ChildIndex'] = array($node['pos']);
|
|
|
1466 |
// We start at 0.
|
|
|
1467 |
$nodeStackIndex = 0;
|
|
|
1468 |
|
|
|
1469 |
// We have not to output text before/after our node, so blank it. We will recover it
|
|
|
1470 |
// later
|
|
|
1471 |
$OldPreceedingStringValue = $nodeStack['Parent'][0]['textParts'][$node['pos']];
|
|
|
1472 |
$OldPreceedingStringRef =& $nodeStack['Parent'][0]['textParts'][$node['pos']];
|
|
|
1473 |
$OldPreceedingStringRef = "";
|
|
|
1474 |
$currentXpath = "";
|
|
|
1475 |
|
|
|
1476 |
// While we still have data on our stack
|
|
|
1477 |
while ($nodeStackIndex >= 0) {
|
|
|
1478 |
// Count the children and get a copy of the current child.
|
|
|
1479 |
$iChildCount = count($nodeStack['Parent'][$nodeStackIndex]['childNodes']);
|
|
|
1480 |
$currentChild = $nodeStack['ChildIndex'][$nodeStackIndex];
|
|
|
1481 |
// Only do the auto indenting if the $parseSkipWhiteCache flag was set.
|
|
|
1482 |
if ($this->parseSkipWhiteCache)
|
|
|
1483 |
$currentIndent = str_repeat($this->indentStep, $nodeStackIndex);
|
|
|
1484 |
|
|
|
1485 |
if ($bDebugThisFunction)
|
|
|
1486 |
echo "Exporting child ".($currentChild+1)." of node {$nodeStack['Parent'][$nodeStackIndex]['xpath']}\n";
|
|
|
1487 |
|
|
|
1488 |
///////////////////////////////////////////
|
|
|
1489 |
// Add the text before our child.
|
|
|
1490 |
|
|
|
1491 |
// Add the text part before the current child
|
|
|
1492 |
$tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild];
|
|
|
1493 |
if (isSet($tmpTxt) AND ($tmpTxt!="")) {
|
|
|
1494 |
// Only add CR indent if there were children
|
|
|
1495 |
if ($iChildCount)
|
|
|
1496 |
$xmlOut .= $CR.$currentIndent;
|
|
|
1497 |
// Hilight if necessary.
|
|
|
1498 |
$highlightStart = $highlightEnd = '';
|
|
|
1499 |
if ($hilightIsActive) {
|
|
|
1500 |
$currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+1).']';
|
|
|
1501 |
if (in_array($currentXpath, $this->hilightXpathList)) {
|
|
|
1502 |
// Yes we hilight
|
|
|
1503 |
$highlightStart = chr(2);
|
|
|
1504 |
$highlightEnd = chr(3);
|
|
|
1505 |
}
|
|
|
1506 |
}
|
|
|
1507 |
$xmlOut .= $highlightStart.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild].$highlightEnd;
|
|
|
1508 |
}
|
|
|
1509 |
if ($iChildCount && $nodeStackIndex) $xmlOut .= $CR;
|
|
|
1510 |
|
|
|
1511 |
///////////////////////////////////////////
|
|
|
1512 |
|
|
|
1513 |
// Are there any more children?
|
|
|
1514 |
if ($iChildCount <= $currentChild) {
|
|
|
1515 |
// Nope, so output the last text before the closing tag
|
|
|
1516 |
$tmpTxt =& $nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1];
|
|
|
1517 |
if (isSet($tmpTxt) AND ($tmpTxt!="")) {
|
|
|
1518 |
// Hilight if necessary.
|
|
|
1519 |
$highlightStart = $highlightEnd = '';
|
|
|
1520 |
if ($hilightIsActive) {
|
|
|
1521 |
$currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'].'/text()['.($currentChild+2).']';
|
|
|
1522 |
if (in_array($currentXpath, $this->hilightXpathList)) {
|
|
|
1523 |
// Yes we hilight
|
|
|
1524 |
$highlightStart = chr(2);
|
|
|
1525 |
$highlightEnd = chr(3);
|
|
|
1526 |
}
|
|
|
1527 |
}
|
|
|
1528 |
$xmlOut .= $highlightStart
|
|
|
1529 |
.$currentIndent.$nodeStack['Parent'][$nodeStackIndex]['textParts'][$currentChild+1].$CR
|
|
|
1530 |
.$highlightEnd;
|
|
|
1531 |
}
|
|
|
1532 |
|
|
|
1533 |
// Now close this tag, as we are finished with this child.
|
|
|
1534 |
|
|
|
1535 |
// Potentially output an (slightly smaller indent).
|
|
|
1536 |
if ($this->parseSkipWhiteCache
|
|
|
1537 |
&& count($nodeStack['Parent'][$nodeStackIndex]['childNodes'])) {
|
|
|
1538 |
$xmlOut .= str_repeat($this->indentStep, $nodeStackIndex - 1);
|
|
|
1539 |
}
|
|
|
1540 |
|
|
|
1541 |
// Check whether the xml-tag is to be hilighted.
|
|
|
1542 |
$highlightStart = $highlightEnd = '';
|
|
|
1543 |
if ($hilightIsActive) {
|
|
|
1544 |
$currentXpath = $nodeStack['Parent'][$nodeStackIndex]['xpath'];
|
|
|
1545 |
if (in_array($currentXpath, $this->hilightXpathList)) {
|
|
|
1546 |
// Yes we hilight
|
|
|
1547 |
$highlightStart = chr(2);
|
|
|
1548 |
$highlightEnd = chr(3);
|
|
|
1549 |
}
|
|
|
1550 |
}
|
|
|
1551 |
$xmlOut .= $highlightStart
|
|
|
1552 |
.'</'.$nodeStack['Parent'][$nodeStackIndex]['name'].'>'
|
|
|
1553 |
.$highlightEnd;
|
|
|
1554 |
// Decrement the $nodeStackIndex to go back to the next unfinished parent.
|
|
|
1555 |
$nodeStackIndex--;
|
|
|
1556 |
|
|
|
1557 |
// If the index is 0 we are finished exporting the last node, as we may have been
|
|
|
1558 |
// exporting an internal node.
|
|
|
1559 |
if ($nodeStackIndex == 0) break;
|
|
|
1560 |
|
|
|
1561 |
// Indicate to the parent that we are finished with this child.
|
|
|
1562 |
$nodeStack['ChildIndex'][$nodeStackIndex]++;
|
|
|
1563 |
|
|
|
1564 |
continue;
|
|
|
1565 |
}
|
|
|
1566 |
|
|
|
1567 |
///////////////////////////////////////////
|
|
|
1568 |
// Ok, there are children still to process.
|
|
|
1569 |
|
|
|
1570 |
// Queue up the next child (I can copy because I won't modify and copying is faster.)
|
|
|
1571 |
$nodeStack['Parent'][$nodeStackIndex + 1] = $nodeStack['Parent'][$nodeStackIndex]['childNodes'][$currentChild];
|
|
|
1572 |
|
|
|
1573 |
// Work out if it is a short child tag.
|
|
|
1574 |
$iGrandChildCount = count($nodeStack['Parent'][$nodeStackIndex + 1]['childNodes']);
|
|
|
1575 |
$shortGrandChild = (($iGrandChildCount == 0) AND (implode('',$nodeStack['Parent'][$nodeStackIndex + 1]['textParts'])==''));
|
|
|
1576 |
|
|
|
1577 |
///////////////////////////////////////////
|
|
|
1578 |
// Assemble the attribute string first.
|
|
|
1579 |
$attrStr = '';
|
|
|
1580 |
foreach($nodeStack['Parent'][$nodeStackIndex + 1]['attributes'] as $key=>$val) {
|
|
|
1581 |
// Should we hilight the attribute?
|
|
|
1582 |
if ($hilightIsActive AND in_array($currentXpath.'/attribute::'.$key, $this->hilightXpathList)) {
|
|
|
1583 |
$hiAttrStart = chr(2);
|
|
|
1584 |
$hiAttrEnd = chr(3);
|
|
|
1585 |
} else {
|
|
|
1586 |
$hiAttrStart = $hiAttrEnd = '';
|
|
|
1587 |
}
|
|
|
1588 |
$attrStr .= ' '.$hiAttrStart.$key.'="'.$val.'"'.$hiAttrEnd;
|
|
|
1589 |
}
|
|
|
1590 |
|
|
|
1591 |
///////////////////////////////////////////
|
|
|
1592 |
// Work out what goes before and after the tag content
|
|
|
1593 |
|
|
|
1594 |
$beforeTagContent = $currentIndent;
|
|
|
1595 |
if ($shortGrandChild) $afterTagContent = '/>';
|
|
|
1596 |
else $afterTagContent = '>';
|
|
|
1597 |
|
|
|
1598 |
// Check whether the xml-tag is to be hilighted.
|
|
|
1599 |
if ($hilightIsActive) {
|
|
|
1600 |
$currentXpath = $nodeStack['Parent'][$nodeStackIndex + 1]['xpath'];
|
|
|
1601 |
if (in_array($currentXpath, $this->hilightXpathList)) {
|
|
|
1602 |
// Yes we hilight
|
|
|
1603 |
$beforeTagContent .= chr(2);
|
|
|
1604 |
$afterTagContent .= chr(3);
|
|
|
1605 |
}
|
|
|
1606 |
}
|
|
|
1607 |
$beforeTagContent .= '<';
|
|
|
1608 |
// if ($shortGrandChild) $afterTagContent .= $CR;
|
|
|
1609 |
|
|
|
1610 |
///////////////////////////////////////////
|
|
|
1611 |
// Output the tag
|
|
|
1612 |
|
|
|
1613 |
$xmlOut .= $beforeTagContent
|
|
|
1614 |
.$nodeStack['Parent'][$nodeStackIndex + 1]['name'].$attrStr
|
|
|
1615 |
.$afterTagContent;
|
|
|
1616 |
|
|
|
1617 |
///////////////////////////////////////////
|
|
|
1618 |
// Carry on.
|
|
|
1619 |
|
|
|
1620 |
// If it is a short tag, then we've already done this child, we just move to the next
|
|
|
1621 |
if ($shortGrandChild) {
|
|
|
1622 |
// Move to the next child, we need not go deeper in the tree.
|
|
|
1623 |
$nodeStack['ChildIndex'][$nodeStackIndex]++;
|
|
|
1624 |
// But if we are just exporting the one node we'd go no further.
|
|
|
1625 |
if ($nodeStackIndex == 0) break;
|
|
|
1626 |
} else {
|
|
|
1627 |
// Else queue up the child going one deeper in the stack
|
|
|
1628 |
$nodeStackIndex++;
|
|
|
1629 |
// Start with it's first child
|
|
|
1630 |
$nodeStack['ChildIndex'][$nodeStackIndex] = 0;
|
|
|
1631 |
}
|
|
|
1632 |
}
|
|
|
1633 |
|
|
|
1634 |
$result = $xmlOut;
|
|
|
1635 |
|
|
|
1636 |
// Repair what we "undid"
|
|
|
1637 |
$OldPreceedingStringRef = $OldPreceedingStringValue;
|
|
|
1638 |
|
|
|
1639 |
////////////////////////////////////////////
|
|
|
1640 |
|
|
|
1641 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
1642 |
|
|
|
1643 |
return $result;
|
|
|
1644 |
}
|
|
|
1645 |
|
|
|
1646 |
//-----------------------------------------------------------------------------------------
|
|
|
1647 |
// XPathEngine ------ Import the XML Source ------
|
|
|
1648 |
//-----------------------------------------------------------------------------------------
|
|
|
1649 |
|
|
|
1650 |
/**
|
|
|
1651 |
* Reads a file or URL and parses the XML data.
|
|
|
1652 |
*
|
|
|
1653 |
* Parse the XML source and (upon success) store the information into an internal structure.
|
|
|
1654 |
*
|
|
|
1655 |
* @param $fileName (string) Path and name (or URL) of the file to be read and parsed.
|
|
|
1656 |
* @return (bool) TRUE on success, FALSE on failure (check getLastError())
|
|
|
1657 |
* @see importFromString(), getLastError(),
|
|
|
1658 |
*/
|
|
|
1659 |
function importFromFile($fileName) {
|
|
|
1660 |
$status = FALSE;
|
|
|
1661 |
$errStr = '';
|
|
|
1662 |
do { // try-block
|
|
|
1663 |
// Remember file name. Used in error output to know in which file it happend
|
|
|
1664 |
$this->properties['xmlFile'] = $fileName;
|
|
|
1665 |
// If we already have content, then complain.
|
|
|
1666 |
if (!empty($this->nodeRoot)) {
|
|
|
1667 |
$errStr = 'Called when this object already contains xml data. Use reset().';
|
|
|
1668 |
break; // try-block
|
|
|
1669 |
}
|
|
|
1670 |
// The the source is an url try to fetch it.
|
|
|
1671 |
if (preg_match(';^http(s)?://;', $fileName)) {
|
|
|
1672 |
// Read the content of the url...this is really prone to errors, and we don't really
|
|
|
1673 |
// check for too many here...for now, suppressing both possible warnings...we need
|
|
|
1674 |
// to check if we get a none xml page or something of that nature in the future
|
|
|
1675 |
$xmlString = @implode('', @file($fileName));
|
|
|
1676 |
if (!empty($xmlString)) {
|
|
|
1677 |
$status = TRUE;
|
|
|
1678 |
} else {
|
|
|
1679 |
$errStr = "The url '{$fileName}' could not be found or read.";
|
|
|
1680 |
}
|
|
|
1681 |
break; // try-block
|
|
|
1682 |
}
|
|
|
1683 |
|
|
|
1684 |
// Reaching this point we're dealing with a real file (not an url). Check if the file exists and is readable.
|
|
|
1685 |
if (!is_readable($fileName)) { // Read the content from the file
|
|
|
1686 |
$errStr = "File '{$fileName}' could not be found or read.";
|
|
|
1687 |
break; // try-block
|
|
|
1688 |
}
|
|
|
1689 |
if (is_dir($fileName)) {
|
|
|
1690 |
$errStr = "'{$fileName}' is a directory.";
|
|
|
1691 |
break; // try-block
|
|
|
1692 |
}
|
|
|
1693 |
// Read the file
|
|
|
1694 |
if (!($fp = @fopen($fileName, 'rb'))) {
|
|
|
1695 |
$errStr = "Failed to open '{$fileName}' for read.";
|
|
|
1696 |
break; // try-block
|
|
|
1697 |
}
|
|
|
1698 |
$xmlString = fread($fp, filesize($fileName));
|
|
|
1699 |
@fclose($fp);
|
|
|
1700 |
|
|
|
1701 |
$status = TRUE;
|
|
|
1702 |
} while (FALSE);
|
|
|
1703 |
|
|
|
1704 |
if (!$status) {
|
|
|
1705 |
$this->_displayError('In importFromFile(): '. $errStr, __LINE__, __FILE__, FALSE);
|
|
|
1706 |
return FALSE;
|
|
|
1707 |
}
|
|
|
1708 |
return $this->importFromString($xmlString);
|
|
|
1709 |
}
|
|
|
1710 |
|
|
|
1711 |
/**
|
|
|
1712 |
* Reads a string and parses the XML data.
|
|
|
1713 |
*
|
|
|
1714 |
* Parse the XML source and (upon success) store the information into an internal structure.
|
|
|
1715 |
* If a parent xpath is given this means that XML data is to be *appended* to that parent.
|
|
|
1716 |
*
|
|
|
1717 |
* ### If a function uses setLastError(), then say in the function header that getLastError() is useful.
|
|
|
1718 |
*
|
|
|
1719 |
* @param $xmlString (string) Name of the string to be read and parsed.
|
|
|
1720 |
* @param $absoluteParentPath (string) Node to append data too (see above)
|
|
|
1721 |
* @return (bool) TRUE on success, FALSE on failure
|
|
|
1722 |
* (check getLastError())
|
|
|
1723 |
*/
|
|
|
1724 |
function importFromString($xmlString, $absoluteParentPath = '') {
|
|
|
1725 |
$ThisFunctionName = 'importFromString';
|
|
|
1726 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
1727 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
1728 |
if ($bDebugThisFunction) {
|
|
|
1729 |
echo "Importing from string of length ".strlen($xmlString)." to node '$absoluteParentPath'\n<br>";
|
|
|
1730 |
echo "Parser options:\n<br>";
|
|
|
1731 |
print_r($this->parseOptions);
|
|
|
1732 |
}
|
|
|
1733 |
|
|
|
1734 |
$status = FALSE;
|
|
|
1735 |
$errStr = '';
|
|
|
1736 |
do { // try-block
|
|
|
1737 |
// If we already have content, then complain.
|
|
|
1738 |
if (!empty($this->nodeRoot) AND empty($absoluteParentPath)) {
|
|
|
1739 |
$errStr = 'Called when this object already contains xml data. Use reset() or pass the parent Xpath as 2ed param to where tie data will append.';
|
|
|
1740 |
break; // try-block
|
|
|
1741 |
}
|
|
|
1742 |
// Check whether content has been read.
|
|
|
1743 |
if (empty($xmlString)) {
|
|
|
1744 |
// Nothing to do!!
|
|
|
1745 |
$status = TRUE;
|
|
|
1746 |
// If we were importing to root, build a blank root.
|
|
|
1747 |
if (empty($absoluteParentPath)) {
|
|
|
1748 |
$this->_createSuperRoot();
|
|
|
1749 |
}
|
|
|
1750 |
$this->reindexNodeTree();
|
|
|
1751 |
// $errStr = 'This xml document (string) was empty';
|
|
|
1752 |
break; // try-block
|
|
|
1753 |
} else {
|
|
|
1754 |
$xmlString = $this->_translateAmpersand($xmlString);
|
|
|
1755 |
}
|
|
|
1756 |
|
|
|
1757 |
// Restart our node index with a root entry.
|
|
|
1758 |
$nodeStack = array();
|
|
|
1759 |
$this->parseStackIndex = 0;
|
|
|
1760 |
|
|
|
1761 |
// If a parent xpath is given this means that XML data is to be *appended* to that parent.
|
|
|
1762 |
if (!empty($absoluteParentPath)) {
|
|
|
1763 |
// Check if parent exists
|
|
|
1764 |
if (!isSet($this->nodeIndex[$absoluteParentPath])) {
|
|
|
1765 |
$errStr = "You tried to append XML data to a parent '$absoluteParentPath' that does not exist.";
|
|
|
1766 |
break; // try-block
|
|
|
1767 |
}
|
|
|
1768 |
// Add it as the starting point in our array.
|
|
|
1769 |
$this->nodeStack[0] =& $this->nodeIndex[$absoluteParentPath];
|
|
|
1770 |
} else {
|
|
|
1771 |
// Build a 'super-root'
|
|
|
1772 |
$this->_createSuperRoot();
|
|
|
1773 |
// Put it in as the start of our node stack.
|
|
|
1774 |
$this->nodeStack[0] =& $this->nodeRoot;
|
|
|
1775 |
}
|
|
|
1776 |
|
|
|
1777 |
// Point our text buffer reference at the next text part of the root
|
|
|
1778 |
$this->parsedTextLocation =& $this->nodeStack[0]['textParts'][];
|
|
|
1779 |
$this->parsInCData = 0;
|
|
|
1780 |
// We cache this now.
|
|
|
1781 |
$this->parseSkipWhiteCache = isSet($this->parseOptions[XML_OPTION_SKIP_WHITE]) ? $this->parseOptions[XML_OPTION_SKIP_WHITE] : FALSE;
|
|
|
1782 |
|
|
|
1783 |
// Create an XML parser.
|
|
|
1784 |
$parser = xml_parser_create();
|
|
|
1785 |
// Set default XML parser options.
|
|
|
1786 |
if (is_array($this->parseOptions)) {
|
|
|
1787 |
foreach($this->parseOptions as $key => $val) {
|
|
|
1788 |
xml_parser_set_option($parser, $key, $val);
|
|
|
1789 |
}
|
|
|
1790 |
}
|
|
|
1791 |
|
|
|
1792 |
// Set the object and the element handlers for the XML parser.
|
|
|
1793 |
xml_set_object($parser, $this);
|
|
|
1794 |
xml_set_element_handler($parser, '_handleStartElement', '_handleEndElement');
|
|
|
1795 |
xml_set_character_data_handler($parser, '_handleCharacterData');
|
|
|
1796 |
xml_set_default_handler($parser, '_handleDefaultData');
|
|
|
1797 |
xml_set_processing_instruction_handler($parser, '_handlePI');
|
|
|
1798 |
|
|
|
1799 |
// Parse the XML source and on error generate an error message.
|
|
|
1800 |
if (!xml_parse($parser, $xmlString, TRUE)) {
|
|
|
1801 |
$source = empty($this->properties['xmlFile']) ? 'string' : 'file ' . basename($this->properties['xmlFile']) . "'";
|
|
|
1802 |
$errStr = "XML error in given {$source} on line ".
|
|
|
1803 |
xml_get_current_line_number($parser). ' column '. xml_get_current_column_number($parser) .
|
|
|
1804 |
'. Reason:'. xml_error_string(xml_get_error_code($parser));
|
|
|
1805 |
break; // try-block
|
|
|
1806 |
}
|
|
|
1807 |
|
|
|
1808 |
// Free the parser.
|
|
|
1809 |
@xml_parser_free($parser);
|
|
|
1810 |
// And we don't need this any more.
|
|
|
1811 |
$this->nodeStack = array();
|
|
|
1812 |
|
|
|
1813 |
$this->reindexNodeTree();
|
|
|
1814 |
|
|
|
1815 |
if ($bDebugThisFunction) {
|
|
|
1816 |
print_r(array_keys($this->nodeIndex));
|
|
|
1817 |
}
|
|
|
1818 |
|
|
|
1819 |
$status = TRUE;
|
|
|
1820 |
} while (FALSE);
|
|
|
1821 |
|
|
|
1822 |
if (!$status) {
|
|
|
1823 |
$this->_displayError('In importFromString(): '. $errStr, __LINE__, __FILE__, FALSE);
|
|
|
1824 |
$bResult = FALSE;
|
|
|
1825 |
} else {
|
|
|
1826 |
$bResult = TRUE;
|
|
|
1827 |
}
|
|
|
1828 |
|
|
|
1829 |
////////////////////////////////////////////
|
|
|
1830 |
|
|
|
1831 |
$this->_closeDebugFunction($ThisFunctionName, $bResult, $bDebugThisFunction);
|
|
|
1832 |
|
|
|
1833 |
return $bResult;
|
|
|
1834 |
}
|
|
|
1835 |
|
|
|
1836 |
|
|
|
1837 |
//-----------------------------------------------------------------------------------------
|
|
|
1838 |
// XPathEngine ------ XML Handlers ------
|
|
|
1839 |
//-----------------------------------------------------------------------------------------
|
|
|
1840 |
|
|
|
1841 |
/**
|
|
|
1842 |
* Handles opening XML tags while parsing.
|
|
|
1843 |
*
|
|
|
1844 |
* While parsing a XML document for each opening tag this method is
|
|
|
1845 |
* called. It'll add the tag found to the tree of document nodes.
|
|
|
1846 |
*
|
|
|
1847 |
* @param $parser (int) Handler for accessing the current XML parser.
|
|
|
1848 |
* @param $name (string) Name of the opening tag found in the document.
|
|
|
1849 |
* @param $attributes (array) Associative array containing a list of
|
|
|
1850 |
* all attributes of the tag found in the document.
|
|
|
1851 |
* @see _handleEndElement(), _handleCharacterData()
|
|
|
1852 |
*/
|
|
|
1853 |
function _handleStartElement($parser, $nodeName, $attributes) {
|
|
|
1854 |
if (empty($nodeName)) {
|
|
|
1855 |
$this->_displayError('XML error in file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
|
|
|
1856 |
return;
|
|
|
1857 |
}
|
|
|
1858 |
|
|
|
1859 |
// Trim accumulated text if necessary.
|
|
|
1860 |
if ($this->parseSkipWhiteCache) {
|
|
|
1861 |
$iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
|
|
|
1862 |
$this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
|
|
|
1863 |
}
|
|
|
1864 |
|
|
|
1865 |
if ($this->bDebugXmlParse) {
|
|
|
1866 |
echo "<blockquote>" . htmlspecialchars("Start node: <".$nodeName . ">")."<br>";
|
|
|
1867 |
echo "Appended to stack entry: $this->parseStackIndex<br>\n";
|
|
|
1868 |
echo "Text part before element is: ".htmlspecialchars($this->parsedTextLocation);
|
|
|
1869 |
/*
|
|
|
1870 |
echo "<pre>";
|
|
|
1871 |
$dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
|
|
|
1872 |
for ($i = 0; $i < $dataPartsCount; $i++) {
|
|
|
1873 |
echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
|
|
|
1874 |
}
|
|
|
1875 |
echo "</pre>";
|
|
|
1876 |
*/
|
|
|
1877 |
}
|
|
|
1878 |
|
|
|
1879 |
// Add a node and set path to current.
|
|
|
1880 |
if (!$this->_internalAppendChild($this->parseStackIndex, $nodeName)) {
|
|
|
1881 |
$this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
|
|
|
1882 |
return;
|
|
|
1883 |
}
|
|
|
1884 |
|
|
|
1885 |
// We will have gone one deeper then in the stack.
|
|
|
1886 |
$this->parseStackIndex++;
|
|
|
1887 |
|
|
|
1888 |
// Point our parseTxtBuffer reference at the new node.
|
|
|
1889 |
$this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][0];
|
|
|
1890 |
|
|
|
1891 |
// Set the attributes.
|
|
|
1892 |
if (!empty($attributes)) {
|
|
|
1893 |
if ($this->bDebugXmlParse) {
|
|
|
1894 |
echo 'Attributes: <br>';
|
|
|
1895 |
print_r($attributes);
|
|
|
1896 |
echo '<br>';
|
|
|
1897 |
}
|
|
|
1898 |
$this->nodeStack[$this->parseStackIndex]['attributes'] = $attributes;
|
|
|
1899 |
}
|
|
|
1900 |
}
|
|
|
1901 |
|
|
|
1902 |
/**
|
|
|
1903 |
* Handles closing XML tags while parsing.
|
|
|
1904 |
*
|
|
|
1905 |
* While parsing a XML document for each closing tag this method is called.
|
|
|
1906 |
*
|
|
|
1907 |
* @param $parser (int) Handler for accessing the current XML parser.
|
|
|
1908 |
* @param $name (string) Name of the closing tag found in the document.
|
|
|
1909 |
* @see _handleStartElement(), _handleCharacterData()
|
|
|
1910 |
*/
|
|
|
1911 |
function _handleEndElement($parser, $name) {
|
|
|
1912 |
if (($this->parsedTextLocation=='')
|
|
|
1913 |
&& empty($this->nodeStack[$this->parseStackIndex]['textParts'])) {
|
|
|
1914 |
// We reach this point when parsing a tag of format <foo/>. The 'textParts'-array
|
|
|
1915 |
// should stay empty and not have an empty string in it.
|
|
|
1916 |
} else {
|
|
|
1917 |
// Trim accumulated text if necessary.
|
|
|
1918 |
if ($this->parseSkipWhiteCache) {
|
|
|
1919 |
$iCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
|
|
|
1920 |
$this->nodeStack[$this->parseStackIndex]['textParts'][$iCount-1] = rtrim($this->parsedTextLocation);
|
|
|
1921 |
}
|
|
|
1922 |
}
|
|
|
1923 |
|
|
|
1924 |
if ($this->bDebugXmlParse) {
|
|
|
1925 |
echo "Text part after element is: ".htmlspecialchars($this->parsedTextLocation)."<br>\n";
|
|
|
1926 |
echo htmlspecialchars("Parent:<{$this->parseStackIndex}>, End-node:</$name> '".$this->parsedTextLocation) . "'<br>Text nodes:<pre>\n";
|
|
|
1927 |
$dataPartsCount = count($this->nodeStack[$this->parseStackIndex]['textParts']);
|
|
|
1928 |
for ($i = 0; $i < $dataPartsCount; $i++) {
|
|
|
1929 |
echo "$i:". htmlspecialchars($this->nodeStack[$this->parseStackIndex]['textParts'][$i])."\n";
|
|
|
1930 |
}
|
|
|
1931 |
var_dump($this->nodeStack[$this->parseStackIndex]['textParts']);
|
|
|
1932 |
echo "</pre></blockquote>\n";
|
|
|
1933 |
}
|
|
|
1934 |
|
|
|
1935 |
// Jump back to the parent element.
|
|
|
1936 |
$this->parseStackIndex--;
|
|
|
1937 |
|
|
|
1938 |
// Set our reference for where we put any more whitespace
|
|
|
1939 |
$this->parsedTextLocation =& $this->nodeStack[$this->parseStackIndex]['textParts'][];
|
|
|
1940 |
|
|
|
1941 |
// Note we leave the entry in the stack, as it will get blanked over by the next element
|
|
|
1942 |
// at this level. The safe thing to do would be to remove it too, but in the interests
|
|
|
1943 |
// of performance, we will not bother, as were it to be a problem, then it would be an
|
|
|
1944 |
// internal bug anyway.
|
|
|
1945 |
if ($this->parseStackIndex < 0) {
|
|
|
1946 |
$this->_displayError('Internal error during parse of XML file at line'. xml_get_current_line_number($parser) .'. Empty name.', __LINE__, __FILE__);
|
|
|
1947 |
return;
|
|
|
1948 |
}
|
|
|
1949 |
}
|
|
|
1950 |
|
|
|
1951 |
/**
|
|
|
1952 |
* Handles character data while parsing.
|
|
|
1953 |
*
|
|
|
1954 |
* While parsing a XML document for each character data this method
|
|
|
1955 |
* is called. It'll add the character data to the document tree.
|
|
|
1956 |
*
|
|
|
1957 |
* @param $parser (int) Handler for accessing the current XML parser.
|
|
|
1958 |
* @param $text (string) Character data found in the document.
|
|
|
1959 |
* @see _handleStartElement(), _handleEndElement()
|
|
|
1960 |
*/
|
|
|
1961 |
function _handleCharacterData($parser, $text) {
|
|
|
1962 |
|
|
|
1963 |
if ($this->parsInCData >0) $text = $this->_translateAmpersand($text, $reverse=TRUE);
|
|
|
1964 |
|
|
|
1965 |
if ($this->bDebugXmlParse) echo "Handling character data: '".htmlspecialchars($text)."'<br>";
|
|
|
1966 |
if ($this->parseSkipWhiteCache AND !empty($text) AND !$this->parsInCData) {
|
|
|
1967 |
// Special case CR. CR always comes in a separate data. Trans. it to '' or ' '.
|
|
|
1968 |
// If txtBuffer is already ending with a space use '' otherwise ' '.
|
|
|
1969 |
$bufferHasEndingSpace = (empty($this->parsedTextLocation) OR substr($this->parsedTextLocation, -1) === ' ') ? TRUE : FALSE;
|
|
|
1970 |
if ($text=="\n") {
|
|
|
1971 |
$text = $bufferHasEndingSpace ? '' : ' ';
|
|
|
1972 |
} else {
|
|
|
1973 |
if ($bufferHasEndingSpace) {
|
|
|
1974 |
$text = ltrim(preg_replace('/\s+/', ' ', $text));
|
|
|
1975 |
} else {
|
|
|
1976 |
$text = preg_replace('/\s+/', ' ', $text);
|
|
|
1977 |
}
|
|
|
1978 |
}
|
|
|
1979 |
if ($this->bDebugXmlParse) echo "'Skip white space' is ON. reduced to : '" .htmlspecialchars($text) . "'<br>";
|
|
|
1980 |
}
|
|
|
1981 |
$this->parsedTextLocation .= $text;
|
|
|
1982 |
}
|
|
|
1983 |
|
|
|
1984 |
/**
|
|
|
1985 |
* Default handler for the XML parser.
|
|
|
1986 |
*
|
|
|
1987 |
* While parsing a XML document for string not caught by one of the other
|
|
|
1988 |
* handler functions, we end up here.
|
|
|
1989 |
*
|
|
|
1990 |
* @param $parser (int) Handler for accessing the current XML parser.
|
|
|
1991 |
* @param $text (string) Character data found in the document.
|
|
|
1992 |
* @see _handleStartElement(), _handleEndElement()
|
|
|
1993 |
*/
|
|
|
1994 |
function _handleDefaultData($parser, $text) {
|
|
|
1995 |
do { // try-block
|
|
|
1996 |
if (!strcmp($text, '<![CDATA[')) {
|
|
|
1997 |
$this->parsInCData++;
|
|
|
1998 |
} elseif (!strcmp($text, ']]>')) {
|
|
|
1999 |
$this->parsInCData--;
|
|
|
2000 |
if ($this->parsInCData < 0) $this->parsInCData = 0;
|
|
|
2001 |
}
|
|
|
2002 |
$this->parsedTextLocation .= $this->_translateAmpersand($text, $reverse=TRUE);
|
|
|
2003 |
if ($this->bDebugXmlParse) echo "Default handler data: ".htmlspecialchars($text)."<br>";
|
|
|
2004 |
break; // try-block
|
|
|
2005 |
} while (FALSE); // END try-block
|
|
|
2006 |
}
|
|
|
2007 |
|
|
|
2008 |
/**
|
|
|
2009 |
* Handles processing instruction (PI)
|
|
|
2010 |
*
|
|
|
2011 |
* A processing instruction has the following format:
|
|
|
2012 |
* <? target data ? > e.g. <? dtd version="1.0" ? >
|
|
|
2013 |
*
|
|
|
2014 |
* Currently I have no bether idea as to left it 'as is' and treat the PI data as normal
|
|
|
2015 |
* text (and adding the surrounding PI-tags <? ? >).
|
|
|
2016 |
*
|
|
|
2017 |
* @param $parser (int) Handler for accessing the current XML parser.
|
|
|
2018 |
* @param $target (string) Name of the PI target. E.g. XML, PHP, DTD, ...
|
|
|
2019 |
* @param $data (string) Associative array containing a list of
|
|
|
2020 |
* @see PHP's manual "xml_set_processing_instruction_handler"
|
|
|
2021 |
*/
|
|
|
2022 |
function _handlePI($parser, $target, $data) {
|
|
|
2023 |
//echo("pi data=".$data."end"); exit;
|
|
|
2024 |
$data = $this->_translateAmpersand($data, $reverse=TRUE);
|
|
|
2025 |
$this->parsedTextLocation .= "<?{$target} {$data}?>";
|
|
|
2026 |
return TRUE;
|
|
|
2027 |
}
|
|
|
2028 |
|
|
|
2029 |
//-----------------------------------------------------------------------------------------
|
|
|
2030 |
// XPathEngine ------ Node Tree Stuff ------
|
|
|
2031 |
//-----------------------------------------------------------------------------------------
|
|
|
2032 |
|
|
|
2033 |
/**
|
|
|
2034 |
* Creates a super root node.
|
|
|
2035 |
*/
|
|
|
2036 |
function _createSuperRoot() {
|
|
|
2037 |
// Build a 'super-root'
|
|
|
2038 |
$this->nodeRoot = $this->emptyNode;
|
|
|
2039 |
$this->nodeRoot['name'] = '';
|
|
|
2040 |
$this->nodeRoot['parentNode'] = NULL;
|
|
|
2041 |
$this->nodeIndex[''] =& $this->nodeRoot;
|
|
|
2042 |
}
|
|
|
2043 |
|
|
|
2044 |
/**
|
|
|
2045 |
* Adds a new node to the XML document tree during xml parsing.
|
|
|
2046 |
*
|
|
|
2047 |
* This method adds a new node to the tree of nodes of the XML document
|
|
|
2048 |
* being handled by this class. The new node is created according to the
|
|
|
2049 |
* parameters passed to this method. This method is a much watered down
|
|
|
2050 |
* version of appendChild(), used in parsing an xml file only.
|
|
|
2051 |
*
|
|
|
2052 |
* It is assumed that adding starts with root and progresses through the
|
|
|
2053 |
* document in parse order. New nodes must have a corresponding parent. And
|
|
|
2054 |
* once we have read the </> tag for the element we will never need to add
|
|
|
2055 |
* any more data to that node. Otherwise the add will be ignored or fail.
|
|
|
2056 |
*
|
|
|
2057 |
* The function is faciliated by a nodeStack, which is an array of nodes that
|
|
|
2058 |
* we have yet to close.
|
|
|
2059 |
*
|
|
|
2060 |
* @param $stackParentIndex (int) The index into the nodeStack[] of the parent
|
|
|
2061 |
* node to which the new node should be added as
|
|
|
2062 |
* a child. *READONLY*
|
|
|
2063 |
* @param $nodeName (string) Name of the new node. *READONLY*
|
|
|
2064 |
* @return (bool) TRUE if we successfully added a new child to
|
|
|
2065 |
* the node stack at index $stackParentIndex + 1,
|
|
|
2066 |
* FALSE on error.
|
|
|
2067 |
*/
|
|
|
2068 |
function _internalAppendChild($stackParentIndex, $nodeName) {
|
|
|
2069 |
// This call is likely to be executed thousands of times, so every 0.01ms counts.
|
|
|
2070 |
// If you want to debug this function, you'll have to comment the stuff back in
|
|
|
2071 |
//$bDebugThisFunction = FALSE;
|
|
|
2072 |
|
|
|
2073 |
/*
|
|
|
2074 |
$ThisFunctionName = '_internalAppendChild';
|
|
|
2075 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
2076 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
2077 |
if ($bDebugThisFunction) {
|
|
|
2078 |
echo "Current Node (parent-index) and the child to append : '{$stackParentIndex}' + '{$nodeName}' \n<br>";
|
|
|
2079 |
}
|
|
|
2080 |
*/
|
|
|
2081 |
//////////////////////////////////////
|
|
|
2082 |
|
|
|
2083 |
if (!isSet($this->nodeStack[$stackParentIndex])) {
|
|
|
2084 |
$errStr = "Invalid parent. You tried to append the tag '{$nodeName}' to an non-existing parent in our node stack '{$stackParentIndex}'.";
|
|
|
2085 |
$this->_displayError('In _internalAppendChild(): '. $errStr, __LINE__, __FILE__, FALSE);
|
|
|
2086 |
|
|
|
2087 |
/*
|
|
|
2088 |
$this->_closeDebugFunction($ThisFunctionName, FALSE, $bDebugThisFunction);
|
|
|
2089 |
*/
|
|
|
2090 |
|
|
|
2091 |
return FALSE;
|
|
|
2092 |
}
|
|
|
2093 |
|
|
|
2094 |
// Retrieve the parent node from the node stack. This is the last node at that
|
|
|
2095 |
// depth that we have yet to close. This is where we should add the text/node.
|
|
|
2096 |
$parentNode =& $this->nodeStack[$stackParentIndex];
|
|
|
2097 |
|
|
|
2098 |
// Brand new node please
|
|
|
2099 |
$newChildNode = $this->emptyNode;
|
|
|
2100 |
|
|
|
2101 |
// Save the vital information about the node.
|
|
|
2102 |
$newChildNode['name'] = $nodeName;
|
|
|
2103 |
$parentNode['childNodes'][] =& $newChildNode;
|
|
|
2104 |
|
|
|
2105 |
// Add to our node stack
|
|
|
2106 |
$this->nodeStack[$stackParentIndex + 1] =& $newChildNode;
|
|
|
2107 |
|
|
|
2108 |
/*
|
|
|
2109 |
if ($bDebugThisFunction) {
|
|
|
2110 |
echo "The new node received index: '".($stackParentIndex + 1)."'\n";
|
|
|
2111 |
foreach($this->nodeStack as $key => $val) echo "$key => ".$val['name']."\n";
|
|
|
2112 |
}
|
|
|
2113 |
$this->_closeDebugFunction($ThisFunctionName, TRUE, $bDebugThisFunction);
|
|
|
2114 |
*/
|
|
|
2115 |
|
|
|
2116 |
return TRUE;
|
|
|
2117 |
}
|
|
|
2118 |
|
|
|
2119 |
/**
|
|
|
2120 |
* Update nodeIndex and every node of the node-tree.
|
|
|
2121 |
*
|
|
|
2122 |
* Call after you have finished any tree modifications other wise a match with
|
|
|
2123 |
* an xPathQuery will produce wrong results. The $this->nodeIndex[] is recreated
|
|
|
2124 |
* and every nodes optimization data is updated. The optimization data is all the
|
|
|
2125 |
* data that is duplicate information, would just take longer to find. Child nodes
|
|
|
2126 |
* with value NULL are removed from the tree.
|
|
|
2127 |
*
|
|
|
2128 |
* By default the modification functions in this component will automatically re-index
|
|
|
2129 |
* the nodes in the tree. Sometimes this is not the behaver you want. To surpress the
|
|
|
2130 |
* reindex, set the functions $autoReindex to FALSE and call reindexNodeTree() at the
|
|
|
2131 |
* end of your changes. This sometimes leads to better code (and less CPU overhead).
|
|
|
2132 |
*
|
|
|
2133 |
* Sample:
|
|
|
2134 |
* =======
|
|
|
2135 |
* Given the xml is <AAA><B/>.<B/>.<B/></AAA> | Goal is <AAA>.<B/>.</AAA> (Delete B[1] and B[3])
|
|
|
2136 |
* $xPathSet = $xPath->match('//B'); # Will result in array('/AAA[1]/B[1]', '/AAA[1]/B[2]', '/AAA[1]/B[3]');
|
|
|
2137 |
* Three ways to do it.
|
|
|
2138 |
* 1) Top-Down (with auto reindexing) - Safe, Slow and you get easily mix up with the the changing node index
|
|
|
2139 |
* removeChild('/AAA[1]/B[1]'); // B[1] removed, thus all B[n] become B[n-1] !!
|
|
|
2140 |
* removeChild('/AAA[1]/B[2]'); // Now remove B[2] (That originaly was B[3])
|
|
|
2141 |
* 2) Bottom-Up (with auto reindexing) - Safe, Slow and the changing node index (caused by auto-reindex) can be ignored.
|
|
|
2142 |
* for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
|
|
|
2143 |
* if ($i==1) continue;
|
|
|
2144 |
* removeChild($xPathSet[$i]);
|
|
|
2145 |
* }
|
|
|
2146 |
* 3) // Top-down (with *NO* auto reindexing) - Fast, Safe as long as you call reindexNodeTree()
|
|
|
2147 |
* foreach($xPathSet as $xPath) {
|
|
|
2148 |
* // Specify no reindexing
|
|
|
2149 |
* if ($xPath == $xPathSet[1]) continue;
|
|
|
2150 |
* removeChild($xPath, $autoReindex=FALSE);
|
|
|
2151 |
* // The object is now in a slightly inconsistent state.
|
|
|
2152 |
* }
|
|
|
2153 |
* // Finally do the reindex and the object is consistent again
|
|
|
2154 |
* reindexNodeTree();
|
|
|
2155 |
*
|
|
|
2156 |
* @return (bool) TRUE on success, FALSE otherwise.
|
|
|
2157 |
* @see _recursiveReindexNodeTree()
|
|
|
2158 |
*/
|
|
|
2159 |
function reindexNodeTree() {
|
|
|
2160 |
//return;
|
|
|
2161 |
$this->_indexIsDirty = FALSE;
|
|
|
2162 |
$this->nodeIndex = array();
|
|
|
2163 |
$this->nodeIndex[''] =& $this->nodeRoot;
|
|
|
2164 |
// Quick out for when the tree has no data.
|
|
|
2165 |
if (empty($this->nodeRoot)) return TRUE;
|
|
|
2166 |
return $this->_recursiveReindexNodeTree('');
|
|
|
2167 |
}
|
|
|
2168 |
|
|
|
2169 |
|
|
|
2170 |
/**
|
|
|
2171 |
* Create the ids that are accessable through the generate-id() function
|
|
|
2172 |
*/
|
|
|
2173 |
function _generate_ids() {
|
|
|
2174 |
// If we have generated them already, then bail.
|
|
|
2175 |
if (isset($this->nodeIndex['']['generate_id'])) return;
|
|
|
2176 |
|
|
|
2177 |
// keys generated are the string 'id0' . hexatridecimal-based (0..9,a-z) index
|
|
|
2178 |
$aNodeIndexes = array_keys($this->nodeIndex);
|
|
|
2179 |
$idNumber = 0;
|
|
|
2180 |
foreach($aNodeIndexes as $index => $key) {
|
|
|
2181 |
// $this->nodeIndex[$key]['generated_id'] = 'id' . base_convert($index,10,36);
|
|
|
2182 |
// Skip attribute and text nodes.
|
|
|
2183 |
// ### Currently don't support attribute and text nodes.
|
|
|
2184 |
if (strstr($key, 'text()') !== FALSE) continue;
|
|
|
2185 |
if (strstr($key, 'attribute::') !== FALSE) continue;
|
|
|
2186 |
$this->nodeIndex[$key]['generated_id'] = 'idPhpXPath' . $idNumber;
|
|
|
2187 |
|
|
|
2188 |
// Make the id's sequential so that we can test predictively.
|
|
|
2189 |
$idNumber++;
|
|
|
2190 |
}
|
|
|
2191 |
}
|
|
|
2192 |
|
|
|
2193 |
/**
|
|
|
2194 |
* Here's where the work is done for reindexing (see reindexNodeTree)
|
|
|
2195 |
*
|
|
|
2196 |
* @param $absoluteParentPath (string) the xPath to the parent node
|
|
|
2197 |
* @return (bool) TRUE on success, FALSE otherwise.
|
|
|
2198 |
* @see reindexNodeTree()
|
|
|
2199 |
*/
|
|
|
2200 |
function _recursiveReindexNodeTree($absoluteParentPath) {
|
|
|
2201 |
$parentNode =& $this->nodeIndex[$absoluteParentPath];
|
|
|
2202 |
|
|
|
2203 |
// Check for any 'dead' child nodes first and concate the text parts if found.
|
|
|
2204 |
for ($iChildIndex=sizeOf($parentNode['childNodes'])-1; $iChildIndex>=0; $iChildIndex--) {
|
|
|
2205 |
// Check if the child node still exits (it may have been removed).
|
|
|
2206 |
if (!empty($parentNode['childNodes'][$iChildIndex])) continue;
|
|
|
2207 |
// Child node was removed. We got to merge the text parts then.
|
|
|
2208 |
$parentNode['textParts'][$iChildIndex] .= $parentNode['textParts'][$iChildIndex+1];
|
|
|
2209 |
array_splice($parentNode['textParts'], $iChildIndex+1, 1);
|
|
|
2210 |
array_splice($parentNode['childNodes'], $iChildIndex, 1);
|
|
|
2211 |
}
|
|
|
2212 |
|
|
|
2213 |
// Now start a reindex.
|
|
|
2214 |
$contextHash = array();
|
|
|
2215 |
$childSize = sizeOf($parentNode['childNodes']);
|
|
|
2216 |
|
|
|
2217 |
// If there are no children, we have to treat this specially:
|
|
|
2218 |
if ($childSize == 0) {
|
|
|
2219 |
// Add a dummy text node.
|
|
|
2220 |
$this->nodeIndex[$absoluteParentPath.'/text()[1]'] =& $parentNode;
|
|
|
2221 |
} else {
|
|
|
2222 |
for ($iChildIndex=0; $iChildIndex<$childSize; $iChildIndex++) {
|
|
|
2223 |
$childNode =& $parentNode['childNodes'][$iChildIndex];
|
|
|
2224 |
// Make sure that there is a text-part in front of every node. (May be empty)
|
|
|
2225 |
if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = '';
|
|
|
2226 |
// Count the nodes with same name (to determine their context position)
|
|
|
2227 |
$childName = $childNode['name'];
|
|
|
2228 |
if (empty($contextHash[$childName])) {
|
|
|
2229 |
$contextPos = $contextHash[$childName] = 1;
|
|
|
2230 |
} else {
|
|
|
2231 |
$contextPos = ++$contextHash[$childName];
|
|
|
2232 |
}
|
|
|
2233 |
// Make the node-index hash
|
|
|
2234 |
$newPath = $absoluteParentPath . '/' . $childName . '['.$contextPos.']';
|
|
|
2235 |
|
|
|
2236 |
// ### Note ultimately we will end up supporting text nodes as actual nodes.
|
|
|
2237 |
|
|
|
2238 |
// Preceed with a dummy entry for the text node.
|
|
|
2239 |
$this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+1).']'] =& $childNode;
|
|
|
2240 |
// Then the node itself
|
|
|
2241 |
$this->nodeIndex[$newPath] =& $childNode;
|
|
|
2242 |
|
|
|
2243 |
// Now some dummy nodes for each of the attribute nodes.
|
|
|
2244 |
$iAttributeCount = sizeOf($childNode['attributes']);
|
|
|
2245 |
if ($iAttributeCount > 0) {
|
|
|
2246 |
$aAttributesNames = array_keys($childNode['attributes']);
|
|
|
2247 |
for ($iAttributeIndex = 0; $iAttributeIndex < $iAttributeCount; $iAttributeIndex++) {
|
|
|
2248 |
$attribute = $aAttributesNames[$iAttributeIndex];
|
|
|
2249 |
$newAttributeNode = $this->emptyNode;
|
|
|
2250 |
$newAttributeNode['name'] = $attribute;
|
|
|
2251 |
$newAttributeNode['textParts'] = array($childNode['attributes'][$attribute]);
|
|
|
2252 |
$newAttributeNode['contextPos'] = $iAttributeIndex;
|
|
|
2253 |
$newAttributeNode['xpath'] = "$newPath/attribute::$attribute";
|
|
|
2254 |
$newAttributeNode['parentNode'] =& $childNode;
|
|
|
2255 |
$newAttributeNode['depth'] =& $parentNode['depth'] + 2;
|
|
|
2256 |
// Insert the node as a master node, not a reference, otherwise there will be
|
|
|
2257 |
// variable "bleeding".
|
|
|
2258 |
$this->nodeIndex["$newPath/attribute::$attribute"] = $newAttributeNode;
|
|
|
2259 |
}
|
|
|
2260 |
}
|
|
|
2261 |
|
|
|
2262 |
// Update the node info (optimisation)
|
|
|
2263 |
$childNode['parentNode'] =& $parentNode;
|
|
|
2264 |
$childNode['depth'] = $parentNode['depth'] + 1;
|
|
|
2265 |
$childNode['pos'] = $iChildIndex;
|
|
|
2266 |
$childNode['contextPos'] = $contextHash[$childName];
|
|
|
2267 |
$childNode['xpath'] = $newPath;
|
|
|
2268 |
$this->_recursiveReindexNodeTree($newPath);
|
|
|
2269 |
|
|
|
2270 |
// Follow with a dummy entry for the text node.
|
|
|
2271 |
$this->nodeIndex[$absoluteParentPath.'/text()['.($childNode['pos']+2).']'] =& $childNode;
|
|
|
2272 |
}
|
|
|
2273 |
|
|
|
2274 |
// Make sure that their is a text-part after the last node.
|
|
|
2275 |
if (!isSet($parentNode['textParts'][$iChildIndex])) $parentNode['textParts'][$iChildIndex] = '';
|
|
|
2276 |
}
|
|
|
2277 |
|
|
|
2278 |
return TRUE;
|
|
|
2279 |
}
|
|
|
2280 |
|
|
|
2281 |
/**
|
|
|
2282 |
* Clone a node and it's child nodes.
|
|
|
2283 |
*
|
|
|
2284 |
* NOTE: If the node has children you *MUST* use the reference operator!
|
|
|
2285 |
* E.g. $clonedNode =& cloneNode($node);
|
|
|
2286 |
* Otherwise the children will not point back to the parent, they will point
|
|
|
2287 |
* back to your temporary variable instead.
|
|
|
2288 |
*
|
|
|
2289 |
* @param $node (mixed) Either a node (hash array) or an abs. Xpath to a node in
|
|
|
2290 |
* the current doc
|
|
|
2291 |
* @return (&array) A node and it's child nodes.
|
|
|
2292 |
*/
|
|
|
2293 |
function &cloneNode($node, $recursive=FALSE) {
|
|
|
2294 |
if (is_string($node) AND isSet($this->nodeIndex[$node])) {
|
|
|
2295 |
$node = $this->nodeIndex[$node];
|
|
|
2296 |
}
|
|
|
2297 |
// Copy the text-parts ()
|
|
|
2298 |
$textParts = $node['textParts'];
|
|
|
2299 |
$node['textParts'] = array();
|
|
|
2300 |
foreach ($textParts as $key => $val) {
|
|
|
2301 |
$node['textParts'][] = $val;
|
|
|
2302 |
}
|
|
|
2303 |
|
|
|
2304 |
$childSize = sizeOf($node['childNodes']);
|
|
|
2305 |
for ($i=0; $i<$childSize; $i++) {
|
|
|
2306 |
$childNode =& $this->cloneNode($node['childNodes'][$i], TRUE); // copy child
|
|
|
2307 |
$node['childNodes'][$i] =& $childNode; // reference the copy
|
|
|
2308 |
$childNode['parentNode'] =& $node; // child references the parent.
|
|
|
2309 |
}
|
|
|
2310 |
|
|
|
2311 |
if (!$recursive) {
|
|
|
2312 |
//$node['childNodes'][0]['parentNode'] = null;
|
|
|
2313 |
//print "<pre>";
|
|
|
2314 |
//var_dump($node);
|
|
|
2315 |
}
|
|
|
2316 |
return $node;
|
|
|
2317 |
}
|
|
|
2318 |
|
|
|
2319 |
|
|
|
2320 |
/** Nice to have but __sleep() has a bug.
|
|
|
2321 |
(2002-2 PHP V4.1. See bug #15350)
|
|
|
2322 |
|
|
|
2323 |
/**
|
|
|
2324 |
* PHP cals this function when you call PHP's serialize.
|
|
|
2325 |
*
|
|
|
2326 |
* It prevents cyclic referencing, which is why print_r() of an XPath object doesn't work.
|
|
|
2327 |
*
|
|
|
2328 |
function __sleep() {
|
|
|
2329 |
// Destroy recursive pointers
|
|
|
2330 |
$keys = array_keys($this->nodeIndex);
|
|
|
2331 |
$size = sizeOf($keys);
|
|
|
2332 |
for ($i=0; $i<$size; $i++) {
|
|
|
2333 |
unset($this->nodeIndex[$keys[$i]]['parentNode']);
|
|
|
2334 |
}
|
|
|
2335 |
unset($this->nodeIndex);
|
|
|
2336 |
}
|
|
|
2337 |
|
|
|
2338 |
/**
|
|
|
2339 |
* PHP cals this function when you call PHP's unserialize.
|
|
|
2340 |
*
|
|
|
2341 |
* It reindexes the node-tree
|
|
|
2342 |
*
|
|
|
2343 |
function __wakeup() {
|
|
|
2344 |
$this->reindexNodeTree();
|
|
|
2345 |
}
|
|
|
2346 |
|
|
|
2347 |
*/
|
|
|
2348 |
|
|
|
2349 |
//-----------------------------------------------------------------------------------------
|
|
|
2350 |
// XPath ------ XPath Query / Evaluation Handlers ------
|
|
|
2351 |
//-----------------------------------------------------------------------------------------
|
|
|
2352 |
|
|
|
2353 |
/**
|
|
|
2354 |
* Matches (evaluates) an XPath query
|
|
|
2355 |
*
|
|
|
2356 |
* This method tries to evaluate an XPath query by parsing it. A XML source must
|
|
|
2357 |
* have been imported before this method is able to work.
|
|
|
2358 |
*
|
|
|
2359 |
* @param $xPathQuery (string) XPath query to be evaluated.
|
|
|
2360 |
* @param $baseXPath (string) (default is super-root) XPath query to a single document node,
|
|
|
2361 |
* from which the XPath query should start evaluating.
|
|
|
2362 |
* @return (mixed) The result of the XPath expression. Either:
|
|
|
2363 |
* node-set (an ordered collection of absolute references to nodes without duplicates)
|
|
|
2364 |
* boolean (true or false)
|
|
|
2365 |
* number (a floating-point number)
|
|
|
2366 |
* string (a sequence of UCS characters)
|
|
|
2367 |
*/
|
|
|
2368 |
function match($xPathQuery, $baseXPath='') {
|
|
|
2369 |
if ($this->_indexIsDirty) $this->reindexNodeTree();
|
|
|
2370 |
|
|
|
2371 |
// Replace a double slashes, because they'll cause problems otherwise.
|
|
|
2372 |
static $slashes2descendant = array(
|
|
|
2373 |
'//@' => '/descendant_or_self::*/attribute::',
|
|
|
2374 |
'//' => '/descendant_or_self::node()/',
|
|
|
2375 |
'/@' => '/attribute::');
|
|
|
2376 |
// Stupid idea from W3C to take axes name containing a '-' (dash) !!!
|
|
|
2377 |
// We replace the '-' with '_' to avoid the conflict with the minus operator.
|
|
|
2378 |
static $dash2underscoreHash = array(
|
|
|
2379 |
'-sibling' => '_sibling',
|
|
|
2380 |
'-or-' => '_or_',
|
|
|
2381 |
'starts-with' => 'starts_with',
|
|
|
2382 |
'substring-before' => 'substring_before',
|
|
|
2383 |
'substring-after' => 'substring_after',
|
|
|
2384 |
'string-length' => 'string_length',
|
|
|
2385 |
'normalize-space' => 'normalize_space',
|
|
|
2386 |
'x-lower' => 'x_lower',
|
|
|
2387 |
'x-upper' => 'x_upper',
|
|
|
2388 |
'generate-id' => 'generate_id');
|
|
|
2389 |
|
|
|
2390 |
if (empty($xPathQuery)) return array();
|
|
|
2391 |
|
|
|
2392 |
// Special case for when document is empty.
|
|
|
2393 |
if (empty($this->nodeRoot)) return array();
|
|
|
2394 |
|
|
|
2395 |
if (!isSet($this->nodeIndex[$baseXPath])) {
|
|
|
2396 |
$xPathSet = $this->_resolveXPathQuery($baseXPath,'match');
|
|
|
2397 |
if (sizeOf($xPathSet) !== 1) {
|
|
|
2398 |
$this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
|
|
|
2399 |
return FALSE;
|
|
|
2400 |
}
|
|
|
2401 |
$baseXPath = $xPathSet[0];
|
|
|
2402 |
}
|
|
|
2403 |
|
|
|
2404 |
// We should possibly do a proper syntactical parse, but instead we will cheat and just
|
|
|
2405 |
// remove any literals that could make things very difficult for us, and replace them with
|
|
|
2406 |
// special tags. Then we can treat the xPathQuery much more easily as JUST "syntax". Provided
|
|
|
2407 |
// there are no literals in the string, then we can guarentee that most of the operators and
|
|
|
2408 |
// syntactical elements are indeed elements and not just part of a literal string.
|
|
|
2409 |
$processedxPathQuery = $this->_removeLiterals($xPathQuery);
|
|
|
2410 |
|
|
|
2411 |
// Replace a double slashes, and '-' (dash) in axes names.
|
|
|
2412 |
$processedxPathQuery = strtr($processedxPathQuery, $slashes2descendant);
|
|
|
2413 |
$processedxPathQuery = strtr($processedxPathQuery, $dash2underscoreHash);
|
|
|
2414 |
|
|
|
2415 |
// Build the context
|
|
|
2416 |
$context = array('nodePath' => $baseXPath, 'pos' => 1, 'size' => 1);
|
|
|
2417 |
|
|
|
2418 |
// The primary syntactic construct in XPath is the expression.
|
|
|
2419 |
$result = $this->_evaluateExpr($processedxPathQuery, $context);
|
|
|
2420 |
|
|
|
2421 |
// We might have been returned a string.. If so convert back to a literal
|
|
|
2422 |
$literalString = $this->_asLiteral($result);
|
|
|
2423 |
if ($literalString != FALSE) return $literalString;
|
|
|
2424 |
else return $result;
|
|
|
2425 |
}
|
|
|
2426 |
|
|
|
2427 |
/**
|
|
|
2428 |
* Alias for the match function
|
|
|
2429 |
*
|
|
|
2430 |
* @see match()
|
|
|
2431 |
*/
|
|
|
2432 |
function evaluate($xPathQuery, $baseXPath='') {
|
|
|
2433 |
return $this->match($xPathQuery, $baseXPath);
|
|
|
2434 |
}
|
|
|
2435 |
|
|
|
2436 |
/**
|
|
|
2437 |
* Parse out the literals of an XPath expression.
|
|
|
2438 |
*
|
|
|
2439 |
* Instead of doing a full lexical parse, we parse out the literal strings, and then
|
|
|
2440 |
* Treat the sections of the string either as parts of XPath or literal strings. So
|
|
|
2441 |
* this function replaces each literal it finds with a literal reference, and then inserts
|
|
|
2442 |
* the reference into an array of strings that we can access. The literals can be accessed
|
|
|
2443 |
* later from the literals associative array.
|
|
|
2444 |
*
|
|
|
2445 |
* Example:
|
|
|
2446 |
* XPathExpr = /AAA[@CCC = "hello"]/BBB[DDD = 'world']
|
|
|
2447 |
* => literals: array("hello", "world")
|
|
|
2448 |
* return value: /AAA[@CCC = $1]/BBB[DDD = $2]
|
|
|
2449 |
*
|
|
|
2450 |
* Note: This does not interfere with the VariableReference syntactical element, as these
|
|
|
2451 |
* elements must not start with a number.
|
|
|
2452 |
*
|
|
|
2453 |
* @param $xPathQuery (string) XPath expression to be processed
|
|
|
2454 |
* @return (string) The XPath expression without the literals.
|
|
|
2455 |
*
|
|
|
2456 |
*/
|
|
|
2457 |
function _removeLiterals($xPathQuery) {
|
|
|
2458 |
// What comes first? A " or a '?
|
|
|
2459 |
if (!preg_match(":^([^\"']*)([\"'].*)$:", $xPathQuery, $aMatches)) {
|
|
|
2460 |
// No " or ' means no more literals.
|
|
|
2461 |
return $xPathQuery;
|
|
|
2462 |
}
|
|
|
2463 |
|
|
|
2464 |
$result = $aMatches[1];
|
|
|
2465 |
$remainder = $aMatches[2];
|
|
|
2466 |
// What kind of literal?
|
|
|
2467 |
if (preg_match(':^"([^"]*)"(.*)$:', $remainder, $aMatches)) {
|
|
|
2468 |
// A "" literal.
|
|
|
2469 |
$literal = $aMatches[1];
|
|
|
2470 |
$remainder = $aMatches[2];
|
|
|
2471 |
} else if (preg_match(":^'([^']*)'(.*)$:", $remainder, $aMatches)) {
|
|
|
2472 |
// A '' literal.
|
|
|
2473 |
$literal = $aMatches[1];
|
|
|
2474 |
$remainder = $aMatches[2];
|
|
|
2475 |
} else {
|
|
|
2476 |
$this->_displayError("The '$xPathQuery' argument began a literal, but did not close it.", __LINE__, __FILE__);
|
|
|
2477 |
}
|
|
|
2478 |
|
|
|
2479 |
// Store the literal
|
|
|
2480 |
$literalNumber = count($this->axPathLiterals);
|
|
|
2481 |
$this->axPathLiterals[$literalNumber] = $literal;
|
|
|
2482 |
$result .= '$'.$literalNumber;
|
|
|
2483 |
return $result.$this->_removeLiterals($remainder);
|
|
|
2484 |
}
|
|
|
2485 |
|
|
|
2486 |
/**
|
|
|
2487 |
* Returns the given string as a literal reference.
|
|
|
2488 |
*
|
|
|
2489 |
* @param $string (string) The string that we are processing
|
|
|
2490 |
* @return (mixed) The literal string. FALSE if the string isn't a literal reference.
|
|
|
2491 |
*/
|
|
|
2492 |
function _asLiteral($string) {
|
|
|
2493 |
if (empty($string)) return FALSE;
|
|
|
2494 |
if (empty($string[0])) return FALSE;
|
|
|
2495 |
if ($string[0] == '$') {
|
|
|
2496 |
$remainder = substr($string, 1);
|
|
|
2497 |
if (is_numeric($remainder)) {
|
|
|
2498 |
// We have a string reference then.
|
|
|
2499 |
$stringNumber = (int)$remainder;
|
|
|
2500 |
if ($stringNumber >= count($this->axPathLiterals)) {
|
|
|
2501 |
$this->_displayError("Internal error. Found a string reference that we didn't set in xPathQuery: '$xPathQuery'.", __LINE__, __FILE__);
|
|
|
2502 |
return FALSE;
|
|
|
2503 |
}
|
|
|
2504 |
return $this->axPathLiterals[$stringNumber];
|
|
|
2505 |
}
|
|
|
2506 |
}
|
|
|
2507 |
|
|
|
2508 |
// It's not a reference then.
|
|
|
2509 |
return FALSE;
|
|
|
2510 |
}
|
|
|
2511 |
|
|
|
2512 |
/**
|
|
|
2513 |
* Adds a literal to our array of literals
|
|
|
2514 |
*
|
|
|
2515 |
* In order to make sure we don't interpret literal strings as XPath expressions, we have to
|
|
|
2516 |
* encode literal strings so that we know that they are not XPaths.
|
|
|
2517 |
*
|
|
|
2518 |
* @param $string (string) The literal string that we need to store for future access
|
|
|
2519 |
* @return (mixed) A reference string to this literal.
|
|
|
2520 |
*/
|
|
|
2521 |
function _addLiteral($string) {
|
|
|
2522 |
// Store the literal
|
|
|
2523 |
$literalNumber = count($this->axPathLiterals);
|
|
|
2524 |
$this->axPathLiterals[$literalNumber] = $string;
|
|
|
2525 |
$result = '$'.$literalNumber;
|
|
|
2526 |
return $result;
|
|
|
2527 |
}
|
|
|
2528 |
|
|
|
2529 |
/**
|
|
|
2530 |
* Look for operators in the expression
|
|
|
2531 |
*
|
|
|
2532 |
* Parses through the given expression looking for operators. If found returns
|
|
|
2533 |
* the operands and the operator in the resulting array.
|
|
|
2534 |
*
|
|
|
2535 |
* @param $xPathQuery (string) XPath query to be evaluated.
|
|
|
2536 |
* @return (array) If an operator is found, it returns an array containing
|
|
|
2537 |
* information about the operator. If no operator is found
|
|
|
2538 |
* then it returns an empty array. If an operator is found,
|
|
|
2539 |
* but has invalid operands, it returns FALSE.
|
|
|
2540 |
* The resulting array has the following entries:
|
|
|
2541 |
* 'operator' => The string version of operator that was found,
|
|
|
2542 |
* trimmed for whitespace
|
|
|
2543 |
* 'left operand' => The left operand, or empty if there was no
|
|
|
2544 |
* left operand for this operator.
|
|
|
2545 |
* 'right operand' => The right operand, or empty if there was no
|
|
|
2546 |
* right operand for this operator.
|
|
|
2547 |
*/
|
|
|
2548 |
function _GetOperator($xPathQuery) {
|
|
|
2549 |
$position = 0;
|
|
|
2550 |
$operator = '';
|
|
|
2551 |
|
|
|
2552 |
// The results of this function can easily be cached.
|
|
|
2553 |
static $aResultsCache = array();
|
|
|
2554 |
if (isset($aResultsCache[$xPathQuery])) {
|
|
|
2555 |
return $aResultsCache[$xPathQuery];
|
|
|
2556 |
}
|
|
|
2557 |
|
|
|
2558 |
// Run through all operators and try to find one.
|
|
|
2559 |
$opSize = sizeOf($this->operators);
|
|
|
2560 |
for ($i=0; $i<$opSize; $i++) {
|
|
|
2561 |
// Pick an operator to try.
|
|
|
2562 |
$operator = $this->operators[$i];
|
|
|
2563 |
// Quickcheck. If not present don't wast time searching 'the hard way'
|
|
|
2564 |
if (strpos($xPathQuery, $operator)===FALSE) continue;
|
|
|
2565 |
// Special check
|
|
|
2566 |
$position = $this->_searchString($xPathQuery, $operator);
|
|
|
2567 |
// Check whether a operator was found.
|
|
|
2568 |
if ($position <= 0 ) continue;
|
|
|
2569 |
|
|
|
2570 |
// Check whether it's the equal operator.
|
|
|
2571 |
if ($operator == '=') {
|
|
|
2572 |
// Also look for other operators containing the equal sign.
|
|
|
2573 |
switch ($xPathQuery[$position-1]) {
|
|
|
2574 |
case '<' :
|
|
|
2575 |
$position--;
|
|
|
2576 |
$operator = '<=';
|
|
|
2577 |
break;
|
|
|
2578 |
case '>' :
|
|
|
2579 |
$position--;
|
|
|
2580 |
$operator = '>=';
|
|
|
2581 |
break;
|
|
|
2582 |
case '!' :
|
|
|
2583 |
$position--;
|
|
|
2584 |
$operator = '!=';
|
|
|
2585 |
break;
|
|
|
2586 |
default:
|
|
|
2587 |
// It's a pure = operator then.
|
|
|
2588 |
}
|
|
|
2589 |
break;
|
|
|
2590 |
}
|
|
|
2591 |
|
|
|
2592 |
if ($operator == '*') {
|
|
|
2593 |
// http://www.w3.org/TR/xpath#exprlex:
|
|
|
2594 |
// "If there is a preceding token and the preceding token is not one of @, ::, (, [,
|
|
|
2595 |
// or an Operator, then a * must be recognized as a MultiplyOperator and an NCName must
|
|
|
2596 |
// be recognized as an OperatorName."
|
|
|
2597 |
|
|
|
2598 |
// Get some substrings.
|
|
|
2599 |
$character = substr($xPathQuery, $position - 1, 1);
|
|
|
2600 |
|
|
|
2601 |
// Check whether it's a multiply operator or a name test.
|
|
|
2602 |
if (strchr('/@:([', $character) != FALSE) {
|
|
|
2603 |
// Don't use the operator.
|
|
|
2604 |
$position = -1;
|
|
|
2605 |
continue;
|
|
|
2606 |
} else {
|
|
|
2607 |
// The operator is good. Lets use it.
|
|
|
2608 |
break;
|
|
|
2609 |
}
|
|
|
2610 |
}
|
|
|
2611 |
|
|
|
2612 |
// Extremely annoyingly, we could have a node name like "for-each" and we should not
|
|
|
2613 |
// parse this as a "-" operator. So if the first char of the right operator is alphabetic,
|
|
|
2614 |
// then this is NOT an interger operator.
|
|
|
2615 |
if (strchr('-+*', $operator) != FALSE) {
|
|
|
2616 |
$rightOperand = trim(substr($xPathQuery, $position + strlen($operator)));
|
|
|
2617 |
if (strlen($rightOperand) > 1) {
|
|
|
2618 |
if (preg_match(':^\D$:', $rightOperand[0])) {
|
|
|
2619 |
// Don't use the operator.
|
|
|
2620 |
$position = -1;
|
|
|
2621 |
continue;
|
|
|
2622 |
} else {
|
|
|
2623 |
// The operator is good. Lets use it.
|
|
|
2624 |
break;
|
|
|
2625 |
}
|
|
|
2626 |
}
|
|
|
2627 |
}
|
|
|
2628 |
|
|
|
2629 |
// The operator must be good then :o)
|
|
|
2630 |
break;
|
|
|
2631 |
|
|
|
2632 |
} // end while each($this->operators)
|
|
|
2633 |
|
|
|
2634 |
// Did we find an operator?
|
|
|
2635 |
if ($position == -1) {
|
|
|
2636 |
$aResultsCache[$xPathQuery] = array();
|
|
|
2637 |
return array();
|
|
|
2638 |
}
|
|
|
2639 |
|
|
|
2640 |
/////////////////////////////////////////////
|
|
|
2641 |
// Get the operands
|
|
|
2642 |
|
|
|
2643 |
// Get the left and the right part of the expression.
|
|
|
2644 |
$leftOperand = trim(substr($xPathQuery, 0, $position));
|
|
|
2645 |
$rightOperand = trim(substr($xPathQuery, $position + strlen($operator)));
|
|
|
2646 |
|
|
|
2647 |
// Remove whitespaces.
|
|
|
2648 |
$leftOperand = trim($leftOperand);
|
|
|
2649 |
$rightOperand = trim($rightOperand);
|
|
|
2650 |
|
|
|
2651 |
/////////////////////////////////////////////
|
|
|
2652 |
// Check the operands.
|
|
|
2653 |
|
|
|
2654 |
if ($leftOperand == '') {
|
|
|
2655 |
$aResultsCache[$xPathQuery] = FALSE;
|
|
|
2656 |
return FALSE;
|
|
|
2657 |
}
|
|
|
2658 |
|
|
|
2659 |
if ($rightOperand == '') {
|
|
|
2660 |
$aResultsCache[$xPathQuery] = FALSE;
|
|
|
2661 |
return FALSE;
|
|
|
2662 |
}
|
|
|
2663 |
|
|
|
2664 |
// Package up and return what we found.
|
|
|
2665 |
$aResult = array('operator' => $operator,
|
|
|
2666 |
'left operand' => $leftOperand,
|
|
|
2667 |
'right operand' => $rightOperand);
|
|
|
2668 |
|
|
|
2669 |
$aResultsCache[$xPathQuery] = $aResult;
|
|
|
2670 |
|
|
|
2671 |
return $aResult;
|
|
|
2672 |
}
|
|
|
2673 |
|
|
|
2674 |
/**
|
|
|
2675 |
* Evaluates an XPath PrimaryExpr
|
|
|
2676 |
*
|
|
|
2677 |
* http://www.w3.org/TR/xpath#section-Basics
|
|
|
2678 |
*
|
|
|
2679 |
* [15] PrimaryExpr ::= VariableReference
|
|
|
2680 |
* | '(' Expr ')'
|
|
|
2681 |
* | Literal
|
|
|
2682 |
* | Number
|
|
|
2683 |
* | FunctionCall
|
|
|
2684 |
*
|
|
|
2685 |
* @param $xPathQuery (string) XPath query to be evaluated.
|
|
|
2686 |
* @param $context (array) The context from which to evaluate
|
|
|
2687 |
* @param $results (mixed) If the expression could be parsed and evaluated as one of these
|
|
|
2688 |
* syntactical elements, then this will be either:
|
|
|
2689 |
* - node-set (an ordered collection of nodes without duplicates)
|
|
|
2690 |
* - boolean (true or false)
|
|
|
2691 |
* - number (a floating-point number)
|
|
|
2692 |
* - string (a sequence of UCS characters)
|
|
|
2693 |
* @return (string) An empty string if the query was successfully parsed and
|
|
|
2694 |
* evaluated, else a string containing the reason for failing.
|
|
|
2695 |
* @see evaluate()
|
|
|
2696 |
*/
|
|
|
2697 |
function _evaluatePrimaryExpr($xPathQuery, $context, &$result) {
|
|
|
2698 |
$ThisFunctionName = '_evaluatePrimaryExpr';
|
|
|
2699 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
2700 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
2701 |
if ($bDebugThisFunction) {
|
|
|
2702 |
echo "Path: $xPathQuery\n";
|
|
|
2703 |
echo "Context:";
|
|
|
2704 |
$this->_printContext($context);
|
|
|
2705 |
echo "\n";
|
|
|
2706 |
}
|
|
|
2707 |
|
|
|
2708 |
// Certain expressions will never be PrimaryExpr, so to speed up processing, cache the
|
|
|
2709 |
// results we do find from this function.
|
|
|
2710 |
static $aResultsCache = array();
|
|
|
2711 |
|
|
|
2712 |
// Do while false loop
|
|
|
2713 |
$error = "";
|
|
|
2714 |
// If the result is independant of context, then we can cache the result and speed this function
|
|
|
2715 |
// up on future calls.
|
|
|
2716 |
$bCacheableResult = FALSE;
|
|
|
2717 |
do {
|
|
|
2718 |
if (isset($aResultsCache[$xPathQuery])) {
|
|
|
2719 |
$error = $aResultsCache[$xPathQuery]['Error'];
|
|
|
2720 |
$result = $aResultsCache[$xPathQuery]['Result'];
|
|
|
2721 |
break;
|
|
|
2722 |
}
|
|
|
2723 |
|
|
|
2724 |
// VariableReference
|
|
|
2725 |
// ### Not supported.
|
|
|
2726 |
|
|
|
2727 |
// Is it a number?
|
|
|
2728 |
// | Number
|
|
|
2729 |
if (is_numeric($xPathQuery)) {
|
|
|
2730 |
$result = doubleval($xPathQuery);
|
|
|
2731 |
$bCacheableResult = TRUE;
|
|
|
2732 |
break;
|
|
|
2733 |
}
|
|
|
2734 |
|
|
|
2735 |
// If it starts with $, and the remainder is a number, then it's a string.
|
|
|
2736 |
// | Literal
|
|
|
2737 |
$literal = $this->_asLiteral($xPathQuery);
|
|
|
2738 |
if ($literal !== FALSE) {
|
|
|
2739 |
$result = $xPathQuery;
|
|
|
2740 |
$bCacheableResult = TRUE;
|
|
|
2741 |
break;
|
|
|
2742 |
}
|
|
|
2743 |
|
|
|
2744 |
// Is it a function?
|
|
|
2745 |
// | FunctionCall
|
|
|
2746 |
{
|
|
|
2747 |
// Check whether it's all wrapped in a function. will be like count(.*) where .* is anything
|
|
|
2748 |
// text() will try to be matched here, so just explicitly ignore it
|
|
|
2749 |
$regex = ":^([^\(\)\[\]/]*)\s*\((.*)\)$:U";
|
|
|
2750 |
if (preg_match($regex, $xPathQuery, $aMatch) && $xPathQuery != "text()") {
|
|
|
2751 |
$function = $aMatch[1];
|
|
|
2752 |
$data = $aMatch[2];
|
|
|
2753 |
// It is possible that we will get "a() or b()" which will match as function "a" with
|
|
|
2754 |
// arguments ") or b(" which is clearly wrong... _bracketsCheck() should catch this.
|
|
|
2755 |
if ($this->_bracketsCheck($data)) {
|
|
|
2756 |
if (in_array($function, $this->functions)) {
|
|
|
2757 |
if ($bDebugThisFunction) echo "XPathExpr: $xPathQuery is a $function() function call:\n";
|
|
|
2758 |
$result = $this->_evaluateFunction($function, $data, $context);
|
|
|
2759 |
break;
|
|
|
2760 |
}
|
|
|
2761 |
}
|
|
|
2762 |
}
|
|
|
2763 |
}
|
|
|
2764 |
|
|
|
2765 |
// Is it a bracketed expression?
|
|
|
2766 |
// | '(' Expr ')'
|
|
|
2767 |
// If it is surrounded by () then trim the brackets
|
|
|
2768 |
$bBrackets = FALSE;
|
|
|
2769 |
if (preg_match(":^\((.*)\):", $xPathQuery, $aMatches)) {
|
|
|
2770 |
// Do not keep trimming off the () as we could have "(() and ())"
|
|
|
2771 |
$bBrackets = TRUE;
|
|
|
2772 |
$xPathQuery = $aMatches[1];
|
|
|
2773 |
}
|
|
|
2774 |
|
|
|
2775 |
if ($bBrackets) {
|
|
|
2776 |
// Must be a Expr then.
|
|
|
2777 |
$result = $this->_evaluateExpr($xPathQuery, $context);
|
|
|
2778 |
break;
|
|
|
2779 |
}
|
|
|
2780 |
|
|
|
2781 |
// Can't be a PrimaryExpr then.
|
|
|
2782 |
$error = "Expression is not a PrimaryExpr";
|
|
|
2783 |
$bCacheableResult = TRUE;
|
|
|
2784 |
} while (FALSE);
|
|
|
2785 |
//////////////////////////////////////////////
|
|
|
2786 |
|
|
|
2787 |
// If possible, cache the result.
|
|
|
2788 |
if ($bCacheableResult) {
|
|
|
2789 |
$aResultsCache[$xPathQuery]['Error'] = $error;
|
|
|
2790 |
$aResultsCache[$xPathQuery]['Result'] = $result;
|
|
|
2791 |
}
|
|
|
2792 |
|
|
|
2793 |
$this->_closeDebugFunction($ThisFunctionName, array('result' => $result, 'error' => $error), $bDebugThisFunction);
|
|
|
2794 |
|
|
|
2795 |
// Return the result.
|
|
|
2796 |
return $error;
|
|
|
2797 |
}
|
|
|
2798 |
|
|
|
2799 |
/**
|
|
|
2800 |
* Evaluates an XPath Expr
|
|
|
2801 |
*
|
|
|
2802 |
* $this->evaluate() is the entry point and does some inits, while this
|
|
|
2803 |
* function is called recursive internaly for every sub-xPath expresion we find.
|
|
|
2804 |
* It handles the following syntax, and calls evaluatePathExpr if it finds that none
|
|
|
2805 |
* of this grammer applies.
|
|
|
2806 |
*
|
|
|
2807 |
* http://www.w3.org/TR/xpath#section-Basics
|
|
|
2808 |
*
|
|
|
2809 |
* [14] Expr ::= OrExpr
|
|
|
2810 |
* [21] OrExpr ::= AndExpr
|
|
|
2811 |
* | OrExpr 'or' AndExpr
|
|
|
2812 |
* [22] AndExpr ::= EqualityExpr
|
|
|
2813 |
* | AndExpr 'and' EqualityExpr
|
|
|
2814 |
* [23] EqualityExpr ::= RelationalExpr
|
|
|
2815 |
* | EqualityExpr '=' RelationalExpr
|
|
|
2816 |
* | EqualityExpr '!=' RelationalExpr
|
|
|
2817 |
* [24] RelationalExpr ::= AdditiveExpr
|
|
|
2818 |
* | RelationalExpr '<' AdditiveExpr
|
|
|
2819 |
* | RelationalExpr '>' AdditiveExpr
|
|
|
2820 |
* | RelationalExpr '<=' AdditiveExpr
|
|
|
2821 |
* | RelationalExpr '>=' AdditiveExpr
|
|
|
2822 |
* [25] AdditiveExpr ::= MultiplicativeExpr
|
|
|
2823 |
* | AdditiveExpr '+' MultiplicativeExpr
|
|
|
2824 |
* | AdditiveExpr '-' MultiplicativeExpr
|
|
|
2825 |
* [26] MultiplicativeExpr ::= UnaryExpr
|
|
|
2826 |
* | MultiplicativeExpr MultiplyOperator UnaryExpr
|
|
|
2827 |
* | MultiplicativeExpr 'div' UnaryExpr
|
|
|
2828 |
* | MultiplicativeExpr 'mod' UnaryExpr
|
|
|
2829 |
* [27] UnaryExpr ::= UnionExpr
|
|
|
2830 |
* | '-' UnaryExpr
|
|
|
2831 |
* [18] UnionExpr ::= PathExpr
|
|
|
2832 |
* | UnionExpr '|' PathExpr
|
|
|
2833 |
*
|
|
|
2834 |
* NOTE: The effect of the above grammar is that the order of precedence is
|
|
|
2835 |
* (lowest precedence first):
|
|
|
2836 |
* 1) or
|
|
|
2837 |
* 2) and
|
|
|
2838 |
* 3) =, !=
|
|
|
2839 |
* 4) <=, <, >=, >
|
|
|
2840 |
* 5) +, -
|
|
|
2841 |
* 6) *, div, mod
|
|
|
2842 |
* 7) - (negate)
|
|
|
2843 |
* 8) |
|
|
|
2844 |
*
|
|
|
2845 |
* @param $xPathQuery (string) XPath query to be evaluated.
|
|
|
2846 |
* @param $context (array) An associative array the describes the context from which
|
|
|
2847 |
* to evaluate the XPath Expr. Contains three members:
|
|
|
2848 |
* 'nodePath' => The absolute XPath expression to the context node
|
|
|
2849 |
* 'size' => The context size
|
|
|
2850 |
* 'pos' => The context position
|
|
|
2851 |
* @return (mixed) The result of the XPath expression. Either:
|
|
|
2852 |
* node-set (an ordered collection of nodes without duplicates)
|
|
|
2853 |
* boolean (true or false)
|
|
|
2854 |
* number (a floating-point number)
|
|
|
2855 |
* string (a sequence of UCS characters)
|
|
|
2856 |
* @see evaluate()
|
|
|
2857 |
*/
|
|
|
2858 |
function _evaluateExpr($xPathQuery, $context) {
|
|
|
2859 |
$ThisFunctionName = '_evaluateExpr';
|
|
|
2860 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
2861 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
2862 |
if ($bDebugThisFunction) {
|
|
|
2863 |
echo "Path: $xPathQuery\n";
|
|
|
2864 |
echo "Context:";
|
|
|
2865 |
$this->_printContext($context);
|
|
|
2866 |
echo "\n";
|
|
|
2867 |
}
|
|
|
2868 |
|
|
|
2869 |
// Numpty check
|
|
|
2870 |
if (!isset($xPathQuery) || ($xPathQuery == '')) {
|
|
|
2871 |
$this->_displayError("The \$xPathQuery argument must have a value.", __LINE__, __FILE__);
|
|
|
2872 |
return FALSE;
|
|
|
2873 |
}
|
|
|
2874 |
|
|
|
2875 |
// At the top level we deal with booleans. Only if the Expr is just an AdditiveExpr will
|
|
|
2876 |
// the result not be a boolean.
|
|
|
2877 |
//
|
|
|
2878 |
//
|
|
|
2879 |
// Between these syntactical elements we get PathExprs.
|
|
|
2880 |
|
|
|
2881 |
// Do while false loop
|
|
|
2882 |
do {
|
|
|
2883 |
static $aKnownPathExprCache = array();
|
|
|
2884 |
|
|
|
2885 |
if (isset($aKnownPathExprCache[$xPathQuery])) {
|
|
|
2886 |
if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
|
|
|
2887 |
$result = $this->_evaluatePathExpr($xPathQuery, $context);
|
|
|
2888 |
break;
|
|
|
2889 |
}
|
|
|
2890 |
|
|
|
2891 |
// Check for operators first, as we could have "() op ()" and the PrimaryExpr will try to
|
|
|
2892 |
// say that that is an Expr called ") op ("
|
|
|
2893 |
// Set the default position and the type of the operator.
|
|
|
2894 |
$aOperatorInfo = $this->_GetOperator($xPathQuery);
|
|
|
2895 |
|
|
|
2896 |
// An expression can be one of these, and we should catch these "first" as they are most common
|
|
|
2897 |
if (empty($aOperatorInfo)) {
|
|
|
2898 |
$error = $this->_evaluatePrimaryExpr($xPathQuery, $context, $result);
|
|
|
2899 |
if (empty($error)) {
|
|
|
2900 |
// It could be parsed as a PrimaryExpr, so look no further :o)
|
|
|
2901 |
break;
|
|
|
2902 |
}
|
|
|
2903 |
}
|
|
|
2904 |
|
|
|
2905 |
// Check whether an operator was found.
|
|
|
2906 |
if (empty($aOperatorInfo)) {
|
|
|
2907 |
if ($bDebugThisFunction) echo "XPathExpr is a PathExpr\n";
|
|
|
2908 |
$aKnownPathExprCache[$xPathQuery] = TRUE;
|
|
|
2909 |
// No operator. Means we have a PathExpr then. Go to the next level.
|
|
|
2910 |
$result = $this->_evaluatePathExpr($xPathQuery, $context);
|
|
|
2911 |
break;
|
|
|
2912 |
}
|
|
|
2913 |
|
|
|
2914 |
if ($bDebugThisFunction) { echo "\nFound and operator:"; print_r($aOperatorInfo); }//LEFT:[$leftOperand] oper:[$operator] RIGHT:[$rightOperand]";
|
|
|
2915 |
|
|
|
2916 |
$operator = $aOperatorInfo['operator'];
|
|
|
2917 |
|
|
|
2918 |
/////////////////////////////////////////////
|
|
|
2919 |
// Recursively process the operator
|
|
|
2920 |
|
|
|
2921 |
// Check the kind of operator.
|
|
|
2922 |
switch ($operator) {
|
|
|
2923 |
case ' or ':
|
|
|
2924 |
case ' and ':
|
|
|
2925 |
$operatorType = 'Boolean';
|
|
|
2926 |
break;
|
|
|
2927 |
case '+':
|
|
|
2928 |
case '-':
|
|
|
2929 |
case '*':
|
|
|
2930 |
case ' div ':
|
|
|
2931 |
case ' mod ':
|
|
|
2932 |
$operatorType = 'Integer';
|
|
|
2933 |
break;
|
|
|
2934 |
case ' | ':
|
|
|
2935 |
$operatorType = 'NodeSet';
|
|
|
2936 |
break;
|
|
|
2937 |
case '<=':
|
|
|
2938 |
case '<':
|
|
|
2939 |
case '>=':
|
|
|
2940 |
case '>':
|
|
|
2941 |
case '=':
|
|
|
2942 |
case '!=':
|
|
|
2943 |
$operatorType = 'Multi';
|
|
|
2944 |
break;
|
|
|
2945 |
default:
|
|
|
2946 |
$this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
|
|
|
2947 |
}
|
|
|
2948 |
|
|
|
2949 |
if ($bDebugThisFunction) echo "\nOperator is a [$operator]($operatorType operator)";
|
|
|
2950 |
|
|
|
2951 |
/////////////////////////////////////////////
|
|
|
2952 |
// Evaluate the operands
|
|
|
2953 |
|
|
|
2954 |
// Evaluate the left part.
|
|
|
2955 |
if ($bDebugThisFunction) echo "\nEvaluating LEFT:[{$aOperatorInfo['left operand']}]\n";
|
|
|
2956 |
$left = $this->_evaluateExpr($aOperatorInfo['left operand'], $context);
|
|
|
2957 |
if ($bDebugThisFunction) {echo "{$aOperatorInfo['left operand']} evals as:\n"; print_r($left); }
|
|
|
2958 |
|
|
|
2959 |
// If it is a boolean operator, it's possible we don't need to evaluate the right part.
|
|
|
2960 |
|
|
|
2961 |
// Only evaluate the right part if we need to.
|
|
|
2962 |
$right = '';
|
|
|
2963 |
if ($operatorType == 'Boolean') {
|
|
|
2964 |
// Is the left part false?
|
|
|
2965 |
$left = $this->_handleFunction_boolean($left, $context);
|
|
|
2966 |
if (!$left and ($operator == ' and ')) {
|
|
|
2967 |
$result = FALSE;
|
|
|
2968 |
break;
|
|
|
2969 |
} else if ($left and ($operator == ' or ')) {
|
|
|
2970 |
$result = TRUE;
|
|
|
2971 |
break;
|
|
|
2972 |
}
|
|
|
2973 |
}
|
|
|
2974 |
|
|
|
2975 |
// Evaluate the right part
|
|
|
2976 |
if ($bDebugThisFunction) echo "\nEvaluating RIGHT:[{$aOperatorInfo['right operand']}]\n";
|
|
|
2977 |
$right = $this->_evaluateExpr($aOperatorInfo['right operand'], $context);
|
|
|
2978 |
if ($bDebugThisFunction) {echo "{$aOperatorInfo['right operand']} evals as:\n"; print_r($right); echo "\n";}
|
|
|
2979 |
|
|
|
2980 |
/////////////////////////////////////////////
|
|
|
2981 |
// Combine the operands
|
|
|
2982 |
|
|
|
2983 |
// If necessary, work out how to treat the multi operators
|
|
|
2984 |
if ($operatorType != 'Multi') {
|
|
|
2985 |
$result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context);
|
|
|
2986 |
} else {
|
|
|
2987 |
// http://www.w3.org/TR/xpath#booleans
|
|
|
2988 |
// If both objects to be compared are node-sets, then the comparison will be true if and
|
|
|
2989 |
// only if there is a node in the first node-set and a node in the second node-set such
|
|
|
2990 |
// that the result of performing the comparison on the string-values of the two nodes is
|
|
|
2991 |
// true.
|
|
|
2992 |
//
|
|
|
2993 |
// If one object to be compared is a node-set and the other is a number, then the
|
|
|
2994 |
// comparison will be true if and only if there is a node in the node-set such that the
|
|
|
2995 |
// result of performing the comparison on the number to be compared and on the result of
|
|
|
2996 |
// converting the string-value of that node to a number using the number function is true.
|
|
|
2997 |
//
|
|
|
2998 |
// If one object to be compared is a node-set and the other is a string, then the comparison
|
|
|
2999 |
// will be true if and only if there is a node in the node-set such that the result of performing
|
|
|
3000 |
// the comparison on the string-value of the node and the other string is true.
|
|
|
3001 |
//
|
|
|
3002 |
// If one object to be compared is a node-set and the other is a boolean, then the comparison
|
|
|
3003 |
// will be true if and only if the result of performing the comparison on the boolean and on
|
|
|
3004 |
// the result of converting the node-set to a boolean using the boolean function is true.
|
|
|
3005 |
if (is_array($left) || is_array($right)) {
|
|
|
3006 |
if ($bDebugThisFunction) echo "As one of the operands is an array, we will need to loop\n";
|
|
|
3007 |
if (is_array($left) && is_array($right)) {
|
|
|
3008 |
$operatorType = 'String';
|
|
|
3009 |
} elseif (is_numeric($left) || is_numeric($right)) {
|
|
|
3010 |
$operatorType = 'Integer';
|
|
|
3011 |
} elseif (is_bool($left)) {
|
|
|
3012 |
$operatorType = 'Boolean';
|
|
|
3013 |
$right = $this->_handleFunction_boolean($right, $context);
|
|
|
3014 |
} elseif (is_bool($right)) {
|
|
|
3015 |
$operatorType = 'Boolean';
|
|
|
3016 |
$left = $this->_handleFunction_boolean($left, $context);
|
|
|
3017 |
} else {
|
|
|
3018 |
$operatorType = 'String';
|
|
|
3019 |
}
|
|
|
3020 |
if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n";
|
|
|
3021 |
// Turn both operands into arrays to simplify logic
|
|
|
3022 |
$aLeft = $left;
|
|
|
3023 |
$aRight = $right;
|
|
|
3024 |
if (!is_array($aLeft)) $aLeft = array($aLeft);
|
|
|
3025 |
if (!is_array($aRight)) $aRight = array($aRight);
|
|
|
3026 |
$result = FALSE;
|
|
|
3027 |
if (!empty($aLeft)) {
|
|
|
3028 |
foreach ($aLeft as $leftItem) {
|
|
|
3029 |
if (empty($aRight)) break;
|
|
|
3030 |
// If the item is from a node set, we should evaluate it's string-value
|
|
|
3031 |
if (is_array($left)) {
|
|
|
3032 |
if ($bDebugThisFunction) echo "\tObtaining string-value of LHS:$leftItem as it's from a nodeset\n";
|
|
|
3033 |
$leftItem = $this->_stringValue($leftItem);
|
|
|
3034 |
}
|
|
|
3035 |
foreach ($aRight as $rightItem) {
|
|
|
3036 |
// If the item is from a node set, we should evaluate it's string-value
|
|
|
3037 |
if (is_array($right)) {
|
|
|
3038 |
if ($bDebugThisFunction) echo "\tObtaining string-value of RHS:$rightItem as it's from a nodeset\n";
|
|
|
3039 |
$rightItem = $this->_stringValue($rightItem);
|
|
|
3040 |
}
|
|
|
3041 |
|
|
|
3042 |
if ($bDebugThisFunction) echo "\tEvaluating $leftItem $operator $rightItem\n";
|
|
|
3043 |
$result = $this->_evaluateOperator($leftItem, $operator, $rightItem, $operatorType, $context);
|
|
|
3044 |
if ($result === TRUE) break;
|
|
|
3045 |
}
|
|
|
3046 |
if ($result === TRUE) break;
|
|
|
3047 |
}
|
|
|
3048 |
}
|
|
|
3049 |
}
|
|
|
3050 |
// When neither object to be compared is a node-set and the operator is = or !=, then the
|
|
|
3051 |
// objects are compared by converting them to a common type as follows and then comparing
|
|
|
3052 |
// them.
|
|
|
3053 |
//
|
|
|
3054 |
// If at least one object to be compared is a boolean, then each object to be compared
|
|
|
3055 |
// is converted to a boolean as if by applying the boolean function.
|
|
|
3056 |
//
|
|
|
3057 |
// Otherwise, if at least one object to be compared is a number, then each object to be
|
|
|
3058 |
// compared is converted to a number as if by applying the number function.
|
|
|
3059 |
//
|
|
|
3060 |
// Otherwise, both objects to be compared are converted to strings as if by applying
|
|
|
3061 |
// the string function.
|
|
|
3062 |
//
|
|
|
3063 |
// The = comparison will be true if and only if the objects are equal; the != comparison
|
|
|
3064 |
// will be true if and only if the objects are not equal. Numbers are compared for equality
|
|
|
3065 |
// according to IEEE 754 [IEEE 754]. Two booleans are equal if either both are true or
|
|
|
3066 |
// both are false. Two strings are equal if and only if they consist of the same sequence
|
|
|
3067 |
// of UCS characters.
|
|
|
3068 |
else {
|
|
|
3069 |
if (is_bool($left) || is_bool($right)) {
|
|
|
3070 |
$operatorType = 'Boolean';
|
|
|
3071 |
} elseif (is_numeric($left) || is_numeric($right)) {
|
|
|
3072 |
$operatorType = 'Integer';
|
|
|
3073 |
} else {
|
|
|
3074 |
$operatorType = 'String';
|
|
|
3075 |
}
|
|
|
3076 |
if ($bDebugThisFunction) echo "Equals operator is a $operatorType operator\n";
|
|
|
3077 |
$result = $this->_evaluateOperator($left, $operator, $right, $operatorType, $context);
|
|
|
3078 |
}
|
|
|
3079 |
}
|
|
|
3080 |
|
|
|
3081 |
} while (FALSE);
|
|
|
3082 |
//////////////////////////////////////////////
|
|
|
3083 |
|
|
|
3084 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
3085 |
|
|
|
3086 |
// Return the result.
|
|
|
3087 |
return $result;
|
|
|
3088 |
}
|
|
|
3089 |
|
|
|
3090 |
/**
|
|
|
3091 |
* Evaluate the result of an operator whose operands have been evaluated
|
|
|
3092 |
*
|
|
|
3093 |
* If the operator type is not "NodeSet", then neither the left or right operators
|
|
|
3094 |
* will be node sets, as the processing when one or other is an array is complex,
|
|
|
3095 |
* and should be handled by the caller.
|
|
|
3096 |
*
|
|
|
3097 |
* @param $left (mixed) The left operand
|
|
|
3098 |
* @param $right (mixed) The right operand
|
|
|
3099 |
* @param $operator (string) The operator to use to combine the operands
|
|
|
3100 |
* @param $operatorType (string) The type of the operator. Either 'Boolean',
|
|
|
3101 |
* 'Integer', 'String', or 'NodeSet'
|
|
|
3102 |
* @param $context (array) The context from which to evaluate
|
|
|
3103 |
* @return (mixed) The result of the XPath expression. Either:
|
|
|
3104 |
* node-set (an ordered collection of nodes without duplicates)
|
|
|
3105 |
* boolean (true or false)
|
|
|
3106 |
* number (a floating-point number)
|
|
|
3107 |
* string (a sequence of UCS characters)
|
|
|
3108 |
*/
|
|
|
3109 |
function _evaluateOperator($left, $operator, $right, $operatorType, $context) {
|
|
|
3110 |
$ThisFunctionName = '_evaluateOperator';
|
|
|
3111 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
3112 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
3113 |
if ($bDebugThisFunction) {
|
|
|
3114 |
echo "left: $left\n";
|
|
|
3115 |
echo "right: $right\n";
|
|
|
3116 |
echo "operator: $operator\n";
|
|
|
3117 |
echo "operator type: $operatorType\n";
|
|
|
3118 |
}
|
|
|
3119 |
|
|
|
3120 |
// Do while false loop
|
|
|
3121 |
do {
|
|
|
3122 |
// Handle the operator depending on the operator type.
|
|
|
3123 |
switch ($operatorType) {
|
|
|
3124 |
case 'Boolean':
|
|
|
3125 |
{
|
|
|
3126 |
// Boolify the arguments. (The left arg is already a bool)
|
|
|
3127 |
$right = $this->_handleFunction_boolean($right, $context);
|
|
|
3128 |
switch ($operator) {
|
|
|
3129 |
case '=': // Compare the two results.
|
|
|
3130 |
$result = (bool)($left == $right);
|
|
|
3131 |
break;
|
|
|
3132 |
case ' or ': // Return the two results connected by an 'or'.
|
|
|
3133 |
$result = (bool)( $left or $right );
|
|
|
3134 |
break;
|
|
|
3135 |
case ' and ': // Return the two results connected by an 'and'.
|
|
|
3136 |
$result = (bool)( $left and $right );
|
|
|
3137 |
break;
|
|
|
3138 |
case '!=': // Check whether the two results are not equal.
|
|
|
3139 |
$result = (bool)( $left != $right );
|
|
|
3140 |
break;
|
|
|
3141 |
default:
|
|
|
3142 |
$this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
|
|
|
3143 |
}
|
|
|
3144 |
}
|
|
|
3145 |
break;
|
|
|
3146 |
case 'Integer':
|
|
|
3147 |
{
|
|
|
3148 |
// Convert both left and right operands into numbers.
|
|
|
3149 |
if (empty($left) && ($operator == '-')) {
|
|
|
3150 |
// There may be no left operator if the op is '-'
|
|
|
3151 |
$left = 0;
|
|
|
3152 |
} else {
|
|
|
3153 |
$left = $this->_handleFunction_number($left, $context);
|
|
|
3154 |
}
|
|
|
3155 |
$right = $this->_handleFunction_number($right, $context);
|
|
|
3156 |
if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
|
|
|
3157 |
switch ($operator) {
|
|
|
3158 |
case '=': // Compare the two results.
|
|
|
3159 |
$result = (bool)($left == $right);
|
|
|
3160 |
break;
|
|
|
3161 |
case '!=': // Compare the two results.
|
|
|
3162 |
$result = (bool)($left != $right);
|
|
|
3163 |
break;
|
|
|
3164 |
case '+': // Return the result by adding one result to the other.
|
|
|
3165 |
$result = $left + $right;
|
|
|
3166 |
break;
|
|
|
3167 |
case '-': // Return the result by decrease one result by the other.
|
|
|
3168 |
$result = $left - $right;
|
|
|
3169 |
break;
|
|
|
3170 |
case '*': // Return a multiplication of the two results.
|
|
|
3171 |
$result = $left * $right;
|
|
|
3172 |
break;
|
|
|
3173 |
case ' div ': // Return a division of the two results.
|
|
|
3174 |
$result = $left / $right;
|
|
|
3175 |
break;
|
|
|
3176 |
case ' mod ': // Return a modulo division of the two results.
|
|
|
3177 |
$result = $left % $right;
|
|
|
3178 |
break;
|
|
|
3179 |
case '<=': // Compare the two results.
|
|
|
3180 |
$result = (bool)( $left <= $right );
|
|
|
3181 |
break;
|
|
|
3182 |
case '<': // Compare the two results.
|
|
|
3183 |
$result = (bool)( $left < $right );
|
|
|
3184 |
break;
|
|
|
3185 |
case '>=': // Compare the two results.
|
|
|
3186 |
$result = (bool)( $left >= $right );
|
|
|
3187 |
break;
|
|
|
3188 |
case '>': // Compare the two results.
|
|
|
3189 |
$result = (bool)( $left > $right );
|
|
|
3190 |
break;
|
|
|
3191 |
default:
|
|
|
3192 |
$this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
|
|
|
3193 |
}
|
|
|
3194 |
}
|
|
|
3195 |
break;
|
|
|
3196 |
case 'NodeSet':
|
|
|
3197 |
// Add the nodes to the result set
|
|
|
3198 |
$result = array_merge($left, $right);
|
|
|
3199 |
// Remove duplicated nodes.
|
|
|
3200 |
$result = array_unique($result);
|
|
|
3201 |
|
|
|
3202 |
// Preserve doc order if there was more than one query.
|
|
|
3203 |
if (count($result) > 1) {
|
|
|
3204 |
$result = $this->_sortByDocOrder($result);
|
|
|
3205 |
}
|
|
|
3206 |
break;
|
|
|
3207 |
case 'String':
|
|
|
3208 |
$left = $this->_handleFunction_string($left, $context);
|
|
|
3209 |
$right = $this->_handleFunction_string($right, $context);
|
|
|
3210 |
if ($bDebugThisFunction) echo "\nLeft is $left, Right is $right\n";
|
|
|
3211 |
switch ($operator) {
|
|
|
3212 |
case '=': // Compare the two results.
|
|
|
3213 |
$result = (bool)($left == $right);
|
|
|
3214 |
break;
|
|
|
3215 |
case '!=': // Compare the two results.
|
|
|
3216 |
$result = (bool)($left != $right);
|
|
|
3217 |
break;
|
|
|
3218 |
default:
|
|
|
3219 |
$this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
|
|
|
3220 |
}
|
|
|
3221 |
break;
|
|
|
3222 |
default:
|
|
|
3223 |
$this->_displayError("Internal error. Default case of switch statement reached.", __LINE__, __FILE__);
|
|
|
3224 |
}
|
|
|
3225 |
} while (FALSE);
|
|
|
3226 |
|
|
|
3227 |
//////////////////////////////////////////////
|
|
|
3228 |
|
|
|
3229 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
3230 |
|
|
|
3231 |
// Return the result.
|
|
|
3232 |
return $result;
|
|
|
3233 |
}
|
|
|
3234 |
|
|
|
3235 |
/**
|
|
|
3236 |
* Evaluates an XPath PathExpr
|
|
|
3237 |
*
|
|
|
3238 |
* It handles the following syntax:
|
|
|
3239 |
*
|
|
|
3240 |
* http://www.w3.org/TR/xpath#node-sets
|
|
|
3241 |
* http://www.w3.org/TR/xpath#NT-LocationPath
|
|
|
3242 |
* http://www.w3.org/TR/xpath#path-abbrev
|
|
|
3243 |
* http://www.w3.org/TR/xpath#NT-Step
|
|
|
3244 |
*
|
|
|
3245 |
* [19] PathExpr ::= LocationPath
|
|
|
3246 |
* | FilterExpr
|
|
|
3247 |
* | FilterExpr '/' RelativeLocationPath
|
|
|
3248 |
* | FilterExpr '//' RelativeLocationPath
|
|
|
3249 |
* [20] FilterExpr ::= PrimaryExpr
|
|
|
3250 |
* | FilterExpr Predicate
|
|
|
3251 |
* [1] LocationPath ::= RelativeLocationPath
|
|
|
3252 |
* | AbsoluteLocationPath
|
|
|
3253 |
* [2] AbsoluteLocationPath ::= '/' RelativeLocationPath?
|
|
|
3254 |
* | AbbreviatedAbsoluteLocationPath
|
|
|
3255 |
* [3] RelativeLocationPath ::= Step
|
|
|
3256 |
* | RelativeLocationPath '/' Step
|
|
|
3257 |
* | AbbreviatedRelativeLocationPath
|
|
|
3258 |
* [4] Step ::= AxisSpecifier NodeTest Predicate*
|
|
|
3259 |
* | AbbreviatedStep
|
|
|
3260 |
* [5] AxisSpecifier ::= AxisName '::'
|
|
|
3261 |
* | AbbreviatedAxisSpecifier
|
|
|
3262 |
* [10] AbbreviatedAbsoluteLocationPath
|
|
|
3263 |
* ::= '//' RelativeLocationPath
|
|
|
3264 |
* [11] AbbreviatedRelativeLocationPath
|
|
|
3265 |
* ::= RelativeLocationPath '//' Step
|
|
|
3266 |
* [12] AbbreviatedStep ::= '.'
|
|
|
3267 |
* | '..'
|
|
|
3268 |
* [13] AbbreviatedAxisSpecifier
|
|
|
3269 |
* ::= '@'?
|
|
|
3270 |
*
|
|
|
3271 |
* If you expand all the abbreviated versions, then the grammer simplifies to:
|
|
|
3272 |
*
|
|
|
3273 |
* [19] PathExpr ::= RelativeLocationPath
|
|
|
3274 |
* | '/' RelativeLocationPath?
|
|
|
3275 |
* | FilterExpr
|
|
|
3276 |
* | FilterExpr '/' RelativeLocationPath
|
|
|
3277 |
* [20] FilterExpr ::= PrimaryExpr
|
|
|
3278 |
* | FilterExpr Predicate
|
|
|
3279 |
* [3] RelativeLocationPath ::= Step
|
|
|
3280 |
* | RelativeLocationPath '/' Step
|
|
|
3281 |
* [4] Step ::= AxisName '::' NodeTest Predicate*
|
|
|
3282 |
*
|
|
|
3283 |
* Conceptually you can say that we should split by '/' and try to treat the parts
|
|
|
3284 |
* as steps, and if that fails then try to treat it as a PrimaryExpr.
|
|
|
3285 |
*
|
|
|
3286 |
* @param $PathExpr (string) PathExpr syntactical element
|
|
|
3287 |
* @param $context (array) The context from which to evaluate
|
|
|
3288 |
* @return (mixed) The result of the XPath expression. Either:
|
|
|
3289 |
* node-set (an ordered collection of nodes without duplicates)
|
|
|
3290 |
* boolean (true or false)
|
|
|
3291 |
* number (a floating-point number)
|
|
|
3292 |
* string (a sequence of UCS characters)
|
|
|
3293 |
* @see evaluate()
|
|
|
3294 |
*/
|
|
|
3295 |
function _evaluatePathExpr($PathExpr, $context) {
|
|
|
3296 |
$ThisFunctionName = '_evaluatePathExpr';
|
|
|
3297 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
3298 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
3299 |
if ($bDebugThisFunction) {
|
|
|
3300 |
echo "PathExpr: $PathExpr\n";
|
|
|
3301 |
echo "Context:";
|
|
|
3302 |
$this->_printContext($context);
|
|
|
3303 |
echo "\n";
|
|
|
3304 |
}
|
|
|
3305 |
|
|
|
3306 |
// Numpty check
|
|
|
3307 |
if (empty($PathExpr)) {
|
|
|
3308 |
$this->_displayError("The \$PathExpr argument must have a value.", __LINE__, __FILE__);
|
|
|
3309 |
return FALSE;
|
|
|
3310 |
}
|
|
|
3311 |
//////////////////////////////////////////////
|
|
|
3312 |
|
|
|
3313 |
// Parsing the expression into steps is a cachable operation as it doesn't depend on the context
|
|
|
3314 |
static $aResultsCache = array();
|
|
|
3315 |
|
|
|
3316 |
if (isset($aResultsCache[$PathExpr])) {
|
|
|
3317 |
$steps = $aResultsCache[$PathExpr];
|
|
|
3318 |
} else {
|
|
|
3319 |
// Note that we have used $this->slashes2descendant to simplify this logic, so the
|
|
|
3320 |
// "Abbreviated" paths basically never exist as '//' never exists.
|
|
|
3321 |
|
|
|
3322 |
// mini syntax check
|
|
|
3323 |
if (!$this->_bracketsCheck($PathExpr)) {
|
|
|
3324 |
$this->_displayError('While parsing an XPath query, in the PathExpr "' .
|
|
|
3325 |
$PathExpr.
|
|
|
3326 |
'", there was an invalid number of brackets or a bracket mismatch.', __LINE__, __FILE__);
|
|
|
3327 |
}
|
|
|
3328 |
// Save the current path.
|
|
|
3329 |
$this->currentXpathQuery = $PathExpr;
|
|
|
3330 |
// Split the path at every slash *outside* a bracket.
|
|
|
3331 |
$steps = $this->_bracketExplode('/', $PathExpr);
|
|
|
3332 |
if ($bDebugThisFunction) { echo "<hr>Split the path '$PathExpr' at every slash *outside* a bracket.\n "; print_r($steps); }
|
|
|
3333 |
// Check whether the first element is empty.
|
|
|
3334 |
if (empty($steps[0])) {
|
|
|
3335 |
// Remove the first and empty element. It's a starting '//'.
|
|
|
3336 |
array_shift($steps);
|
|
|
3337 |
}
|
|
|
3338 |
$aResultsCache[$PathExpr] = $steps;
|
|
|
3339 |
}
|
|
|
3340 |
|
|
|
3341 |
// Start to evaluate the steps.
|
|
|
3342 |
// ### Consider implementing an evaluateSteps() function that removes recursion from
|
|
|
3343 |
// evaluateStep()
|
|
|
3344 |
$result = $this->_evaluateStep($steps, $context);
|
|
|
3345 |
|
|
|
3346 |
// Preserve doc order if there was more than one result
|
|
|
3347 |
if (count($result) > 1) {
|
|
|
3348 |
$result = $this->_sortByDocOrder($result);
|
|
|
3349 |
}
|
|
|
3350 |
//////////////////////////////////////////////
|
|
|
3351 |
|
|
|
3352 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
3353 |
|
|
|
3354 |
// Return the result.
|
|
|
3355 |
return $result;
|
|
|
3356 |
}
|
|
|
3357 |
|
|
|
3358 |
/**
|
|
|
3359 |
* Sort an xPathSet by doc order.
|
|
|
3360 |
*
|
|
|
3361 |
* @param $xPathSet (array) Array of full paths to nodes that need to be sorted
|
|
|
3362 |
* @return (array) Array containing the same contents as $xPathSet, but
|
|
|
3363 |
* with the contents in doc order
|
|
|
3364 |
*/
|
|
|
3365 |
function _sortByDocOrder($xPathSet) {
|
|
|
3366 |
$ThisFunctionName = '_sortByDocOrder';
|
|
|
3367 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
3368 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
3369 |
if ($bDebugThisFunction) {
|
|
|
3370 |
echo "_sortByDocOrder(xPathSet:[".count($xPathSet)."])";
|
|
|
3371 |
echo "xPathSet:\n";
|
|
|
3372 |
print_r($xPathSet);
|
|
|
3373 |
echo "<hr>\n";
|
|
|
3374 |
}
|
|
|
3375 |
//////////////////////////////////////////////
|
|
|
3376 |
|
|
|
3377 |
$aResult = array();
|
|
|
3378 |
|
|
|
3379 |
// Spot some common shortcuts.
|
|
|
3380 |
if (count($xPathSet) < 1) {
|
|
|
3381 |
$aResult = $xPathSet;
|
|
|
3382 |
} else {
|
|
|
3383 |
// Build an array of doc-pos indexes.
|
|
|
3384 |
$aDocPos = array();
|
|
|
3385 |
$nodeCount = count($this->nodeIndex);
|
|
|
3386 |
$aPaths = array_keys($this->nodeIndex);
|
|
|
3387 |
if ($bDebugThisFunction) {
|
|
|
3388 |
echo "searching for path indices in array_keys(this->nodeIndex)...\n";
|
|
|
3389 |
//print_r($aPaths);
|
|
|
3390 |
}
|
|
|
3391 |
|
|
|
3392 |
// The last index we found. In general the elements will be in groups
|
|
|
3393 |
// that are themselves in order.
|
|
|
3394 |
$iLastIndex = 0;
|
|
|
3395 |
foreach ($xPathSet as $path) {
|
|
|
3396 |
// Cycle round the nodes, starting at the last index, looking for the path.
|
|
|
3397 |
$foundNode = FALSE;
|
|
|
3398 |
for ($iIndex = $iLastIndex; $iIndex < $nodeCount + $iLastIndex; $iIndex++) {
|
|
|
3399 |
$iThisIndex = $iIndex % $nodeCount;
|
|
|
3400 |
if (!strcmp($aPaths[$iThisIndex],$path)) {
|
|
|
3401 |
// we have found the doc-position index of the path
|
|
|
3402 |
$aDocPos[] = $iThisIndex;
|
|
|
3403 |
$iLastIndex = $iThisIndex;
|
|
|
3404 |
$foundNode = TRUE;
|
|
|
3405 |
break;
|
|
|
3406 |
}
|
|
|
3407 |
}
|
|
|
3408 |
if ($bDebugThisFunction) {
|
|
|
3409 |
if (!$foundNode)
|
|
|
3410 |
echo "Error: $path not found in \$this->nodeIndex\n";
|
|
|
3411 |
else
|
|
|
3412 |
echo "Found node after ".($iIndex - $iLastIndex)." iterations\n";
|
|
|
3413 |
}
|
|
|
3414 |
}
|
|
|
3415 |
// Now count the number of doc pos we have and the number of results and
|
|
|
3416 |
// confirm that we have the same number of each.
|
|
|
3417 |
$iDocPosCount = count($aDocPos);
|
|
|
3418 |
$iResultCount = count($xPathSet);
|
|
|
3419 |
if ($iDocPosCount != $iResultCount) {
|
|
|
3420 |
if ($bDebugThisFunction) {
|
|
|
3421 |
echo "count(\$aDocPos)=$iDocPosCount; count(\$result)=$iResultCount\n";
|
|
|
3422 |
print_r(array_keys($this->nodeIndex));
|
|
|
3423 |
}
|
|
|
3424 |
$this->_displayError('Results from _InternalEvaluate() are corrupt. '.
|
|
|
3425 |
'Do you need to call reindexNodeTree()?', __LINE__, __FILE__);
|
|
|
3426 |
}
|
|
|
3427 |
|
|
|
3428 |
// Now sort the indexes.
|
|
|
3429 |
sort($aDocPos);
|
|
|
3430 |
|
|
|
3431 |
// And now convert back to paths.
|
|
|
3432 |
$iPathCount = count($aDocPos);
|
|
|
3433 |
for ($iIndex = 0; $iIndex < $iPathCount; $iIndex++) {
|
|
|
3434 |
$aResult[] = $aPaths[$aDocPos[$iIndex]];
|
|
|
3435 |
}
|
|
|
3436 |
}
|
|
|
3437 |
|
|
|
3438 |
// Our result from the function is this array.
|
|
|
3439 |
$result = $aResult;
|
|
|
3440 |
|
|
|
3441 |
//////////////////////////////////////////////
|
|
|
3442 |
|
|
|
3443 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
3444 |
|
|
|
3445 |
// Return the result.
|
|
|
3446 |
return $result;
|
|
|
3447 |
}
|
|
|
3448 |
|
|
|
3449 |
/**
|
|
|
3450 |
* Evaluate a step from a XPathQuery expression at a specific contextPath.
|
|
|
3451 |
*
|
|
|
3452 |
* Steps are the arguments of a XPathQuery when divided by a '/'. A contextPath is a
|
|
|
3453 |
* absolute XPath (or vector of XPaths) to a starting node(s) from which the step should
|
|
|
3454 |
* be evaluated.
|
|
|
3455 |
*
|
|
|
3456 |
* @param $steps (array) Vector containing the remaining steps of the current
|
|
|
3457 |
* XPathQuery expression.
|
|
|
3458 |
* @param $context (array) The context from which to evaluate
|
|
|
3459 |
* @return (array) Vector of absolute XPath's as a result of the step
|
|
|
3460 |
* evaluation. The results will not necessarily be in doc order
|
|
|
3461 |
* @see _evaluatePathExpr()
|
|
|
3462 |
*/
|
|
|
3463 |
function _evaluateStep($steps, $context) {
|
|
|
3464 |
$ThisFunctionName = '_evaluateStep';
|
|
|
3465 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
3466 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
3467 |
if ($bDebugThisFunction) {
|
|
|
3468 |
echo "Context:";
|
|
|
3469 |
$this->_printContext($context);
|
|
|
3470 |
echo "\n";
|
|
|
3471 |
echo "Steps: ";
|
|
|
3472 |
print_r($steps);
|
|
|
3473 |
echo "<hr>\n";
|
|
|
3474 |
}
|
|
|
3475 |
//////////////////////////////////////////////
|
|
|
3476 |
|
|
|
3477 |
$result = array(); // Create an empty array for saving the abs. XPath's found.
|
|
|
3478 |
|
|
|
3479 |
$contextPaths = array(); // Create an array to save the new contexts.
|
|
|
3480 |
$step = trim(array_shift($steps)); // Get this step.
|
|
|
3481 |
if ($bDebugThisFunction) echo __LINE__.":Evaluating step $step\n";
|
|
|
3482 |
|
|
|
3483 |
$axis = $this->_getAxis($step); // Get the axis of the current step.
|
|
|
3484 |
|
|
|
3485 |
// If there was no axis, then it must be a PrimaryExpr
|
|
|
3486 |
if ($axis == FALSE) {
|
|
|
3487 |
if ($bDebugThisFunction) echo __LINE__.":Step is not an axis but a PrimaryExpr\n";
|
|
|
3488 |
// ### This isn't correct, as the result of this function might not be a node set.
|
|
|
3489 |
$error = $this->_evaluatePrimaryExpr($step, $context, $contextPaths);
|
|
|
3490 |
if (!empty($error)) {
|
|
|
3491 |
$this->_displayError("Expression failed to parse as PrimaryExpr because: $error"
|
|
|
3492 |
, __LINE__, __FILE__, FALSE);
|
|
|
3493 |
}
|
|
|
3494 |
} else {
|
|
|
3495 |
if ($bDebugThisFunction) { echo __LINE__.":Axis of step is:\n"; print_r($axis); echo "\n";}
|
|
|
3496 |
$method = '_handleAxis_' . $axis['axis']; // Create the name of the method.
|
|
|
3497 |
|
|
|
3498 |
// Check whether the axis handler is defined. If not display an error message.
|
|
|
3499 |
if (!method_exists($this, $method)) {
|
|
|
3500 |
$this->_displayError('While parsing an XPath query, the axis ' .
|
|
|
3501 |
$axis['axis'] . ' could not be handled, because this version does not support this axis.', __LINE__, __FILE__);
|
|
|
3502 |
}
|
|
|
3503 |
if ($bDebugThisFunction) echo __LINE__.":Calling user method $method\n";
|
|
|
3504 |
|
|
|
3505 |
// Perform an axis action.
|
|
|
3506 |
$contextPaths = $this->$method($axis, $context['nodePath']);
|
|
|
3507 |
if ($bDebugThisFunction) { echo __LINE__.":We found these contexts from this step:\n"; print_r( $contextPaths ); echo "\n";}
|
|
|
3508 |
}
|
|
|
3509 |
|
|
|
3510 |
// Check whether there are predicates.
|
|
|
3511 |
if (count($contextPaths) > 0 && count($axis['predicate']) > 0) {
|
|
|
3512 |
if ($bDebugThisFunction) echo __LINE__.":Filtering contexts by predicate...\n";
|
|
|
3513 |
|
|
|
3514 |
// Check whether each node fits the predicates.
|
|
|
3515 |
$contextPaths = $this->_checkPredicates($contextPaths, $axis['predicate']);
|
|
|
3516 |
}
|
|
|
3517 |
|
|
|
3518 |
// Check whether there are more steps left.
|
|
|
3519 |
if (count($steps) > 0) {
|
|
|
3520 |
if ($bDebugThisFunction) echo __LINE__.":Evaluating next step given the context of the first step...\n";
|
|
|
3521 |
|
|
|
3522 |
// Continue the evaluation of the next steps.
|
|
|
3523 |
|
|
|
3524 |
// Run through the array.
|
|
|
3525 |
$size = sizeOf($contextPaths);
|
|
|
3526 |
for ($pos=0; $pos<$size; $pos++) {
|
|
|
3527 |
// Build new context
|
|
|
3528 |
$newContext = array('nodePath' => $contextPaths[$pos], 'size' => $size, 'pos' => $pos + 1);
|
|
|
3529 |
if ($bDebugThisFunction) echo __LINE__.":Evaluating step for the {$contextPaths[$pos]} context...\n";
|
|
|
3530 |
// Call this method for this single path.
|
|
|
3531 |
$xPathSetNew = $this->_evaluateStep($steps, $newContext);
|
|
|
3532 |
if ($bDebugThisFunction) {echo "New results for this context:\n"; print_r($xPathSetNew);}
|
|
|
3533 |
$result = array_merge($result, $xPathSetNew);
|
|
|
3534 |
}
|
|
|
3535 |
|
|
|
3536 |
// Remove duplicated nodes.
|
|
|
3537 |
$result = array_unique($result);
|
|
|
3538 |
} else {
|
|
|
3539 |
$result = $contextPaths; // Save the found contexts.
|
|
|
3540 |
}
|
|
|
3541 |
|
|
|
3542 |
//////////////////////////////////////////////
|
|
|
3543 |
|
|
|
3544 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
3545 |
|
|
|
3546 |
// Return the result.
|
|
|
3547 |
return $result;
|
|
|
3548 |
}
|
|
|
3549 |
|
|
|
3550 |
/**
|
|
|
3551 |
* Checks whether a node matches predicates.
|
|
|
3552 |
*
|
|
|
3553 |
* This method checks whether a list of nodes passed to this method match
|
|
|
3554 |
* a given list of predicates.
|
|
|
3555 |
*
|
|
|
3556 |
* @param $xPathSet (array) Array of full paths of all nodes to be tested.
|
|
|
3557 |
* @param $predicates (array) Array of predicates to use.
|
|
|
3558 |
* @return (array) Vector of absolute XPath's that match the given predicates.
|
|
|
3559 |
* @see _evaluateStep()
|
|
|
3560 |
*/
|
|
|
3561 |
function _checkPredicates($xPathSet, $predicates) {
|
|
|
3562 |
$ThisFunctionName = '_checkPredicates';
|
|
|
3563 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
3564 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
3565 |
if ($bDebugThisFunction) {
|
|
|
3566 |
echo "XPathSet:";
|
|
|
3567 |
print_r($xPathSet);
|
|
|
3568 |
echo "Predicates:";
|
|
|
3569 |
print_r($predicates);
|
|
|
3570 |
echo "<hr>";
|
|
|
3571 |
}
|
|
|
3572 |
//////////////////////////////////////////////
|
|
|
3573 |
// Create an empty set of nodes.
|
|
|
3574 |
$result = array();
|
|
|
3575 |
|
|
|
3576 |
// Run through all predicates.
|
|
|
3577 |
$pSize = sizeOf($predicates);
|
|
|
3578 |
for ($j=0; $j<$pSize; $j++) {
|
|
|
3579 |
$predicate = $predicates[$j];
|
|
|
3580 |
if ($bDebugThisFunction) echo "Evaluating predicate \"$predicate\"\n";
|
|
|
3581 |
|
|
|
3582 |
// This will contain all the nodes that match this predicate
|
|
|
3583 |
$aNewSet = array();
|
|
|
3584 |
|
|
|
3585 |
// Run through all nodes.
|
|
|
3586 |
$contextSize = count($xPathSet);
|
|
|
3587 |
for ($contextPos=0; $contextPos<$contextSize; $contextPos++) {
|
|
|
3588 |
$xPath = $xPathSet[$contextPos];
|
|
|
3589 |
|
|
|
3590 |
// Build the context for this predicate
|
|
|
3591 |
$context = array('nodePath' => $xPath, 'size' => $contextSize, 'pos' => $contextPos + 1);
|
|
|
3592 |
|
|
|
3593 |
// Check whether the predicate is just an number.
|
|
|
3594 |
if (preg_match('/^\d+$/', $predicate)) {
|
|
|
3595 |
if ($bDebugThisFunction) echo "Taking short cut and calling _handleFunction_position() directly.\n";
|
|
|
3596 |
// Take a short cut. If it is just a position, then call
|
|
|
3597 |
// _handleFunction_position() directly. 70% of the
|
|
|
3598 |
// time this will be the case. ## N.S
|
|
|
3599 |
// $check = (bool) ($predicate == $context['pos']);
|
|
|
3600 |
$check = (bool) ($predicate == $this->_handleFunction_position('', $context));
|
|
|
3601 |
} else {
|
|
|
3602 |
// Else do the predicate check the long and through way.
|
|
|
3603 |
$check = $this->_evaluateExpr($predicate, $context);
|
|
|
3604 |
}
|
|
|
3605 |
if ($bDebugThisFunction) {
|
|
|
3606 |
echo "Evaluating the predicate returned ";
|
|
|
3607 |
var_dump($check);
|
|
|
3608 |
echo "\n";
|
|
|
3609 |
}
|
|
|
3610 |
|
|
|
3611 |
if (is_int($check)) { // Check whether it's an integer.
|
|
|
3612 |
// Check whether it's the current position.
|
|
|
3613 |
$check = (bool) ($check == $this->_handleFunction_position('', $context));
|
|
|
3614 |
} else {
|
|
|
3615 |
$check = (bool) ($this->_handleFunction_boolean($check, $context));
|
|
|
3616 |
// if ($bDebugThisFunction) {echo $this->_handleFunction_string($check, $context);}
|
|
|
3617 |
}
|
|
|
3618 |
|
|
|
3619 |
if ($bDebugThisFunction) echo "Node $xPath matches predicate $predicate: " . (($check) ? "TRUE" : "FALSE") ."\n";
|
|
|
3620 |
|
|
|
3621 |
// Do we add it?
|
|
|
3622 |
if ($check) $aNewSet[] = $xPath;
|
|
|
3623 |
}
|
|
|
3624 |
|
|
|
3625 |
// Use the newly filtered list.
|
|
|
3626 |
$xPathSet = $aNewSet;
|
|
|
3627 |
|
|
|
3628 |
if ($bDebugThisFunction) {echo "Node set now contains : "; print_r($xPathSet); }
|
|
|
3629 |
}
|
|
|
3630 |
|
|
|
3631 |
$result = $xPathSet;
|
|
|
3632 |
|
|
|
3633 |
//////////////////////////////////////////////
|
|
|
3634 |
|
|
|
3635 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
3636 |
|
|
|
3637 |
// Return the array of nodes.
|
|
|
3638 |
return $result;
|
|
|
3639 |
}
|
|
|
3640 |
|
|
|
3641 |
/**
|
|
|
3642 |
* Evaluates an XPath function
|
|
|
3643 |
*
|
|
|
3644 |
* This method evaluates a given XPath function with its arguments on a
|
|
|
3645 |
* specific node of the document.
|
|
|
3646 |
*
|
|
|
3647 |
* @param $function (string) Name of the function to be evaluated.
|
|
|
3648 |
* @param $arguments (string) String containing the arguments being
|
|
|
3649 |
* passed to the function.
|
|
|
3650 |
* @param $context (array) The context from which to evaluate
|
|
|
3651 |
* @return (mixed) This method returns the result of the evaluation of
|
|
|
3652 |
* the function. Depending on the function the type of the
|
|
|
3653 |
* return value can be different.
|
|
|
3654 |
* @see evaluate()
|
|
|
3655 |
*/
|
|
|
3656 |
function _evaluateFunction($function, $arguments, $context) {
|
|
|
3657 |
$ThisFunctionName = '_evaluateFunction';
|
|
|
3658 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
3659 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
3660 |
if ($bDebugThisFunction) {
|
|
|
3661 |
if (is_array($arguments)) {
|
|
|
3662 |
echo "Arguments:\n";
|
|
|
3663 |
print_r($arguments);
|
|
|
3664 |
} else {
|
|
|
3665 |
echo "Arguments: $arguments\n";
|
|
|
3666 |
}
|
|
|
3667 |
echo "Context:";
|
|
|
3668 |
$this->_printContext($context);
|
|
|
3669 |
echo "\n";
|
|
|
3670 |
echo "<hr>\n";
|
|
|
3671 |
}
|
|
|
3672 |
/////////////////////////////////////
|
|
|
3673 |
// Remove whitespaces.
|
|
|
3674 |
$function = trim($function);
|
|
|
3675 |
$arguments = trim($arguments);
|
|
|
3676 |
// Create the name of the function handling function.
|
|
|
3677 |
$method = '_handleFunction_'. $function;
|
|
|
3678 |
|
|
|
3679 |
// Check whether the function handling function is available.
|
|
|
3680 |
if (!method_exists($this, $method)) {
|
|
|
3681 |
// Display an error message.
|
|
|
3682 |
$this->_displayError("While parsing an XPath query, ".
|
|
|
3683 |
"the function \"$function\" could not be handled, because this ".
|
|
|
3684 |
"version does not support this function.", __LINE__, __FILE__);
|
|
|
3685 |
}
|
|
|
3686 |
if ($bDebugThisFunction) echo "Calling function $method($arguments)\n";
|
|
|
3687 |
|
|
|
3688 |
// Return the result of the function.
|
|
|
3689 |
$result = $this->$method($arguments, $context);
|
|
|
3690 |
|
|
|
3691 |
//////////////////////////////////////////////
|
|
|
3692 |
// Return the nodes found.
|
|
|
3693 |
|
|
|
3694 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
3695 |
|
|
|
3696 |
// Return the result.
|
|
|
3697 |
return $result;
|
|
|
3698 |
}
|
|
|
3699 |
|
|
|
3700 |
/**
|
|
|
3701 |
* Checks whether a node matches a node-test.
|
|
|
3702 |
*
|
|
|
3703 |
* This method checks whether a node in the document matches a given node-test.
|
|
|
3704 |
* A node test is something like text(), node(), or an element name.
|
|
|
3705 |
*
|
|
|
3706 |
* @param $contextPath (string) Full xpath of the node, which should be tested for
|
|
|
3707 |
* matching the node-test.
|
|
|
3708 |
* @param $nodeTest (string) String containing the node-test for the node.
|
|
|
3709 |
* @return (boolean) This method returns TRUE if the node matches the
|
|
|
3710 |
* node-test, otherwise FALSE.
|
|
|
3711 |
* @see evaluate()
|
|
|
3712 |
*/
|
|
|
3713 |
function _checkNodeTest($contextPath, $nodeTest) {
|
|
|
3714 |
// Empty node test means that it must match
|
|
|
3715 |
if (empty($nodeTest)) return TRUE;
|
|
|
3716 |
|
|
|
3717 |
if ($nodeTest == '*') {
|
|
|
3718 |
// * matches all element nodes.
|
|
|
3719 |
return (!preg_match(':/[^/]+\(\)\[\d+\]$:U', $contextPath));
|
|
|
3720 |
}
|
|
|
3721 |
elseif (preg_match('/^[\w-:\.]+$/', $nodeTest)) {
|
|
|
3722 |
// http://www.w3.org/TR/2000/REC-xml-20001006#NT-Name
|
|
|
3723 |
// The real spec for what constitutes whitespace is quite elaborate, and
|
|
|
3724 |
// we currently just hope that "\w" catches them all. In reality it should
|
|
|
3725 |
// start with a letter too, not a number, but we've just left it simple.
|
|
|
3726 |
// It's just a node name test. It should end with "/$nodeTest[x]"
|
|
|
3727 |
return (preg_match('"/'.$nodeTest.'\[\d+\]$"', $contextPath));
|
|
|
3728 |
}
|
|
|
3729 |
elseif (preg_match('/\(/U', $nodeTest)) { // Check whether it's a function.
|
|
|
3730 |
// Get the type of function to use.
|
|
|
3731 |
$function = $this->_prestr($nodeTest, '(');
|
|
|
3732 |
// Check whether the node fits the method.
|
|
|
3733 |
switch ($function) {
|
|
|
3734 |
case 'node': // Add this node to the list of nodes.
|
|
|
3735 |
return TRUE;
|
|
|
3736 |
case 'text': // Check whether the node has some text.
|
|
|
3737 |
$tmp = implode('', $this->nodeIndex[$contextPath]['textParts']);
|
|
|
3738 |
if (!empty($tmp)) {
|
|
|
3739 |
return TRUE; // Add this node to the list of nodes.
|
|
|
3740 |
}
|
|
|
3741 |
break;
|
|
|
3742 |
/******** NOT supported (yet?)
|
|
|
3743 |
case 'comment': // Check whether the node has some comment.
|
|
|
3744 |
if (!empty($this->nodeIndex[$contextPath]['comment'])) {
|
|
|
3745 |
return TRUE; // Add this node to the list of nodes.
|
|
|
3746 |
}
|
|
|
3747 |
break;
|
|
|
3748 |
case 'processing-instruction':
|
|
|
3749 |
$literal = $this->_afterstr($axis['node-test'], '('); // Get the literal argument.
|
|
|
3750 |
$literal = substr($literal, 0, strlen($literal) - 1); // Cut the literal.
|
|
|
3751 |
|
|
|
3752 |
// Check whether a literal was given.
|
|
|
3753 |
if (!empty($literal)) {
|
|
|
3754 |
// Check whether the node's processing instructions are matching the literals given.
|
|
|
3755 |
if ($this->nodeIndex[$context]['processing-instructions'] == $literal) {
|
|
|
3756 |
return TRUE; // Add this node to the node-set.
|
|
|
3757 |
}
|
|
|
3758 |
} else {
|
|
|
3759 |
// Check whether the node has processing instructions.
|
|
|
3760 |
if (!empty($this->nodeIndex[$contextPath]['processing-instructions'])) {
|
|
|
3761 |
return TRUE; // Add this node to the node-set.
|
|
|
3762 |
}
|
|
|
3763 |
}
|
|
|
3764 |
break;
|
|
|
3765 |
***********/
|
|
|
3766 |
default: // Display an error message.
|
|
|
3767 |
$this->_displayError('While parsing an XPath query there was an undefined function called "' .
|
|
|
3768 |
str_replace($function, '<b>'.$function.'</b>', $this->currentXpathQuery) .'"', __LINE__, __FILE__);
|
|
|
3769 |
}
|
|
|
3770 |
}
|
|
|
3771 |
else { // Display an error message.
|
|
|
3772 |
$this->_displayError("While parsing the XPath query \"{$this->currentXpathQuery}\" ".
|
|
|
3773 |
"an empty and therefore invalid node-test has been found.", __LINE__, __FILE__, FALSE);
|
|
|
3774 |
}
|
|
|
3775 |
|
|
|
3776 |
return FALSE; // Don't add this context.
|
|
|
3777 |
}
|
|
|
3778 |
|
|
|
3779 |
//-----------------------------------------------------------------------------------------
|
|
|
3780 |
// XPath ------ XPath AXIS Handlers ------
|
|
|
3781 |
//-----------------------------------------------------------------------------------------
|
|
|
3782 |
|
|
|
3783 |
/**
|
|
|
3784 |
* Retrieves axis information from an XPath query step.
|
|
|
3785 |
*
|
|
|
3786 |
* This method tries to extract the name of the axis and its node-test
|
|
|
3787 |
* from a given step of an XPath query at a given node. If it can't parse
|
|
|
3788 |
* the step, then we treat it as a PrimaryExpr.
|
|
|
3789 |
*
|
|
|
3790 |
* [4] Step ::= AxisSpecifier NodeTest Predicate*
|
|
|
3791 |
* | AbbreviatedStep
|
|
|
3792 |
* [5] AxisSpecifier ::= AxisName '::'
|
|
|
3793 |
* | AbbreviatedAxisSpecifier
|
|
|
3794 |
* [12] AbbreviatedStep ::= '.'
|
|
|
3795 |
* | '..'
|
|
|
3796 |
* [13] AbbreviatedAxisSpecifier
|
|
|
3797 |
* ::= '@'?
|
|
|
3798 |
*
|
|
|
3799 |
* [7] NodeTest ::= NameTest
|
|
|
3800 |
* | NodeType '(' ')'
|
|
|
3801 |
* | 'processing-instruction' '(' Literal ')'
|
|
|
3802 |
* [37] NameTest ::= '*'
|
|
|
3803 |
* | NCName ':' '*'
|
|
|
3804 |
* | QName
|
|
|
3805 |
* [38] NodeType ::= 'comment'
|
|
|
3806 |
* | 'text'
|
|
|
3807 |
* | 'processing-instruction'
|
|
|
3808 |
* | 'node'
|
|
|
3809 |
*
|
|
|
3810 |
* @param $step (string) String containing a step of an XPath query.
|
|
|
3811 |
* @return (array) Contains information about the axis found in the step, or FALSE
|
|
|
3812 |
* if the string isn't a valid step.
|
|
|
3813 |
* @see _evaluateStep()
|
|
|
3814 |
*/
|
|
|
3815 |
function _getAxis($step) {
|
|
|
3816 |
// The results of this function are very cachable, as it is completely independant of context.
|
|
|
3817 |
static $aResultsCache = array();
|
|
|
3818 |
|
|
|
3819 |
// Create an array to save the axis information.
|
|
|
3820 |
$axis = array(
|
|
|
3821 |
'axis' => '',
|
|
|
3822 |
'node-test' => '',
|
|
|
3823 |
'predicate' => array()
|
|
|
3824 |
);
|
|
|
3825 |
|
|
|
3826 |
$cacheKey = $step;
|
|
|
3827 |
do { // parse block
|
|
|
3828 |
$parseBlock = 1;
|
|
|
3829 |
|
|
|
3830 |
if (isset($aResultsCache[$cacheKey])) {
|
|
|
3831 |
return $aResultsCache[$cacheKey];
|
|
|
3832 |
} else {
|
|
|
3833 |
// We have some danger of causing recursion here if we refuse to parse a step as having an
|
|
|
3834 |
// axis, and demand it be treated as a PrimaryExpr. So if we are going to fail, make sure
|
|
|
3835 |
// we record what we tried, so that we can catch to see if it comes straight back.
|
|
|
3836 |
$guess = array(
|
|
|
3837 |
'axis' => 'child',
|
|
|
3838 |
'node-test' => $step,
|
|
|
3839 |
'predicate' => array());
|
|
|
3840 |
$aResultsCache[$cacheKey] = $guess;
|
|
|
3841 |
}
|
|
|
3842 |
|
|
|
3843 |
///////////////////////////////////////////////////
|
|
|
3844 |
// Spot the steps that won't come with an axis
|
|
|
3845 |
|
|
|
3846 |
// Check whether the step is empty or only self.
|
|
|
3847 |
if (empty($step) OR ($step == '.') OR ($step == 'current()')) {
|
|
|
3848 |
// Set it to the default value.
|
|
|
3849 |
$step = '.';
|
|
|
3850 |
$axis['axis'] = 'self';
|
|
|
3851 |
$axis['node-test'] = '*';
|
1393 |
richard |
3852 |
break;
|
325 |
richard |
3853 |
}
|
|
|
3854 |
|
|
|
3855 |
if ($step == '..') {
|
|
|
3856 |
// Select the parent axis.
|
|
|
3857 |
$axis['axis'] = 'parent';
|
|
|
3858 |
$axis['node-test'] = '*';
|
1393 |
richard |
3859 |
break;
|
325 |
richard |
3860 |
}
|
|
|
3861 |
|
|
|
3862 |
///////////////////////////////////////////////////
|
|
|
3863 |
// Pull off the predicates
|
|
|
3864 |
|
|
|
3865 |
// Check whether there are predicates and add the predicate to the list
|
|
|
3866 |
// of predicates without []. Get contents of every [] found.
|
|
|
3867 |
$groups = $this->_getEndGroups($step);
|
|
|
3868 |
//print_r($groups);
|
|
|
3869 |
$groupCount = count($groups);
|
|
|
3870 |
while (($groupCount > 0) && ($groups[$groupCount - 1][0] == '[')) {
|
|
|
3871 |
// Remove the [] and add the predicate to the top of the list
|
|
|
3872 |
$predicate = substr($groups[$groupCount - 1], 1, -1);
|
|
|
3873 |
array_unshift($axis['predicate'], $predicate);
|
|
|
3874 |
// Pop a group off the end of the list
|
|
|
3875 |
array_pop($groups);
|
|
|
3876 |
$groupCount--;
|
|
|
3877 |
}
|
|
|
3878 |
|
|
|
3879 |
// Finally stick the rest back together and this is the rest of our step
|
|
|
3880 |
if ($groupCount > 0) {
|
|
|
3881 |
$step = implode('', $groups);
|
|
|
3882 |
}
|
|
|
3883 |
|
|
|
3884 |
///////////////////////////////////////////////////
|
|
|
3885 |
// Pull off the axis
|
|
|
3886 |
|
|
|
3887 |
// Check for abbreviated syntax
|
|
|
3888 |
if ($step[0] == '@') {
|
|
|
3889 |
// Use the attribute axis and select the attribute.
|
|
|
3890 |
$axis['axis'] = 'attribute';
|
|
|
3891 |
$step = substr($step, 1);
|
|
|
3892 |
} else {
|
|
|
3893 |
// Check whether the axis is given in plain text.
|
|
|
3894 |
if (preg_match("/^([^:]*)::(.*)$/", $step, $match)) {
|
|
|
3895 |
// Split the step to extract axis and node-test.
|
|
|
3896 |
$axis['axis'] = $match[1];
|
|
|
3897 |
$step = $match[2];
|
|
|
3898 |
} else {
|
|
|
3899 |
// The default axis is child
|
|
|
3900 |
$axis['axis'] = 'child';
|
|
|
3901 |
}
|
|
|
3902 |
}
|
|
|
3903 |
|
|
|
3904 |
///////////////////////////////////////////////////
|
|
|
3905 |
// Process the rest which will either a node test, or else this isn't a step.
|
|
|
3906 |
|
|
|
3907 |
// Check whether is an abbreviated syntax.
|
|
|
3908 |
if ($step == '*') {
|
|
|
3909 |
// Use the child axis and select all children.
|
|
|
3910 |
$axis['node-test'] = '*';
|
1393 |
richard |
3911 |
break;
|
325 |
richard |
3912 |
}
|
|
|
3913 |
|
|
|
3914 |
// ### I'm pretty sure our current handling of cdata is a fudge, and we should
|
|
|
3915 |
// really do this better, but leave this as is for now.
|
|
|
3916 |
if ($step == "text()") {
|
|
|
3917 |
// Handle the text node
|
|
|
3918 |
$axis["node-test"] = "cdata";
|
1393 |
richard |
3919 |
break;
|
325 |
richard |
3920 |
}
|
|
|
3921 |
|
|
|
3922 |
// There are a few node tests that we match verbatim.
|
|
|
3923 |
if ($step == "node()"
|
|
|
3924 |
|| $step == "comment()"
|
|
|
3925 |
|| $step == "text()"
|
|
|
3926 |
|| $step == "processing-instruction") {
|
|
|
3927 |
$axis["node-test"] = $step;
|
1393 |
richard |
3928 |
break;
|
325 |
richard |
3929 |
}
|
|
|
3930 |
|
|
|
3931 |
// processing-instruction() is allowed to take an argument, but if it does, the argument
|
|
|
3932 |
// is a literal, which we will have parsed out to $[number].
|
|
|
3933 |
if (preg_match(":processing-instruction\(\$\d*\):", $step)) {
|
|
|
3934 |
$axis["node-test"] = $step;
|
1393 |
richard |
3935 |
break;
|
325 |
richard |
3936 |
}
|
|
|
3937 |
|
|
|
3938 |
// The only remaining way this can be a step, is if the remaining string is a simple name
|
|
|
3939 |
// or else a :* name.
|
|
|
3940 |
// http://www.w3.org/TR/xpath#NT-NameTest
|
|
|
3941 |
// NameTest ::= '*'
|
|
|
3942 |
// | NCName ':' '*'
|
|
|
3943 |
// | QName
|
|
|
3944 |
// QName ::= (Prefix ':')? LocalPart
|
|
|
3945 |
// Prefix ::= NCName
|
|
|
3946 |
// LocalPart ::= NCName
|
|
|
3947 |
//
|
|
|
3948 |
// ie
|
|
|
3949 |
// NameTest ::= '*'
|
|
|
3950 |
// | NCName ':' '*'
|
|
|
3951 |
// | (NCName ':')? NCName
|
|
|
3952 |
// NCName ::= (Letter | '_') (NCNameChar)*
|
|
|
3953 |
$NCName = "[a-zA-Z_][\w\.\-_]*";
|
|
|
3954 |
if (preg_match("/^$NCName:$NCName$/", $step)
|
|
|
3955 |
|| preg_match("/^$NCName:*$/", $step)) {
|
|
|
3956 |
$axis['node-test'] = $step;
|
|
|
3957 |
if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
|
|
|
3958 |
// Case in-sensitive
|
|
|
3959 |
$axis['node-test'] = strtoupper($axis['node-test']);
|
|
|
3960 |
}
|
|
|
3961 |
// Not currently recursing
|
|
|
3962 |
$LastFailedStep = '';
|
|
|
3963 |
$LastFailedContext = '';
|
1393 |
richard |
3964 |
break;
|
325 |
richard |
3965 |
}
|
|
|
3966 |
|
|
|
3967 |
// It's not a node then, we must treat it as a PrimaryExpr
|
|
|
3968 |
// Check for recursion
|
|
|
3969 |
if ($LastFailedStep == $step) {
|
|
|
3970 |
$this->_displayError('Recursion detected while parsing an XPath query, in the step ' .
|
|
|
3971 |
str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery)
|
|
|
3972 |
, __LINE__, __FILE__, FALSE);
|
|
|
3973 |
$axis['node-test'] = $step;
|
|
|
3974 |
} else {
|
|
|
3975 |
$LastFailedStep = $step;
|
|
|
3976 |
$axis = FALSE;
|
|
|
3977 |
}
|
|
|
3978 |
|
|
|
3979 |
} while(FALSE); // end parse block
|
|
|
3980 |
|
|
|
3981 |
// Check whether it's a valid axis.
|
|
|
3982 |
if ($axis !== FALSE) {
|
|
|
3983 |
if (!in_array($axis['axis'], array_merge($this->axes, array('function')))) {
|
|
|
3984 |
// Display an error message.
|
|
|
3985 |
$this->_displayError('While parsing an XPath query, in the step ' .
|
|
|
3986 |
str_replace($step, '<b>'.$step.'</b>', $this->currentXpathQuery) .
|
|
|
3987 |
' the invalid axis ' . $axis['axis'] . ' was found.', __LINE__, __FILE__, FALSE);
|
|
|
3988 |
}
|
|
|
3989 |
}
|
|
|
3990 |
|
|
|
3991 |
// Cache the real axis information
|
|
|
3992 |
$aResultsCache[$cacheKey] = $axis;
|
|
|
3993 |
|
|
|
3994 |
// Return the axis information.
|
|
|
3995 |
return $axis;
|
|
|
3996 |
}
|
|
|
3997 |
|
|
|
3998 |
|
|
|
3999 |
/**
|
|
|
4000 |
* Handles the XPath child axis.
|
|
|
4001 |
*
|
|
|
4002 |
* This method handles the XPath child axis. It essentially filters out the
|
|
|
4003 |
* children to match the name specified after the '/'.
|
|
|
4004 |
*
|
|
|
4005 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4006 |
* @param $contextPath (string) xpath to starting node from which the axis should
|
|
|
4007 |
* be processed.
|
|
|
4008 |
* @return (array) A vector containing all nodes that were found, during
|
|
|
4009 |
* the evaluation of the axis.
|
|
|
4010 |
* @see evaluate()
|
|
|
4011 |
*/
|
|
|
4012 |
function _handleAxis_child($axis, $contextPath) {
|
|
|
4013 |
$xPathSet = array(); // Create an empty node-set to hold the results of the child matches
|
|
|
4014 |
if ($axis["node-test"] == "cdata") {
|
|
|
4015 |
if (!isSet($this->nodeIndex[$contextPath]['textParts']) ) return '';
|
|
|
4016 |
$tSize = sizeOf($this->nodeIndex[$contextPath]['textParts']);
|
|
|
4017 |
for ($i=1; $i<=$tSize; $i++) {
|
|
|
4018 |
$xPathSet[] = $contextPath . '/text()['.$i.']';
|
|
|
4019 |
}
|
|
|
4020 |
}
|
|
|
4021 |
else {
|
|
|
4022 |
// Get a list of all children.
|
|
|
4023 |
$allChildren = $this->nodeIndex[$contextPath]['childNodes'];
|
|
|
4024 |
|
|
|
4025 |
// Run through all children in the order they where set.
|
|
|
4026 |
$cSize = sizeOf($allChildren);
|
|
|
4027 |
for ($i=0; $i<$cSize; $i++) {
|
|
|
4028 |
$childPath = $contextPath .'/'. $allChildren[$i]['name'] .'['. $allChildren[$i]['contextPos'] .']';
|
|
|
4029 |
$textChildPath = $contextPath.'/text()['.($i + 1).']';
|
|
|
4030 |
// Check the text node
|
|
|
4031 |
if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check
|
|
|
4032 |
$xPathSet[] = $textChildPath; // Add the child to the node-set.
|
|
|
4033 |
}
|
|
|
4034 |
// Check the actual node
|
|
|
4035 |
if ($this->_checkNodeTest($childPath, $axis['node-test'])) { // node test check
|
|
|
4036 |
$xPathSet[] = $childPath; // Add the child to the node-set.
|
|
|
4037 |
}
|
|
|
4038 |
}
|
|
|
4039 |
|
|
|
4040 |
// Finally there will be one more text node to try
|
|
|
4041 |
$textChildPath = $contextPath.'/text()['.($cSize + 1).']';
|
|
|
4042 |
// Check the text node
|
|
|
4043 |
if ($this->_checkNodeTest($textChildPath, $axis['node-test'])) { // node test check
|
|
|
4044 |
$xPathSet[] = $textChildPath; // Add the child to the node-set.
|
|
|
4045 |
}
|
|
|
4046 |
}
|
|
|
4047 |
return $xPathSet; // Return the nodeset.
|
|
|
4048 |
}
|
|
|
4049 |
|
|
|
4050 |
/**
|
|
|
4051 |
* Handles the XPath parent axis.
|
|
|
4052 |
*
|
|
|
4053 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4054 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4055 |
* @return (array) A vector containing all nodes that were found, during the
|
|
|
4056 |
* evaluation of the axis.
|
|
|
4057 |
* @see evaluate()
|
|
|
4058 |
*/
|
|
|
4059 |
function _handleAxis_parent($axis, $contextPath) {
|
|
|
4060 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4061 |
|
|
|
4062 |
// Check whether the parent matches the node-test.
|
|
|
4063 |
$parentPath = $this->getParentXPath($contextPath);
|
|
|
4064 |
if ($this->_checkNodeTest($parentPath, $axis['node-test'])) {
|
|
|
4065 |
$xPathSet[] = $parentPath; // Add this node to the list of nodes.
|
|
|
4066 |
}
|
|
|
4067 |
return $xPathSet; // Return the nodeset.
|
|
|
4068 |
}
|
|
|
4069 |
|
|
|
4070 |
/**
|
|
|
4071 |
* Handles the XPath attribute axis.
|
|
|
4072 |
*
|
|
|
4073 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4074 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4075 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4076 |
* @see evaluate()
|
|
|
4077 |
*/
|
|
|
4078 |
function _handleAxis_attribute($axis, $contextPath) {
|
|
|
4079 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4080 |
|
|
|
4081 |
// Check whether all nodes should be selected.
|
|
|
4082 |
$nodeAttr = $this->nodeIndex[$contextPath]['attributes'];
|
|
|
4083 |
if ($axis['node-test'] == '*'
|
|
|
4084 |
|| $axis['node-test'] == 'node()') {
|
|
|
4085 |
foreach($nodeAttr as $key=>$dummy) { // Run through the attributes.
|
|
|
4086 |
$xPathSet[] = $contextPath.'/attribute::'.$key; // Add this node to the node-set.
|
|
|
4087 |
}
|
|
|
4088 |
}
|
|
|
4089 |
elseif (isset($nodeAttr[$axis['node-test']])) {
|
|
|
4090 |
$xPathSet[] = $contextPath . '/attribute::'. $axis['node-test']; // Add this node to the node-set.
|
|
|
4091 |
}
|
|
|
4092 |
return $xPathSet; // Return the nodeset.
|
|
|
4093 |
}
|
|
|
4094 |
|
|
|
4095 |
/**
|
|
|
4096 |
* Handles the XPath self axis.
|
|
|
4097 |
*
|
|
|
4098 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4099 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4100 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4101 |
* @see evaluate()
|
|
|
4102 |
*/
|
|
|
4103 |
function _handleAxis_self($axis, $contextPath) {
|
|
|
4104 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4105 |
|
|
|
4106 |
// Check whether the context match the node-test.
|
|
|
4107 |
if ($this->_checkNodeTest($contextPath, $axis['node-test'])) {
|
|
|
4108 |
$xPathSet[] = $contextPath; // Add this node to the node-set.
|
|
|
4109 |
}
|
|
|
4110 |
return $xPathSet; // Return the nodeset.
|
|
|
4111 |
}
|
|
|
4112 |
|
|
|
4113 |
/**
|
|
|
4114 |
* Handles the XPath descendant axis.
|
|
|
4115 |
*
|
|
|
4116 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4117 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4118 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4119 |
* @see evaluate()
|
|
|
4120 |
*/
|
|
|
4121 |
function _handleAxis_descendant($axis, $contextPath) {
|
|
|
4122 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4123 |
|
|
|
4124 |
// Get a list of all children.
|
|
|
4125 |
$allChildren = $this->nodeIndex[$contextPath]['childNodes'];
|
|
|
4126 |
|
|
|
4127 |
// Run through all children in the order they where set.
|
|
|
4128 |
$cSize = sizeOf($allChildren);
|
|
|
4129 |
for ($i=0; $i<$cSize; $i++) {
|
|
|
4130 |
$childPath = $allChildren[$i]['xpath'];
|
|
|
4131 |
// Check whether the child matches the node-test.
|
|
|
4132 |
if ($this->_checkNodeTest($childPath, $axis['node-test'])) {
|
|
|
4133 |
$xPathSet[] = $childPath; // Add the child to the list of nodes.
|
|
|
4134 |
}
|
|
|
4135 |
// Recurse to the next level.
|
|
|
4136 |
$xPathSet = array_merge($xPathSet, $this->_handleAxis_descendant($axis, $childPath));
|
|
|
4137 |
}
|
|
|
4138 |
return $xPathSet; // Return the nodeset.
|
|
|
4139 |
}
|
|
|
4140 |
|
|
|
4141 |
/**
|
|
|
4142 |
* Handles the XPath ancestor axis.
|
|
|
4143 |
*
|
|
|
4144 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4145 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4146 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4147 |
* @see evaluate()
|
|
|
4148 |
*/
|
|
|
4149 |
function _handleAxis_ancestor($axis, $contextPath) {
|
|
|
4150 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4151 |
|
|
|
4152 |
$parentPath = $this->getParentXPath($contextPath); // Get the parent of the current node.
|
|
|
4153 |
|
|
|
4154 |
// Check whether the parent isn't super-root.
|
|
|
4155 |
if (!empty($parentPath)) {
|
|
|
4156 |
// Check whether the parent matches the node-test.
|
|
|
4157 |
if ($this->_checkNodeTest($parentPath, $axis['node-test'])) {
|
|
|
4158 |
$xPathSet[] = $parentPath; // Add the parent to the list of nodes.
|
|
|
4159 |
}
|
|
|
4160 |
// Handle all other ancestors.
|
|
|
4161 |
$xPathSet = array_merge($this->_handleAxis_ancestor($axis, $parentPath), $xPathSet);
|
|
|
4162 |
}
|
|
|
4163 |
return $xPathSet; // Return the nodeset.
|
|
|
4164 |
}
|
|
|
4165 |
|
|
|
4166 |
/**
|
|
|
4167 |
* Handles the XPath namespace axis.
|
|
|
4168 |
*
|
|
|
4169 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4170 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4171 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4172 |
* @see evaluate()
|
|
|
4173 |
*/
|
|
|
4174 |
function _handleAxis_namespace($axis, $contextPath) {
|
|
|
4175 |
$this->_displayError("The axis 'namespace is not suported'", __LINE__, __FILE__, FALSE);
|
|
|
4176 |
}
|
|
|
4177 |
|
|
|
4178 |
/**
|
|
|
4179 |
* Handles the XPath following axis.
|
|
|
4180 |
*
|
|
|
4181 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4182 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4183 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4184 |
* @see evaluate()
|
|
|
4185 |
*/
|
|
|
4186 |
function _handleAxis_following($axis, $contextPath) {
|
|
|
4187 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4188 |
|
|
|
4189 |
do { // try-block
|
|
|
4190 |
$node = $this->nodeIndex[$contextPath]; // Get the current node
|
|
|
4191 |
$position = $node['pos']; // Get the current tree position.
|
|
|
4192 |
$parent = $node['parentNode'];
|
|
|
4193 |
// Check if there is a following sibling at all; if not end.
|
|
|
4194 |
if ($position >= sizeOf($parent['childNodes'])) break; // try-block
|
|
|
4195 |
// Build the starting abs. XPath
|
|
|
4196 |
$startXPath = $parent['childNodes'][$position+1]['xpath'];
|
|
|
4197 |
// Run through all nodes of the document.
|
|
|
4198 |
$nodeKeys = array_keys($this->nodeIndex);
|
|
|
4199 |
$nodeSize = sizeOf($nodeKeys);
|
|
|
4200 |
for ($k=0; $k<$nodeSize; $k++) {
|
|
|
4201 |
if ($nodeKeys[$k] == $startXPath) break; // Check whether this is the starting abs. XPath
|
|
|
4202 |
}
|
|
|
4203 |
for (; $k<$nodeSize; $k++) {
|
|
|
4204 |
// Check whether the node fits the node-test.
|
|
|
4205 |
if ($this->_checkNodeTest($nodeKeys[$k], $axis['node-test'])) {
|
|
|
4206 |
$xPathSet[] = $nodeKeys[$k]; // Add the node to the list of nodes.
|
|
|
4207 |
}
|
|
|
4208 |
}
|
|
|
4209 |
} while(FALSE);
|
|
|
4210 |
return $xPathSet; // Return the nodeset.
|
|
|
4211 |
}
|
|
|
4212 |
|
|
|
4213 |
/**
|
|
|
4214 |
* Handles the XPath preceding axis.
|
|
|
4215 |
*
|
|
|
4216 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4217 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4218 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4219 |
* @see evaluate()
|
|
|
4220 |
*/
|
|
|
4221 |
function _handleAxis_preceding($axis, $contextPath) {
|
|
|
4222 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4223 |
|
|
|
4224 |
// Run through all nodes of the document.
|
|
|
4225 |
foreach ($this->nodeIndex as $xPath=>$dummy) {
|
|
|
4226 |
if (empty($xPath)) continue; // skip super-Root
|
|
|
4227 |
|
|
|
4228 |
// Check whether this is the context node.
|
|
|
4229 |
if ($xPath == $contextPath) {
|
|
|
4230 |
break; // After this we won't look for more nodes.
|
|
|
4231 |
}
|
|
|
4232 |
if (!strncmp($xPath, $contextPath, strLen($xPath))) {
|
|
|
4233 |
continue;
|
|
|
4234 |
}
|
|
|
4235 |
// Check whether the node fits the node-test.
|
|
|
4236 |
if ($this->_checkNodeTest($xPath, $axis['node-test'])) {
|
|
|
4237 |
$xPathSet[] = $xPath; // Add the node to the list of nodes.
|
|
|
4238 |
}
|
|
|
4239 |
}
|
|
|
4240 |
return $xPathSet; // Return the nodeset.
|
|
|
4241 |
}
|
|
|
4242 |
|
|
|
4243 |
/**
|
|
|
4244 |
* Handles the XPath following-sibling axis.
|
|
|
4245 |
*
|
|
|
4246 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4247 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4248 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4249 |
* @see evaluate()
|
|
|
4250 |
*/
|
|
|
4251 |
function _handleAxis_following_sibling($axis, $contextPath) {
|
|
|
4252 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4253 |
|
|
|
4254 |
// Get all children from the parent.
|
|
|
4255 |
$siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath));
|
|
|
4256 |
// Create a flag whether the context node was already found.
|
|
|
4257 |
$found = FALSE;
|
|
|
4258 |
|
|
|
4259 |
// Run through all siblings.
|
|
|
4260 |
$size = sizeOf($siblings);
|
|
|
4261 |
for ($i=0; $i<$size; $i++) {
|
|
|
4262 |
$sibling = $siblings[$i];
|
|
|
4263 |
|
|
|
4264 |
// Check whether the context node was already found.
|
|
|
4265 |
if ($found) {
|
|
|
4266 |
// Check whether the sibling matches the node-test.
|
|
|
4267 |
if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
|
|
|
4268 |
$xPathSet[] = $sibling; // Add the sibling to the list of nodes.
|
|
|
4269 |
}
|
|
|
4270 |
}
|
|
|
4271 |
// Check if we reached *this* context node.
|
|
|
4272 |
if ($sibling == $contextPath) {
|
|
|
4273 |
$found = TRUE; // Continue looking for other siblings.
|
|
|
4274 |
}
|
|
|
4275 |
}
|
|
|
4276 |
return $xPathSet; // Return the nodeset.
|
|
|
4277 |
}
|
|
|
4278 |
|
|
|
4279 |
/**
|
|
|
4280 |
* Handles the XPath preceding-sibling axis.
|
|
|
4281 |
*
|
|
|
4282 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4283 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4284 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4285 |
* @see evaluate()
|
|
|
4286 |
*/
|
|
|
4287 |
function _handleAxis_preceding_sibling($axis, $contextPath) {
|
|
|
4288 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4289 |
|
|
|
4290 |
// Get all children from the parent.
|
|
|
4291 |
$siblings = $this->_handleAxis_child($axis, $this->getParentXPath($contextPath));
|
|
|
4292 |
|
|
|
4293 |
// Run through all siblings.
|
|
|
4294 |
$size = sizeOf($siblings);
|
|
|
4295 |
for ($i=0; $i<$size; $i++) {
|
|
|
4296 |
$sibling = $siblings[$i];
|
|
|
4297 |
// Check whether this is the context node.
|
|
|
4298 |
if ($sibling == $contextPath) {
|
|
|
4299 |
break; // Don't continue looking for other siblings.
|
|
|
4300 |
}
|
|
|
4301 |
// Check whether the sibling matches the node-test.
|
|
|
4302 |
if ($this->_checkNodeTest($sibling, $axis['node-test'])) {
|
|
|
4303 |
$xPathSet[] = $sibling; // Add the sibling to the list of nodes.
|
|
|
4304 |
}
|
|
|
4305 |
}
|
|
|
4306 |
return $xPathSet; // Return the nodeset.
|
|
|
4307 |
}
|
|
|
4308 |
|
|
|
4309 |
/**
|
|
|
4310 |
* Handles the XPath descendant-or-self axis.
|
|
|
4311 |
*
|
|
|
4312 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4313 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4314 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4315 |
* @see evaluate()
|
|
|
4316 |
*/
|
|
|
4317 |
function _handleAxis_descendant_or_self($axis, $contextPath) {
|
|
|
4318 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4319 |
|
|
|
4320 |
// Read the nodes.
|
|
|
4321 |
$xPathSet = array_merge(
|
|
|
4322 |
$this->_handleAxis_self($axis, $contextPath),
|
|
|
4323 |
$this->_handleAxis_descendant($axis, $contextPath)
|
|
|
4324 |
);
|
|
|
4325 |
return $xPathSet; // Return the nodeset.
|
|
|
4326 |
}
|
|
|
4327 |
|
|
|
4328 |
/**
|
|
|
4329 |
* Handles the XPath ancestor-or-self axis.
|
|
|
4330 |
*
|
|
|
4331 |
* This method handles the XPath ancestor-or-self axis.
|
|
|
4332 |
*
|
|
|
4333 |
* @param $axis (array) Array containing information about the axis.
|
|
|
4334 |
* @param $contextPath (string) xpath to starting node from which the axis should be processed.
|
|
|
4335 |
* @return (array) A vector containing all nodes that were found, during the evaluation of the axis.
|
|
|
4336 |
* @see evaluate()
|
|
|
4337 |
*/
|
|
|
4338 |
function _handleAxis_ancestor_or_self ( $axis, $contextPath) {
|
|
|
4339 |
$xPathSet = array(); // Create an empty node-set.
|
|
|
4340 |
|
|
|
4341 |
// Read the nodes.
|
|
|
4342 |
$xPathSet = array_merge(
|
|
|
4343 |
$this->_handleAxis_ancestor($axis, $contextPath),
|
|
|
4344 |
$this->_handleAxis_self($axis, $contextPath)
|
|
|
4345 |
);
|
|
|
4346 |
return $xPathSet; // Return the nodeset.
|
|
|
4347 |
}
|
|
|
4348 |
|
|
|
4349 |
|
|
|
4350 |
//-----------------------------------------------------------------------------------------
|
|
|
4351 |
// XPath ------ XPath FUNCTION Handlers ------
|
|
|
4352 |
//-----------------------------------------------------------------------------------------
|
|
|
4353 |
|
|
|
4354 |
/**
|
|
|
4355 |
* Handles the XPath function last.
|
|
|
4356 |
*
|
|
|
4357 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4358 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4359 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4360 |
* @see evaluate()
|
|
|
4361 |
*/
|
|
|
4362 |
function _handleFunction_last($arguments, $context) {
|
|
|
4363 |
return $context['size'];
|
|
|
4364 |
}
|
|
|
4365 |
|
|
|
4366 |
/**
|
|
|
4367 |
* Handles the XPath function position.
|
|
|
4368 |
*
|
|
|
4369 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4370 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4371 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4372 |
* @see evaluate()
|
|
|
4373 |
*/
|
|
|
4374 |
function _handleFunction_position($arguments, $context) {
|
|
|
4375 |
return $context['pos'];
|
|
|
4376 |
}
|
|
|
4377 |
|
|
|
4378 |
/**
|
|
|
4379 |
* Handles the XPath function count.
|
|
|
4380 |
*
|
|
|
4381 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4382 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4383 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4384 |
* @see evaluate()
|
|
|
4385 |
*/
|
|
|
4386 |
function _handleFunction_count($arguments, $context) {
|
|
|
4387 |
// Evaluate the argument of the method as an XPath and return the number of results.
|
|
|
4388 |
return count($this->_evaluateExpr($arguments, $context));
|
|
|
4389 |
}
|
|
|
4390 |
|
|
|
4391 |
/**
|
|
|
4392 |
* Handles the XPath function id.
|
|
|
4393 |
*
|
|
|
4394 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4395 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4396 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4397 |
* @see evaluate()
|
|
|
4398 |
*/
|
|
|
4399 |
function _handleFunction_id($arguments, $context) {
|
|
|
4400 |
$arguments = trim($arguments); // Trim the arguments.
|
|
|
4401 |
$arguments = explode(' ', $arguments); // Now split the arguments into an array.
|
|
|
4402 |
// Create a list of nodes.
|
|
|
4403 |
$resultXPaths = array();
|
|
|
4404 |
// Run through all nodes of the document.
|
|
|
4405 |
$keys = array_keys($this->nodeIndex);
|
|
|
4406 |
$kSize = $sizeOf($keys);
|
|
|
4407 |
for ($i=0; $i<$kSize; $i++) {
|
|
|
4408 |
if (empty($keys[$i])) continue; // skip super-Root
|
|
|
4409 |
if (in_array($this->nodeIndex[$keys[$i]]['attributes']['id'], $arguments)) {
|
|
|
4410 |
$resultXPaths[] = $context['nodePath']; // Add this node to the list of nodes.
|
|
|
4411 |
}
|
|
|
4412 |
}
|
|
|
4413 |
return $resultXPaths; // Return the list of nodes.
|
|
|
4414 |
}
|
|
|
4415 |
|
|
|
4416 |
/**
|
|
|
4417 |
* Handles the XPath function name.
|
|
|
4418 |
*
|
|
|
4419 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4420 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4421 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4422 |
* @see evaluate()
|
|
|
4423 |
*/
|
|
|
4424 |
function _handleFunction_name($arguments, $context) {
|
|
|
4425 |
// If the argument it omitted, it defaults to a node-set with the context node as its only member.
|
|
|
4426 |
if (empty($arguments)) {
|
|
|
4427 |
return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['name']);
|
|
|
4428 |
}
|
|
|
4429 |
|
|
|
4430 |
// Evaluate the argument to get a node set.
|
|
|
4431 |
$nodeSet = $this->_evaluateExpr($arguments, $context);
|
|
|
4432 |
if (!is_array($nodeSet)) return '';
|
|
|
4433 |
if (count($nodeSet) < 1) return '';
|
|
|
4434 |
if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
|
|
|
4435 |
// Return a reference to the name of the node.
|
|
|
4436 |
return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['name']);
|
|
|
4437 |
}
|
|
|
4438 |
|
|
|
4439 |
/**
|
|
|
4440 |
* Handles the XPath function string.
|
|
|
4441 |
*
|
|
|
4442 |
* http://www.w3.org/TR/xpath#section-String-Functions
|
|
|
4443 |
*
|
|
|
4444 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4445 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4446 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4447 |
* @see evaluate()
|
|
|
4448 |
*/
|
|
|
4449 |
function _handleFunction_string($arguments, $context) {
|
|
|
4450 |
// Check what type of parameter is given
|
|
|
4451 |
if (is_array($arguments)) {
|
|
|
4452 |
// Get the value of the first result (which means we want to concat all the text...unless
|
|
|
4453 |
// a specific text() node has been given, and it will switch off to substringData
|
|
|
4454 |
if (!count($arguments)) $result = '';
|
|
|
4455 |
else {
|
|
|
4456 |
$result = $this->_stringValue($arguments[0]);
|
|
|
4457 |
if (($literal = $this->_asLiteral($result)) !== FALSE) {
|
|
|
4458 |
$result = $literal;
|
|
|
4459 |
}
|
|
|
4460 |
}
|
|
|
4461 |
}
|
|
|
4462 |
// Is it a number string?
|
|
|
4463 |
elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) OR preg_match('/^\.[0-9]+$/', $arguments)) {
|
|
|
4464 |
// ### Note no support for NaN and Infinity.
|
|
|
4465 |
$number = doubleval($arguments); // Convert the digits to a number.
|
|
|
4466 |
$result = strval($number); // Return the number.
|
|
|
4467 |
}
|
|
|
4468 |
elseif (is_bool($arguments)) { // Check whether it's TRUE or FALSE and return as string.
|
|
|
4469 |
// ### Note that we used to return TRUE and FALSE which was incorrect according to the standard.
|
|
|
4470 |
if ($arguments === TRUE) {
|
|
|
4471 |
$result = 'true';
|
|
|
4472 |
} else {
|
|
|
4473 |
$result = 'false';
|
|
|
4474 |
}
|
|
|
4475 |
}
|
|
|
4476 |
elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
|
|
|
4477 |
return $literal;
|
|
|
4478 |
}
|
|
|
4479 |
elseif (!empty($arguments)) {
|
|
|
4480 |
// Spec says:
|
|
|
4481 |
// "An object of a type other than the four basic types is converted to a string in a way that
|
|
|
4482 |
// is dependent on that type."
|
|
|
4483 |
// Use the argument as an XPath.
|
|
|
4484 |
$result = $this->_evaluateExpr($arguments, $context);
|
|
|
4485 |
if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
|
|
|
4486 |
$this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_string($result)", __LINE__, __FILE__, FALSE);
|
|
|
4487 |
return '';
|
|
|
4488 |
} else {
|
|
|
4489 |
$result = $this->_handleFunction_string($result, $context);
|
|
|
4490 |
}
|
|
|
4491 |
}
|
|
|
4492 |
else {
|
|
|
4493 |
$result = ''; // Return an empty string.
|
|
|
4494 |
}
|
|
|
4495 |
return $result;
|
|
|
4496 |
}
|
|
|
4497 |
|
|
|
4498 |
/**
|
|
|
4499 |
* Handles the XPath function concat.
|
|
|
4500 |
*
|
|
|
4501 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4502 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4503 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4504 |
* @see evaluate()
|
|
|
4505 |
*/
|
|
|
4506 |
function _handleFunction_concat($arguments, $context) {
|
|
|
4507 |
// Split the arguments.
|
|
|
4508 |
$arguments = explode(',', $arguments);
|
|
|
4509 |
// Run through each argument and evaluate it.
|
|
|
4510 |
$size = sizeof($arguments);
|
|
|
4511 |
for ($i=0; $i<$size; $i++) {
|
|
|
4512 |
$arguments[$i] = trim($arguments[$i]); // Trim each argument.
|
|
|
4513 |
// Evaluate it.
|
|
|
4514 |
$arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
|
|
|
4515 |
}
|
|
|
4516 |
$arguments = implode('', $arguments); // Put the string together and return it.
|
|
|
4517 |
return $this->_addLiteral($arguments);
|
|
|
4518 |
}
|
|
|
4519 |
|
|
|
4520 |
/**
|
|
|
4521 |
* Handles the XPath function starts-with.
|
|
|
4522 |
*
|
|
|
4523 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4524 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4525 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4526 |
* @see evaluate()
|
|
|
4527 |
*/
|
|
|
4528 |
function _handleFunction_starts_with($arguments, $context) {
|
|
|
4529 |
// Get the arguments.
|
|
|
4530 |
$first = trim($this->_prestr($arguments, ','));
|
|
|
4531 |
$second = trim($this->_afterstr($arguments, ','));
|
|
|
4532 |
// Evaluate each argument.
|
|
|
4533 |
$first = $this->_handleFunction_string($first, $context);
|
|
|
4534 |
$second = $this->_handleFunction_string($second, $context);
|
|
|
4535 |
// Check whether the first string starts with the second one.
|
|
|
4536 |
return (bool) ereg('^'.$second, $first);
|
|
|
4537 |
}
|
|
|
4538 |
|
|
|
4539 |
/**
|
|
|
4540 |
* Handles the XPath function contains.
|
|
|
4541 |
*
|
|
|
4542 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4543 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4544 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4545 |
* @see evaluate()
|
|
|
4546 |
*/
|
|
|
4547 |
function _handleFunction_contains($arguments, $context) {
|
|
|
4548 |
// Get the arguments.
|
|
|
4549 |
$first = trim($this->_prestr($arguments, ','));
|
|
|
4550 |
$second = trim($this->_afterstr($arguments, ','));
|
|
|
4551 |
//echo "Predicate: $arguments First: ".$first." Second: ".$second."\n";
|
|
|
4552 |
// Evaluate each argument.
|
|
|
4553 |
$first = $this->_handleFunction_string($first, $context);
|
|
|
4554 |
$second = $this->_handleFunction_string($second, $context);
|
|
|
4555 |
//echo $second.": ".$first."\n";
|
|
|
4556 |
// If the search string is null, then the provided there is a value it will contain it as
|
|
|
4557 |
// it is considered that all strings contain the empty string. ## N.S.
|
|
|
4558 |
if ($second==='') return TRUE;
|
|
|
4559 |
// Check whether the first string starts with the second one.
|
|
|
4560 |
if (strpos($first, $second) === FALSE) {
|
|
|
4561 |
return FALSE;
|
|
|
4562 |
} else {
|
|
|
4563 |
return TRUE;
|
|
|
4564 |
}
|
|
|
4565 |
}
|
|
|
4566 |
|
|
|
4567 |
/**
|
|
|
4568 |
* Handles the XPath function substring-before.
|
|
|
4569 |
*
|
|
|
4570 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4571 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4572 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4573 |
* @see evaluate()
|
|
|
4574 |
*/
|
|
|
4575 |
function _handleFunction_substring_before($arguments, $context) {
|
|
|
4576 |
// Get the arguments.
|
|
|
4577 |
$first = trim($this->_prestr($arguments, ','));
|
|
|
4578 |
$second = trim($this->_afterstr($arguments, ','));
|
|
|
4579 |
// Evaluate each argument.
|
|
|
4580 |
$first = $this->_handleFunction_string($first, $context);
|
|
|
4581 |
$second = $this->_handleFunction_string($second, $context);
|
|
|
4582 |
// Return the substring.
|
|
|
4583 |
return $this->_addLiteral($this->_prestr(strval($first), strval($second)));
|
|
|
4584 |
}
|
|
|
4585 |
|
|
|
4586 |
/**
|
|
|
4587 |
* Handles the XPath function substring-after.
|
|
|
4588 |
*
|
|
|
4589 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4590 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4591 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4592 |
* @see evaluate()
|
|
|
4593 |
*/
|
|
|
4594 |
function _handleFunction_substring_after($arguments, $context) {
|
|
|
4595 |
// Get the arguments.
|
|
|
4596 |
$first = trim($this->_prestr($arguments, ','));
|
|
|
4597 |
$second = trim($this->_afterstr($arguments, ','));
|
|
|
4598 |
// Evaluate each argument.
|
|
|
4599 |
$first = $this->_handleFunction_string($first, $context);
|
|
|
4600 |
$second = $this->_handleFunction_string($second, $context);
|
|
|
4601 |
// Return the substring.
|
|
|
4602 |
return $this->_addLiteral($this->_afterstr(strval($first), strval($second)));
|
|
|
4603 |
}
|
|
|
4604 |
|
|
|
4605 |
/**
|
|
|
4606 |
* Handles the XPath function substring.
|
|
|
4607 |
*
|
|
|
4608 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4609 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4610 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4611 |
* @see evaluate()
|
|
|
4612 |
*/
|
|
|
4613 |
function _handleFunction_substring($arguments, $context) {
|
|
|
4614 |
// Split the arguments.
|
|
|
4615 |
$arguments = explode(",", $arguments);
|
|
|
4616 |
$size = sizeOf($arguments);
|
|
|
4617 |
for ($i=0; $i<$size; $i++) { // Run through all arguments.
|
|
|
4618 |
$arguments[$i] = trim($arguments[$i]); // Trim the string.
|
|
|
4619 |
// Evaluate each argument.
|
|
|
4620 |
$arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
|
|
|
4621 |
}
|
|
|
4622 |
// Check whether a third argument was given and return the substring..
|
|
|
4623 |
if (!empty($arguments[2])) {
|
|
|
4624 |
return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1, $arguments[2]));
|
|
|
4625 |
} else {
|
|
|
4626 |
return $this->_addLiteral(substr(strval($arguments[0]), $arguments[1] - 1));
|
|
|
4627 |
}
|
|
|
4628 |
}
|
|
|
4629 |
|
|
|
4630 |
/**
|
|
|
4631 |
* Handles the XPath function string-length.
|
|
|
4632 |
*
|
|
|
4633 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4634 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4635 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4636 |
* @see evaluate()
|
|
|
4637 |
*/
|
|
|
4638 |
function _handleFunction_string_length($arguments, $context) {
|
|
|
4639 |
$arguments = trim($arguments); // Trim the argument.
|
|
|
4640 |
// Evaluate the argument.
|
|
|
4641 |
$arguments = $this->_handleFunction_string($arguments, $context);
|
|
|
4642 |
return strlen(strval($arguments)); // Return the length of the string.
|
|
|
4643 |
}
|
|
|
4644 |
|
|
|
4645 |
/**
|
|
|
4646 |
* Handles the XPath function normalize-space.
|
|
|
4647 |
*
|
|
|
4648 |
* The normalize-space function returns the argument string with whitespace
|
|
|
4649 |
* normalized by stripping leading and trailing whitespace and replacing sequences
|
|
|
4650 |
* of whitespace characters by a single space.
|
|
|
4651 |
* If the argument is omitted, it defaults to the context node converted to a string,
|
|
|
4652 |
* in other words the string-value of the context node
|
|
|
4653 |
*
|
|
|
4654 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4655 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4656 |
* @return (stri)g trimed string
|
|
|
4657 |
* @see evaluate()
|
|
|
4658 |
*/
|
|
|
4659 |
function _handleFunction_normalize_space($arguments, $context) {
|
|
|
4660 |
if (empty($arguments)) {
|
|
|
4661 |
$arguments = $this->getParentXPath($context['nodePath']).'/'.$this->nodeIndex[$context['nodePath']]['name'].'['.$this->nodeIndex[$context['nodePath']]['contextPos'].']';
|
|
|
4662 |
} else {
|
|
|
4663 |
$arguments = $this->_handleFunction_string($arguments, $context);
|
|
|
4664 |
}
|
|
|
4665 |
$arguments = trim(preg_replace (";[[:space:]]+;s",' ',$arguments));
|
|
|
4666 |
return $this->_addLiteral($arguments);
|
|
|
4667 |
}
|
|
|
4668 |
|
|
|
4669 |
/**
|
|
|
4670 |
* Handles the XPath function translate.
|
|
|
4671 |
*
|
|
|
4672 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4673 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4674 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4675 |
* @see evaluate()
|
|
|
4676 |
*/
|
|
|
4677 |
function _handleFunction_translate($arguments, $context) {
|
|
|
4678 |
$arguments = explode(',', $arguments); // Split the arguments.
|
|
|
4679 |
$size = sizeOf($arguments);
|
|
|
4680 |
for ($i=0; $i<$size; $i++) { // Run through all arguments.
|
|
|
4681 |
$arguments[$i] = trim($arguments[$i]); // Trim the argument.
|
|
|
4682 |
// Evaluate the argument.
|
|
|
4683 |
$arguments[$i] = $this->_handleFunction_string($arguments[$i], $context);
|
|
|
4684 |
}
|
|
|
4685 |
// Return the translated string.
|
|
|
4686 |
return $this->_addLiteral(strtr($arguments[0], $arguments[1], $arguments[2]));
|
|
|
4687 |
}
|
|
|
4688 |
|
|
|
4689 |
/**
|
|
|
4690 |
* Handles the XPath function boolean.
|
|
|
4691 |
*
|
|
|
4692 |
* http://www.w3.org/TR/xpath#section-Boolean-Functions
|
|
|
4693 |
*
|
|
|
4694 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4695 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4696 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4697 |
* @see evaluate()
|
|
|
4698 |
*/
|
|
|
4699 |
function _handleFunction_boolean($arguments, $context) {
|
|
|
4700 |
if (empty($arguments)) {
|
|
|
4701 |
return FALSE; // Sorry, there were no arguments.
|
|
|
4702 |
}
|
|
|
4703 |
// a bool is dead obvious
|
|
|
4704 |
elseif (is_bool($arguments)) {
|
|
|
4705 |
return $arguments;
|
|
|
4706 |
}
|
|
|
4707 |
// a node-set is true if and only if it is non-empty
|
|
|
4708 |
elseif (is_array($arguments)) {
|
|
|
4709 |
return (count($arguments) > 0);
|
|
|
4710 |
}
|
|
|
4711 |
// a number is true if and only if it is neither positive or negative zero nor NaN
|
|
|
4712 |
// (Straight out of the XPath spec.. makes no sense?????)
|
|
|
4713 |
elseif (preg_match('/^[0-9]+(\.[0-9]+)?$/', $arguments) || preg_match('/^\.[0-9]+$/', $arguments)) {
|
|
|
4714 |
$number = doubleval($arguments); // Convert the digits to a number.
|
|
|
4715 |
// If number zero return FALSE else TRUE.
|
|
|
4716 |
if ($number == 0) return FALSE; else return TRUE;
|
|
|
4717 |
}
|
|
|
4718 |
// a string is true if and only if its length is non-zero
|
|
|
4719 |
elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
|
|
|
4720 |
return (strlen($literal) != 0);
|
|
|
4721 |
}
|
|
|
4722 |
// an object of a type other than the four basic types is converted to a boolean in a
|
|
|
4723 |
// way that is dependent on that type
|
|
|
4724 |
else {
|
|
|
4725 |
// Spec says:
|
|
|
4726 |
// "An object of a type other than the four basic types is converted to a number in a way
|
|
|
4727 |
// that is dependent on that type"
|
|
|
4728 |
// Try to evaluate the argument as an XPath.
|
|
|
4729 |
$result = $this->_evaluateExpr($arguments, $context);
|
|
|
4730 |
if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
|
|
|
4731 |
$this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_boolean($result)", __LINE__, __FILE__, FALSE);
|
|
|
4732 |
return FALSE;
|
|
|
4733 |
} else {
|
|
|
4734 |
return $this->_handleFunction_boolean($result, $context);
|
|
|
4735 |
}
|
|
|
4736 |
}
|
|
|
4737 |
}
|
|
|
4738 |
|
|
|
4739 |
/**
|
|
|
4740 |
* Handles the XPath function not.
|
|
|
4741 |
*
|
|
|
4742 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4743 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4744 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4745 |
* @see evaluate()
|
|
|
4746 |
*/
|
|
|
4747 |
function _handleFunction_not($arguments, $context) {
|
|
|
4748 |
// Return the negative value of the content of the brackets.
|
|
|
4749 |
$bArgResult = $this->_handleFunction_boolean($arguments, $context);
|
|
|
4750 |
//echo "Before inversion: ".($bArgResult?"TRUE":"FALSE")."\n";
|
|
|
4751 |
return !$bArgResult;
|
|
|
4752 |
}
|
|
|
4753 |
|
|
|
4754 |
/**
|
|
|
4755 |
* Handles the XPath function TRUE.
|
|
|
4756 |
*
|
|
|
4757 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4758 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4759 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4760 |
* @see evaluate()
|
|
|
4761 |
*/
|
|
|
4762 |
function _handleFunction_true($arguments, $context) {
|
|
|
4763 |
return TRUE; // Return TRUE.
|
|
|
4764 |
}
|
|
|
4765 |
|
|
|
4766 |
/**
|
|
|
4767 |
* Handles the XPath function FALSE.
|
|
|
4768 |
*
|
|
|
4769 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4770 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4771 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4772 |
* @see evaluate()
|
|
|
4773 |
*/
|
|
|
4774 |
function _handleFunction_false($arguments, $context) {
|
|
|
4775 |
return FALSE; // Return FALSE.
|
|
|
4776 |
}
|
|
|
4777 |
|
|
|
4778 |
/**
|
|
|
4779 |
* Handles the XPath function lang.
|
|
|
4780 |
*
|
|
|
4781 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4782 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4783 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4784 |
* @see evaluate()
|
|
|
4785 |
*/
|
|
|
4786 |
function _handleFunction_lang($arguments, $context) {
|
|
|
4787 |
$arguments = trim($arguments); // Trim the arguments.
|
|
|
4788 |
$currentNode = $this->nodeIndex[$context['nodePath']];
|
|
|
4789 |
while (!empty($currentNode['name'])) { // Run through the ancestors.
|
|
|
4790 |
// Check whether the node has an language attribute.
|
|
|
4791 |
if (isSet($currentNode['attributes']['xml:lang'])) {
|
|
|
4792 |
// Check whether it's the language, the user asks for; if so return TRUE else FALSE
|
|
|
4793 |
return eregi('^'.$arguments, $currentNode['attributes']['xml:lang']);
|
|
|
4794 |
}
|
|
|
4795 |
$currentNode = $currentNode['parentNode']; // Move up to parent
|
|
|
4796 |
} // End while
|
|
|
4797 |
return FALSE;
|
|
|
4798 |
}
|
|
|
4799 |
|
|
|
4800 |
/**
|
|
|
4801 |
* Handles the XPath function number.
|
|
|
4802 |
*
|
|
|
4803 |
* http://www.w3.org/TR/xpath#section-Number-Functions
|
|
|
4804 |
*
|
|
|
4805 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4806 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4807 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4808 |
* @see evaluate()
|
|
|
4809 |
*/
|
|
|
4810 |
function _handleFunction_number($arguments, $context) {
|
|
|
4811 |
// Check the type of argument.
|
|
|
4812 |
|
|
|
4813 |
// A string that is a number
|
|
|
4814 |
if (is_numeric($arguments)) {
|
|
|
4815 |
return doubleval($arguments); // Return the argument as a number.
|
|
|
4816 |
}
|
|
|
4817 |
// A bool
|
|
|
4818 |
elseif (is_bool($arguments)) { // Return TRUE/FALSE as a number.
|
|
|
4819 |
if ($arguments === TRUE) return 1; else return 0;
|
|
|
4820 |
}
|
|
|
4821 |
// A node set
|
|
|
4822 |
elseif (is_array($arguments)) {
|
|
|
4823 |
// Is converted to a string then handled like a string
|
|
|
4824 |
$string = $this->_handleFunction_string($arguments, $context);
|
|
|
4825 |
if (is_numeric($string))
|
|
|
4826 |
return doubleval($string);
|
|
|
4827 |
}
|
|
|
4828 |
elseif (($literal = $this->_asLiteral($arguments)) !== FALSE) {
|
|
|
4829 |
if (is_numeric($literal)) {
|
|
|
4830 |
return doubleval($literal);
|
|
|
4831 |
} else {
|
|
|
4832 |
// If we are to stick strictly to the spec, we should return NaN, but lets just
|
|
|
4833 |
// leave PHP to see if can do some dynamic conversion.
|
|
|
4834 |
return $literal;
|
|
|
4835 |
}
|
|
|
4836 |
}
|
|
|
4837 |
else {
|
|
|
4838 |
// Spec says:
|
|
|
4839 |
// "An object of a type other than the four basic types is converted to a number in a way
|
|
|
4840 |
// that is dependent on that type"
|
|
|
4841 |
// Try to evaluate the argument as an XPath.
|
|
|
4842 |
$result = $this->_evaluateExpr($arguments, $context);
|
|
|
4843 |
if (is_string($result) && is_string($arguments) && (!strcmp($result, $arguments))) {
|
|
|
4844 |
$this->_displayError("Loop detected in XPath expression. Probably an internal error :o/. _handleFunction_number($result)", __LINE__, __FILE__, FALSE);
|
|
|
4845 |
return FALSE;
|
|
|
4846 |
} else {
|
|
|
4847 |
return $this->_handleFunction_number($result, $context);
|
|
|
4848 |
}
|
|
|
4849 |
}
|
|
|
4850 |
}
|
|
|
4851 |
|
|
|
4852 |
/**
|
|
|
4853 |
* Handles the XPath function sum.
|
|
|
4854 |
*
|
|
|
4855 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4856 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4857 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4858 |
* @see evaluate()
|
|
|
4859 |
*/
|
|
|
4860 |
function _handleFunction_sum($arguments, $context) {
|
|
|
4861 |
$arguments = trim($arguments); // Trim the arguments.
|
|
|
4862 |
// Evaluate the arguments as an XPath query.
|
|
|
4863 |
$result = $this->_evaluateExpr($arguments, $context);
|
|
|
4864 |
$sum = 0; // Create a variable to save the sum.
|
|
|
4865 |
// The sum function expects a node set as an argument.
|
|
|
4866 |
if (is_array($result)) {
|
|
|
4867 |
// Run through all results.
|
|
|
4868 |
$size = sizeOf($result);
|
|
|
4869 |
for ($i=0; $i<$size; $i++) {
|
|
|
4870 |
$value = $this->_stringValue($result[$i], $context);
|
|
|
4871 |
if (($literal = $this->_asLiteral($value)) !== FALSE) {
|
|
|
4872 |
$value = $literal;
|
|
|
4873 |
}
|
|
|
4874 |
$sum += doubleval($value); // Add it to the sum.
|
|
|
4875 |
}
|
|
|
4876 |
}
|
|
|
4877 |
return $sum; // Return the sum.
|
|
|
4878 |
}
|
|
|
4879 |
|
|
|
4880 |
/**
|
|
|
4881 |
* Handles the XPath function floor.
|
|
|
4882 |
*
|
|
|
4883 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4884 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4885 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4886 |
* @see evaluate()
|
|
|
4887 |
*/
|
|
|
4888 |
function _handleFunction_floor($arguments, $context) {
|
|
|
4889 |
if (!is_numeric($arguments)) {
|
|
|
4890 |
$arguments = $this->_handleFunction_number($arguments, $context);
|
|
|
4891 |
}
|
|
|
4892 |
$arguments = doubleval($arguments); // Convert the arguments to a number.
|
|
|
4893 |
return floor($arguments); // Return the result
|
|
|
4894 |
}
|
|
|
4895 |
|
|
|
4896 |
/**
|
|
|
4897 |
* Handles the XPath function ceiling.
|
|
|
4898 |
*
|
|
|
4899 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4900 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4901 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4902 |
* @see evaluate()
|
|
|
4903 |
*/
|
|
|
4904 |
function _handleFunction_ceiling($arguments, $context) {
|
|
|
4905 |
if (!is_numeric($arguments)) {
|
|
|
4906 |
$arguments = $this->_handleFunction_number($arguments, $context);
|
|
|
4907 |
}
|
|
|
4908 |
$arguments = doubleval($arguments); // Convert the arguments to a number.
|
|
|
4909 |
return ceil($arguments); // Return the result
|
|
|
4910 |
}
|
|
|
4911 |
|
|
|
4912 |
/**
|
|
|
4913 |
* Handles the XPath function round.
|
|
|
4914 |
*
|
|
|
4915 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4916 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4917 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4918 |
* @see evaluate()
|
|
|
4919 |
*/
|
|
|
4920 |
function _handleFunction_round($arguments, $context) {
|
|
|
4921 |
if (!is_numeric($arguments)) {
|
|
|
4922 |
$arguments = $this->_handleFunction_number($arguments, $context);
|
|
|
4923 |
}
|
|
|
4924 |
$arguments = doubleval($arguments); // Convert the arguments to a number.
|
|
|
4925 |
return round($arguments); // Return the result
|
|
|
4926 |
}
|
|
|
4927 |
|
|
|
4928 |
//-----------------------------------------------------------------------------------------
|
|
|
4929 |
// XPath ------ XPath Extension FUNCTION Handlers ------
|
|
|
4930 |
//-----------------------------------------------------------------------------------------
|
|
|
4931 |
|
|
|
4932 |
/**
|
|
|
4933 |
* Handles the XPath function x-lower.
|
|
|
4934 |
*
|
|
|
4935 |
* lower case a string.
|
|
|
4936 |
* string x-lower(string)
|
|
|
4937 |
*
|
|
|
4938 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4939 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4940 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4941 |
* @see evaluate()
|
|
|
4942 |
*/
|
|
|
4943 |
function _handleFunction_x_lower($arguments, $context) {
|
|
|
4944 |
// Evaluate the argument.
|
|
|
4945 |
$string = $this->_handleFunction_string($arguments, $context);
|
|
|
4946 |
// Return a reference to the lowercased string
|
|
|
4947 |
return $this->_addLiteral(strtolower(strval($string)));
|
|
|
4948 |
}
|
|
|
4949 |
|
|
|
4950 |
/**
|
|
|
4951 |
* Handles the XPath function x-upper.
|
|
|
4952 |
*
|
|
|
4953 |
* upper case a string.
|
|
|
4954 |
* string x-upper(string)
|
|
|
4955 |
*
|
|
|
4956 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4957 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4958 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4959 |
* @see evaluate()
|
|
|
4960 |
*/
|
|
|
4961 |
function _handleFunction_x_upper($arguments, $context) {
|
|
|
4962 |
// Evaluate the argument.
|
|
|
4963 |
$string = $this->_handleFunction_string($arguments, $context);
|
|
|
4964 |
// Return a reference to the lowercased string
|
|
|
4965 |
return $this->_addLiteral(strtoupper(strval($string)));
|
|
|
4966 |
}
|
|
|
4967 |
|
|
|
4968 |
/**
|
|
|
4969 |
* Handles the XPath function generate-id.
|
|
|
4970 |
*
|
|
|
4971 |
* Produce a unique id for the first node of the node set.
|
|
|
4972 |
*
|
|
|
4973 |
* Example usage, produces an index of all the nodes in an .xml document, where the content of each
|
|
|
4974 |
* "section" is the exported node as XML.
|
|
|
4975 |
*
|
|
|
4976 |
* $aFunctions = $xPath->match('//');
|
|
|
4977 |
*
|
|
|
4978 |
* foreach ($aFunctions as $Function) {
|
|
|
4979 |
* $id = $xPath->match("generate-id($Function)");
|
|
|
4980 |
* echo "<a href='#$id'>$Function</a><br>";
|
|
|
4981 |
* }
|
|
|
4982 |
*
|
|
|
4983 |
* foreach ($aFunctions as $Function) {
|
|
|
4984 |
* $id = $xPath->match("generate-id($Function)");
|
|
|
4985 |
* echo "<h2 id='$id'>$Function</h2>";
|
|
|
4986 |
* echo htmlspecialchars($xPath->exportAsXml($Function));
|
|
|
4987 |
* }
|
|
|
4988 |
*
|
|
|
4989 |
* @param $arguments (string) String containing the arguments that were passed to the function.
|
|
|
4990 |
* @param $context (array) The context from which to evaluate the function
|
|
|
4991 |
* @return (mixed) Depending on the type of function being processed
|
|
|
4992 |
* @author Ricardo Garcia
|
|
|
4993 |
* @see evaluate()
|
|
|
4994 |
*/
|
|
|
4995 |
function _handleFunction_generate_id($arguments, $context) {
|
|
|
4996 |
// If the argument is omitted, it defaults to a node-set with the context node as its only member.
|
|
|
4997 |
if (is_string($arguments) && empty($arguments)) {
|
|
|
4998 |
// We need ids then
|
|
|
4999 |
$this->_generate_ids();
|
|
|
5000 |
return $this->_addLiteral($this->nodeIndex[$context['nodePath']]['generated_id']);
|
|
|
5001 |
}
|
|
|
5002 |
|
|
|
5003 |
// Evaluate the argument to get a node set.
|
|
|
5004 |
$nodeSet = $this->_evaluateExpr($arguments, $context);
|
|
|
5005 |
|
|
|
5006 |
if (!is_array($nodeSet)) return '';
|
|
|
5007 |
if (count($nodeSet) < 1) return '';
|
|
|
5008 |
if (!isset($this->nodeIndex[$nodeSet[0]])) return '';
|
|
|
5009 |
// Return a reference to the name of the node.
|
|
|
5010 |
// We need ids then
|
|
|
5011 |
$this->_generate_ids();
|
|
|
5012 |
return $this->_addLiteral($this->nodeIndex[$nodeSet[0]]['generated_id']);
|
|
|
5013 |
}
|
|
|
5014 |
|
|
|
5015 |
//-----------------------------------------------------------------------------------------
|
|
|
5016 |
// XPathEngine ------ Help Stuff ------
|
|
|
5017 |
//-----------------------------------------------------------------------------------------
|
|
|
5018 |
|
|
|
5019 |
/**
|
|
|
5020 |
* Decodes the character set entities in the given string.
|
|
|
5021 |
*
|
|
|
5022 |
* This function is given for convenience, as all text strings or attributes
|
|
|
5023 |
* are going to come back to you with their entities still encoded. You can
|
|
|
5024 |
* use this function to remove these entites.
|
|
|
5025 |
*
|
|
|
5026 |
* It makes use of the get_html_translation_table(HTML_ENTITIES) php library
|
|
|
5027 |
* call, so is limited in the same ways. At the time of writing this seemed
|
|
|
5028 |
* be restricted to iso-8859-1
|
|
|
5029 |
*
|
|
|
5030 |
* ### Provide an option that will do this by default.
|
|
|
5031 |
*
|
|
|
5032 |
* @param $encodedData (mixed) The string or array that has entities you would like to remove
|
|
|
5033 |
* @param $reverse (bool) If TRUE entities will be encoded rather than decoded, ie
|
|
|
5034 |
* < to < rather than < to <.
|
|
|
5035 |
* @return (mixed) The string or array returned with entities decoded.
|
|
|
5036 |
*/
|
|
|
5037 |
function decodeEntities($encodedData, $reverse=FALSE) {
|
|
|
5038 |
static $aEncodeTbl;
|
|
|
5039 |
static $aDecodeTbl;
|
|
|
5040 |
// Get the translation entities, but we'll cache the result to enhance performance.
|
|
|
5041 |
if (empty($aDecodeTbl)) {
|
|
|
5042 |
// Get the translation entities.
|
|
|
5043 |
$aEncodeTbl = get_html_translation_table(HTML_ENTITIES);
|
|
|
5044 |
$aDecodeTbl = array_flip($aEncodeTbl);
|
|
|
5045 |
}
|
|
|
5046 |
|
|
|
5047 |
// If it's just a single string.
|
|
|
5048 |
if (!is_array($encodedData)) {
|
|
|
5049 |
if ($reverse) {
|
|
|
5050 |
return strtr($encodedData, $aEncodeTbl);
|
|
|
5051 |
} else {
|
|
|
5052 |
return strtr($encodedData, $aDecodeTbl);
|
|
|
5053 |
}
|
|
|
5054 |
}
|
|
|
5055 |
|
|
|
5056 |
$result = array();
|
|
|
5057 |
foreach($encodedData as $string) {
|
|
|
5058 |
if ($reverse) {
|
|
|
5059 |
$result[] = strtr($string, $aEncodeTbl);
|
|
|
5060 |
} else {
|
|
|
5061 |
$result[] = strtr($string, $aDecodeTbl);
|
|
|
5062 |
}
|
|
|
5063 |
}
|
|
|
5064 |
|
|
|
5065 |
return $result;
|
|
|
5066 |
}
|
|
|
5067 |
|
|
|
5068 |
/**
|
|
|
5069 |
* Compare two nodes to see if they are equal (point to the same node in the doc)
|
|
|
5070 |
*
|
|
|
5071 |
* 2 nodes are considered equal if the absolute XPath is equal.
|
|
|
5072 |
*
|
|
|
5073 |
* @param $node1 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array)
|
|
|
5074 |
* @param $node2 (mixed) Either an absolute XPath to an node OR a real tree-node (hash-array)
|
|
|
5075 |
* @return (bool) TRUE if equal (see text above), FALSE if not (and on error).
|
|
|
5076 |
*/
|
|
|
5077 |
function equalNodes($node1, $node2) {
|
|
|
5078 |
$xPath_1 = is_string($node1) ? $node1 : $this->getNodePath($node1);
|
|
|
5079 |
$xPath_2 = is_string($node2) ? $node2 : $this->getNodePath($node2);
|
|
|
5080 |
return (strncasecmp ($xPath_1, $xPath_2, strLen($xPath_1)) == 0);
|
|
|
5081 |
}
|
|
|
5082 |
|
|
|
5083 |
/**
|
|
|
5084 |
* Get the absolute XPath of a node that is in a document tree.
|
|
|
5085 |
*
|
|
|
5086 |
* @param $node (array) A real tree-node (hash-array)
|
|
|
5087 |
* @return (string) The string path to the node or FALSE on error.
|
|
|
5088 |
*/
|
|
|
5089 |
function getNodePath($node) {
|
|
|
5090 |
if (!empty($node['xpath'])) return $node['xpath'];
|
|
|
5091 |
$pathInfo = array();
|
|
|
5092 |
do {
|
|
|
5093 |
if (empty($node['name']) OR empty($node['parentNode'])) break; // End criteria
|
|
|
5094 |
$pathInfo[] = array('name' => $node['name'], 'contextPos' => $node['contextPos']);
|
|
|
5095 |
$node = $node['parentNode'];
|
|
|
5096 |
} while (TRUE);
|
|
|
5097 |
|
|
|
5098 |
$xPath = '';
|
|
|
5099 |
for ($i=sizeOf($pathInfo)-1; $i>=0; $i--) {
|
|
|
5100 |
$xPath .= '/' . $pathInfo[$i]['name'] . '[' . $pathInfo[$i]['contextPos'] . ']';
|
|
|
5101 |
}
|
|
|
5102 |
if (empty($xPath)) return FALSE;
|
|
|
5103 |
return $xPath;
|
|
|
5104 |
}
|
|
|
5105 |
|
|
|
5106 |
/**
|
|
|
5107 |
* Retrieves the absolute parent XPath query.
|
|
|
5108 |
*
|
|
|
5109 |
* The parents stored in the tree are only relative parents...but all the parent
|
|
|
5110 |
* information is stored in the XPath query itself...so instead we use a function
|
|
|
5111 |
* to extract the parent from the absolute Xpath query
|
|
|
5112 |
*
|
|
|
5113 |
* @param $childPath (string) String containing an absolute XPath query
|
|
|
5114 |
* @return (string) returns the absolute XPath of the parent
|
|
|
5115 |
*/
|
|
|
5116 |
function getParentXPath($absoluteXPath) {
|
|
|
5117 |
$lastSlashPos = strrpos($absoluteXPath, '/');
|
|
|
5118 |
if ($lastSlashPos == 0) { // it's already the root path
|
|
|
5119 |
return ''; // 'super-root'
|
|
|
5120 |
} else {
|
|
|
5121 |
return (substr($absoluteXPath, 0, $lastSlashPos));
|
|
|
5122 |
}
|
|
|
5123 |
}
|
|
|
5124 |
|
|
|
5125 |
/**
|
|
|
5126 |
* Returns TRUE if the given node has child nodes below it
|
|
|
5127 |
*
|
|
|
5128 |
* @param $absoluteXPath (string) full path of the potential parent node
|
|
|
5129 |
* @return (bool) TRUE if this node exists and has a child, FALSE otherwise
|
|
|
5130 |
*/
|
|
|
5131 |
function hasChildNodes($absoluteXPath) {
|
|
|
5132 |
if ($this->_indexIsDirty) $this->reindexNodeTree();
|
|
|
5133 |
return (bool) (isSet($this->nodeIndex[$absoluteXPath])
|
|
|
5134 |
AND sizeOf($this->nodeIndex[$absoluteXPath]['childNodes']));
|
|
|
5135 |
}
|
|
|
5136 |
|
|
|
5137 |
/**
|
|
|
5138 |
* Translate all ampersands to it's literal entities '&' and back.
|
|
|
5139 |
*
|
|
|
5140 |
* I wasn't aware of this problem at first but it's important to understand why we do this.
|
|
|
5141 |
* At first you must know:
|
|
|
5142 |
* a) PHP's XML parser *translates* all entities to the equivalent char E.g. < is returned as '<'
|
|
|
5143 |
* b) PHP's XML parser (in V 4.1.0) has problems with most *literal* entities! The only one's that are
|
|
|
5144 |
* recognized are &, < > and ". *ALL* others (like © a.s.o.) cause an
|
|
|
5145 |
* XML_ERROR_UNDEFINED_ENTITY error. I reported this as bug at http://bugs.php.net/bug.php?id=15092
|
|
|
5146 |
* (It turned out not to be a 'real' bug, but one of those nice W3C-spec things).
|
|
|
5147 |
*
|
|
|
5148 |
* Forget position b) now. It's just for info. Because the way we will solve a) will also solve b) too.
|
|
|
5149 |
*
|
|
|
5150 |
* THE PROBLEM
|
|
|
5151 |
* To understand the problem, here a sample:
|
|
|
5152 |
* Given is the following XML: "<AAA> < > </AAA>"
|
|
|
5153 |
* Try to parse it and PHP's XML parser will fail with a XML_ERROR_UNDEFINED_ENTITY becaus of
|
|
|
5154 |
* the unknown litteral-entity ' '. (The numeric equivalent ' ' would work though).
|
|
|
5155 |
* Next try is to use the numeric equivalent 160 for ' ', thus "<AAA> <   > </AAA>"
|
|
|
5156 |
* The data we receive in the tag <AAA> is " < > ". So we get the *translated entities* and
|
|
|
5157 |
* NOT the 3 entities <   >. Thus, we will not even notice that there were entities at all!
|
|
|
5158 |
* In *most* cases we're not able to tell if the data was given as entity or as 'normal' char.
|
|
|
5159 |
* E.g. When receiving a quote or a single space were not able to tell if it was given as 'normal' char
|
|
|
5160 |
* or as or ". Thus we loose the entity-information of the XML-data!
|
|
|
5161 |
*
|
|
|
5162 |
* THE SOLUTION
|
|
|
5163 |
* The better solution is to keep the data 'as is' by replacing the '&' before parsing begins.
|
|
|
5164 |
* E.g. Taking the original input from above, this would result in "<AAA> &lt; &nbsp; &gt; </AAA>"
|
|
|
5165 |
* The data we receive now for the tag <AAA> is " < > ". and that's what we want.
|
|
|
5166 |
*
|
|
|
5167 |
* The bad thing is, that a global replace will also replace data in section that are NOT translated by the
|
|
|
5168 |
* PHP XML-parser. That is comments (<!-- -->), IP-sections (stuff between <? ? >) and CDATA-block too.
|
|
|
5169 |
* So all data comming from those sections must be reversed. This is done during the XML parse phase.
|
|
|
5170 |
* So:
|
|
|
5171 |
* a) Replacement of all '&' in the XML-source.
|
|
|
5172 |
* b) All data that is not char-data or in CDATA-block have to be reversed during the XML-parse phase.
|
|
|
5173 |
*
|
|
|
5174 |
* @param $xmlSource (string) The XML string
|
|
|
5175 |
* @return (string) The XML string with translated ampersands.
|
|
|
5176 |
*/
|
|
|
5177 |
function _translateAmpersand($xmlSource, $reverse=FALSE) {
|
|
|
5178 |
$PHP5 = (substr(phpversion(), 0, 1) == '5');
|
|
|
5179 |
if ($PHP5) {
|
|
|
5180 |
//otherwise we receive &nbsp; instead of
|
|
|
5181 |
return $xmlSource;
|
|
|
5182 |
} else {
|
|
|
5183 |
return ($reverse ? str_replace('&', '&', $xmlSource) : str_replace('&', '&', $xmlSource));
|
|
|
5184 |
}
|
|
|
5185 |
}
|
|
|
5186 |
|
|
|
5187 |
} // END OF CLASS XPathEngine
|
|
|
5188 |
|
|
|
5189 |
|
|
|
5190 |
/************************************************************************************************
|
|
|
5191 |
* ===============================================================================================
|
|
|
5192 |
* X P a t h - Class
|
|
|
5193 |
* ===============================================================================================
|
|
|
5194 |
************************************************************************************************/
|
|
|
5195 |
|
|
|
5196 |
define('XPATH_QUERYHIT_ALL' , 1);
|
|
|
5197 |
define('XPATH_QUERYHIT_FIRST' , 2);
|
|
|
5198 |
define('XPATH_QUERYHIT_UNIQUE', 3);
|
|
|
5199 |
|
|
|
5200 |
class XPath extends XPathEngine {
|
|
|
5201 |
|
|
|
5202 |
/**
|
|
|
5203 |
* Constructor of the class
|
|
|
5204 |
*
|
|
|
5205 |
* Optionally you may call this constructor with the XML-filename to parse and the
|
|
|
5206 |
* XML option vector. A option vector sample:
|
|
|
5207 |
* $xmlOpt = array(XML_OPTION_CASE_FOLDING => FALSE, XML_OPTION_SKIP_WHITE => TRUE);
|
|
|
5208 |
*
|
|
|
5209 |
* @param $userXmlOptions (array) (optional) Vector of (<optionID>=><value>, <optionID>=><value>, ...)
|
|
|
5210 |
* @param $fileName (string) (optional) Filename of XML file to load from.
|
|
|
5211 |
* It is recommended that you call importFromFile()
|
|
|
5212 |
* instead as you will get an error code. If the
|
|
|
5213 |
* import fails, the object will be set to FALSE.
|
|
|
5214 |
* @see parent::XPathEngine()
|
|
|
5215 |
*/
|
|
|
5216 |
function XPath($fileName='', $userXmlOptions=array()) {
|
|
|
5217 |
parent::XPathEngine($userXmlOptions);
|
|
|
5218 |
$this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
|
|
|
5219 |
if ($fileName) {
|
|
|
5220 |
if (!$this->importFromFile($fileName)) {
|
|
|
5221 |
// Re-run the base constructor to "reset" the object. If the user has any sense, then
|
|
|
5222 |
// they will have created the object, and then explicitly called importFromFile(), giving
|
|
|
5223 |
// them the chance to catch and handle the error properly.
|
|
|
5224 |
parent::XPathEngine($userXmlOptions);
|
|
|
5225 |
}
|
|
|
5226 |
}
|
|
|
5227 |
}
|
|
|
5228 |
|
|
|
5229 |
/**
|
|
|
5230 |
* Resets the object so it's able to take a new xml sting/file
|
|
|
5231 |
*
|
|
|
5232 |
* Constructing objects is slow. If you can, reuse ones that you have used already
|
|
|
5233 |
* by using this reset() function.
|
|
|
5234 |
*/
|
|
|
5235 |
function reset() {
|
|
|
5236 |
parent::reset();
|
|
|
5237 |
$this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
|
|
|
5238 |
}
|
|
|
5239 |
|
|
|
5240 |
//-----------------------------------------------------------------------------------------
|
|
|
5241 |
// XPath ------ Get / Set Stuff ------
|
|
|
5242 |
//-----------------------------------------------------------------------------------------
|
|
|
5243 |
|
|
|
5244 |
/**
|
|
|
5245 |
* Resolves and xPathQuery array depending on the property['modMatch']
|
|
|
5246 |
*
|
|
|
5247 |
* Most of the modification functions of XPath will also accept a xPathQuery (instead
|
|
|
5248 |
* of an absolute Xpath). The only problem is that the query could match more the one
|
|
|
5249 |
* node. The question is, if the none, the fist or all nodes are to be modified.
|
|
|
5250 |
* The behaver can be set with setModMatch()
|
|
|
5251 |
*
|
|
|
5252 |
* @param $modMatch (int) One of the following:
|
|
|
5253 |
* - XPATH_QUERYHIT_ALL (default)
|
|
|
5254 |
* - XPATH_QUERYHIT_FIRST
|
|
|
5255 |
* - XPATH_QUERYHIT_UNIQUE // If the query matches more then one node.
|
|
|
5256 |
* @see _resolveXPathQuery()
|
|
|
5257 |
*/
|
|
|
5258 |
function setModMatch($modMatch = XPATH_QUERYHIT_ALL) {
|
|
|
5259 |
switch($modMatch) {
|
|
|
5260 |
case XPATH_QUERYHIT_UNIQUE : $this->properties['modMatch'] = XPATH_QUERYHIT_UNIQUE; break;
|
|
|
5261 |
case XPATH_QUERYHIT_FIRST: $this->properties['modMatch'] = XPATH_QUERYHIT_FIRST; break;
|
|
|
5262 |
default: $this->properties['modMatch'] = XPATH_QUERYHIT_ALL;
|
|
|
5263 |
}
|
|
|
5264 |
}
|
|
|
5265 |
|
|
|
5266 |
//-----------------------------------------------------------------------------------------
|
|
|
5267 |
// XPath ------ DOM Like Modification ------
|
|
|
5268 |
//-----------------------------------------------------------------------------------------
|
|
|
5269 |
|
|
|
5270 |
//-----------------------------------------------------------------------------------------
|
|
|
5271 |
// XPath ------ Child (Node) Set/Get ------
|
|
|
5272 |
//-----------------------------------------------------------------------------------------
|
|
|
5273 |
|
|
|
5274 |
/**
|
|
|
5275 |
* Retrieves the name(s) of a node or a group of document nodes.
|
|
|
5276 |
*
|
|
|
5277 |
* This method retrieves the names of a group of document nodes
|
|
|
5278 |
* specified in the argument. So if the argument was '/A[1]/B[2]' then it
|
|
|
5279 |
* would return 'B' if the node did exist in the tree.
|
|
|
5280 |
*
|
|
|
5281 |
* @param $xPathQuery (mixed) Array or single full document path(s) of the node(s),
|
|
|
5282 |
* from which the names should be retrieved.
|
|
|
5283 |
* @return (mixed) Array or single string of the names of the specified
|
|
|
5284 |
* nodes, or just the individual name. If the node did
|
|
|
5285 |
* not exist, then returns FALSE.
|
|
|
5286 |
*/
|
|
|
5287 |
function nodeName($xPathQuery) {
|
|
|
5288 |
if (is_array($xPathQuery)) {
|
|
|
5289 |
$xPathSet = $xPathQuery;
|
|
|
5290 |
} else {
|
|
|
5291 |
// Check for a valid xPathQuery
|
|
|
5292 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery,'nodeName');
|
|
|
5293 |
}
|
|
|
5294 |
if (count($xPathSet) == 0) return FALSE;
|
|
|
5295 |
// For each node, get it's name
|
|
|
5296 |
$result = array();
|
|
|
5297 |
foreach($xPathSet as $xPath) {
|
|
|
5298 |
$node = &$this->getNode($xPath);
|
|
|
5299 |
if (!$node) {
|
|
|
5300 |
// ### Fatal internal error??
|
|
|
5301 |
continue;
|
|
|
5302 |
}
|
|
|
5303 |
$result[] = $node['name'];
|
|
|
5304 |
}
|
|
|
5305 |
// If just a single string, return string
|
|
|
5306 |
if (count($xPathSet) == 1) $result = $result[0];
|
|
|
5307 |
// Return result.
|
|
|
5308 |
return $result;
|
|
|
5309 |
}
|
|
|
5310 |
|
|
|
5311 |
/**
|
|
|
5312 |
* Removes a node from the XML document.
|
|
|
5313 |
*
|
|
|
5314 |
* This method removes a node from the tree of nodes of the XML document. If the node
|
|
|
5315 |
* is a document node, all children of the node and its character data will be removed.
|
|
|
5316 |
* If the node is an attribute node, only this attribute will be removed, the node to which
|
|
|
5317 |
* the attribute belongs as well as its children will remain unmodified.
|
|
|
5318 |
*
|
|
|
5319 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5320 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5321 |
*
|
|
|
5322 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
5323 |
* @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
|
|
|
5324 |
* the changes. A performance helper. See reindexNodeTree()
|
|
|
5325 |
* @return (bool) TRUE on success, FALSE on error;
|
|
|
5326 |
* @see setModMatch(), reindexNodeTree()
|
|
|
5327 |
*/
|
|
|
5328 |
function removeChild($xPathQuery, $autoReindex=TRUE) {
|
|
|
5329 |
$ThisFunctionName = 'removeChild';
|
|
|
5330 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
5331 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
5332 |
if ($bDebugThisFunction) {
|
|
|
5333 |
echo "Node: $xPathQuery\n";
|
|
|
5334 |
echo '<hr>';
|
|
|
5335 |
}
|
|
|
5336 |
|
|
|
5337 |
$NULL = NULL;
|
|
|
5338 |
$status = FALSE;
|
|
|
5339 |
do { // try-block
|
|
|
5340 |
// Check for a valid xPathQuery
|
|
|
5341 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery,'removeChild');
|
|
|
5342 |
if (sizeOf($xPathSet) === 0) {
|
|
|
5343 |
$this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
|
|
|
5344 |
break; // try-block
|
|
|
5345 |
}
|
|
|
5346 |
$mustReindex = FALSE;
|
|
|
5347 |
// Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
|
|
|
5348 |
for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
|
|
|
5349 |
$absoluteXPath = $xPathSet[$i];
|
|
|
5350 |
if (preg_match(';/attribute::;', $absoluteXPath)) { // Handle the case of an attribute node
|
|
|
5351 |
$xPath = $this->_prestr($absoluteXPath, '/attribute::'); // Get the path to the attribute node's parent.
|
|
|
5352 |
$attribute = $this->_afterstr($absoluteXPath, '/attribute::'); // Get the name of the attribute.
|
|
|
5353 |
unSet($this->nodeIndex[$xPath]['attributes'][$attribute]); // Unset the attribute
|
|
|
5354 |
if ($bDebugThisFunction) echo "We removed the attribute '$attribute' of node '$xPath'.\n";
|
|
|
5355 |
continue;
|
|
|
5356 |
}
|
|
|
5357 |
// Otherwise remove the node by setting it to NULL. It will be removed on the next reindexNodeTree() call.
|
|
|
5358 |
$mustReindex = $autoReindex;
|
|
|
5359 |
// Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
|
|
|
5360 |
$this->_indexIsDirty = TRUE;
|
|
|
5361 |
|
|
|
5362 |
$theNode = $this->nodeIndex[$absoluteXPath];
|
|
|
5363 |
$theNode['parentNode']['childNodes'][$theNode['pos']] =& $NULL;
|
|
|
5364 |
if ($bDebugThisFunction) echo "We removed the node '$absoluteXPath'.\n";
|
|
|
5365 |
}
|
|
|
5366 |
// Reindex the node tree again
|
|
|
5367 |
if ($mustReindex) $this->reindexNodeTree();
|
|
|
5368 |
$status = TRUE;
|
|
|
5369 |
} while(FALSE);
|
|
|
5370 |
|
|
|
5371 |
$this->_closeDebugFunction($ThisFunctionName, $status, $bDebugThisFunction);
|
|
|
5372 |
|
|
|
5373 |
return $status;
|
|
|
5374 |
}
|
|
|
5375 |
|
|
|
5376 |
/**
|
|
|
5377 |
* Replace a node with any data string. The $data is taken 1:1.
|
|
|
5378 |
*
|
|
|
5379 |
* This function will delete the node you define by $absoluteXPath (plus it's sub-nodes) and
|
|
|
5380 |
* substitute it by the string $text. Often used to push in not well formed HTML.
|
|
|
5381 |
* WARNING:
|
|
|
5382 |
* The $data is taken 1:1.
|
|
|
5383 |
* You are in charge that the data you enter is valid XML if you intend
|
|
|
5384 |
* to export and import the content again.
|
|
|
5385 |
*
|
|
|
5386 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5387 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5388 |
*
|
|
|
5389 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
5390 |
* @param $data (string) String containing the content to be set. *READONLY*
|
|
|
5391 |
* @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
|
|
|
5392 |
* the changes. A performance helper. See reindexNodeTree()
|
|
|
5393 |
* @return (bool) TRUE on success, FALSE on error;
|
|
|
5394 |
* @see setModMatch(), replaceChild(), reindexNodeTree()
|
|
|
5395 |
*/
|
|
|
5396 |
function replaceChildByData($xPathQuery, $data, $autoReindex=TRUE) {
|
|
|
5397 |
$ThisFunctionName = 'replaceChildByData';
|
|
|
5398 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
5399 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
5400 |
if ($bDebugThisFunction) {
|
|
|
5401 |
echo "Node: $xPathQuery\n";
|
|
|
5402 |
}
|
|
|
5403 |
|
|
|
5404 |
$NULL = NULL;
|
|
|
5405 |
$status = FALSE;
|
|
|
5406 |
do { // try-block
|
|
|
5407 |
// Check for a valid xPathQuery
|
|
|
5408 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChildByData');
|
|
|
5409 |
if (sizeOf($xPathSet) === 0) {
|
|
|
5410 |
$this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
|
|
|
5411 |
break; // try-block
|
|
|
5412 |
}
|
|
|
5413 |
$mustReindex = FALSE;
|
|
|
5414 |
// Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
|
|
|
5415 |
for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
|
|
|
5416 |
$mustReindex = $autoReindex;
|
|
|
5417 |
// Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
|
|
|
5418 |
$this->_indexIsDirty = TRUE;
|
|
|
5419 |
|
|
|
5420 |
$absoluteXPath = $xPathSet[$i];
|
|
|
5421 |
$theNode = $this->nodeIndex[$absoluteXPath];
|
|
|
5422 |
$pos = $theNode['pos'];
|
|
|
5423 |
$theNode['parentNode']['textParts'][$pos] .= $data;
|
|
|
5424 |
$theNode['parentNode']['childNodes'][$pos] =& $NULL;
|
|
|
5425 |
if ($bDebugThisFunction) echo "We replaced the node '$absoluteXPath' with data.\n";
|
|
|
5426 |
}
|
|
|
5427 |
// Reindex the node tree again
|
|
|
5428 |
if ($mustReindex) $this->reindexNodeTree();
|
|
|
5429 |
$status = TRUE;
|
|
|
5430 |
} while(FALSE);
|
|
|
5431 |
|
|
|
5432 |
$this->_closeDebugFunction($ThisFunctionName, ($status) ? 'Success' : '!!! FAILD !!!', $bDebugThisFunction);
|
|
|
5433 |
|
|
|
5434 |
return $status;
|
|
|
5435 |
}
|
|
|
5436 |
|
|
|
5437 |
/**
|
|
|
5438 |
* Replace the node(s) that matches the xQuery with the passed node (or passed node-tree)
|
|
|
5439 |
*
|
|
|
5440 |
* If the passed node is a string it's assumed to be XML and replaceChildByXml()
|
|
|
5441 |
* will be called.
|
|
|
5442 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5443 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5444 |
*
|
|
|
5445 |
* @param $xPathQuery (string) Xpath to the node being replaced.
|
|
|
5446 |
* @param $node (mixed) String or Array (Usually a String)
|
|
|
5447 |
* If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
|
|
|
5448 |
* If array: A Node (can be a whole sub-tree) (See comment in header)
|
|
|
5449 |
* @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
|
|
|
5450 |
* the changes. A performance helper. See reindexNodeTree()
|
|
|
5451 |
* @return (array) The last replaced $node (can be a whole sub-tree)
|
|
|
5452 |
* @see reindexNodeTree()
|
|
|
5453 |
*/
|
|
|
5454 |
function &replaceChild($xPathQuery, $node, $autoReindex=TRUE) {
|
|
|
5455 |
$NULL = NULL;
|
|
|
5456 |
if (is_string($node)) {
|
|
|
5457 |
if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
|
|
|
5458 |
return array();
|
|
|
5459 |
} else {
|
|
|
5460 |
if (!($node = $this->_xml2Document($node))) return FALSE;
|
|
|
5461 |
}
|
|
|
5462 |
}
|
|
|
5463 |
|
|
|
5464 |
// Special case if it's 'super root'. We then have to take the child node == top node
|
|
|
5465 |
if (empty($node['parentNode'])) $node = $node['childNodes'][0];
|
|
|
5466 |
|
|
|
5467 |
$status = FALSE;
|
|
|
5468 |
do { // try-block
|
|
|
5469 |
// Check for a valid xPathQuery
|
|
|
5470 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery,'replaceChild');
|
|
|
5471 |
if (sizeOf($xPathSet) === 0) {
|
|
|
5472 |
$this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
|
|
|
5473 |
break; // try-block
|
|
|
5474 |
}
|
|
|
5475 |
$mustReindex = FALSE;
|
|
|
5476 |
|
|
|
5477 |
// Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
|
|
|
5478 |
for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
|
|
|
5479 |
$mustReindex = $autoReindex;
|
|
|
5480 |
// Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
|
|
|
5481 |
$this->_indexIsDirty = TRUE;
|
|
|
5482 |
|
|
|
5483 |
$absoluteXPath = $xPathSet[$i];
|
|
|
5484 |
$childNode =& $this->nodeIndex[$absoluteXPath];
|
|
|
5485 |
$parentNode =& $childNode['parentNode'];
|
|
|
5486 |
$childNode['parentNode'] =& $NULL;
|
|
|
5487 |
$childPos = $childNode['pos'];
|
|
|
5488 |
$parentNode['childNodes'][$childPos] =& $this->cloneNode($node);
|
|
|
5489 |
}
|
|
|
5490 |
if ($mustReindex) $this->reindexNodeTree();
|
|
|
5491 |
$status = TRUE;
|
|
|
5492 |
} while(FALSE);
|
|
|
5493 |
|
|
|
5494 |
if (!$status) return FALSE;
|
|
|
5495 |
return $childNode;
|
|
|
5496 |
}
|
|
|
5497 |
|
|
|
5498 |
/**
|
|
|
5499 |
* Insert passed node (or passed node-tree) at the node(s) that matches the xQuery.
|
|
|
5500 |
*
|
|
|
5501 |
* With parameters you can define if the 'hit'-node is shifted to the right or left
|
|
|
5502 |
* and if it's placed before of after the text-part.
|
|
|
5503 |
* Per derfault the 'hit'-node is shifted to the right and the node takes the place
|
|
|
5504 |
* the of the 'hit'-node.
|
|
|
5505 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5506 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5507 |
*
|
|
|
5508 |
* E.g. Following is given: AAA[1]
|
|
|
5509 |
* / \
|
|
|
5510 |
* ..BBB[1]..BBB[2] ..
|
|
|
5511 |
*
|
|
|
5512 |
* a) insertChild('/AAA[1]/BBB[2]', <node CCC>)
|
|
|
5513 |
* b) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE)
|
|
|
5514 |
* c) insertChild('/AAA[1]/BBB[2]', <node CCC>, $shiftRight=FALSE, $afterText=FALSE)
|
|
|
5515 |
*
|
|
|
5516 |
* a) b) c)
|
|
|
5517 |
* AAA[1] AAA[1] AAA[1]
|
|
|
5518 |
* / | \ / | \ / | \
|
|
|
5519 |
* ..BBB[1]..CCC[1]BBB[2].. ..BBB[1]..BBB[2]..CCC[1] ..BBB[1]..BBB[2]CCC[1]..
|
|
|
5520 |
*
|
|
|
5521 |
* #### Do a complete review of the "(optional)" tag after several arguments.
|
|
|
5522 |
*
|
|
|
5523 |
* @param $xPathQuery (string) Xpath to the node to append.
|
|
|
5524 |
* @param $node (mixed) String or Array (Usually a String)
|
|
|
5525 |
* If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
|
|
|
5526 |
* If array: A Node (can be a whole sub-tree) (See comment in header)
|
|
|
5527 |
* @param $shiftRight (bool) (optional, default=TRUE) Shift the target node to the right.
|
|
|
5528 |
* @param $afterText (bool) (optional, default=TRUE) Insert after the text.
|
|
|
5529 |
* @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
|
|
|
5530 |
* the changes. A performance helper. See reindexNodeTree()
|
|
|
5531 |
* @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly
|
|
|
5532 |
* appended nodes. That is: Array of paths if more then 1 node was added or
|
|
|
5533 |
* a single path string if only one node was added.
|
|
|
5534 |
* NOTE: If autoReindex is FALSE, then we can't return the *complete* path
|
|
|
5535 |
* as the exact doc-pos isn't available without reindexing. In that case we leave
|
|
|
5536 |
* out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2]
|
|
|
5537 |
* @see appendChildByXml(), reindexNodeTree()
|
|
|
5538 |
*/
|
|
|
5539 |
function insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText=TRUE, $autoReindex=TRUE) {
|
|
|
5540 |
if (is_string($node)) {
|
|
|
5541 |
if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
|
|
|
5542 |
return FALSE;
|
|
|
5543 |
} else {
|
|
|
5544 |
if (!($node = $this->_xml2Document($node))) return FALSE;
|
|
|
5545 |
}
|
|
|
5546 |
}
|
|
|
5547 |
|
|
|
5548 |
// Special case if it's 'super root'. We then have to take the child node == top node
|
|
|
5549 |
if (empty($node['parentNode'])) $node = $node['childNodes'][0];
|
|
|
5550 |
|
|
|
5551 |
// Check for a valid xPathQuery
|
|
|
5552 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery,'insertChild');
|
|
|
5553 |
if (sizeOf($xPathSet) === 0) {
|
|
|
5554 |
$this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
|
|
|
5555 |
return FALSE;
|
|
|
5556 |
}
|
|
|
5557 |
$mustReindex = FALSE;
|
|
|
5558 |
$newNodes = array();
|
|
|
5559 |
$result = array();
|
|
|
5560 |
// Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
|
|
|
5561 |
for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
|
|
|
5562 |
$absoluteXPath = $xPathSet[$i];
|
|
|
5563 |
$childNode =& $this->nodeIndex[$absoluteXPath];
|
|
|
5564 |
$parentNode =& $childNode['parentNode'];
|
|
|
5565 |
|
|
|
5566 |
// We can't insert at the super root or at the root.
|
|
|
5567 |
if (empty($absoluteXPath) || (!$parentNode['parentNode'])) {
|
|
|
5568 |
$this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE);
|
|
|
5569 |
return FALSE;
|
|
|
5570 |
}
|
|
|
5571 |
|
|
|
5572 |
$mustReindex = $autoReindex;
|
|
|
5573 |
// Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
|
|
|
5574 |
$this->_indexIsDirty = TRUE;
|
|
|
5575 |
|
|
|
5576 |
//Special case: It not possible to add siblings to the top node.
|
|
|
5577 |
if (empty($parentNode['name'])) continue;
|
|
|
5578 |
$newNode =& $this->cloneNode($node);
|
|
|
5579 |
$pos = $shiftRight ? $childNode['pos'] : $childNode['pos']+1;
|
|
|
5580 |
$parentNode['childNodes'] = array_merge(
|
|
|
5581 |
array_slice($parentNode['childNodes'], 0, $pos),
|
|
|
5582 |
array(&$newNode),
|
|
|
5583 |
array_slice($parentNode['childNodes'], $pos)
|
|
|
5584 |
);
|
|
|
5585 |
$pos += $afterText ? 1 : 0;
|
|
|
5586 |
$parentNode['textParts'] = array_merge(
|
|
|
5587 |
array_slice($parentNode['textParts'], 0, $pos),
|
|
|
5588 |
array(''),
|
|
|
5589 |
array_slice($parentNode['textParts'], $pos)
|
|
|
5590 |
);
|
|
|
5591 |
|
|
|
5592 |
// We are going from bottom to top, but the user will want results from top to bottom.
|
|
|
5593 |
if ($mustReindex) {
|
|
|
5594 |
// We'll have to wait till after the reindex to get the full path to this new node.
|
|
|
5595 |
$newNodes[] = &$newNode;
|
|
|
5596 |
} else {
|
|
|
5597 |
// If we are reindexing the tree later, then we can't return the user any
|
|
|
5598 |
// useful results, so we just return them the count.
|
|
|
5599 |
$newNodePath = $parentNode['xpath'].'/'.$newNode['name'];
|
|
|
5600 |
array_unshift($result, $newNodePath);
|
|
|
5601 |
}
|
|
|
5602 |
}
|
|
|
5603 |
if ($mustReindex) {
|
|
|
5604 |
$this->reindexNodeTree();
|
|
|
5605 |
// Now we must fill in the result array. Because until now we did not
|
|
|
5606 |
// know what contextpos our newly added entries had, just their pos within
|
|
|
5607 |
// the siblings.
|
|
|
5608 |
foreach ($newNodes as $newNode) {
|
|
|
5609 |
array_unshift($result, $newNode['xpath']);
|
|
|
5610 |
}
|
|
|
5611 |
}
|
|
|
5612 |
if (count($result) == 1) $result = $result[0];
|
|
|
5613 |
return $result;
|
|
|
5614 |
}
|
|
|
5615 |
|
|
|
5616 |
/**
|
|
|
5617 |
* Appends a child to anothers children.
|
|
|
5618 |
*
|
|
|
5619 |
* If you intend to do a lot of appending, you should leave autoIndex as FALSE
|
|
|
5620 |
* and then call reindexNodeTree() when you are finished all the appending.
|
|
|
5621 |
*
|
|
|
5622 |
* @param $xPathQuery (string) Xpath to the node to append to.
|
|
|
5623 |
* @param $node (mixed) String or Array (Usually a String)
|
|
|
5624 |
* If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
|
|
|
5625 |
* If array: A Node (can be a whole sub-tree) (See comment in header)
|
|
|
5626 |
* @param $afterText (bool) (optional, default=FALSE) Insert after the text.
|
|
|
5627 |
* @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
|
|
|
5628 |
* the changes. A performance helper. See reindexNodeTree()
|
|
|
5629 |
* @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly
|
|
|
5630 |
* appended nodes. That is: Array of paths if more then 1 node was added or
|
|
|
5631 |
* a single path string if only one node was added.
|
|
|
5632 |
* NOTE: If autoReindex is FALSE, then we can't return the *complete* path
|
|
|
5633 |
* as the exact doc-pos isn't available without reindexing. In that case we leave
|
|
|
5634 |
* out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2]
|
|
|
5635 |
* @see insertChild(), reindexNodeTree()
|
|
|
5636 |
*/
|
|
|
5637 |
function appendChild($xPathQuery, $node, $afterText=FALSE, $autoReindex=TRUE) {
|
|
|
5638 |
if (is_string($node)) {
|
|
|
5639 |
if (empty($node)) { //--sam. Not sure how to react on an empty string - think it's an error.
|
|
|
5640 |
return FALSE;
|
|
|
5641 |
} else {
|
|
|
5642 |
if (!($node = $this->_xml2Document($node))) return FALSE;
|
|
|
5643 |
}
|
|
|
5644 |
}
|
|
|
5645 |
|
|
|
5646 |
// Special case if it's 'super root'. We then have to take the child node == top node
|
|
|
5647 |
if (empty($node['parentNode'])) $node = $node['childNodes'][0];
|
|
|
5648 |
|
|
|
5649 |
// Check for a valid xPathQuery
|
|
|
5650 |
$xPathSet = $this->_resolveXPathQueryForNodeMod($xPathQuery, 'appendChild');
|
|
|
5651 |
if (sizeOf($xPathSet) === 0) return FALSE;
|
|
|
5652 |
|
|
|
5653 |
$mustReindex = FALSE;
|
|
|
5654 |
$newNodes = array();
|
|
|
5655 |
$result = array();
|
|
|
5656 |
// Make chages from 'bottom-up'. In this manner the modifications will not affect itself.
|
|
|
5657 |
for ($i=sizeOf($xPathSet)-1; $i>=0; $i--) {
|
|
|
5658 |
$mustReindex = $autoReindex;
|
|
|
5659 |
// Flag the index as dirty; it's not uptodate. A reindex will be forced (if dirty) when exporting the XML doc
|
|
|
5660 |
$this->_indexIsDirty = TRUE;
|
|
|
5661 |
|
|
|
5662 |
$absoluteXPath = $xPathSet[$i];
|
|
|
5663 |
$parentNode =& $this->nodeIndex[$absoluteXPath];
|
|
|
5664 |
$newNode =& $this->cloneNode($node);
|
|
|
5665 |
$parentNode['childNodes'][] =& $newNode;
|
|
|
5666 |
$pos = count($parentNode['textParts']);
|
|
|
5667 |
$pos -= $afterText ? 0 : 1;
|
|
|
5668 |
$parentNode['textParts'] = array_merge(
|
|
|
5669 |
array_slice($parentNode['textParts'], 0, $pos),
|
|
|
5670 |
array(''),
|
|
|
5671 |
array_slice($parentNode['textParts'], $pos)
|
|
|
5672 |
);
|
|
|
5673 |
// We are going from bottom to top, but the user will want results from top to bottom.
|
|
|
5674 |
if ($mustReindex) {
|
|
|
5675 |
// We'll have to wait till after the reindex to get the full path to this new node.
|
|
|
5676 |
$newNodes[] = &$newNode;
|
|
|
5677 |
} else {
|
|
|
5678 |
// If we are reindexing the tree later, then we can't return the user any
|
|
|
5679 |
// useful results, so we just return them the count.
|
|
|
5680 |
array_unshift($result, "$absoluteXPath/{$newNode['name']}");
|
|
|
5681 |
}
|
|
|
5682 |
}
|
|
|
5683 |
if ($mustReindex) {
|
|
|
5684 |
$this->reindexNodeTree();
|
|
|
5685 |
// Now we must fill in the result array. Because until now we did not
|
|
|
5686 |
// know what contextpos our newly added entries had, just their pos within
|
|
|
5687 |
// the siblings.
|
|
|
5688 |
foreach ($newNodes as $newNode) {
|
|
|
5689 |
array_unshift($result, $newNode['xpath']);
|
|
|
5690 |
}
|
|
|
5691 |
}
|
|
|
5692 |
if (count($result) == 1) $result = $result[0];
|
|
|
5693 |
return $result;
|
|
|
5694 |
}
|
|
|
5695 |
|
|
|
5696 |
/**
|
|
|
5697 |
* Inserts a node before the reference node with the same parent.
|
|
|
5698 |
*
|
|
|
5699 |
* If you intend to do a lot of appending, you should leave autoIndex as FALSE
|
|
|
5700 |
* and then call reindexNodeTree() when you are finished all the appending.
|
|
|
5701 |
*
|
|
|
5702 |
* @param $xPathQuery (string) Xpath to the node to insert new node before
|
|
|
5703 |
* @param $node (mixed) String or Array (Usually a String)
|
|
|
5704 |
* If string: Vaild XML. E.g. "<A/>" or "<A> foo <B/> bar <A/>"
|
|
|
5705 |
* If array: A Node (can be a whole sub-tree) (See comment in header)
|
|
|
5706 |
* @param $afterText (bool) (optional, default=FLASE) Insert after the text.
|
|
|
5707 |
* @param $autoReindex (bool) (optional, default=TRUE) Reindex the document to reflect
|
|
|
5708 |
* the changes. A performance helper. See reindexNodeTree()
|
|
|
5709 |
* @return (mixed) FALSE on error (or no match). On success we return the path(s) to the newly
|
|
|
5710 |
* appended nodes. That is: Array of paths if more then 1 node was added or
|
|
|
5711 |
* a single path string if only one node was added.
|
|
|
5712 |
* NOTE: If autoReindex is FALSE, then we can't return the *complete* path
|
|
|
5713 |
* as the exact doc-pos isn't available without reindexing. In that case we leave
|
|
|
5714 |
* out the last [docpos] in the path(s). ie we'd return /A[3]/B instead of /A[3]/B[2]
|
|
|
5715 |
* @see reindexNodeTree()
|
|
|
5716 |
*/
|
|
|
5717 |
function insertBefore($xPathQuery, $node, $afterText=TRUE, $autoReindex=TRUE) {
|
|
|
5718 |
return $this->insertChild($xPathQuery, $node, $shiftRight=TRUE, $afterText, $autoReindex);
|
|
|
5719 |
}
|
|
|
5720 |
|
|
|
5721 |
|
|
|
5722 |
//-----------------------------------------------------------------------------------------
|
|
|
5723 |
// XPath ------ Attribute Set/Get ------
|
|
|
5724 |
//-----------------------------------------------------------------------------------------
|
|
|
5725 |
|
|
|
5726 |
/**
|
|
|
5727 |
* Retrieves a dedecated attribute value or a hash-array of all attributes of a node.
|
|
|
5728 |
*
|
|
|
5729 |
* The first param $absoluteXPath must be a valid xpath OR a xpath-query that results
|
|
|
5730 |
* to *one* xpath. If the second param $attrName is not set, a hash-array of all attributes
|
|
|
5731 |
* of that node is returned.
|
|
|
5732 |
*
|
|
|
5733 |
* Optionally you may pass an attrubute name in $attrName and the function will return the
|
|
|
5734 |
* string value of that attribute.
|
|
|
5735 |
*
|
|
|
5736 |
* @param $absoluteXPath (string) Full xpath OR a xpath-query that results to *one* xpath.
|
|
|
5737 |
* @param $attrName (string) (Optional) The name of the attribute. See above.
|
|
|
5738 |
* @return (mixed) hash-array or a string of attributes depending if the
|
|
|
5739 |
* parameter $attrName was set (see above). FALSE if the
|
|
|
5740 |
* node or attribute couldn't be found.
|
|
|
5741 |
* @see setAttribute(), removeAttribute()
|
|
|
5742 |
*/
|
|
|
5743 |
function getAttributes($absoluteXPath, $attrName=NULL) {
|
|
|
5744 |
// Numpty check
|
|
|
5745 |
if (!isSet($this->nodeIndex[$absoluteXPath])) {
|
|
|
5746 |
$xPathSet = $this->_resolveXPathQuery($absoluteXPath,'getAttributes');
|
|
|
5747 |
if (empty($xPathSet)) return FALSE;
|
|
|
5748 |
// only use the first entry
|
|
|
5749 |
$absoluteXPath = $xPathSet[0];
|
|
|
5750 |
}
|
|
|
5751 |
if (!empty($this->parseOptions[XML_OPTION_CASE_FOLDING])) {
|
|
|
5752 |
// Case in-sensitive
|
|
|
5753 |
$attrName = strtoupper($attrName);
|
|
|
5754 |
}
|
|
|
5755 |
|
|
|
5756 |
// Return the complete list or just the desired element
|
|
|
5757 |
if (is_null($attrName)) {
|
|
|
5758 |
return $this->nodeIndex[$absoluteXPath]['attributes'];
|
|
|
5759 |
} elseif (isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attrName])) {
|
|
|
5760 |
return $this->nodeIndex[$absoluteXPath]['attributes'][$attrName];
|
|
|
5761 |
}
|
|
|
5762 |
return FALSE;
|
|
|
5763 |
}
|
|
|
5764 |
|
|
|
5765 |
/**
|
|
|
5766 |
* Set attributes of a node(s).
|
|
|
5767 |
*
|
|
|
5768 |
* This method sets a number single attributes. An existing attribute is overwritten (default)
|
|
|
5769 |
* with the new value, but setting the last param to FALSE will prevent overwritten.
|
|
|
5770 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5771 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5772 |
*
|
|
|
5773 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
5774 |
* @param $name (string) Attribute name.
|
|
|
5775 |
* @param $value (string) Attribute value.
|
|
|
5776 |
* @param $overwrite (bool) If the attribute is already set we overwrite it (see text above)
|
|
|
5777 |
* @return (bool) TRUE on success, FALSE on failure.
|
|
|
5778 |
* @see getAttributes(), removeAttribute()
|
|
|
5779 |
*/
|
|
|
5780 |
function setAttribute($xPathQuery, $name, $value, $overwrite=TRUE) {
|
|
|
5781 |
return $this->setAttributes($xPathQuery, array($name => $value), $overwrite);
|
|
|
5782 |
}
|
|
|
5783 |
|
|
|
5784 |
/**
|
|
|
5785 |
* Version of setAttribute() that sets multiple attributes to node(s).
|
|
|
5786 |
*
|
|
|
5787 |
* This method sets a number of attributes. Existing attributes are overwritten (default)
|
|
|
5788 |
* with the new values, but setting the last param to FALSE will prevent overwritten.
|
|
|
5789 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5790 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5791 |
*
|
|
|
5792 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
5793 |
* @param $attributes (array) associative array of attributes to set.
|
|
|
5794 |
* @param $overwrite (bool) If the attributes are already set we overwrite them (see text above)
|
|
|
5795 |
* @return (bool) TRUE on success, FALSE otherwise
|
|
|
5796 |
* @see setAttribute(), getAttributes(), removeAttribute()
|
|
|
5797 |
*/
|
|
|
5798 |
function setAttributes($xPathQuery, $attributes, $overwrite=TRUE) {
|
|
|
5799 |
$status = FALSE;
|
|
|
5800 |
do { // try-block
|
|
|
5801 |
// The attributes parameter should be an associative array.
|
|
|
5802 |
if (!is_array($attributes)) break; // try-block
|
|
|
5803 |
|
|
|
5804 |
// Check for a valid xPathQuery
|
|
|
5805 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery,'setAttributes');
|
|
|
5806 |
foreach($xPathSet as $absoluteXPath) {
|
|
|
5807 |
// Add the attributes to the node.
|
|
|
5808 |
$theNode =& $this->nodeIndex[$absoluteXPath];
|
|
|
5809 |
if (empty($theNode['attributes'])) {
|
|
|
5810 |
$this->nodeIndex[$absoluteXPath]['attributes'] = $attributes;
|
|
|
5811 |
} else {
|
|
|
5812 |
$theNode['attributes'] = $overwrite ? array_merge($theNode['attributes'],$attributes) : array_merge($attributes, $theNode['attributes']);
|
|
|
5813 |
}
|
|
|
5814 |
}
|
|
|
5815 |
$status = TRUE;
|
|
|
5816 |
} while(FALSE); // END try-block
|
|
|
5817 |
|
|
|
5818 |
return $status;
|
|
|
5819 |
}
|
|
|
5820 |
|
|
|
5821 |
/**
|
|
|
5822 |
* Removes an attribute of a node(s).
|
|
|
5823 |
*
|
|
|
5824 |
* This method removes *ALL* attributres per default unless the second parameter $attrList is set.
|
|
|
5825 |
* $attrList can be either a single attr-name as string OR a vector of attr-names as array.
|
|
|
5826 |
* E.g.
|
|
|
5827 |
* removeAttribute(<xPath>); # will remove *ALL* attributes.
|
|
|
5828 |
* removeAttribute(<xPath>, 'A'); # will only remove attributes called 'A'.
|
|
|
5829 |
* removeAttribute(<xPath>, array('A_1','A_2')); # will remove attribute 'A_1' and 'A_2'.
|
|
|
5830 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5831 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5832 |
*
|
|
|
5833 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
5834 |
* @param $attrList (mixed) (optional) if not set will delete *all* (see text above)
|
|
|
5835 |
* @return (bool) TRUE on success, FALSE if the node couldn't be found
|
|
|
5836 |
* @see getAttributes(), setAttribute()
|
|
|
5837 |
*/
|
|
|
5838 |
function removeAttribute($xPathQuery, $attrList=NULL) {
|
|
|
5839 |
// Check for a valid xPathQuery
|
|
|
5840 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery, 'removeAttribute');
|
|
|
5841 |
|
|
|
5842 |
if (!empty($attrList) AND is_string($attrList)) $attrList = array($attrList);
|
|
|
5843 |
if (!is_array($attrList)) return FALSE;
|
|
|
5844 |
|
|
|
5845 |
foreach($xPathSet as $absoluteXPath) {
|
|
|
5846 |
// If the attribute parameter wasn't set then remove all the attributes
|
|
|
5847 |
if ($attrList[0] === NULL) {
|
|
|
5848 |
$this->nodeIndex[$absoluteXPath]['attributes'] = array();
|
|
|
5849 |
continue;
|
|
|
5850 |
}
|
|
|
5851 |
// Remove all the elements in the array then.
|
|
|
5852 |
foreach($attrList as $name) {
|
|
|
5853 |
unset($this->nodeIndex[$absoluteXPath]['attributes'][$name]);
|
|
|
5854 |
}
|
|
|
5855 |
}
|
|
|
5856 |
return TRUE;
|
|
|
5857 |
}
|
|
|
5858 |
|
|
|
5859 |
//-----------------------------------------------------------------------------------------
|
|
|
5860 |
// XPath ------ Text Set/Get ------
|
|
|
5861 |
//-----------------------------------------------------------------------------------------
|
|
|
5862 |
|
|
|
5863 |
/**
|
|
|
5864 |
* Retrieve all the text from a node as a single string.
|
|
|
5865 |
*
|
|
|
5866 |
* Sample
|
|
|
5867 |
* Given is: <AA> This <BB\>is <BB\> some<BB\>text </AA>
|
|
|
5868 |
* Return of getData('/AA[1]') would be: " This is sometext "
|
|
|
5869 |
* The first param $xPathQuery must be a valid xpath OR a xpath-query that
|
|
|
5870 |
* results to *one* xpath.
|
|
|
5871 |
*
|
|
|
5872 |
* @param $xPathQuery (string) xpath to the node - resolves to *one* xpath.
|
|
|
5873 |
* @return (mixed) The returned string (see above), FALSE if the node
|
|
|
5874 |
* couldn't be found or is not unique.
|
|
|
5875 |
* @see getDataParts()
|
|
|
5876 |
*/
|
|
|
5877 |
function getData($xPathQuery) {
|
|
|
5878 |
$aDataParts = $this->getDataParts($xPathQuery);
|
|
|
5879 |
if ($aDataParts === FALSE) return FALSE;
|
|
|
5880 |
return implode('', $aDataParts);
|
|
|
5881 |
}
|
|
|
5882 |
|
|
|
5883 |
/**
|
|
|
5884 |
* Retrieve all the text from a node as a vector of strings
|
|
|
5885 |
*
|
|
|
5886 |
* Where each element of the array was interrupted by a non-text child element.
|
|
|
5887 |
*
|
|
|
5888 |
* Sample
|
|
|
5889 |
* Given is: <AA> This <BB\>is <BB\> some<BB\>text </AA>
|
|
|
5890 |
* Return of getDataParts('/AA[1]') would be: array([0]=>' This ', [1]=>'is ', [2]=>' some', [3]=>'text ');
|
|
|
5891 |
* The first param $absoluteXPath must be a valid xpath OR a xpath-query that results
|
|
|
5892 |
* to *one* xpath.
|
|
|
5893 |
*
|
|
|
5894 |
* @param $xPathQuery (string) xpath to the node - resolves to *one* xpath.
|
|
|
5895 |
* @return (mixed) The returned array (see above), or FALSE if node is not
|
|
|
5896 |
* found or is not unique.
|
|
|
5897 |
* @see getData()
|
|
|
5898 |
*/
|
|
|
5899 |
function getDataParts($xPathQuery) {
|
|
|
5900 |
// Resolve xPath argument
|
|
|
5901 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery, 'getDataParts');
|
|
|
5902 |
if (1 !== ($setSize=count($xPathSet))) {
|
|
|
5903 |
$this->_displayError(sprintf($this->errorStrings['AbsoluteXPathRequired'], $xPathQuery) . "Not unique xpath-query, matched {$setSize}-times.", __LINE__, __FILE__, FALSE);
|
|
|
5904 |
return FALSE;
|
|
|
5905 |
}
|
|
|
5906 |
$absoluteXPath = $xPathSet[0];
|
|
|
5907 |
// Is it an attribute node?
|
|
|
5908 |
if (preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches)) {
|
|
|
5909 |
$absoluteXPath = $matches[1];
|
|
|
5910 |
$attribute = $matches[2];
|
|
|
5911 |
if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
|
|
|
5912 |
$this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
|
|
|
5913 |
continue;
|
|
|
5914 |
}
|
|
|
5915 |
return array($this->nodeIndex[$absoluteXPath]['attributes'][$attribute]);
|
|
|
5916 |
} else if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) {
|
|
|
5917 |
$absoluteXPath = $matches[1];
|
|
|
5918 |
$textPartNr = $matches[2];
|
|
|
5919 |
return array($this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr]);
|
|
|
5920 |
} else {
|
|
|
5921 |
return $this->nodeIndex[$absoluteXPath]['textParts'];
|
|
|
5922 |
}
|
|
|
5923 |
}
|
|
|
5924 |
|
|
|
5925 |
/**
|
|
|
5926 |
* Retrieves a sub string of a text-part OR attribute-value.
|
|
|
5927 |
*
|
|
|
5928 |
* This method retrieves the sub string of a specific text-part OR (if the
|
|
|
5929 |
* $absoluteXPath references an attribute) the the sub string of the attribute value.
|
|
|
5930 |
* If no 'direct referencing' is used (Xpath ends with text()[<part-number>]), then
|
|
|
5931 |
* the first text-part of the node ist returned (if exsiting).
|
|
|
5932 |
*
|
|
|
5933 |
* @param $absoluteXPath (string) Xpath to the node (See note above).
|
|
|
5934 |
* @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr())
|
|
|
5935 |
* @param $count (number) (optional, default is ALL) Character count (Just like PHP's substr())
|
|
|
5936 |
* @return (mixed) The sub string, FALSE if not found or on error
|
|
|
5937 |
* @see XPathEngine::wholeText(), PHP's substr()
|
|
|
5938 |
*/
|
|
|
5939 |
function substringData($absoluteXPath, $offset = 0, $count = NULL) {
|
|
|
5940 |
if (!($text = $this->wholeText($absoluteXPath))) return FALSE;
|
|
|
5941 |
if (is_null($count)) {
|
|
|
5942 |
return substr($text, $offset);
|
|
|
5943 |
} else {
|
|
|
5944 |
return substr($text, $offset, $count);
|
|
|
5945 |
}
|
|
|
5946 |
}
|
|
|
5947 |
|
|
|
5948 |
/**
|
|
|
5949 |
* Replace a sub string of a text-part OR attribute-value.
|
|
|
5950 |
*
|
|
|
5951 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5952 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5953 |
*
|
|
|
5954 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
5955 |
* @param $replacement (string) The string to replace with.
|
|
|
5956 |
* @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr_replace ())
|
|
|
5957 |
* @param $count (number) (optional, default is 0=ALL) Character count (Just like PHP's substr_replace())
|
|
|
5958 |
* @param $textPartNr (int) (optional) (see _getTextSet() )
|
|
|
5959 |
* @return (bool) The new string value on success, FALSE if not found or on error
|
|
|
5960 |
* @see substringData()
|
|
|
5961 |
*/
|
|
|
5962 |
function replaceData($xPathQuery, $replacement, $offset = 0, $count = 0, $textPartNr=1) {
|
|
|
5963 |
if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
|
|
|
5964 |
$tSize=sizeOf($textSet);
|
|
|
5965 |
for ($i=0; $i<$tSize; $i++) {
|
|
|
5966 |
if ($count) {
|
|
|
5967 |
$textSet[$i] = substr_replace($textSet[$i], $replacement, $offset, $count);
|
|
|
5968 |
} else {
|
|
|
5969 |
$textSet[$i] = substr_replace($textSet[$i], $replacement, $offset);
|
|
|
5970 |
}
|
|
|
5971 |
}
|
|
|
5972 |
return TRUE;
|
|
|
5973 |
}
|
|
|
5974 |
|
|
|
5975 |
/**
|
|
|
5976 |
* Insert a sub string in a text-part OR attribute-value.
|
|
|
5977 |
*
|
|
|
5978 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5979 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
5980 |
*
|
|
|
5981 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
5982 |
* @param $data (string) The string to replace with.
|
|
|
5983 |
* @param $offset (int) (optional, default is 0) Offset at which to insert the data.
|
|
|
5984 |
* @return (bool) The new string on success, FALSE if not found or on error
|
|
|
5985 |
* @see replaceData()
|
|
|
5986 |
*/
|
|
|
5987 |
function insertData($xPathQuery, $data, $offset=0) {
|
|
|
5988 |
return $this->replaceData($xPathQuery, $data, $offset, 0);
|
|
|
5989 |
}
|
|
|
5990 |
|
|
|
5991 |
/**
|
|
|
5992 |
* Append text data to the end of the text for an attribute OR node text-part.
|
|
|
5993 |
*
|
|
|
5994 |
* This method adds content to a node. If it's an attribute node, then
|
|
|
5995 |
* the value of the attribute will be set, otherwise the passed data will append to
|
|
|
5996 |
* character data of the node text-part. Per default the first text-part is taken.
|
|
|
5997 |
*
|
|
|
5998 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
5999 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
6000 |
*
|
|
|
6001 |
* @param $xPathQuery (string) to the node(s) (See note above).
|
|
|
6002 |
* @param $data (string) String containing the content to be added.
|
|
|
6003 |
* @param $textPartNr (int) (optional, default is 1) (see _getTextSet())
|
|
|
6004 |
* @return (bool) TRUE on success, otherwise FALSE
|
|
|
6005 |
* @see _getTextSet()
|
|
|
6006 |
*/
|
|
|
6007 |
function appendData($xPathQuery, $data, $textPartNr=1) {
|
|
|
6008 |
if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
|
|
|
6009 |
$tSize=sizeOf($textSet);
|
|
|
6010 |
for ($i=0; $i<$tSize; $i++) {
|
|
|
6011 |
$textSet[$i] .= $data;
|
|
|
6012 |
}
|
|
|
6013 |
return TRUE;
|
|
|
6014 |
}
|
|
|
6015 |
|
|
|
6016 |
/**
|
|
|
6017 |
* Delete the data of a node.
|
|
|
6018 |
*
|
|
|
6019 |
* This method deletes content of a node. If it's an attribute node, then
|
|
|
6020 |
* the value of the attribute will be removed, otherwise the node text-part.
|
|
|
6021 |
* will be deleted. Per default the first text-part is deleted.
|
|
|
6022 |
*
|
|
|
6023 |
* NOTE: When passing a xpath-query instead of an abs. Xpath.
|
|
|
6024 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
6025 |
*
|
|
|
6026 |
* @param $xPathQuery (string) to the node(s) (See note above).
|
|
|
6027 |
* @param $offset (int) (optional, default is 0) Starting offset. (Just like PHP's substr_replace())
|
|
|
6028 |
* @param $count (number) (optional, default is 0=ALL) Character count. (Just like PHP's substr_replace())
|
|
|
6029 |
* @param $textPartNr (int) (optional, default is 0) the text part to delete (see _getTextSet())
|
|
|
6030 |
* @return (bool) TRUE on success, otherwise FALSE
|
|
|
6031 |
* @see _getTextSet()
|
|
|
6032 |
*/
|
|
|
6033 |
function deleteData($xPathQuery, $offset=0, $count=0, $textPartNr=1) {
|
|
|
6034 |
if (!($textSet = $this->_getTextSet($xPathQuery, $textPartNr))) return FALSE;
|
|
|
6035 |
$tSize=sizeOf($textSet);
|
|
|
6036 |
for ($i=0; $i<$tSize; $i++) {
|
|
|
6037 |
if (!$count)
|
|
|
6038 |
$textSet[$i] = "";
|
|
|
6039 |
else
|
|
|
6040 |
$textSet[$i] = substr_replace($textSet[$i],'', $offset, $count);
|
|
|
6041 |
}
|
|
|
6042 |
return TRUE;
|
|
|
6043 |
}
|
|
|
6044 |
|
|
|
6045 |
//-----------------------------------------------------------------------------------------
|
|
|
6046 |
// XPath ------ Help Stuff ------
|
|
|
6047 |
//-----------------------------------------------------------------------------------------
|
|
|
6048 |
|
|
|
6049 |
/**
|
|
|
6050 |
* Parse the XML to a node-tree. A so called 'document'
|
|
|
6051 |
*
|
|
|
6052 |
* @param $xmlString (string) The string to turn into a document node.
|
|
|
6053 |
* @return (&array) a node-tree
|
|
|
6054 |
*/
|
|
|
6055 |
function &_xml2Document($xmlString) {
|
|
|
6056 |
$xmlOptions = array(
|
|
|
6057 |
XML_OPTION_CASE_FOLDING => $this->getProperties('caseFolding'),
|
|
|
6058 |
XML_OPTION_SKIP_WHITE => $this->getProperties('skipWhiteSpaces')
|
|
|
6059 |
);
|
|
|
6060 |
$xmlParser = new XPathEngine($xmlOptions);
|
|
|
6061 |
$xmlParser->setVerbose($this->properties['verboseLevel']);
|
|
|
6062 |
// Parse the XML string
|
|
|
6063 |
if (!$xmlParser->importFromString($xmlString)) {
|
|
|
6064 |
$this->_displayError($xmlParser->getLastError(), __LINE__, __FILE__, FALSE);
|
|
|
6065 |
return FALSE;
|
|
|
6066 |
}
|
|
|
6067 |
return $xmlParser->getNode('/');
|
|
|
6068 |
}
|
|
|
6069 |
|
|
|
6070 |
/**
|
|
|
6071 |
* Get a reference-list to node text part(s) or node attribute(s).
|
|
|
6072 |
*
|
|
|
6073 |
* If the Xquery references an attribute(s) (Xquery ends with attribute::),
|
|
|
6074 |
* then the text value of the node-attribute(s) is/are returned.
|
|
|
6075 |
* Otherwise the Xquery is referencing to text part(s) of node(s). This can be either a
|
|
|
6076 |
* direct reference to text part(s) (Xquery ends with text()[<nr>]) or indirect reference
|
|
|
6077 |
* (a simple Xquery to node(s)).
|
|
|
6078 |
* 1) Direct Reference (Xquery ends with text()[<part-number>]):
|
|
|
6079 |
* If the 'part-number' is omitted, the first text-part is assumed; starting by 1.
|
|
|
6080 |
* Negative numbers are allowed, where -1 is the last text-part a.s.o.
|
|
|
6081 |
* 2) Indirect Reference (a simple Xquery to node(s)):
|
|
|
6082 |
* Default is to return the first text part(s). Optionally you may pass a parameter
|
|
|
6083 |
* $textPartNr to define the text-part you want; starting by 1.
|
|
|
6084 |
* Negative numbers are allowed, where -1 is the last text-part a.s.o.
|
|
|
6085 |
*
|
|
|
6086 |
* NOTE I : The returned vector is a set of references to the text parts / attributes.
|
|
|
6087 |
* This is handy, if you wish to modify the contents.
|
|
|
6088 |
* NOTE II: text-part numbers out of range will not be in the list
|
|
|
6089 |
* NOTE III:Instead of an absolute xpath you may also pass a xpath-query.
|
|
|
6090 |
* Depending on setModMatch() one, none or multiple nodes are affected.
|
|
|
6091 |
*
|
|
|
6092 |
* @param $xPathQuery (string) xpath to the node (See note above).
|
|
|
6093 |
* @param $textPartNr (int) String containing the content to be set.
|
|
|
6094 |
* @return (mixed) A vector of *references* to the text that match, or
|
|
|
6095 |
* FALSE on error
|
|
|
6096 |
* @see XPathEngine::wholeText()
|
|
|
6097 |
*/
|
|
|
6098 |
function _getTextSet($xPathQuery, $textPartNr=1) {
|
|
|
6099 |
$ThisFunctionName = '_getTextSet';
|
|
|
6100 |
$bDebugThisFunction = in_array($ThisFunctionName, $this->aDebugFunctions);
|
|
|
6101 |
$this->_beginDebugFunction($ThisFunctionName, $bDebugThisFunction);
|
|
|
6102 |
if ($bDebugThisFunction) {
|
|
|
6103 |
echo "Node: $xPathQuery\n";
|
|
|
6104 |
echo "Text Part Number: $textPartNr\n";
|
|
|
6105 |
echo "<hr>";
|
|
|
6106 |
}
|
|
|
6107 |
|
|
|
6108 |
$status = FALSE;
|
|
|
6109 |
$funcName = '_getTextSet';
|
|
|
6110 |
$textSet = array();
|
|
|
6111 |
|
|
|
6112 |
do { // try-block
|
|
|
6113 |
// Check if it's a Xpath reference to an attribut(s). Xpath ends with attribute::)
|
|
|
6114 |
if (preg_match(";(.*)/(attribute::|@)([^/]*)$;U", $xPathQuery, $matches)) {
|
|
|
6115 |
$xPathQuery = $matches[1];
|
|
|
6116 |
$attribute = $matches[3];
|
|
|
6117 |
// Quick out
|
|
|
6118 |
if (isSet($this->nodeIndex[$xPathQuery])) {
|
|
|
6119 |
$xPathSet[] = $xPathQuery;
|
|
|
6120 |
} else {
|
|
|
6121 |
// Try to evaluate the absoluteXPath (since it seems to be an Xquery and not an abs. Xpath)
|
|
|
6122 |
$xPathSet = $this->_resolveXPathQuery("$xPathQuery/attribute::$attribute", $funcName);
|
|
|
6123 |
}
|
|
|
6124 |
foreach($xPathSet as $absoluteXPath) {
|
|
|
6125 |
preg_match(";(.*)/attribute::([^/]*)$;U", $xPathSet[0], $matches);
|
|
|
6126 |
$absoluteXPath = $matches[1];
|
|
|
6127 |
$attribute = $matches[2];
|
|
|
6128 |
if (!isSet($this->nodeIndex[$absoluteXPath]['attributes'][$attribute])) {
|
|
|
6129 |
$this->_displayError("The $absoluteXPath/attribute::$attribute value isn't a node in this document.", __LINE__, __FILE__, FALSE);
|
|
|
6130 |
continue;
|
|
|
6131 |
}
|
|
|
6132 |
$textSet[] =& $this->nodes[$absoluteXPath]['attributes'][$attribute];
|
|
|
6133 |
}
|
|
|
6134 |
$status = TRUE;
|
|
|
6135 |
break; // try-block
|
|
|
6136 |
}
|
|
|
6137 |
|
|
|
6138 |
// Check if it's a Xpath reference direct to a text-part(s). (xpath ends with text()[<part-number>])
|
|
|
6139 |
if (preg_match(":(.*)/text\(\)(\[(.*)\])?$:U", $xPathQuery, $matches)) {
|
|
|
6140 |
$xPathQuery = $matches[1];
|
|
|
6141 |
// default to the first text node if a text node was not specified
|
|
|
6142 |
$textPartNr = isSet($matches[2]) ? substr($matches[2],1,-1) : 1;
|
|
|
6143 |
// Quick check
|
|
|
6144 |
if (isSet($this->nodeIndex[$xPathQuery])) {
|
|
|
6145 |
$xPathSet[] = $xPathQuery;
|
|
|
6146 |
} else {
|
|
|
6147 |
// Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath)
|
|
|
6148 |
$xPathSet = $this->_resolveXPathQuery("$xPathQuery/text()[$textPartNr]", $funcName);
|
|
|
6149 |
}
|
|
|
6150 |
}
|
|
|
6151 |
else {
|
|
|
6152 |
// At this point we have been given an xpath with neither a 'text()' or 'attribute::' axis at the end
|
|
|
6153 |
// So this means to get the text-part of the node. If parameter $textPartNr was not set, use the last
|
|
|
6154 |
// text-part.
|
|
|
6155 |
if (isSet($this->nodeIndex[$xPathQuery])) {
|
|
|
6156 |
$xPathSet[] = $xPathQuery;
|
|
|
6157 |
} else {
|
|
|
6158 |
// Try to evaluate the absoluteXPath (since it seams to be an Xquery and not an abs. Xpath)
|
|
|
6159 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery, $funcName);
|
|
|
6160 |
}
|
|
|
6161 |
}
|
|
|
6162 |
|
|
|
6163 |
if ($bDebugThisFunction) {
|
|
|
6164 |
echo "Looking up paths for:\n";
|
|
|
6165 |
print_r($xPathSet);
|
|
|
6166 |
}
|
|
|
6167 |
|
|
|
6168 |
// Now fetch all text-parts that match. (May be 0,1 or many)
|
|
|
6169 |
foreach($xPathSet as $absoluteXPath) {
|
|
|
6170 |
unset($text);
|
|
|
6171 |
if ($text =& $this->wholeText($absoluteXPath, $textPartNr)) {
|
|
|
6172 |
$textSet[] =& $text;
|
|
|
6173 |
} else {
|
|
|
6174 |
// The node does not yet have any text, so we have to add a '' string so that
|
|
|
6175 |
// if we insert or replace to it, then we'll actually have something to op on.
|
|
|
6176 |
$this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1] = '';
|
|
|
6177 |
$textSet[] =& $this->nodeIndex[$absoluteXPath]['textParts'][$textPartNr-1];
|
|
|
6178 |
}
|
|
|
6179 |
}
|
|
|
6180 |
|
|
|
6181 |
$status = TRUE;
|
|
|
6182 |
} while (FALSE); // END try-block
|
|
|
6183 |
|
|
|
6184 |
if (!$status) $result = FALSE;
|
|
|
6185 |
else $result = $textSet;
|
|
|
6186 |
|
|
|
6187 |
$this->_closeDebugFunction($ThisFunctionName, $result, $bDebugThisFunction);
|
|
|
6188 |
|
|
|
6189 |
return $result;
|
|
|
6190 |
}
|
|
|
6191 |
|
|
|
6192 |
|
|
|
6193 |
/**
|
|
|
6194 |
* Resolves an xPathQuery vector for a node op for modification
|
|
|
6195 |
*
|
|
|
6196 |
* It is possible to create a brand new object, and try to append and insert nodes
|
|
|
6197 |
* into it, so this is a version of _resolveXPathQuery() that will autocreate the
|
|
|
6198 |
* super root if it detects that it is not present and the $xPathQuery is empty.
|
|
|
6199 |
*
|
|
|
6200 |
* Also it demands that there be at least one node returned, and displays a suitable
|
|
|
6201 |
* error message if the returned xPathSet does not contain any nodes.
|
|
|
6202 |
*
|
|
|
6203 |
* @param $xPathQuery (string) An xpath query targeting a single node. If empty()
|
|
|
6204 |
* returns the root node and auto creates the root node
|
|
|
6205 |
* if it doesn't exist.
|
|
|
6206 |
* @param $function (string) The function in which this check was called
|
|
|
6207 |
* @return (array) Vector of $absoluteXPath's (May be empty)
|
|
|
6208 |
* @see _resolveXPathQuery()
|
|
|
6209 |
*/
|
|
|
6210 |
function _resolveXPathQueryForNodeMod($xPathQuery, $functionName) {
|
|
|
6211 |
$xPathSet = array();
|
|
|
6212 |
if (empty($xPathQuery)) {
|
|
|
6213 |
// You can append even if the root node doesn't exist.
|
|
|
6214 |
if (!isset($this->nodeIndex[$xPathQuery])) $this->_createSuperRoot();
|
|
|
6215 |
$xPathSet[] = '';
|
|
|
6216 |
// However, you can only append to the super root, if there isn't already a root entry.
|
|
|
6217 |
$rootNodes = $this->_resolveXPathQuery('/*','appendChild');
|
|
|
6218 |
if (count($rootNodes) !== 0) {
|
|
|
6219 |
$this->_displayError(sprintf($this->errorStrings['RootNodeAlreadyExists']), __LINE__, __FILE__, FALSE);
|
|
|
6220 |
return array();
|
|
|
6221 |
}
|
|
|
6222 |
} else {
|
|
|
6223 |
$xPathSet = $this->_resolveXPathQuery($xPathQuery,'appendChild');
|
|
|
6224 |
if (sizeOf($xPathSet) === 0) {
|
|
|
6225 |
$this->_displayError(sprintf($this->errorStrings['NoNodeMatch'], $xPathQuery), __LINE__, __FILE__, FALSE);
|
|
|
6226 |
return array();
|
|
|
6227 |
}
|
|
|
6228 |
}
|
|
|
6229 |
return $xPathSet;
|
|
|
6230 |
}
|
|
|
6231 |
|
|
|
6232 |
/**
|
|
|
6233 |
* Resolves an xPathQuery vector depending on the property['modMatch']
|
|
|
6234 |
*
|
|
|
6235 |
* To:
|
|
|
6236 |
* - all matches,
|
|
|
6237 |
* - the first
|
|
|
6238 |
* - none (If the query matches more then one node.)
|
|
|
6239 |
* see setModMatch() for details
|
|
|
6240 |
*
|
|
|
6241 |
* @param $xPathQuery (string) An xpath query targeting a single node. If empty()
|
|
|
6242 |
* returns the root node (if it exists).
|
|
|
6243 |
* @param $function (string) The function in which this check was called
|
|
|
6244 |
* @return (array) Vector of $absoluteXPath's (May be empty)
|
|
|
6245 |
* @see setModMatch()
|
|
|
6246 |
*/
|
|
|
6247 |
function _resolveXPathQuery($xPathQuery, $function) {
|
|
|
6248 |
$xPathSet = array();
|
|
|
6249 |
do { // try-block
|
|
|
6250 |
if (isSet($this->nodeIndex[$xPathQuery])) {
|
|
|
6251 |
$xPathSet[] = $xPathQuery;
|
|
|
6252 |
break; // try-block
|
|
|
6253 |
}
|
|
|
6254 |
if (empty($xPathQuery)) break; // try-block
|
|
|
6255 |
if (substr($xPathQuery, -1) === '/') break; // If the xPathQuery ends with '/' then it cannot be a good query.
|
|
|
6256 |
// If this xPathQuery is not absolute then attempt to evaluate it
|
|
|
6257 |
$xPathSet = $this->match($xPathQuery);
|
|
|
6258 |
|
|
|
6259 |
$resultSize = sizeOf($xPathSet);
|
|
|
6260 |
switch($this->properties['modMatch']) {
|
|
|
6261 |
case XPATH_QUERYHIT_UNIQUE :
|
|
|
6262 |
if ($resultSize >1) {
|
|
|
6263 |
$xPathSet = array();
|
|
|
6264 |
if ($this->properties['verboseLevel']) $this->_displayError("Canceled function '{$function}'. The query '{$xPathQuery}' mached {$resultSize} nodes and 'modMatch' is set to XPATH_QUERYHIT_UNIQUE.", __LINE__, __FILE__, FALSE);
|
|
|
6265 |
}
|
|
|
6266 |
break;
|
|
|
6267 |
case XPATH_QUERYHIT_FIRST :
|
|
|
6268 |
if ($resultSize >1) {
|
|
|
6269 |
$xPathSet = array($xPathSet[0]);
|
|
|
6270 |
if ($this->properties['verboseLevel']) $this->_displayError("Only modified first node in function '{$function}' because the query '{$xPathQuery}' mached {$resultSize} nodes and 'modMatch' is set to XPATH_QUERYHIT_FIRST.", __LINE__, __FILE__, FALSE);
|
|
|
6271 |
}
|
|
|
6272 |
break;
|
|
|
6273 |
default: ; // DO NOTHING
|
|
|
6274 |
}
|
|
|
6275 |
} while (FALSE);
|
|
|
6276 |
|
|
|
6277 |
if ($this->properties['verboseLevel'] >= 2) $this->_displayMessage("'{$xPathQuery}' parameter from '{$function}' returned the following nodes: ".(count($xPathSet)?implode('<br>', $xPathSet):'[none]'), __LINE__, __FILE__);
|
|
|
6278 |
return $xPathSet;
|
|
|
6279 |
}
|
|
|
6280 |
} // END OF CLASS XPath
|
|
|
6281 |
|
|
|
6282 |
// -----------------------------------------------------------------------------------------
|
|
|
6283 |
// -----------------------------------------------------------------------------------------
|
|
|
6284 |
// -----------------------------------------------------------------------------------------
|
|
|
6285 |
// -----------------------------------------------------------------------------------------
|
|
|
6286 |
|
|
|
6287 |
/**************************************************************************************************
|
|
|
6288 |
// Usage Sample:
|
|
|
6289 |
// -------------
|
|
|
6290 |
// Following code will give you an idea how to work with PHP.XPath. It's a working sample
|
|
|
6291 |
// to help you get started. :o)
|
|
|
6292 |
// Take the comment tags away and run this file.
|
|
|
6293 |
**************************************************************************************************/
|
|
|
6294 |
|
|
|
6295 |
/**
|
|
|
6296 |
* Produces a short title line.
|
|
|
6297 |
*/
|
|
|
6298 |
function _title($title) {
|
|
|
6299 |
echo "<br><hr><b>" . htmlspecialchars($title) . "</b><hr>\n";
|
|
|
6300 |
}
|
|
|
6301 |
|
|
|
6302 |
$self = isSet($_SERVER) ? $_SERVER['PHP_SELF'] : $PHP_SELF;
|
|
|
6303 |
if (basename($self) == 'XPath.class.php') {
|
|
|
6304 |
// The sampe source:
|
|
|
6305 |
$q = '?';
|
|
|
6306 |
$xmlSource = <<< EOD
|
|
|
6307 |
<{$q}Process_Instruction test="© All right reserved" {$q}>
|
|
|
6308 |
<AAA foo="bar"> ,,1,,
|
|
|
6309 |
..1.. <![CDATA[ bla bla
|
|
|
6310 |
newLine blo blo ]]>
|
|
|
6311 |
<BBB foo="bar">
|
|
|
6312 |
..2..
|
|
|
6313 |
</BBB>..3..<CC/> ..4..</AAA>
|
|
|
6314 |
EOD;
|
|
|
6315 |
|
|
|
6316 |
// The sample code:
|
|
|
6317 |
$xmlOptions = array(XML_OPTION_CASE_FOLDING => TRUE, XML_OPTION_SKIP_WHITE => TRUE);
|
|
|
6318 |
$xPath = new XPath(FALSE, $xmlOptions);
|
|
|
6319 |
//$xPath->bDebugXmlParse = TRUE;
|
|
|
6320 |
if (!$xPath->importFromString($xmlSource)) { echo $xPath->getLastError(); exit; }
|
|
|
6321 |
|
|
|
6322 |
_title("Following was imported:");
|
|
|
6323 |
echo $xPath->exportAsHtml();
|
|
|
6324 |
|
|
|
6325 |
_title("Get some content");
|
|
|
6326 |
echo "Last text part in <AAA>: '" . $xPath->wholeText('/AAA[1]', -1) ."'<br>\n";
|
|
|
6327 |
echo "All the text in <AAA>: '" . $xPath->wholeText('/AAA[1]') ."'<br>\n";
|
|
|
6328 |
echo "The attibute value in <BBB> using getAttributes('/AAA[1]/BBB[1]', 'FOO'): '" . $xPath->getAttributes('/AAA[1]', 'FOO') ."'<br>\n";
|
|
|
6329 |
echo "The attibute value in <BBB> using getData('/AAA[1]/@FOO'): '" . $xPath->getData('/AAA[1]/@FOO') ."'<br>\n";
|
|
|
6330 |
|
|
|
6331 |
_title("Append some additional XML below /AAA/BBB:");
|
|
|
6332 |
$xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 1. Append new node </CCC>', $afterText=FALSE);
|
|
|
6333 |
$xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 2. Append new node </CCC>', $afterText=TRUE);
|
|
|
6334 |
$xPath->appendChild('/AAA[1]/BBB[1]', '<CCC> Step 3. Append new node </CCC>', $afterText=TRUE);
|
|
|
6335 |
echo $xPath->exportAsHtml();
|
|
|
6336 |
|
|
|
6337 |
_title("Insert some additional XML below <AAA>:");
|
|
|
6338 |
$xPath->reindexNodeTree();
|
|
|
6339 |
$xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 1. Insert new node </BB>', $shiftRight=TRUE, $afterText=TRUE);
|
|
|
6340 |
$xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 2. Insert new node </BB>', $shiftRight=FALSE, $afterText=TRUE);
|
|
|
6341 |
$xPath->insertChild('/AAA[1]/BBB[1]', '<BB> Step 3. Insert new node </BB>', $shiftRight=FALSE, $afterText=FALSE);
|
|
|
6342 |
echo $xPath->exportAsHtml();
|
|
|
6343 |
|
|
|
6344 |
_title("Replace the last <BB> node with new XML data '<DDD> Replaced last BB </DDD>':");
|
|
|
6345 |
$xPath->reindexNodeTree();
|
|
|
6346 |
$xPath->replaceChild('/AAA[1]/BB[last()]', '<DDD> Replaced last BB </DDD>', $afterText=FALSE);
|
|
|
6347 |
echo $xPath->exportAsHtml();
|
|
|
6348 |
|
|
|
6349 |
_title("Replace second <BB> node with normal text");
|
|
|
6350 |
$xPath->reindexNodeTree();
|
|
|
6351 |
$xPath->replaceChildByData('/AAA[1]/BB[2]', '"Some new text"');
|
|
|
6352 |
echo $xPath->exportAsHtml();
|
|
|
6353 |
}
|
|
|
6354 |
|
|
|
6355 |
?>
|