Passed
Push — master ( a99a98...b36977 )
by Arthur
02:30
created

DOMQuery::__clone()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @file
4
 * This houses the class formerly called QueryPath.
5
 *
6
 * As of QueryPath 3.0.0, the class was renamed QueryPath::DOMQuery. This
7
 * was done for a few reasons:
8
 * - The library has been refactored, and it made more sense to call the top
9
 *   level class QueryPath. This is not the top level class.
10
 * - There have been requests for a JSONQuery class, which would be the
11
 *   natural complement of DOMQuery.
12
 */
13
14
namespace QueryPath;
15
16
use QueryPath\CSS\DOMTraverser;
17
use \QueryPath\CSS\QueryPathEventHandler;
18
use \Masterminds\HTML5;
19
20
21
/**
22
 * The DOMQuery object is the primary tool in this library.
23
 *
24
 * To create a new DOMQuery, use QueryPath::with() or qp() function.
25
 *
26
 * If you are new to these documents, start at the QueryPath.php page.
27
 * There you will find a quick guide to the tools contained in this project.
28
 *
29
 * A note on serialization: DOMQuery uses DOM classes internally, and those
30
 * do not serialize well at all. In addition, DOMQuery may contain many
31
 * extensions, and there is no guarantee that extensions can serialize. The
32
 * moral of the story: Don't serialize DOMQuery.
33
 *
34
 * @see     qp()
35
 * @see     QueryPath.php
36
 * @ingroup querypath_core
37
 */
38
class DOMQuery extends DOM
39
{
40
41
    /**
42
     * The last array of matches.
43
     */
44
    protected $last = []; // Last set of matches.
45
    private $ext = []; // Extensions array.
46
47
    /**
48
     * The number of current matches.
49
     *
50
     * @see count()
51
     */
52
    public $length = 0;
53
54
    /**
55
     * Get the effective options for the current DOMQuery object.
56
     *
57
     * This returns an associative array of all of the options as set
58
     * for the current DOMQuery object. This includes default options,
59
     * options directly passed in via {@link qp()} or the constructor,
60
     * an options set in the QueryPath::Options object.
61
     *
62
     * The order of merging options is this:
63
     *  - Options passed in using qp() are highest priority, and will
64
     *    override other options.
65
     *  - Options set with QueryPath::Options will override default options,
66
     *    but can be overridden by options passed into qp().
67
     *  - Default options will be used when no overrides are present.
68
     *
69
     * This function will return the options currently used, with the above option
70
     * overriding having been calculated already.
71
     *
72
     * @return array
73
     *  An associative array of options, calculated from defaults and overridden
74
     *  options.
75
     * @see   qp()
76
     * @see   QueryPath::Options::set()
77
     * @see   QueryPath::Options::merge()
78
     * @since 2.0
79
     */
80
    public function getOptions(): array
81
    {
82
        return $this->options;
83
    }
84
85
    /**
86
     * Select the root element of the document.
87
     *
88
     * This sets the current match to the document's root element. For
89
     * practical purposes, this is the same as:
90
     *
91
     * @code
92
     * qp($someDoc)->find(':root');
93
     * @endcode
94
     * However, since it doesn't invoke a parser, it has less overhead. It also
95
     * works in cases where the QueryPath has been reduced to zero elements (a
96
     * case that is not handled by find(':root') because there is no element
97
     * whose root can be found).
98
     *
99
     * @param string $selector
100
     *  A selector. If this is supplied, QueryPath will navigate to the
101
     *  document root and then run the query. (Added in QueryPath 2.0 Beta 2)
102
     * @return \QueryPath\DOMQuery
103
     *  The DOMQuery object, wrapping the root element (document element)
104
     *  for the current document.
105
     */
106
    public function top($selector = NULL): DOMQuery
107
    {
108
        return $this->inst($this->document->documentElement, $selector, $this->options);
109
    }
110
111
    /**
112
     * Given a CSS Selector, find matching items.
113
     *
114
     * @param string $selector
115
     *   CSS 3 Selector
116
     * @return \QueryPath\DOMQuery
117
     * @see  filter()
118
     * @see  is()
119
     * @todo If a find() returns zero matches, then a subsequent find() will
120
     *   also return zero matches, even if that find has a selector like :root.
121
     *   The reason for this is that the {@link QueryPathEventHandler} does
122
     *   not set the root of the document tree if it cannot find any elements
123
     *   from which to determine what the root is. The workaround is to use
124
     *   {@link top()} to select the root element again.
125
     * @throws CSS\ParseException
126
     */
127
    public function find($selector): DOMQuery
128
    {
129
        $query = new DOMTraverser($this->matches);
0 ignored issues
show
Bug introduced by
It seems like $this->matches can also be of type array; however, parameter $splos of QueryPath\CSS\DOMTraverser::__construct() does only seem to accept SPLObjectStorage, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

129
        $query = new DOMTraverser(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
130
        $query->find($selector);
131
        return $this->inst($query->matches(), NULL, $this->options);
132
    }
133
134
    /**
135
     * @param $selector
136
     * @return $this
137
     * @throws CSS\ParseException
138
     */
139
    public function findInPlace($selector)
140
    {
141
        $query = new DOMTraverser($this->matches);
0 ignored issues
show
Bug introduced by
It seems like $this->matches can also be of type array; however, parameter $splos of QueryPath\CSS\DOMTraverser::__construct() does only seem to accept SPLObjectStorage, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

141
        $query = new DOMTraverser(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
142
        $query->find($selector);
143
        $this->setMatches($query->matches());
144
145
        return $this;
146
    }
147
148
    /**
149
     * Execute an XPath query and store the results in the QueryPath.
150
     *
151
     * Most methods in this class support CSS 3 Selectors. Sometimes, though,
152
     * XPath provides a finer-grained query language. Use this to execute
153
     * XPath queries.
154
     *
155
     * Beware, though. DOMQuery works best on DOM Elements, but an XPath
156
     * query can return other nodes, strings, and values. These may not work with
157
     * other QueryPath functions (though you will be able to access the
158
     * values with {@link get()}).
159
     *
160
     * @param string $query
161
     *      An XPath query.
162
     * @param array $options
163
     *      Currently supported options are:
164
     *      - 'namespace_prefix': And XML namespace prefix to be used as the default. Used
165
     *      in conjunction with 'namespace_uri'
166
     *      - 'namespace_uri': The URI to be used as the default namespace URI. Used
167
     *      with 'namespace_prefix'
168
     * @return \QueryPath\DOMQuery
169
     *      A DOMQuery object wrapping the results of the query.
170
     * @see    find()
171
     * @author M Butcher
172
     * @author Xavier Prud'homme
173
     */
174
    public function xpath($query, $options = [])
175
    {
176
        $xpath = new \DOMXPath($this->document);
177
178
        // Register a default namespace.
179
        if (!empty($options['namespace_prefix']) && !empty($options['namespace_uri'])) {
180
            $xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
181
        }
182
183
        $found = new \SplObjectStorage();
184
        foreach ($this->matches as $item) {
185
            $nl = $xpath->query($query, $item);
186
            if ($nl->length > 0) {
187
                for ($i = 0; $i < $nl->length; ++$i) {
188
                    $found->attach($nl->item($i));
189
                }
190
            }
191
        }
192
193
        return $this->inst($found, NULL, $this->options);
194
    }
195
196
    /**
197
     * Get the number of elements currently wrapped by this object.
198
     *
199
     * Note that there is no length property on this object.
200
     *
201
     * @return int
202
     *  Number of items in the object.
203
     * @deprecated QueryPath now implements Countable, so use count().
204
     */
205
    public function size()
206
    {
207
        return $this->matches->count();
0 ignored issues
show
Bug introduced by
The method count() does not exist on Traversable. It seems like you code against a sub-type of Traversable such as DOMNodeList or Yaf_Config_Simple or Yaf\Session or Threaded or MockeryTest_InterfaceWithTraversable or SimpleXMLElement or DOMNamedNodeMap or Thread or Yaf_Session or Yaf\Config\Simple or Yaf\Config\Ini or Worker or Yaf_Config_Ini or Stackable or MongoGridFSCursor or ResourceBundle or PharIo\Manifest\AuthorCollection or QueryPath\CSS\Selector or QueryPath\DOM or QueryPathImpl or PharIo\Manifest\BundledComponentCollection or SebastianBergmann\CodeCoverage\Node\Directory or ArrayObject or PHPUnit\Framework\TestSuite or PharIo\Manifest\RequirementCollection or SplDoublyLinkedList or HttpMessage or HttpRequestPool or Yaf_Config_Simple or SplFixedArray or SplObjectStorage or Yaf\Session or SQLiteResult or Imagick or TheSeer\Tokenizer\TokenCollection or Yaf_Session or SplPriorityQueue or Yaf\Config\Simple or Yaf\Config\Ini or MongoCursor or Yaf_Config_Ini or SplHeap or MongoGridFSCursor or CachingIterator or PHP_Token_Stream or Phar or ArrayIterator or GlobIterator or Phar or Phar or RecursiveCachingIterator or RecursiveArrayIterator or SimpleXMLIterator or Phar. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

207
        return $this->matches->/** @scrutinizer ignore-call */ count();
Loading history...
208
    }
209
210
    /**
211
     * Get the number of elements currently wrapped by this object.
212
     *
213
     * Since DOMQuery is Countable, the PHP count() function can also
214
     * be used on a DOMQuery.
215
     *
216
     * @code
217
     * <?php
218
     *  count(qp($xml, 'div'));
219
     * ?>
220
     * @endcode
221
     *
222
     * @return int
223
     *  The number of matches in the DOMQuery.
224
     */
225
    public function count(): int
226
    {
227
        return $this->matches->count();
228
    }
229
230
    /**
231
     * Get one or all elements from this object.
232
     *
233
     * When called with no paramaters, this returns all objects wrapped by
234
     * the DOMQuery. Typically, these are DOMElement objects (unless you have
235
     * used map(), xpath(), or other methods that can select
236
     * non-elements).
237
     *
238
     * When called with an index, it will return the item in the DOMQuery with
239
     * that index number.
240
     *
241
     * Calling this method does not change the DOMQuery (e.g. it is
242
     * non-destructive).
243
     *
244
     * You can use qp()->get() to iterate over all elements matched. You can
245
     * also iterate over qp() itself (DOMQuery implementations must be Traversable).
246
     * In the later case, though, each item
247
     * will be wrapped in a DOMQuery object. To learn more about iterating
248
     * in QueryPath, see {@link examples/techniques.php}.
249
     *
250
     * @param int $index
251
     *   If specified, then only this index value will be returned. If this
252
     *   index is out of bounds, a NULL will be returned.
253
     * @param boolean $asObject
254
     *   If this is TRUE, an SplObjectStorage object will be returned
255
     *   instead of an array. This is the preferred method for extensions to use.
256
     * @return mixed
257
     *   If an index is passed, one element will be returned. If no index is
258
     *   present, an array of all matches will be returned.
259
     * @see eq()
260
     * @see SplObjectStorage
261
     */
262
    public function get($index = NULL, $asObject = false)
263
    {
264
        if ($index !== NULL) {
265
            return ($this->count() > $index) ? $this->getNthMatch($index) : NULL;
266
        }
267
        // Retain support for legacy.
268
        if (!$asObject) {
269
            $matches = [];
270
            foreach ($this->matches as $m) {
271
                $matches[] = $m;
272
            }
273
274
            return $matches;
275
        }
276
277
        return $this->matches;
278
    }
279
280
    /**
281
     * Get the namespace of the current element.
282
     *
283
     * If QP is currently pointed to a list of elements, this will get the
284
     * namespace of the first element.
285
     */
286
    public function ns()
287
    {
288
        return $this->get(0)->namespaceURI;
289
    }
290
291
    /**
292
     * Get the DOMDocument that we currently work with.
293
     *
294
     * This returns the current DOMDocument. Any changes made to this document will be
295
     * accessible to DOMQuery, as both will share access to the same object.
296
     *
297
     * @return DOMDocument
0 ignored issues
show
Bug introduced by
The type QueryPath\DOMDocument was not found. Did you mean DOMDocument? If so, make sure to prefix the type with \.
Loading history...
298
     */
299
    public function document()
300
    {
301
        return $this->document;
302
    }
303
304
    /**
305
     * On an XML document, load all XIncludes.
306
     *
307
     * @return \QueryPath\DOMQuery
308
     */
309
    public function xinclude()
310
    {
311
        $this->document->xinclude();
312
313
        return $this;
314
    }
315
316
    /**
317
     * Get all current elements wrapped in an array.
318
     * Compatibility function for jQuery 1.4, but identical to calling {@link get()}
319
     * with no parameters.
320
     *
321
     * @return array
322
     *  An array of DOMNodes (typically DOMElements).
323
     */
324
    public function toArray()
325
    {
326
        return $this->get();
327
    }
328
329
    /**
330
     * Get/set an attribute.
331
     * - If no parameters are specified, this returns an associative array of all
332
     *   name/value pairs.
333
     * - If both $name and $value are set, then this will set the attribute name/value
334
     *   pair for all items in this object.
335
     * - If $name is set, and is an array, then
336
     *   all attributes in the array will be set for all items in this object.
337
     * - If $name is a string and is set, then the attribute value will be returned.
338
     *
339
     * When an attribute value is retrieved, only the attribute value of the FIRST
340
     * match is returned.
341
     *
342
     * @param mixed $name
343
     *   The name of the attribute or an associative array of name/value pairs.
344
     * @param string $value
345
     *   A value (used only when setting an individual property).
346
     * @return mixed
347
     *   If this was a setter request, return the DOMQuery object. If this was
348
     *   an access request (getter), return the string value.
349
     * @see removeAttr()
350
     * @see tag()
351
     * @see hasAttr()
352
     * @see hasClass()
353
     */
354
    public function attr($name = NULL, $value = NULL)
355
    {
356
357
        // Default case: Return all attributes as an assoc array.
358
        if (is_null($name)) {
359
            if ($this->matches->count() == 0) {
360
                return NULL;
361
            }
362
            $ele = $this->getFirstMatch();
363
            $buffer = [];
364
365
            // This does not appear to be part of the DOM
366
            // spec. Nor is it documented. But it works.
367
            foreach ($ele->attributes as $name => $attrNode) {
368
                $buffer[$name] = $attrNode->value;
369
            }
370
371
            return $buffer;
372
        }
373
374
        // multi-setter
375
        if (is_array($name)) {
376
            foreach ($name as $k => $v) {
377
                foreach ($this->matches as $m) {
378
                    $m->setAttribute($k, $v);
379
                }
380
            }
381
382
            return $this;
383
        }
384
        // setter
385
        if (isset($value)) {
386
            foreach ($this->matches as $m) {
387
                $m->setAttribute($name, $value);
388
            }
389
390
            return $this;
391
        }
392
393
        //getter
394
        if ($this->matches->count() == 0) {
395
            return NULL;
396
        }
397
398
        // Special node type handler:
399
        if ($name == 'nodeType') {
400
            return $this->getFirstMatch()->nodeType;
401
        }
402
403
        // Always return first match's attr.
404
        return $this->getFirstMatch()->getAttribute($name);
405
    }
406
407
    /**
408
     * Check to see if the given attribute is present.
409
     *
410
     * This returns TRUE if <em>all</em> selected items have the attribute, or
411
     * FALSE if at least one item does not have the attribute.
412
     *
413
     * @param string $attrName
414
     *  The attribute name.
415
     * @return boolean
416
     *  TRUE if all matches have the attribute, FALSE otherwise.
417
     * @since 2.0
418
     * @see   attr()
419
     * @see   hasClass()
420
     */
421
    public function hasAttr($attrName)
422
    {
423
        foreach ($this->matches as $match) {
424
            if (!$match->hasAttribute($attrName)) {
425
                return false;
426
            }
427
        }
428
429
        return true;
430
    }
431
432
    /**
433
     * Set/get a CSS value for the current element(s).
434
     * This sets the CSS value for each element in the DOMQuery object.
435
     * It does this by setting (or getting) the style attribute (without a namespace).
436
     *
437
     * For example, consider this code:
438
     *
439
     * @code
440
     * <?php
441
     * qp(HTML_STUB, 'body')->css('background-color','red')->html();
442
     * ?>
443
     * @endcode
444
     * This will return the following HTML:
445
     * @code
446
     * <body style="background-color: red"/>
447
     * @endcode
448
     *
449
     * If no parameters are passed into this function, then the current style
450
     * element will be returned unparsed. Example:
451
     * @code
452
     * <?php
453
     * qp(HTML_STUB, 'body')->css('background-color','red')->css();
454
     * ?>
455
     * @endcode
456
     * This will return the following:
457
     * @code
458
     * background-color: red
459
     * @endcode
460
     *
461
     * As of QueryPath 2.1, existing style attributes will be merged with new attributes.
462
     * (In previous versions of QueryPath, a call to css() overwrite the existing style
463
     * values).
464
     *
465
     * @param mixed $name
466
     *  If this is a string, it will be used as a CSS name. If it is an array,
467
     *  this will assume it is an array of name/value pairs of CSS rules. It will
468
     *  apply all rules to all elements in the set.
469
     * @param string $value
470
     *  The value to set. This is only set if $name is a string.
471
     * @return \QueryPath\DOMQuery
472
     */
473
    public function css($name = NULL, $value = '')
474
    {
475
        if (empty($name)) {
476
            return $this->attr('style');
477
        }
478
479
        // Get any existing CSS.
480
        $css = [];
481
        foreach ($this->matches as $match) {
482
            $style = $match->getAttribute('style');
483
            if (!empty($style)) {
484
                // XXX: Is this sufficient?
485
                $style_array = explode(';', $style);
486
                foreach ($style_array as $item) {
487
                    $item = trim($item);
488
489
                    // Skip empty attributes.
490
                    if (strlen($item) == 0) {
491
                        continue;
492
                    }
493
494
                    list($css_att, $css_val) = explode(':', $item, 2);
495
                    $css[$css_att] = trim($css_val);
496
                }
497
            }
498
        }
499
500
        if (is_array($name)) {
501
            // Use array_merge instead of + to preserve order.
502
            $css = array_merge($css, $name);
503
        } else {
504
            $css[$name] = $value;
505
        }
506
507
        // Collapse CSS into a string.
508
        $format = '%s: %s;';
509
        $css_string = '';
510
        foreach ($css as $n => $v) {
511
            $css_string .= sprintf($format, $n, trim($v));
512
        }
513
514
        $this->attr('style', $css_string);
515
516
        return $this;
517
    }
518
519
    /**
520
     * Insert or retrieve a Data URL.
521
     *
522
     * When called with just $attr, it will fetch the result, attempt to decode it, and
523
     * return an array with the MIME type and the application data.
524
     *
525
     * When called with both $attr and $data, it will inject the data into all selected elements
526
     * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject
527
     * the given PNG image into the selected elements.
528
     *
529
     * The current implementation only knows how to encode and decode Base 64 data.
530
     *
531
     * Note that this is known *not* to work on IE 6, but should render fine in other browsers.
532
     *
533
     * @param string $attr
534
     *    The name of the attribute.
535
     * @param mixed $data
536
     *    The contents to inject as the data. The value can be any one of the following:
537
     *    - A URL: If this is given, then the subsystem will read the content from that URL. THIS
538
     *    MUST BE A FULL URL, not a relative path.
539
     *    - A string of data: If this is given, then the subsystem will encode the string.
540
     *    - A stream or file handle: If this is given, the stream's contents will be encoded
541
     *    and inserted as data.
542
     *    (Note that we make the assumption here that you would never want to set data to be
543
     *    a URL. If this is an incorrect assumption, file a bug.)
544
     * @param string $mime
545
     *    The MIME type of the document.
546
     * @param resource $context
547
     *    A valid context. Use this only if you need to pass a stream context. This is only necessary
548
     *    if $data is a URL. (See {@link stream_context_create()}).
549
     * @return \QueryPath\DOMQuery|string
550
     *    If this is called as a setter, this will return a DOMQuery object. Otherwise, it
551
     *    will attempt to fetch data out of the attribute and return that.
552
     * @see   http://en.wikipedia.org/wiki/Data:_URL
553
     * @see   attr()
554
     * @since 2.1
555
     */
556
    public function dataURL($attr, $data = NULL, $mime = 'application/octet-stream', $context = NULL)
557
    {
558
        if (is_null($data)) {
559
            // Attempt to fetch the data
560
            $data = $this->attr($attr);
561
            if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) {
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type QueryPath\DOMQuery; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

561
            if (empty($data) || is_array($data) || strpos(/** @scrutinizer ignore-type */ $data, 'data:') !== 0) {
Loading history...
562
                return;
563
            }
564
565
            // So 1 and 2 should be MIME types, and 3 should be the base64-encoded data.
566
            $regex = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/';
567
            $matches = [];
568
            preg_match($regex, $data, $matches);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type QueryPath\DOMQuery; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

568
            preg_match($regex, /** @scrutinizer ignore-type */ $data, $matches);
Loading history...
569
570
            if (!empty($matches)) {
571
                $result = [
572
                    'mime' => $matches[1] . '/' . $matches[2],
573
                    'data' => base64_decode($matches[3]),
574
                ];
575
576
                return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type array<string,string> which is incompatible with the documented return type QueryPath\DOMQuery|string.
Loading history...
577
            }
578
        } else {
579
            $attVal = QueryPath::encodeDataURL($data, $mime, $context);
580
581
            return $this->attr($attr, $attVal);
582
        }
583
    }
584
585
    /**
586
     * Remove the named attribute from all elements in the current DOMQuery.
587
     *
588
     * This will remove any attribute with the given name. It will do this on each
589
     * item currently wrapped by DOMQuery.
590
     *
591
     * As is the case in jQuery, this operation is not considered destructive.
592
     *
593
     * @param string $name
594
     *  Name of the parameter to remove.
595
     * @return \QueryPath\DOMQuery
596
     *  The DOMQuery object with the same elements.
597
     * @see attr()
598
     */
599
    public function removeAttr($name)
600
    {
601
        foreach ($this->matches as $m) {
602
            //if ($m->hasAttribute($name))
603
            $m->removeAttribute($name);
604
        }
605
606
        return $this;
607
    }
608
609
    /**
610
     * Reduce the matched set to just one.
611
     *
612
     * This will take a matched set and reduce it to just one item -- the item
613
     * at the index specified. This is a destructive operation, and can be undone
614
     * with {@link end()}.
615
     *
616
     * @param $index
617
     *  The index of the element to keep. The rest will be
618
     *  discarded.
619
     * @return \QueryPath\DOMQuery
620
     * @see get()
621
     * @see is()
622
     * @see end()
623
     */
624
    public function eq($index)
625
    {
626
        return $this->inst($this->getNthMatch($index), NULL, $this->options);
627
        // XXX: Might there be a more efficient way of doing this?
628
        //$this->setMatches($this->getNthMatch($index));
629
        //return $this;
630
    }
631
632
    /**
633
     * Given a selector, this checks to see if the current set has one or more matches.
634
     *
635
     * Unlike jQuery's version, this supports full selectors (not just simple ones).
636
     *
637
     * @param string $selector
638
     *   The selector to search for. As of QueryPath 2.1.1, this also supports passing a
639
     *   DOMNode object.
640
     * @return boolean
641
     *   TRUE if one or more elements match. FALSE if no match is found.
642
     * @see get()
643
     * @see eq()
644
     */
645
    public function is($selector)
646
    {
647
648
        if (is_object($selector)) {
0 ignored issues
show
introduced by
The condition is_object($selector) is always false.
Loading history...
649
            if ($selector instanceof \DOMNode) {
650
                return count($this->matches) == 1 && $selector->isSameNode($this->get(0));
651
            } elseif ($selector instanceof \Traversable) {
652
                if (count($selector) != count($this->matches)) {
653
                    return false;
654
                }
655
                // Without $seen, there is an edge case here if $selector contains the same object
656
                // more than once, but the counts are equal. For example, [a, a, a, a] will
657
                // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this.
658
                $seen = new \SplObjectStorage();
659
                foreach ($selector as $item) {
660
                    if (!$this->matches->contains($item) || $seen->contains($item)) {
661
                        return false;
662
                    }
663
                    $seen->attach($item);
664
                }
665
666
                return true;
667
            }
668
            throw new \QueryPath\Exception('Cannot compare an object to a DOMQuery.');
669
670
            return false;
0 ignored issues
show
Unused Code introduced by
return false is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
671
        }
672
673
        // Testing based on Issue #70.
674
        //fprintf(STDOUT, __FUNCTION__  .' found %d', $this->find($selector)->count());
675
        return $this->branch($selector)->count() > 0;
676
677
        // Old version:
678
        //foreach ($this->matches as $m) {
679
        //$q = new \QueryPath\CSS\QueryPathEventHandler($m);
680
        //if ($q->find($selector)->getMatches()->count()) {
681
        //return TRUE;
682
        //}
683
        //}
684
        //return FALSE;
685
    }
686
687
    /**
688
     * Filter a list down to only elements that match the selector.
689
     * Use this, for example, to find all elements with a class, or with
690
     * certain children.
691
     *
692
     * @param string $selector
693
     *   The selector to use as a filter.
694
     * @return \QueryPath\DOMQuery
695
     *   The DOMQuery with non-matching items filtered out.
696
     * @see filterLambda()
697
     * @see filterCallback()
698
     * @see map()
699
     * @see find()
700
     * @see is()
701
     */
702
    public function filter($selector)
703
    {
704
705
        $found = new \SplObjectStorage();
706
        $tmp = new \SplObjectStorage();
707
        foreach ($this->matches as $m) {
708
            $tmp->attach($m);
709
            // Seems like this should be right... but it fails unit
710
            // tests. Need to compare to jQuery.
711
            // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m);
712
            $query = new DOMTraverser($tmp);
713
            $query->find($selector);
714
            if (count($query->matches())) {
715
                $found->attach($m);
716
            }
717
            $tmp->detach($m);
718
        }
719
720
        return $this->inst($found, NULL, $this->options);
721
    }
722
723
    /**
724
     * Sort the contents of the QueryPath object.
725
     *
726
     * By default, this does not change the order of the elements in the
727
     * DOM. Instead, it just sorts the internal list. However, if TRUE
728
     * is passed in as the second parameter then QueryPath will re-order
729
     * the DOM, too.
730
     *
731
     * @attention
732
     * DOM re-ordering is done by finding the location of the original first
733
     * item in the list, and then placing the sorted list at that location.
734
     *
735
     * The argument $compartor is a callback, such as a function name or a
736
     * closure. The callback receives two DOMNode objects, which you can use
737
     * as DOMNodes, or wrap in QueryPath objects.
738
     *
739
     * A simple callback:
740
     * @code
741
     * <?php
742
     * $comp = function (\DOMNode $a, \DOMNode $b) {
743
     *   if ($a->textContent == $b->textContent) {
744
     *     return 0;
745
     *   }
746
     *   return $a->textContent > $b->textContent ? 1 : -1;
747
     * };
748
     * $qp = QueryPath::with($xml, $selector)->sort($comp);
749
     * ?>
750
     * @endcode
751
     *
752
     * The above sorts the matches into lexical order using the text of each node.
753
     * If you would prefer to work with QueryPath objects instead of DOMNode
754
     * objects, you may prefer something like this:
755
     *
756
     * @code
757
     * <?php
758
     * $comp = function (\DOMNode $a, \DOMNode $b) {
759
     *   $qpa = qp($a);
760
     *   $qpb = qp($b);
761
     *
762
     *   if ($qpa->text() == $qpb->text()) {
763
     *     return 0;
764
     *   }
765
     *   return $qpa->text()> $qpb->text()? 1 : -1;
766
     * };
767
     *
768
     * $qp = QueryPath::with($xml, $selector)->sort($comp);
769
     * ?>
770
     * @endcode
771
     *
772
     * @param callback $comparator
773
     *   A callback. This will be called during sorting to compare two DOMNode
774
     *   objects.
775
     * @param boolean $modifyDOM
776
     *   If this is TRUE, the sorted results will be inserted back into
777
     *   the DOM at the position of the original first element.
778
     * @return \QueryPath\DOMQuery
779
     *   This object.
780
     */
781
    public function sort($comparator, $modifyDOM = false)
782
    {
783
        // Sort as an array.
784
        $list = iterator_to_array($this->matches);
0 ignored issues
show
Bug introduced by
It seems like $this->matches can also be of type array; however, parameter $iterator of iterator_to_array() does only seem to accept Traversable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

784
        $list = iterator_to_array(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
785
786
        if (empty($list)) {
787
            return $this;
788
        }
789
790
        $oldFirst = $list[0];
791
792
        usort($list, $comparator);
793
794
        // Copy back into SplObjectStorage.
795
        $found = new \SplObjectStorage();
796
        foreach ($list as $node) {
797
            $found->attach($node);
798
        }
799
        //$this->setMatches($found);
800
801
802
        // Do DOM modifications only if necessary.
803
        if ($modifyDOM) {
804
            $placeholder = $oldFirst->ownerDocument->createElement('_PLACEHOLDER_');
805
            $placeholder = $oldFirst->parentNode->insertBefore($placeholder, $oldFirst);
806
            $len = count($list);
807
            for ($i = 0; $i < $len; ++$i) {
808
                $node = $list[$i];
809
                $node = $node->parentNode->removeChild($node);
810
                $placeholder->parentNode->insertBefore($node, $placeholder);
811
            }
812
            $placeholder->parentNode->removeChild($placeholder);
813
        }
814
815
        return $this->inst($found, NULL, $this->options);
816
    }
817
818
    /**
819
     * Filter based on a lambda function.
820
     *
821
     * The function string will be executed as if it were the body of a
822
     * function. It is passed two arguments:
823
     * - $index: The index of the item.
824
     * - $item: The current Element.
825
     * If the function returns boolean FALSE, the item will be removed from
826
     * the list of elements. Otherwise it will be kept.
827
     *
828
     * Example:
829
     *
830
     * @code
831
     * qp('li')->filterLambda('qp($item)->attr("id") == "test"');
832
     * @endcode
833
     *
834
     * The above would filter down the list to only an item whose ID is
835
     * 'text'.
836
     *
837
     * @param string $fn
838
     *  Inline lambda function in a string.
839
     * @return \QueryPath\DOMQuery
840
     * @see filter()
841
     * @see map()
842
     * @see mapLambda()
843
     * @see filterCallback()
844
     */
845
    public function filterLambda($fn)
846
    {
847
        $function = create_function('$index, $item', $fn);
0 ignored issues
show
Deprecated Code introduced by
The function create_function() has been deprecated: 7.2 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

847
        $function = /** @scrutinizer ignore-deprecated */ create_function('$index, $item', $fn);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
848
        $found = new \SplObjectStorage();
849
        $i = 0;
850
        foreach ($this->matches as $item) {
851
            if ($function($i++, $item) !== false) {
852
                $found->attach($item);
853
            }
854
        }
855
856
        return $this->inst($found, NULL, $this->options);
857
    }
858
859
    /**
860
     * Use regular expressions to filter based on the text content of matched elements.
861
     *
862
     * Only items that match the given regular expression will be kept. All others will
863
     * be removed.
864
     *
865
     * The regular expression is run against the <i>text content</i> (the PCDATA) of the
866
     * elements. This is a way of filtering elements based on their content.
867
     *
868
     * Example:
869
     *
870
     * @code
871
     *  <?xml version="1.0"?>
872
     *  <div>Hello <i>World</i></div>
873
     * @endcode
874
     *
875
     * @code
876
     *  <?php
877
     *    // This will be 1.
878
     *    qp($xml, 'div')->filterPreg('/World/')->size();
879
     *  ?>
880
     * @endcode
881
     *
882
     * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is
883
     * @codeHello World@endcode.
884
     *
885
     * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class.
886
     *
887
     * @param string $regex
888
     *  A regular expression.
889
     * @return \QueryPath\DOMQuery
890
     * @see       filter()
891
     * @see       filterCallback()
892
     * @see       preg_match()
893
     */
894
    public function filterPreg($regex)
895
    {
896
897
        $found = new \SplObjectStorage();
898
899
        foreach ($this->matches as $item) {
900
            if (preg_match($regex, $item->textContent) > 0) {
901
                $found->attach($item);
902
            }
903
        }
904
905
        return $this->inst($found, NULL, $this->options);
906
    }
907
908
    /**
909
     * Filter based on a callback function.
910
     *
911
     * A callback may be any of the following:
912
     *  - a function: 'my_func'.
913
     *  - an object/method combo: $obj, 'myMethod'
914
     *  - a class/method combo: 'MyClass', 'myMethod'
915
     * Note that classes are passed in strings. Objects are not.
916
     *
917
     * Each callback is passed to arguments:
918
     *  - $index: The index position of the object in the array.
919
     *  - $item: The item to be operated upon.
920
     *
921
     * If the callback function returns FALSE, the item will be removed from the
922
     * set of matches. Otherwise the item will be considered a match and left alone.
923
     *
924
     * @param callback $callback .
925
     *                           A callback either as a string (function) or an array (object, method OR
926
     *                           classname, method).
927
     * @return \QueryPath\DOMQuery
928
     *                           Query path object augmented according to the function.
929
     * @see filter()
930
     * @see filterLambda()
931
     * @see map()
932
     * @see is()
933
     * @see find()
934
     */
935
    public function filterCallback($callback)
936
    {
937
        $found = new \SplObjectStorage();
938
        $i = 0;
939
        if (is_callable($callback)) {
940
            foreach ($this->matches as $item) {
941
                if (call_user_func($callback, $i++, $item) !== false) {
942
                    $found->attach($item);
943
                }
944
            }
945
        } else {
946
            throw new \QueryPath\Exception('The specified callback is not callable.');
947
        }
948
949
        return $this->inst($found, NULL, $this->options);
950
    }
951
952
    /**
953
     * Filter a list to contain only items that do NOT match.
954
     *
955
     * @param string $selector
956
     *  A selector to use as a negation filter. If the filter is matched, the
957
     *  element will be removed from the list.
958
     * @return \QueryPath\DOMQuery
959
     *  The DOMQuery object with matching items filtered out.
960
     * @see find()
961
     */
962
    public function not($selector)
963
    {
964
        $found = new \SplObjectStorage();
965
        if ($selector instanceof \DOMElement) {
0 ignored issues
show
introduced by
$selector is never a sub-type of DOMElement.
Loading history...
966
            foreach ($this->matches as $m) {
967
                if ($m !== $selector) {
968
                    $found->attach($m);
969
                }
970
            }
971
        } elseif (is_array($selector)) {
0 ignored issues
show
introduced by
The condition is_array($selector) is always false.
Loading history...
972
            foreach ($this->matches as $m) {
973
                if (!in_array($m, $selector, true)) {
974
                    $found->attach($m);
975
                }
976
            }
977
        } elseif ($selector instanceof \SplObjectStorage) {
0 ignored issues
show
introduced by
$selector is never a sub-type of SplObjectStorage.
Loading history...
978
            foreach ($this->matches as $m) {
979
                if ($selector->contains($m)) {
980
                    $found->attach($m);
981
                }
982
            }
983
        } else {
984
            foreach ($this->matches as $m) {
985
                if (!QueryPath::with($m, NULL, $this->options)->is($selector)) {
986
                    $found->attach($m);
987
                }
988
            }
989
        }
990
991
        return $this->inst($found, NULL, $this->options);
992
    }
993
994
    /**
995
     * Get an item's index.
996
     *
997
     * Given a DOMElement, get the index from the matches. This is the
998
     * converse of {@link get()}.
999
     *
1000
     * @param DOMElement $subject
0 ignored issues
show
Bug introduced by
The type QueryPath\DOMElement was not found. Did you mean DOMElement? If so, make sure to prefix the type with \.
Loading history...
1001
     *  The item to match.
1002
     *
1003
     * @return mixed
1004
     *  The index as an integer (if found), or boolean FALSE. Since 0 is a
1005
     *  valid index, you should use strong equality (===) to test..
1006
     * @see get()
1007
     * @see is()
1008
     */
1009
    public function index($subject)
1010
    {
1011
1012
        $i = 0;
1013
        foreach ($this->matches as $m) {
1014
            if ($m === $subject) {
1015
                return $i;
1016
            }
1017
            ++$i;
1018
        }
1019
1020
        return false;
1021
    }
1022
1023
    /**
1024
     * Run a function on each item in a set.
1025
     *
1026
     * The mapping callback can return anything. Whatever it returns will be
1027
     * stored as a match in the set, though. This means that afer a map call,
1028
     * there is no guarantee that the elements in the set will behave correctly
1029
     * with other DOMQuery functions.
1030
     *
1031
     * Callback rules:
1032
     * - If the callback returns NULL, the item will be removed from the array.
1033
     * - If the callback returns an array, the entire array will be stored in
1034
     *   the results.
1035
     * - If the callback returns anything else, it will be appended to the array
1036
     *   of matches.
1037
     *
1038
     * @param callback $callback
1039
     *  The function or callback to use. The callback will be passed two params:
1040
     *  - $index: The index position in the list of items wrapped by this object.
1041
     *  - $item: The current item.
1042
     *
1043
     * @return \QueryPath\DOMQuery
1044
     *  The DOMQuery object wrapping a list of whatever values were returned
1045
     *  by each run of the callback.
1046
     *
1047
     * @see DOMQuery::get()
1048
     * @see filter()
1049
     * @see find()
1050
     */
1051
    public function map($callback)
1052
    {
1053
        $found = new \SplObjectStorage();
1054
1055
        if (is_callable($callback)) {
1056
            $i = 0;
1057
            foreach ($this->matches as $item) {
1058
                $c = call_user_func($callback, $i, $item);
1059
                if (isset($c)) {
1060
                    if (is_array($c) || $c instanceof \Iterable) {
1061
                        foreach ($c as $retval) {
1062
                            if (!is_object($retval)) {
1063
                                $tmp = new \stdClass();
1064
                                $tmp->textContent = $retval;
1065
                                $retval = $tmp;
1066
                            }
1067
                            $found->attach($retval);
1068
                        }
1069
                    } else {
1070
                        if (!is_object($c)) {
1071
                            $tmp = new \stdClass();
1072
                            $tmp->textContent = $c;
1073
                            $c = $tmp;
1074
                        }
1075
                        $found->attach($c);
1076
                    }
1077
                }
1078
                ++$i;
1079
            }
1080
        } else {
1081
            throw new \QueryPath\Exception('Callback is not callable.');
1082
        }
1083
1084
        return $this->inst($found, NULL, $this->options);
1085
    }
1086
1087
    /**
1088
     * Narrow the items in this object down to only a slice of the starting items.
1089
     *
1090
     * @param integer $start
1091
     *  Where in the list of matches to begin the slice.
1092
     * @param integer $length
1093
     *  The number of items to include in the slice. If nothing is specified, the
1094
     *  all remaining matches (from $start onward) will be included in the sliced
1095
     *  list.
1096
     * @return \QueryPath\DOMQuery
1097
     * @see array_slice()
1098
     */
1099
    public function slice($start, $length = 0)
1100
    {
1101
        $end = $length;
1102
        $found = new \SplObjectStorage();
1103
        if ($start >= $this->size()) {
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1103
        if ($start >= /** @scrutinizer ignore-deprecated */ $this->size()) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1104
            return $this->inst($found, NULL, $this->options);
1105
        }
1106
1107
        $i = $j = 0;
1108
        foreach ($this->matches as $m) {
1109
            if ($i >= $start) {
1110
                if ($end > 0 && $j >= $end) {
1111
                    break;
1112
                }
1113
                $found->attach($m);
1114
                ++$j;
1115
            }
1116
            ++$i;
1117
        }
1118
1119
        return $this->inst($found, NULL, $this->options);
1120
    }
1121
1122
    /**
1123
     * Run a callback on each item in the list of items.
1124
     *
1125
     * Rules of the callback:
1126
     * - A callback is passed two variables: $index and $item. (There is no
1127
     *   special treatment of $this, as there is in jQuery.)
1128
     *   - You will want to pass $item by reference if it is not an
1129
     *     object (DOMNodes are all objects).
1130
     * - A callback that returns FALSE will stop execution of the each() loop. This
1131
     *   works like break in a standard loop.
1132
     * - A TRUE return value from the callback is analogous to a continue statement.
1133
     * - All other return values are ignored.
1134
     *
1135
     * @param callback $callback
1136
     *  The callback to run.
1137
     * @return \QueryPath\DOMQuery
1138
     *  The DOMQuery.
1139
     * @see eachLambda()
1140
     * @see filter()
1141
     * @see map()
1142
     */
1143
    public function each($callback)
1144
    {
1145
        if (is_callable($callback)) {
1146
            $i = 0;
1147
            foreach ($this->matches as $item) {
1148
                if (call_user_func($callback, $i, $item) === false) {
1149
                    return $this;
1150
                }
1151
                ++$i;
1152
            }
1153
        } else {
1154
            throw new \QueryPath\Exception('Callback is not callable.');
1155
        }
1156
1157
        return $this;
1158
    }
1159
1160
    /**
1161
     * An each() iterator that takes a lambda function.
1162
     *
1163
     * @deprecated
1164
     *   Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this
1165
     *   method is not necessary and should be avoided.
1166
     * @param string $lambda
1167
     *  The lambda function. This will be passed ($index, &$item).
1168
     * @return \QueryPath\DOMQuery
1169
     *  The DOMQuery object.
1170
     * @see each()
1171
     * @see filterLambda()
1172
     * @see filterCallback()
1173
     * @see map()
1174
     */
1175
    public function eachLambda($lambda)
1176
    {
1177
        $index = 0;
1178
        foreach ($this->matches as $item) {
1179
            $fn = create_function('$index, &$item', $lambda);
0 ignored issues
show
Deprecated Code introduced by
The function create_function() has been deprecated: 7.2 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1179
            $fn = /** @scrutinizer ignore-deprecated */ create_function('$index, &$item', $lambda);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1180
            if ($fn($index, $item) === false) {
1181
                return $this;
1182
            }
1183
            ++$index;
1184
        }
1185
1186
        return $this;
1187
    }
1188
1189
    /**
1190
     * Insert the given markup as the last child.
1191
     *
1192
     * The markup will be inserted into each match in the set.
1193
     *
1194
     * The same element cannot be inserted multiple times into a document. DOM
1195
     * documents do not allow a single object to be inserted multiple times
1196
     * into the DOM. To insert the same XML repeatedly, we must first clone
1197
     * the object. This has one practical implication: Once you have inserted
1198
     * an element into the object, you cannot further manipulate the original
1199
     * element and expect the changes to be replciated in the appended object.
1200
     * (They are not the same -- there is no shared reference.) Instead, you
1201
     * will need to retrieve the appended object and operate on that.
1202
     *
1203
     * @param mixed $data
1204
     *  This can be either a string (the usual case), or a DOM Element.
1205
     * @return \QueryPath\DOMQuery
1206
     *  The DOMQuery object.
1207
     * @see appendTo()
1208
     * @see prepend()
1209
     * @throws QueryPath::Exception
1210
     *  Thrown if $data is an unsupported object type.
1211
     */
1212
    public function append($data)
1213
    {
1214
        $data = $this->prepareInsert($data);
1215
        if (isset($data)) {
1216
            if (empty($this->document->documentElement) && $this->matches->count() == 0) {
1217
                // Then we assume we are writing to the doc root
1218
                $this->document->appendChild($data);
1219
                $found = new \SplObjectStorage();
1220
                $found->attach($this->document->documentElement);
1221
                $this->setMatches($found);
1222
            } else {
1223
                // You can only append in item once. So in cases where we
1224
                // need to append multiple times, we have to clone the node.
1225
                foreach ($this->matches as $m) {
1226
                    // DOMDocumentFragments are even more troublesome, as they don't
1227
                    // always clone correctly. So we have to clone their children.
1228
                    if ($data instanceof \DOMDocumentFragment) {
1229
                        foreach ($data->childNodes as $n) {
1230
                            $m->appendChild($n->cloneNode(true));
1231
                        }
1232
                    } else {
1233
                        // Otherwise a standard clone will do.
1234
                        $m->appendChild($data->cloneNode(true));
1235
                    }
1236
1237
                }
1238
            }
1239
1240
        }
1241
1242
        return $this;
1243
    }
1244
1245
    /**
1246
     * Append the current elements to the destination passed into the function.
1247
     *
1248
     * This cycles through all of the current matches and appends them to
1249
     * the context given in $destination. If a selector is provided then the
1250
     * $destination is queried (using that selector) prior to the data being
1251
     * appended. The data is then appended to the found items.
1252
     *
1253
     * @param DOMQuery $dest
1254
     *  A DOMQuery object that will be appended to.
1255
     * @return \QueryPath\DOMQuery
1256
     *  The original DOMQuery, unaltered. Only the destination DOMQuery will
1257
     *  be modified.
1258
     * @see append()
1259
     * @see prependTo()
1260
     * @throws QueryPath::Exception
1261
     *  Thrown if $data is an unsupported object type.
1262
     */
1263
    public function appendTo(DOMQuery $dest)
1264
    {
1265
        foreach ($this->matches as $m) {
1266
            $dest->append($m);
1267
        }
1268
1269
        return $this;
1270
    }
1271
1272
    /**
1273
     * Insert the given markup as the first child.
1274
     *
1275
     * The markup will be inserted into each match in the set.
1276
     *
1277
     * @param mixed $data
1278
     *  This can be either a string (the usual case), or a DOM Element.
1279
     * @return \QueryPath\DOMQuery
1280
     * @see append()
1281
     * @see before()
1282
     * @see after()
1283
     * @see prependTo()
1284
     * @throws QueryPath::Exception
1285
     *  Thrown if $data is an unsupported object type.
1286
     */
1287
    public function prepend($data)
1288
    {
1289
        $data = $this->prepareInsert($data);
1290
        if (isset($data)) {
1291
            foreach ($this->matches as $m) {
1292
                $ins = $data->cloneNode(true);
1293
                if ($m->hasChildNodes()) {
1294
                    $m->insertBefore($ins, $m->childNodes->item(0));
1295
                } else {
1296
                    $m->appendChild($ins);
1297
                }
1298
            }
1299
        }
1300
1301
        return $this;
1302
    }
1303
1304
    /**
1305
     * Take all nodes in the current object and prepend them to the children nodes of
1306
     * each matched node in the passed-in DOMQuery object.
1307
     *
1308
     * This will iterate through each item in the current DOMQuery object and
1309
     * add each item to the beginning of the children of each element in the
1310
     * passed-in DOMQuery object.
1311
     *
1312
     * @see insertBefore()
1313
     * @see insertAfter()
1314
     * @see prepend()
1315
     * @see appendTo()
1316
     * @param DOMQuery $dest
1317
     *  The destination DOMQuery object.
1318
     * @return \QueryPath\DOMQuery
1319
     *  The original DOMQuery, unmodified. NOT the destination DOMQuery.
1320
     * @throws QueryPath::Exception
1321
     *  Thrown if $data is an unsupported object type.
1322
     */
1323
    public function prependTo(DOMQuery $dest)
1324
    {
1325
        foreach ($this->matches as $m) {
1326
            $dest->prepend($m);
1327
        }
1328
1329
        return $this;
1330
    }
1331
1332
    /**
1333
     * Insert the given data before each element in the current set of matches.
1334
     *
1335
     * This will take the give data (XML or HTML) and put it before each of the items that
1336
     * the DOMQuery object currently contains. Contrast this with after().
1337
     *
1338
     * @param mixed $data
1339
     *  The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
1340
     *  or the other usual suspects. (See {@link qp()}).
1341
     * @return \QueryPath\DOMQuery
1342
     *  Returns the DOMQuery with the new modifications. The list of elements currently
1343
     *  selected will remain the same.
1344
     * @see insertBefore()
1345
     * @see after()
1346
     * @see append()
1347
     * @see prepend()
1348
     * @throws QueryPath::Exception
1349
     *  Thrown if $data is an unsupported object type.
1350
     */
1351
    public function before($data)
1352
    {
1353
        $data = $this->prepareInsert($data);
1354
        foreach ($this->matches as $m) {
1355
            $ins = $data->cloneNode(true);
1356
            $m->parentNode->insertBefore($ins, $m);
1357
        }
1358
1359
        return $this;
1360
    }
1361
1362
    /**
1363
     * Insert the current elements into the destination document.
1364
     * The items are inserted before each element in the given DOMQuery document.
1365
     * That is, they will be siblings with the current elements.
1366
     *
1367
     * @param DOMQuery $dest
1368
     *  Destination DOMQuery document.
1369
     * @return \QueryPath\DOMQuery
1370
     *  The current DOMQuery object, unaltered. Only the destination DOMQuery
1371
     *  object is altered.
1372
     * @see before()
1373
     * @see insertAfter()
1374
     * @see appendTo()
1375
     * @throws QueryPath::Exception
1376
     *  Thrown if $data is an unsupported object type.
1377
     */
1378
    public function insertBefore(DOMQuery $dest)
1379
    {
1380
        foreach ($this->matches as $m) {
1381
            $dest->before($m);
1382
        }
1383
1384
        return $this;
1385
    }
1386
1387
    /**
1388
     * Insert the contents of the current DOMQuery after the nodes in the
1389
     * destination DOMQuery object.
1390
     *
1391
     * @param DOMQuery $dest
1392
     *  Destination object where the current elements will be deposited.
1393
     * @return \QueryPath\DOMQuery
1394
     *  The present DOMQuery, unaltered. Only the destination object is altered.
1395
     * @see after()
1396
     * @see insertBefore()
1397
     * @see append()
1398
     * @throws QueryPath::Exception
1399
     *  Thrown if $data is an unsupported object type.
1400
     */
1401
    public function insertAfter(DOMQuery $dest)
1402
    {
1403
        foreach ($this->matches as $m) {
1404
            $dest->after($m);
1405
        }
1406
1407
        return $this;
1408
    }
1409
1410
    /**
1411
     * Insert the given data after each element in the current DOMQuery object.
1412
     *
1413
     * This inserts the element as a peer to the currently matched elements.
1414
     * Contrast this with {@link append()}, which inserts the data as children
1415
     * of matched elements.
1416
     *
1417
     * @param mixed $data
1418
     *  The data to be appended.
1419
     * @return \QueryPath\DOMQuery
1420
     *  The DOMQuery object (with the items inserted).
1421
     * @see before()
1422
     * @see append()
1423
     * @throws QueryPath::Exception
1424
     *  Thrown if $data is an unsupported object type.
1425
     */
1426
    public function after($data)
1427
    {
1428
        if (empty($data)) {
1429
            return $this;
1430
        }
1431
        $data = $this->prepareInsert($data);
1432
        foreach ($this->matches as $m) {
1433
            $ins = $data->cloneNode(true);
1434
            if (isset($m->nextSibling)) {
1435
                $m->parentNode->insertBefore($ins, $m->nextSibling);
1436
            } else {
1437
                $m->parentNode->appendChild($ins);
1438
            }
1439
        }
1440
1441
        return $this;
1442
    }
1443
1444
    /**
1445
     * Replace the existing element(s) in the list with a new one.
1446
     *
1447
     * @param mixed $new
1448
     *  A DOMElement or XML in a string. This will replace all elements
1449
     *  currently wrapped in the DOMQuery object.
1450
     * @return \QueryPath\DOMQuery
1451
     *  The DOMQuery object wrapping <b>the items that were removed</b>.
1452
     *  This remains consistent with the jQuery API.
1453
     * @see append()
1454
     * @see prepend()
1455
     * @see before()
1456
     * @see after()
1457
     * @see remove()
1458
     * @see replaceAll()
1459
     */
1460
    public function replaceWith($new)
1461
    {
1462
        $data = $this->prepareInsert($new);
1463
        $found = new \SplObjectStorage();
1464
        foreach ($this->matches as $m) {
1465
            $parent = $m->parentNode;
1466
            $parent->insertBefore($data->cloneNode(true), $m);
1467
            $found->attach($parent->removeChild($m));
1468
        }
1469
1470
        return $this->inst($found, NULL, $this->options);
1471
    }
1472
1473
    /**
1474
     * Remove the parent element from the selected node or nodes.
1475
     *
1476
     * This takes the given list of nodes and "unwraps" them, moving them out of their parent
1477
     * node, and then deleting the parent node.
1478
     *
1479
     * For example, consider this:
1480
     *
1481
     * @code
1482
     *   <root><wrapper><content/></wrapper></root>
1483
     * @endcode
1484
     *
1485
     * Now we can run this code:
1486
     * @code
1487
     *   qp($xml, 'content')->unwrap();
1488
     * @endcode
1489
     *
1490
     * This will result in:
1491
     *
1492
     * @code
1493
     *   <root><content/></root>
1494
     * @endcode
1495
     * This is the opposite of wrap().
1496
     *
1497
     * <b>The root element cannot be unwrapped.</b> It has no parents.
1498
     * If you attempt to use unwrap on a root element, this will throw a
1499
     * QueryPath::Exception. (You can, however, "Unwrap" a child that is
1500
     * a direct descendant of the root element. This will remove the root
1501
     * element, and replace the child as the root element. Be careful, though.
1502
     * You cannot set more than one child as a root element.)
1503
     *
1504
     * @return \QueryPath\DOMQuery
1505
     *  The DOMQuery object, with the same element(s) selected.
1506
     * @throws QueryPath::Exception
1507
     *  An exception is thrown if one attempts to unwrap a root element.
1508
     * @see    wrap()
1509
     * @since  2.1
1510
     * @author mbutcher
1511
     */
1512
    public function unwrap()
1513
    {
1514
1515
        // We do this in two loops in order to
1516
        // capture the case where two matches are
1517
        // under the same parent. Othwerwise we might
1518
        // remove a match before we can move it.
1519
        $parents = new \SplObjectStorage();
1520
        foreach ($this->matches as $m) {
1521
1522
            // Cannot unwrap the root element.
1523
            if ($m->isSameNode($m->ownerDocument->documentElement)) {
1524
                throw new \QueryPath\Exception('Cannot unwrap the root element.');
1525
            }
1526
1527
            // Move children to peer of parent.
1528
            $parent = $m->parentNode;
1529
            $old = $parent->removeChild($m);
1530
            $parent->parentNode->insertBefore($old, $parent);
1531
            $parents->attach($parent);
1532
        }
1533
1534
        // Now that all the children are moved, we
1535
        // remove all of the parents.
1536
        foreach ($parents as $ele) {
1537
            $ele->parentNode->removeChild($ele);
1538
        }
1539
1540
        return $this;
1541
    }
1542
1543
    /**
1544
     * Wrap each element inside of the given markup.
1545
     *
1546
     * Markup is usually a string, but it can also be a DOMNode, a document
1547
     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
1548
     * the first item in the list will be used.)
1549
     *
1550
     * @param mixed $markup
1551
     *  Markup that will wrap each element in the current list.
1552
     * @return \QueryPath\DOMQuery
1553
     *  The DOMQuery object with the wrapping changes made.
1554
     * @see wrapAll()
1555
     * @see wrapInner()
1556
     */
1557
    public function wrap($markup)
1558
    {
1559
        $data = $this->prepareInsert($markup);
1560
1561
        // If the markup passed in is empty, we don't do any wrapping.
1562
        if (empty($data)) {
1563
            return $this;
1564
        }
1565
1566
        foreach ($this->matches as $m) {
1567
            if ($data instanceof \DOMDocumentFragment) {
1568
                $copy = $data->firstChild->cloneNode(true);
1569
            } else {
1570
                $copy = $data->cloneNode(true);
1571
            }
1572
1573
            // XXX: Should be able to avoid doing this over and over.
1574
            if ($copy->hasChildNodes()) {
1575
                $deepest = $this->deepestNode($copy);
1576
                // FIXME: Does this need a different data structure?
1577
                $bottom = $deepest[0];
1578
            } else {
1579
                $bottom = $copy;
1580
            }
1581
1582
            $parent = $m->parentNode;
1583
            $parent->insertBefore($copy, $m);
1584
            $m = $parent->removeChild($m);
1585
            $bottom->appendChild($m);
1586
            //$parent->appendChild($copy);
1587
        }
1588
1589
        return $this;
1590
    }
1591
1592
    /**
1593
     * Wrap all elements inside of the given markup.
1594
     *
1595
     * So all elements will be grouped together under this single marked up
1596
     * item. This works by first determining the parent element of the first item
1597
     * in the list. It then moves all of the matching elements under the wrapper
1598
     * and inserts the wrapper where that first element was found. (This is in
1599
     * accordance with the way jQuery works.)
1600
     *
1601
     * Markup is usually XML in a string, but it can also be a DOMNode, a document
1602
     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
1603
     * the first item in the list will be used.)
1604
     *
1605
     * @param string $markup
1606
     *  Markup that will wrap all elements in the current list.
1607
     * @return \QueryPath\DOMQuery
1608
     *  The DOMQuery object with the wrapping changes made.
1609
     * @see wrap()
1610
     * @see wrapInner()
1611
     */
1612
    public function wrapAll($markup)
1613
    {
1614
        if ($this->matches->count() === 0) {
1615
            return;
1616
        }
1617
1618
        $data = $this->prepareInsert($markup);
1619
1620
        if (empty($data)) {
1621
            return $this;
1622
        }
1623
1624
        if ($data instanceof \DOMDocumentFragment) {
1625
            $data = $data->firstChild->cloneNode(true);
1626
        } else {
1627
            $data = $data->cloneNode(true);
1628
        }
1629
1630
        if ($data->hasChildNodes()) {
1631
            $deepest = $this->deepestNode($data);
1632
            // FIXME: Does this need fixing?
1633
            $bottom = $deepest[0];
1634
        } else {
1635
            $bottom = $data;
1636
        }
1637
1638
        $first = $this->getFirstMatch();
1639
        $parent = $first->parentNode;
1640
        $parent->insertBefore($data, $first);
1641
        foreach ($this->matches as $m) {
1642
            $bottom->appendChild($m->parentNode->removeChild($m));
1643
        }
1644
1645
        return $this;
1646
    }
1647
1648
    /**
1649
     * Wrap the child elements of each item in the list with the given markup.
1650
     *
1651
     * Markup is usually a string, but it can also be a DOMNode, a document
1652
     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
1653
     * the first item in the list will be used.)
1654
     *
1655
     * @param string $markup
1656
     *  Markup that will wrap children of each element in the current list.
1657
     * @return \QueryPath\DOMQuery
1658
     *  The DOMQuery object with the wrapping changes made.
1659
     * @see wrap()
1660
     * @see wrapAll()
1661
     */
1662
    public function wrapInner($markup)
1663
    {
1664
        $data = $this->prepareInsert($markup);
1665
1666
        // No data? Short circuit.
1667
        if (empty($data)) {
1668
            return $this;
1669
        }
1670
1671
        foreach ($this->matches as $m) {
1672
            if ($data instanceof \DOMDocumentFragment) {
1673
                $wrapper = $data->firstChild->cloneNode(true);
1674
            } else {
1675
                $wrapper = $data->cloneNode(true);
1676
            }
1677
1678
            if ($wrapper->hasChildNodes()) {
1679
                $deepest = $this->deepestNode($wrapper);
1680
                // FIXME: ???
1681
                $bottom = $deepest[0];
1682
            } else {
1683
                $bottom = $wrapper;
1684
            }
1685
1686
            if ($m->hasChildNodes()) {
1687
                while ($m->firstChild) {
1688
                    $kid = $m->removeChild($m->firstChild);
1689
                    $bottom->appendChild($kid);
1690
                }
1691
            }
1692
1693
            $m->appendChild($wrapper);
1694
        }
1695
1696
        return $this;
1697
    }
1698
1699
    /**
1700
     * Reduce the set of matches to the deepest child node in the tree.
1701
     *
1702
     * This loops through the matches and looks for the deepest child node of all of
1703
     * the matches. "Deepest", here, is relative to the nodes in the list. It is
1704
     * calculated as the distance from the starting node to the most distant child
1705
     * node. In other words, it is not necessarily the farthest node from the root
1706
     * element, but the farthest note from the matched element.
1707
     *
1708
     * In the case where there are multiple nodes at the same depth, all of the
1709
     * nodes at that depth will be included.
1710
     *
1711
     * @return \QueryPath\DOMQuery
1712
     *  The DOMQuery wrapping the single deepest node.
1713
     */
1714
    public function deepest()
1715
    {
1716
        $deepest = 0;
1717
        $winner = new \SplObjectStorage();
1718
        foreach ($this->matches as $m) {
1719
            $local_deepest = 0;
1720
            $local_ele = $this->deepestNode($m, 0, NULL, $local_deepest);
0 ignored issues
show
Bug introduced by
$local_deepest of type integer is incompatible with the type DOMNode expected by parameter $deepest of QueryPath\DOM::deepestNode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1720
            $local_ele = $this->deepestNode($m, 0, NULL, /** @scrutinizer ignore-type */ $local_deepest);
Loading history...
1721
1722
            // Replace with the new deepest.
1723
            if ($local_deepest > $deepest) {
1724
                $winner = new \SplObjectStorage();
1725
                foreach ($local_ele as $lele) {
1726
                    $winner->attach($lele);
1727
                }
1728
                $deepest = $local_deepest;
1729
            } // Augument with other equally deep elements.
1730
            elseif ($local_deepest == $deepest) {
1731
                foreach ($local_ele as $lele) {
1732
                    $winner->attach($lele);
1733
                }
1734
            }
1735
        }
1736
1737
        return $this->inst($winner, NULL, $this->options);
1738
    }
1739
1740
    /**
1741
     * The tag name of the first element in the list.
1742
     *
1743
     * This returns the tag name of the first element in the list of matches. If
1744
     * the list is empty, an empty string will be used.
1745
     *
1746
     * @see replaceAll()
1747
     * @see replaceWith()
1748
     * @return string
1749
     *  The tag name of the first element in the list.
1750
     */
1751
    public function tag()
1752
    {
1753
        return ($this->size() > 0) ? $this->getFirstMatch()->tagName : '';
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1753
        return (/** @scrutinizer ignore-deprecated */ $this->size() > 0) ? $this->getFirstMatch()->tagName : '';

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1754
    }
1755
1756
    /**
1757
     * Remove any items from the list if they match the selector.
1758
     *
1759
     * In other words, each item that matches the selector will be remove
1760
     * from the DOM document. The returned DOMQuery wraps the list of
1761
     * removed elements.
1762
     *
1763
     * If no selector is specified, this will remove all current matches from
1764
     * the document.
1765
     *
1766
     * @param string $selector
1767
     *  A CSS Selector.
1768
     * @return \QueryPath\DOMQuery
1769
     *  The Query path wrapping a list of removed items.
1770
     * @see replaceAll()
1771
     * @see replaceWith()
1772
     * @see removeChildren()
1773
     */
1774
    public function remove($selector = NULL)
1775
    {
1776
        if (!empty($selector)) {
1777
            // Do a non-destructive find.
1778
            $query = new QueryPathEventHandler($this->matches);
1779
            $query->find($selector);
1780
            $matches = $query->getMatches();
1781
        } else {
1782
            $matches = $this->matches;
1783
        }
1784
1785
        $found = new \SplObjectStorage();
1786
        foreach ($matches as $item) {
1787
            // The item returned is (according to docs) different from
1788
            // the one passed in, so we have to re-store it.
1789
            $found->attach($item->parentNode->removeChild($item));
1790
        }
1791
1792
        // Return a clone DOMQuery with just the removed items. If
1793
        // no items are found, this will return an empty DOMQuery.
1794
        return count($found) == 0 ? new static() : new static($found);
1795
    }
1796
1797
    /**
1798
     * This replaces everything that matches the selector with the first value
1799
     * in the current list.
1800
     *
1801
     * This is the reverse of replaceWith.
1802
     *
1803
     * Unlike jQuery, DOMQuery cannot assume a default document. Consequently,
1804
     * you must specify the intended destination document. If it is omitted, the
1805
     * present document is assumed to be tthe document. However, that can result
1806
     * in undefined behavior if the selector and the replacement are not sufficiently
1807
     * distinct.
1808
     *
1809
     * @param string $selector
1810
     *  The selector.
1811
     * @param DOMDocument $document
1812
     *  The destination document.
1813
     * @return \QueryPath\DOMQuery
1814
     *  The DOMQuery wrapping the modified document.
1815
     * @deprecated Due to the fact that this is not a particularly friendly method,
1816
     *  and that it can be easily replicated using {@see replaceWith()}, it is to be
1817
     *  considered deprecated.
1818
     * @see        remove()
1819
     * @see        replaceWith()
1820
     */
1821
    public function replaceAll($selector, \DOMDocument $document)
1822
    {
1823
        $replacement = $this->size() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1823
        $replacement = /** @scrutinizer ignore-deprecated */ $this->size() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1824
1825
        $c = new QueryPathEventHandler($document);
1826
        $c->find($selector);
1827
        $temp = $c->getMatches();
1828
        foreach ($temp as $item) {
1829
            $node = $replacement->cloneNode();
1830
            $node = $document->importNode($node);
1831
            $item->parentNode->replaceChild($node, $item);
1832
        }
1833
1834
        return QueryPath::with($document, NULL, $this->options);
1835
    }
1836
1837
    /**
1838
     * Add more elements to the current set of matches.
1839
     *
1840
     * This begins the new query at the top of the DOM again. The results found
1841
     * when running this selector are then merged into the existing results. In
1842
     * this way, you can add additional elements to the existing set.
1843
     *
1844
     * @param string $selector
1845
     *  A valid selector.
1846
     * @return \QueryPath\DOMQuery
1847
     *  The DOMQuery object with the newly added elements.
1848
     * @see append()
1849
     * @see after()
1850
     * @see andSelf()
1851
     * @see end()
1852
     */
1853
    public function add($selector)
1854
    {
1855
1856
        // This is destructive, so we need to set $last:
1857
        $this->last = $this->matches;
1858
1859
        foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) {
1860
            $this->matches->attach($item);
0 ignored issues
show
Bug introduced by
The method attach() does not exist on Traversable. It seems like you code against a sub-type of Traversable such as QueryPathImpl or QueryPath\DOMQuery or HttpRequestPool or SplObjectStorage. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1860
            $this->matches->/** @scrutinizer ignore-call */ 
1861
                            attach($item);
Loading history...
1861
        }
1862
1863
        return $this;
1864
    }
1865
1866
    /**
1867
     * Revert to the previous set of matches.
1868
     *
1869
     * <b>DEPRECATED</b> Do not use.
1870
     *
1871
     * This will revert back to the last set of matches (before the last
1872
     * "destructive" set of operations). This undoes any change made to the set of
1873
     * matched objects. Functions like find() and filter() change the
1874
     * list of matched objects. The end() function will revert back to the last set of
1875
     * matched items.
1876
     *
1877
     * Note that functions that modify the document, but do not change the list of
1878
     * matched objects, are not "destructive". Thus, calling append('something')->end()
1879
     * will not undo the append() call.
1880
     *
1881
     * Only one level of changes is stored. Reverting beyond that will result in
1882
     * an empty set of matches. Example:
1883
     *
1884
     * @code
1885
     * // The line below returns the same thing as qp(document, 'p');
1886
     * qp(document, 'p')->find('div')->end();
1887
     * // This returns an empty array:
1888
     * qp(document, 'p')->end();
1889
     * // This returns an empty array:
1890
     * qp(document, 'p')->find('div')->find('span')->end()->end();
1891
     * @endcode
1892
     *
1893
     * The last one returns an empty array because only one level of changes is stored.
1894
     *
1895
     * @return \QueryPath\DOMQuery
1896
     *  A DOMNode object reflecting the list of matches prior to the last destructive
1897
     *  operation.
1898
     * @see        andSelf()
1899
     * @see        add()
1900
     * @deprecated This function will be removed.
1901
     */
1902
    public function end()
1903
    {
1904
        // Note that this does not use setMatches because it must set the previous
1905
        // set of matches to empty array.
1906
        $this->matches = $this->last;
1907
        $this->last = new \SplObjectStorage();
1908
1909
        return $this;
1910
    }
1911
1912
    /**
1913
     * Combine the current and previous set of matched objects.
1914
     *
1915
     * Example:
1916
     *
1917
     * @code
1918
     * qp(document, 'p')->find('div')->andSelf();
1919
     * @endcode
1920
     *
1921
     * The code above will contain a list of all p elements and all div elements that
1922
     * are beneath p elements.
1923
     *
1924
     * @see end();
1925
     * @return \QueryPath\DOMQuery
1926
     *  A DOMNode object with the results of the last two "destructive" operations.
1927
     * @see add()
1928
     * @see end()
1929
     */
1930
    public function andSelf()
1931
    {
1932
        // This is destructive, so we need to set $last:
1933
        $last = $this->matches;
1934
1935
        foreach ($this->last as $item) {
1936
            $this->matches->attach($item);
1937
        }
1938
1939
        $this->last = $last;
1940
1941
        return $this;
1942
    }
1943
1944
    /**
1945
     * Remove all child nodes.
1946
     *
1947
     * This is equivalent to jQuery's empty() function. (However, empty() is a
1948
     * PHP built-in, and cannot be used as a method name.)
1949
     *
1950
     * @return \QueryPath\DOMQuery
1951
     *  The DOMQuery object with the child nodes removed.
1952
     * @see replaceWith()
1953
     * @see replaceAll()
1954
     * @see remove()
1955
     */
1956
    public function removeChildren()
1957
    {
1958
        foreach ($this->matches as $m) {
1959
            while ($kid = $m->firstChild) {
1960
                $m->removeChild($kid);
1961
            }
1962
        }
1963
1964
        return $this;
1965
    }
1966
1967
    /**
1968
     * Get the children of the elements in the DOMQuery object.
1969
     *
1970
     * If a selector is provided, the list of children will be filtered through
1971
     * the selector.
1972
     *
1973
     * @param string $selector
1974
     *  A valid selector.
1975
     * @return \QueryPath\DOMQuery
1976
     *  A DOMQuery wrapping all of the children.
1977
     * @see removeChildren()
1978
     * @see parent()
1979
     * @see parents()
1980
     * @see next()
1981
     * @see prev()
1982
     */
1983
    public function children($selector = NULL)
1984
    {
1985
        $found = new \SplObjectStorage();
1986
        $filter = strlen($selector) > 0;
1987
1988
        if ($filter) {
1989
            $tmp = new \SplObjectStorage();
1990
        }
1991
        foreach ($this->matches as $m) {
1992
            foreach ($m->childNodes as $c) {
1993
                if ($c->nodeType == XML_ELEMENT_NODE) {
1994
                    // This is basically an optimized filter() just for children().
1995
                    if ($filter) {
1996
                        $tmp->attach($c);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tmp does not seem to be defined for all execution paths leading up to this point.
Loading history...
1997
                        $query = new DOMTraverser($tmp, true, $c);
1998
                        $query->find($selector);
1999
                        if (count($query->matches()) > 0) {
2000
                            $found->attach($c);
2001
                        }
2002
                        $tmp->detach($c);
2003
2004
                    } // No filter. Just attach it.
2005
                    else {
2006
                        $found->attach($c);
2007
                    }
2008
                }
2009
            }
2010
        }
2011
        $new = $this->inst($found, NULL, $this->options);
2012
2013
        return $new;
2014
    }
2015
2016
    /**
2017
     * Get all child nodes (not just elements) of all items in the matched set.
2018
     *
2019
     * It gets only the immediate children, not all nodes in the subtree.
2020
     *
2021
     * This does not process iframes. Xinclude processing is dependent on the
2022
     * DOM implementation and configuration.
2023
     *
2024
     * @return \QueryPath\DOMQuery
2025
     *  A DOMNode object wrapping all child nodes for all elements in the
2026
     *  DOMNode object.
2027
     * @see find()
2028
     * @see text()
2029
     * @see html()
2030
     * @see innerHTML()
2031
     * @see xml()
2032
     * @see innerXML()
2033
     */
2034
    public function contents()
2035
    {
2036
        $found = new \SplObjectStorage();
2037
        foreach ($this->matches as $m) {
2038
            if (empty($m->childNodes)) {
2039
                continue;
2040
            } // Issue #51
2041
            foreach ($m->childNodes as $c) {
2042
                $found->attach($c);
2043
            }
2044
        }
2045
2046
        return $this->inst($found, NULL, $this->options);
2047
    }
2048
2049
    /**
2050
     * Get a list of siblings for elements currently wrapped by this object.
2051
     *
2052
     * This will compile a list of every sibling of every element in the
2053
     * current list of elements.
2054
     *
2055
     * Note that if two siblings are present in the DOMQuery object to begin with,
2056
     * then both will be returned in the matched set, since they are siblings of each
2057
     * other. In other words,if the matches contain a and b, and a and b are siblings of
2058
     * each other, than running siblings will return a set that contains
2059
     * both a and b.
2060
     *
2061
     * @param string $selector
2062
     *  If the optional selector is provided, siblings will be filtered through
2063
     *  this expression.
2064
     * @return \QueryPath\DOMQuery
2065
     *  The DOMQuery containing the matched siblings.
2066
     * @see contents()
2067
     * @see children()
2068
     * @see parent()
2069
     * @see parents()
2070
     */
2071
    public function siblings($selector = NULL)
2072
    {
2073
        $found = new \SplObjectStorage();
2074
        foreach ($this->matches as $m) {
2075
            $parent = $m->parentNode;
2076
            foreach ($parent->childNodes as $n) {
2077
                if ($n->nodeType == XML_ELEMENT_NODE && $n !== $m) {
2078
                    $found->attach($n);
2079
                }
2080
            }
2081
        }
2082
        if (empty($selector)) {
2083
            return $this->inst($found, NULL, $this->options);
2084
        } else {
2085
            return $this->inst($found, NULL, $this->options)->filter($selector);
2086
        }
2087
    }
2088
2089
    /**
2090
     * Find the closest element matching the selector.
2091
     *
2092
     * This finds the closest match in the ancestry chain. It first checks the
2093
     * present element. If the present element does not match, this traverses up
2094
     * the ancestry chain (e.g. checks each parent) looking for an item that matches.
2095
     *
2096
     * It is provided for jQuery 1.3 compatibility.
2097
     *
2098
     * @param string $selector
2099
     *  A CSS Selector to match.
2100
     * @return \QueryPath\DOMQuery
2101
     *  The set of matches.
2102
     * @since 2.0
2103
     */
2104
    public function closest($selector)
2105
    {
2106
        $found = new \SplObjectStorage();
2107
        foreach ($this->matches as $m) {
2108
2109
            if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
2110
                $found->attach($m);
2111
            } else {
2112
                while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
2113
                    $m = $m->parentNode;
2114
                    // Is there any case where parent node is not an element?
2115
                    if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with($m, NULL,
2116
                            $this->options)->is($selector) > 0) {
2117
                        $found->attach($m);
2118
                        break;
2119
                    }
2120
                }
2121
            }
2122
2123
        }
2124
2125
        // XXX: Should this be an in-place modification?
2126
        return $this->inst($found, NULL, $this->options);
2127
        //$this->setMatches($found);
2128
        //return $this;
2129
    }
2130
2131
    /**
2132
     * Get the immediate parent of each element in the DOMQuery.
2133
     *
2134
     * If a selector is passed, this will return the nearest matching parent for
2135
     * each element in the DOMQuery.
2136
     *
2137
     * @param string $selector
2138
     *  A valid CSS3 selector.
2139
     * @return \QueryPath\DOMQuery
2140
     *  A DOMNode object wrapping the matching parents.
2141
     * @see children()
2142
     * @see siblings()
2143
     * @see parents()
2144
     */
2145
    public function parent($selector = NULL)
2146
    {
2147
        $found = new \SplObjectStorage();
2148
        foreach ($this->matches as $m) {
2149
            while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
2150
                $m = $m->parentNode;
2151
                // Is there any case where parent node is not an element?
2152
                if ($m->nodeType === XML_ELEMENT_NODE) {
2153
                    if (!empty($selector)) {
2154
                        if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
2155
                            $found->attach($m);
2156
                            break;
2157
                        }
2158
                    } else {
2159
                        $found->attach($m);
2160
                        break;
2161
                    }
2162
                }
2163
            }
2164
        }
2165
2166
        return $this->inst($found, NULL, $this->options);
2167
    }
2168
2169
    /**
2170
     * Get all ancestors of each element in the DOMQuery.
2171
     *
2172
     * If a selector is present, only matching ancestors will be retrieved.
2173
     *
2174
     * @see parent()
2175
     * @param string $selector
2176
     *  A valid CSS 3 Selector.
2177
     * @return \QueryPath\DOMQuery
2178
     *  A DOMNode object containing the matching ancestors.
2179
     * @see siblings()
2180
     * @see children()
2181
     */
2182
    public function parents($selector = NULL)
2183
    {
2184
        $found = new \SplObjectStorage();
2185
        foreach ($this->matches as $m) {
2186
            while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
2187
                $m = $m->parentNode;
2188
                // Is there any case where parent node is not an element?
2189
                if ($m->nodeType === XML_ELEMENT_NODE) {
2190
                    if (!empty($selector)) {
2191
                        if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
2192
                            $found->attach($m);
2193
                        }
2194
                    } else {
2195
                        $found->attach($m);
2196
                    }
2197
                }
2198
            }
2199
        }
2200
2201
        return $this->inst($found, NULL, $this->options);
2202
    }
2203
2204
    /**
2205
     * Set or get the markup for an element.
2206
     *
2207
     * If $markup is set, then the giving markup will be injected into each
2208
     * item in the set. All other children of that node will be deleted, and this
2209
     * new code will be the only child or children. The markup MUST BE WELL FORMED.
2210
     *
2211
     * If no markup is given, this will return a string representing the child
2212
     * markup of the first node.
2213
     *
2214
     * <b>Important:</b> This differs from jQuery's html() function. This function
2215
     * returns <i>the current node</i> and all of its children. jQuery returns only
2216
     * the children. This means you do not need to do things like this:
2217
     * @code$qp->parent()->html()@endcode.
2218
     *
2219
     * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML.
2220
     *
2221
     * @param string $markup
2222
     *  The text to insert.
2223
     * @return mixed
2224
     *  A string if no markup was passed, or a DOMQuery if markup was passed.
2225
     * @see xml()
2226
     * @see text()
2227
     * @see contents()
2228
     */
2229
    public function html($markup = NULL)
2230
    {
2231
        if (isset($markup)) {
2232
2233
            if ($this->options['replace_entities']) {
2234
                $markup = \QueryPath\Entities::replaceAllEntities($markup);
2235
            }
2236
2237
            // Parse the HTML and insert it into the DOM
2238
            //$doc = DOMDocument::loadHTML($markup);
2239
            $doc = $this->document->createDocumentFragment();
2240
            $doc->appendXML($markup);
2241
            $this->removeChildren();
2242
            $this->append($doc);
2243
2244
            return $this;
2245
        }
2246
        $length = $this->size();
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2246
        $length = /** @scrutinizer ignore-deprecated */ $this->size();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2247
        if ($length == 0) {
2248
            return NULL;
2249
        }
2250
        // Only return the first item -- that's what JQ does.
2251
        $first = $this->getFirstMatch();
2252
2253
        // Catch cases where first item is not a legit DOM object.
2254
        if (!($first instanceof \DOMNode)) {
2255
            return NULL;
2256
        }
2257
2258
        // Added by eabrand.
2259
        if (!$first->ownerDocument->documentElement) {
2260
            return NULL;
2261
        }
2262
2263
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
2264
            return $this->document->saveHTML();
2265
        }
2266
2267
        // saveHTML cannot take a node and serialize it.
2268
        return $this->document->saveXML($first);
2269
    }
2270
2271
    /**
2272
     * Write the QueryPath document to HTML5.
2273
     *
2274
     * See html()
2275
     *
2276
     * @param null $markup
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $markup is correct as it would always require null to be passed?
Loading history...
2277
     * @return null|DOMQuery|string
2278
     * @throws \QueryPath\QueryPath
2279
     */
2280
    public function html5($markup = NULL)
2281
    {
2282
        $html5 = new HTML5($this->options);
2283
2284
        // append HTML to existing
2285
        if ($markup === NULL) {
2286
2287
            // Parse the HTML and insert it into the DOM
2288
            $doc = $html5->loadHTMLFragment($markup);
2289
            $this->removeChildren();
2290
            $this->append($doc);
2291
2292
            return $this;
2293
        }
2294
2295
        $length = $this->count();
2296
        if ($length === 0) {
2297
            return NULL;
2298
        }
2299
        // Only return the first item -- that's what JQ does.
2300
        $first = $this->getFirstMatch();
2301
2302
        // Catch cases where first item is not a legit DOM object.
2303
        if (!($first instanceof \DOMNode)) {
2304
            return NULL;
2305
        }
2306
2307
        // Added by eabrand.
2308
        if (!$first->ownerDocument->documentElement) {
2309
            return NULL;
2310
        }
2311
2312
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
2313
            return $html5->saveHTML($this->document); //$this->document->saveHTML();
2314
        }
2315
2316
        return $html5->saveHTML($first);
2317
    }
2318
2319
    /**
2320
     * Fetch the HTML contents INSIDE of the first DOMQuery item.
2321
     *
2322
     * <b>This behaves the way jQuery's @codehtml()@endcode function behaves.</b>
2323
     *
2324
     * This gets all children of the first match in DOMQuery.
2325
     *
2326
     * Consider this fragment:
2327
     *
2328
     * @code
2329
     * <div>
2330
     * test <p>foo</p> test
2331
     * </div>
2332
     * @endcode
2333
     *
2334
     * We can retrieve just the contents of this code by doing something like
2335
     * this:
2336
     * @code
2337
     * qp($xml, 'div')->innerHTML();
2338
     * @endcode
2339
     *
2340
     * This would return the following:
2341
     * @codetest <p>foo</p> test@endcode
2342
     *
2343
     * @return string
2344
     *  Returns a string representation of the child nodes of the first
2345
     *  matched element.
2346
     * @see   html()
2347
     * @see   innerXML()
2348
     * @see   innerXHTML()
2349
     * @since 2.0
2350
     */
2351
    public function innerHTML()
2352
    {
2353
        return $this->innerXML();
2354
    }
2355
2356
    /**
2357
     * Fetch child (inner) nodes of the first match.
2358
     *
2359
     * This will return the children of the present match. For an example,
2360
     * see {@link innerHTML()}.
2361
     *
2362
     * @see   innerHTML()
2363
     * @see   innerXML()
2364
     * @return string
2365
     *  Returns a string of XHTML that represents the children of the present
2366
     *  node.
2367
     * @since 2.0
2368
     */
2369
    public function innerXHTML()
2370
    {
2371
        $length = $this->size();
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2371
        $length = /** @scrutinizer ignore-deprecated */ $this->size();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2372
        if ($length == 0) {
2373
            return NULL;
2374
        }
2375
        // Only return the first item -- that's what JQ does.
2376
        $first = $this->getFirstMatch();
2377
2378
        // Catch cases where first item is not a legit DOM object.
2379
        if (!($first instanceof \DOMNode)) {
2380
            return NULL;
2381
        } elseif (!$first->hasChildNodes()) {
2382
            return '';
2383
        }
2384
2385
        $buffer = '';
2386
        foreach ($first->childNodes as $child) {
2387
            $buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
2388
        }
2389
2390
        return $buffer;
2391
    }
2392
2393
    /**
2394
     * Fetch child (inner) nodes of the first match.
2395
     *
2396
     * This will return the children of the present match. For an example,
2397
     * see {@link innerHTML()}.
2398
     *
2399
     * @see   innerHTML()
2400
     * @see   innerXHTML()
2401
     * @return string
2402
     *  Returns a string of XHTML that represents the children of the present
2403
     *  node.
2404
     * @since 2.0
2405
     */
2406
    public function innerXML()
2407
    {
2408
        $length = $this->size();
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2408
        $length = /** @scrutinizer ignore-deprecated */ $this->size();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2409
        if ($length == 0) {
2410
            return NULL;
2411
        }
2412
        // Only return the first item -- that's what JQ does.
2413
        $first = $this->getFirstMatch();
2414
2415
        // Catch cases where first item is not a legit DOM object.
2416
        if (!($first instanceof \DOMNode)) {
2417
            return NULL;
2418
        } elseif (!$first->hasChildNodes()) {
2419
            return '';
2420
        }
2421
2422
        $buffer = '';
2423
        foreach ($first->childNodes as $child) {
2424
            $buffer .= $this->document->saveXML($child);
2425
        }
2426
2427
        return $buffer;
2428
    }
2429
2430
    /**
2431
     * Get child elements as an HTML5 string.
2432
     *
2433
     * TODO: This is a very simple alteration of innerXML. Do we need better
2434
     * support?
2435
     */
2436
    public function innerHTML5()
2437
    {
2438
        $length = $this->size();
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2438
        $length = /** @scrutinizer ignore-deprecated */ $this->size();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2439
        if ($length == 0) {
2440
            return NULL;
2441
        }
2442
        // Only return the first item -- that's what JQ does.
2443
        $first = $this->getFirstMatch();
2444
2445
        // Catch cases where first item is not a legit DOM object.
2446
        if (!($first instanceof \DOMNode)) {
2447
            return NULL;
2448
        } elseif (!$first->hasChildNodes()) {
2449
            return '';
2450
        }
2451
2452
        $html5 = new HTML5($this->options);
2453
        $buffer = '';
2454
        foreach ($first->childNodes as $child) {
2455
            $buffer .= $html5->saveHTML($child);
2456
        }
2457
2458
        return $buffer;
2459
    }
2460
2461
    /**
2462
     * Retrieve the text of each match and concatenate them with the given separator.
2463
     *
2464
     * This has the effect of looping through all children, retrieving their text
2465
     * content, and then concatenating the text with a separator.
2466
     *
2467
     * @param string $sep
2468
     *  The string used to separate text items. The default is a comma followed by a
2469
     *  space.
2470
     * @param boolean $filterEmpties
2471
     *  If this is true, empty items will be ignored.
2472
     * @return string
2473
     *  The text contents, concatenated together with the given separator between
2474
     *  every pair of items.
2475
     * @see   implode()
2476
     * @see   text()
2477
     * @since 2.0
2478
     */
2479
    public function textImplode($sep = ', ', $filterEmpties = true)
2480
    {
2481
        $tmp = [];
2482
        foreach ($this->matches as $m) {
2483
            $txt = $m->textContent;
2484
            $trimmed = trim($txt);
2485
            // If filter empties out, then we only add items that have content.
2486
            if ($filterEmpties) {
2487
                if (strlen($trimmed) > 0) {
2488
                    $tmp[] = $txt;
2489
                }
2490
            } // Else add all content, even if it's empty.
2491
            else {
2492
                $tmp[] = $txt;
2493
            }
2494
        }
2495
2496
        return implode($sep, $tmp);
2497
    }
2498
2499
    /**
2500
     * Get the text contents from just child elements.
2501
     *
2502
     * This is a specialized variant of textImplode() that implodes text for just the
2503
     * child elements of the current element.
2504
     *
2505
     * @param string $separator
2506
     *  The separator that will be inserted between found text content.
2507
     * @return string
2508
     *  The concatenated values of all children.
2509
     */
2510
    public function childrenText($separator = ' ')
2511
    {
2512
        // Branch makes it non-destructive.
2513
        return $this->branch()->xpath('descendant::text()')->textImplode($separator);
2514
    }
2515
2516
    /**
2517
     * Get or set the text contents of a node.
2518
     *
2519
     * @param string $text
2520
     *  If this is not NULL, this value will be set as the text of the node. It
2521
     *  will replace any existing content.
2522
     * @return mixed
2523
     *  A DOMQuery if $text is set, or the text content if no text
2524
     *  is passed in as a pram.
2525
     * @see html()
2526
     * @see xml()
2527
     * @see contents()
2528
     */
2529
    public function text($text = NULL)
2530
    {
2531
        if (isset($text)) {
2532
            $this->removeChildren();
2533
            foreach ($this->matches as $m) {
2534
                $m->appendChild($this->document->createTextNode($text));
2535
            }
2536
2537
            return $this;
2538
        }
2539
        // Returns all text as one string:
2540
        $buf = '';
2541
        foreach ($this->matches as $m) {
2542
            $buf .= $m->textContent;
2543
        }
2544
2545
        return $buf;
2546
    }
2547
2548
    /**
2549
     * Get or set the text before each selected item.
2550
     *
2551
     * If $text is passed in, the text is inserted before each currently selected item.
2552
     *
2553
     * If no text is given, this will return the concatenated text after each selected element.
2554
     *
2555
     * @code
2556
     * <?php
2557
     * $xml = '<?xml version="1.0"?><root>Foo<a>Bar</a><b/></root>';
2558
     *
2559
     * // This will return 'Foo'
2560
     * qp($xml, 'a')->textBefore();
2561
     *
2562
     * // This will insert 'Baz' right before <b/>.
2563
     * qp($xml, 'b')->textBefore('Baz');
2564
     * ?>
2565
     * @endcode
2566
     *
2567
     * @param string $text
2568
     *  If this is set, it will be inserted before each node in the current set of
2569
     *  selected items.
2570
     * @return mixed
2571
     *  Returns the DOMQuery object if $text was set, and returns a string (possibly empty)
2572
     *  if no param is passed.
2573
     */
2574
    public function textBefore($text = NULL)
2575
    {
2576
        if (isset($text)) {
2577
            $textNode = $this->document->createTextNode($text);
2578
2579
            return $this->before($textNode);
2580
        }
2581
        $buffer = '';
2582
        foreach ($this->matches as $m) {
2583
            $p = $m;
2584
            while (isset($p->previousSibling) && $p->previousSibling->nodeType == XML_TEXT_NODE) {
2585
                $p = $p->previousSibling;
2586
                $buffer .= $p->textContent;
2587
            }
2588
        }
2589
2590
        return $buffer;
2591
    }
2592
2593
    public function textAfter($text = NULL)
2594
    {
2595
        if (isset($text)) {
2596
            $textNode = $this->document->createTextNode($text);
2597
2598
            return $this->after($textNode);
2599
        }
2600
        $buffer = '';
2601
        foreach ($this->matches as $m) {
2602
            $n = $m;
2603
            while (isset($n->nextSibling) && $n->nextSibling->nodeType == XML_TEXT_NODE) {
2604
                $n = $n->nextSibling;
2605
                $buffer .= $n->textContent;
2606
            }
2607
        }
2608
2609
        return $buffer;
2610
    }
2611
2612
    /**
2613
     * Set or get the value of an element's 'value' attribute.
2614
     *
2615
     * The 'value' attribute is common in HTML form elements. This is a
2616
     * convenience function for accessing the values. Since this is not  common
2617
     * task on the server side, this method may be removed in future releases. (It
2618
     * is currently provided for jQuery compatibility.)
2619
     *
2620
     * If a value is provided in the params, then the value will be set for all
2621
     * matches. If no params are given, then the value of the first matched element
2622
     * will be returned. This may be NULL.
2623
     *
2624
     * @deprecated Just use attr(). There's no reason to use this on the server.
2625
     * @see        attr()
2626
     * @param string $value
2627
     * @return mixed
2628
     *  Returns a DOMQuery if a string was passed in, and a string if no string
2629
     *  was passed in. In the later case, an error will produce NULL.
2630
     */
2631
    public function val($value = NULL)
2632
    {
2633
        if (isset($value)) {
2634
            $this->attr('value', $value);
2635
2636
            return $this;
2637
        }
2638
2639
        return $this->attr('value');
2640
    }
2641
2642
    /**
2643
     * Set or get XHTML markup for an element or elements.
2644
     *
2645
     * This differs from {@link html()} in that it processes (and produces)
2646
     * strictly XML 1.0 compliant markup.
2647
     *
2648
     * Like {@link xml()} and {@link html()}, this functions as both a
2649
     * setter and a getter.
2650
     *
2651
     * This is a convenience function for fetching HTML in XML format.
2652
     * It does no processing of the markup (such as schema validation).
2653
     *
2654
     * @param string $markup
2655
     *  A string containing XML data.
2656
     * @return mixed
2657
     *  If markup is passed in, a DOMQuery is returned. If no markup is passed
2658
     *  in, XML representing the first matched element is returned.
2659
     * @see html()
2660
     * @see innerXHTML()
2661
     */
2662
    public function xhtml($markup = NULL)
2663
    {
2664
2665
        // XXX: This is a minor reworking of the original xml() method.
2666
        // This should be refactored, probably.
2667
        // See http://github.com/technosophos/querypath/issues#issue/10
2668
2669
        $omit_xml_decl = $this->options['omit_xml_declaration'];
2670
        if ($markup === true) {
0 ignored issues
show
introduced by
The condition $markup === true is always false.
Loading history...
2671
            // Basically, we handle the special case where we don't
2672
            // want the XML declaration to be displayed.
2673
            $omit_xml_decl = true;
2674
        } elseif (isset($markup)) {
2675
            return $this->xml($markup);
2676
        }
2677
2678
        $length = $this->size();
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2678
        $length = /** @scrutinizer ignore-deprecated */ $this->size();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2679
        if ($length == 0) {
2680
            return NULL;
2681
        }
2682
2683
        // Only return the first item -- that's what JQ does.
2684
        $first = $this->getFirstMatch();
2685
        // Catch cases where first item is not a legit DOM object.
2686
        if (!($first instanceof \DOMNode)) {
2687
            return NULL;
2688
        }
2689
2690
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
2691
2692
            // Has the unfortunate side-effect of stripping doctype.
2693
            //$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG));
2694
            $text = $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG);
2695
        } else {
2696
            $text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG);
2697
        }
2698
2699
        // Issue #47: Using the old trick for removing the XML tag also removed the
2700
        // doctype. So we remove it with a regex:
2701
        if ($omit_xml_decl) {
2702
            $text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text);
2703
        }
2704
2705
        // This is slightly lenient: It allows for cases where code incorrectly places content
2706
        // inside of these supposedly unary elements.
2707
        $unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i';
2708
        $text = preg_replace($unary, '<\\1\\2 />', $text);
2709
2710
        // Experimental: Support for enclosing CDATA sections with comments to be both XML compat
2711
        // and HTML 4/5 compat
2712
        $cdata = '/(<!\[CDATA\[|\]\]>)/i';
2713
        $replace = $this->options['escape_xhtml_js_css_sections'];
2714
        $text = preg_replace($cdata, $replace, $text);
2715
2716
        return $text;
2717
    }
2718
2719
    /**
2720
     * Set or get the XML markup for an element or elements.
2721
     *
2722
     * Like {@link html()}, this functions in both a setter and a getter mode.
2723
     *
2724
     * In setter mode, the string passed in will be parsed and then appended to the
2725
     * elements wrapped by this DOMNode object.When in setter mode, this parses
2726
     * the XML using the DOMFragment parser. For that reason, an XML declaration
2727
     * is not necessary.
2728
     *
2729
     * In getter mode, the first element wrapped by this DOMNode object will be
2730
     * converted to an XML string and returned.
2731
     *
2732
     * @param string $markup
2733
     *  A string containing XML data.
2734
     * @return mixed
2735
     *  If markup is passed in, a DOMQuery is returned. If no markup is passed
2736
     *  in, XML representing the first matched element is returned.
2737
     * @see xhtml()
2738
     * @see html()
2739
     * @see text()
2740
     * @see content()
2741
     * @see innerXML()
2742
     */
2743
    public function xml($markup = NULL)
2744
    {
2745
        $omit_xml_decl = $this->options['omit_xml_declaration'];
2746
        if ($markup === true) {
0 ignored issues
show
introduced by
The condition $markup === true is always false.
Loading history...
2747
            // Basically, we handle the special case where we don't
2748
            // want the XML declaration to be displayed.
2749
            $omit_xml_decl = true;
2750
        } elseif (isset($markup)) {
2751
            if ($this->options['replace_entities']) {
2752
                $markup = \QueryPath\Entities::replaceAllEntities($markup);
2753
            }
2754
            $doc = $this->document->createDocumentFragment();
2755
            $doc->appendXML($markup);
2756
            $this->removeChildren();
2757
            $this->append($doc);
2758
2759
            return $this;
2760
        }
2761
        $length = $this->size();
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\DOMQuery::size() has been deprecated: QueryPath now implements Countable, so use count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2761
        $length = /** @scrutinizer ignore-deprecated */ $this->size();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
2762
        if ($length == 0) {
2763
            return NULL;
2764
        }
2765
        // Only return the first item -- that's what JQ does.
2766
        $first = $this->getFirstMatch();
2767
2768
        // Catch cases where first item is not a legit DOM object.
2769
        if (!($first instanceof \DOMNode)) {
2770
            return NULL;
2771
        }
2772
2773
        if ($first instanceof \DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
2774
2775
            return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML());
2776
        }
2777
2778
        return $this->document->saveXML($first);
2779
    }
2780
2781
    /**
2782
     * Send the XML document to the client.
2783
     *
2784
     * Write the document to a file path, if given, or
2785
     * to stdout (usually the client).
2786
     *
2787
     * This prints the entire document.
2788
     *
2789
     * @param string $path
2790
     *  The path to the file into which the XML should be written. if
2791
     *  this is NULL, data will be written to STDOUT, which is usually
2792
     *  sent to the remote browser.
2793
     * @param int $options
2794
     *  (As of QueryPath 2.1) Pass libxml options to the saving mechanism.
2795
     * @return \QueryPath\DOMQuery
2796
     *  The DOMQuery object, unmodified.
2797
     * @see xml()
2798
     * @see innerXML()
2799
     * @see writeXHTML()
2800
     * @throws Exception
2801
     *  In the event that a file cannot be written, an Exception will be thrown.
2802
     */
2803
    public function writeXML($path = NULL, $options = NULL)
2804
    {
2805
        if ($path == NULL) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $path of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2806
            print $this->document->saveXML(NULL, $options);
2807
        } else {
2808
            try {
2809
                set_error_handler([IOException::class, 'initializeFromError']);
2810
                $this->document->save($path, $options);
2811
            } catch (Exception $e) {
2812
                restore_error_handler();
2813
                throw $e;
2814
            }
2815
            restore_error_handler();
2816
        }
2817
2818
        return $this;
2819
    }
2820
2821
    /**
2822
     * Writes HTML to output.
2823
     *
2824
     * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for
2825
     * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}.
2826
     *
2827
     * Write the document to stdout (usually the client) or to a file.
2828
     *
2829
     * @param string $path
2830
     *  The path to the file into which the XML should be written. if
2831
     *  this is NULL, data will be written to STDOUT, which is usually
2832
     *  sent to the remote browser.
2833
     * @return \QueryPath\DOMQuery
2834
     *  The DOMQuery object, unmodified.
2835
     * @see html()
2836
     * @see innerHTML()
2837
     * @throws Exception
2838
     *  In the event that a file cannot be written, an Exception will be thrown.
2839
     */
2840
    public function writeHTML($path = NULL)
2841
    {
2842
        if ($path == NULL) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $path of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
2843
            print $this->document->saveHTML();
2844
        } else {
2845
            try {
2846
                set_error_handler(['\QueryPath\ParseException', 'initializeFromError']);
2847
                $this->document->saveHTMLFile($path);
2848
            } catch (Exception $e) {
2849
                restore_error_handler();
2850
                throw $e;
2851
            }
2852
            restore_error_handler();
2853
        }
2854
2855
        return $this;
2856
    }
2857
2858
    /**
2859
     * Write the document to HTML5.
2860
     *
2861
     * This works the same as the other write* functions, but it encodes the output
2862
     * as HTML5 with UTF-8.
2863
     *
2864
     * @see html5()
2865
     * @see innerHTML5()
2866
     * @throws Exception
2867
     *  In the event that a file cannot be written, an Exception will be thrown.
2868
     */
2869
    public function writeHTML5($path = NULL)
2870
    {
2871
        $html5 = new HTML5();
2872
        if ($path === NULL) {
2873
            // Print the document to stdout.
2874
            print $html5->saveHTML($this->document);
2875
2876
            return;
2877
        }
2878
2879
        $html5->save($this->document, $path);
2880
    }
2881
2882
    /**
2883
     * Write an XHTML file to output.
2884
     *
2885
     * Typically, you should use this instead of {@link writeHTML()}.
2886
     *
2887
     * Currently, this functions identically to {@link toXML()} <i>except that</i>
2888
     * it always uses closing tags (e.g. always @code<script></script>@endcode,
2889
     * never @code<script/>@endcode). It will
2890
     * write the file as well-formed XML. No XHTML schema validation is done.
2891
     *
2892
     * @see   writeXML()
2893
     * @see   xml()
2894
     * @see   writeHTML()
2895
     * @see   innerXHTML()
2896
     * @see   xhtml()
2897
     * @param string $path
2898
     *  The filename of the file to write to.
2899
     * @return \QueryPath\DOMQuery
2900
     *  Returns the DOMQuery, unmodified.
2901
     * @throws Exception
2902
     *  In the event that the output file cannot be written, an exception is
2903
     *  thrown.
2904
     * @since 2.0
2905
     */
2906
    public function writeXHTML($path = NULL)
2907
    {
2908
        return $this->writeXML($path, LIBXML_NOEMPTYTAG);
2909
    }
2910
2911
    /**
2912
     * Get the next sibling of each element in the DOMQuery.
2913
     *
2914
     * If a selector is provided, the next matching sibling will be returned.
2915
     *
2916
     * @param string $selector
2917
     *  A CSS3 selector.
2918
     * @return \QueryPath\DOMQuery
2919
     *  The DOMQuery object.
2920
     * @see nextAll()
2921
     * @see prev()
2922
     * @see children()
2923
     * @see contents()
2924
     * @see parent()
2925
     * @see parents()
2926
     */
2927
    public function next($selector = NULL)
2928
    {
2929
        $found = new \SplObjectStorage();
2930
        foreach ($this->matches as $m) {
2931
            while (isset($m->nextSibling)) {
2932
                $m = $m->nextSibling;
2933
                if ($m->nodeType === XML_ELEMENT_NODE) {
2934
                    if (!empty($selector)) {
2935
                        if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
2936
                            $found->attach($m);
2937
                            break;
2938
                        }
2939
                    } else {
2940
                        $found->attach($m);
2941
                        break;
2942
                    }
2943
                }
2944
            }
2945
        }
2946
2947
        return $this->inst($found, NULL, $this->options);
2948
    }
2949
2950
    /**
2951
     * Get all siblings after an element.
2952
     *
2953
     * For each element in the DOMQuery, get all siblings that appear after
2954
     * it. If a selector is passed in, then only siblings that match the
2955
     * selector will be included.
2956
     *
2957
     * @param string $selector
2958
     *  A valid CSS 3 selector.
2959
     * @return \QueryPath\DOMQuery
2960
     *  The DOMQuery object, now containing the matching siblings.
2961
     * @see next()
2962
     * @see prevAll()
2963
     * @see children()
2964
     * @see siblings()
2965
     */
2966
    public function nextAll($selector = NULL)
2967
    {
2968
        $found = new \SplObjectStorage();
2969
        foreach ($this->matches as $m) {
2970
            while (isset($m->nextSibling)) {
2971
                $m = $m->nextSibling;
2972
                if ($m->nodeType === XML_ELEMENT_NODE) {
2973
                    if (!empty($selector)) {
2974
                        if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
2975
                            $found->attach($m);
2976
                        }
2977
                    } else {
2978
                        $found->attach($m);
2979
                    }
2980
                }
2981
            }
2982
        }
2983
2984
        return $this->inst($found, NULL, $this->options);
2985
    }
2986
2987
    /**
2988
     * Get the next sibling before each element in the DOMQuery.
2989
     *
2990
     * For each element in the DOMQuery, this retrieves the previous sibling
2991
     * (if any). If a selector is supplied, it retrieves the first matching
2992
     * sibling (if any is found).
2993
     *
2994
     * @param string $selector
2995
     *  A valid CSS 3 selector.
2996
     * @return \QueryPath\DOMQuery
2997
     *  A DOMNode object, now containing any previous siblings that have been
2998
     *  found.
2999
     * @see prevAll()
3000
     * @see next()
3001
     * @see siblings()
3002
     * @see children()
3003
     */
3004
    public function prev($selector = NULL)
3005
    {
3006
        $found = new \SplObjectStorage();
3007
        foreach ($this->matches as $m) {
3008
            while (isset($m->previousSibling)) {
3009
                $m = $m->previousSibling;
3010
                if ($m->nodeType === XML_ELEMENT_NODE) {
3011
                    if (!empty($selector)) {
3012
                        if (QueryPath::with($m, NULL, $this->options)->is($selector)) {
3013
                            $found->attach($m);
3014
                            break;
3015
                        }
3016
                    } else {
3017
                        $found->attach($m);
3018
                        break;
3019
                    }
3020
                }
3021
            }
3022
        }
3023
3024
        return $this->inst($found, NULL, $this->options);
3025
    }
3026
3027
    /**
3028
     * Get the previous siblings for each element in the DOMQuery.
3029
     *
3030
     * For each element in the DOMQuery, get all previous siblings. If a
3031
     * selector is provided, only matching siblings will be retrieved.
3032
     *
3033
     * @param string $selector
3034
     *  A valid CSS 3 selector.
3035
     * @return \QueryPath\DOMQuery
3036
     *  The DOMQuery object, now wrapping previous sibling elements.
3037
     * @see prev()
3038
     * @see nextAll()
3039
     * @see siblings()
3040
     * @see contents()
3041
     * @see children()
3042
     */
3043
    public function prevAll($selector = NULL)
3044
    {
3045
        $found = new \SplObjectStorage();
3046
        foreach ($this->matches as $m) {
3047
            while (isset($m->previousSibling)) {
3048
                $m = $m->previousSibling;
3049
                if ($m->nodeType === XML_ELEMENT_NODE) {
3050
                    if (!empty($selector)) {
3051
                        if (QueryPath::with($m, NULL, $this->options)->is($selector)) {
3052
                            $found->attach($m);
3053
                        }
3054
                    } else {
3055
                        $found->attach($m);
3056
                    }
3057
                }
3058
            }
3059
        }
3060
3061
        return $this->inst($found, NULL, $this->options);
3062
    }
3063
3064
    /**
3065
     * Add a class to all elements in the current DOMQuery.
3066
     *
3067
     * This searchers for a class attribute on each item wrapped by the current
3068
     * DOMNode object. If no attribute is found, a new one is added and its value
3069
     * is set to $class. If a class attribute is found, then the value is appended
3070
     * on to the end.
3071
     *
3072
     * @param string $class
3073
     *  The name of the class.
3074
     * @return \QueryPath\DOMQuery
3075
     *  Returns the DOMQuery object.
3076
     * @see css()
3077
     * @see attr()
3078
     * @see removeClass()
3079
     * @see hasClass()
3080
     */
3081
    public function addClass($class)
3082
    {
3083
        foreach ($this->matches as $m) {
3084
            if ($m->hasAttribute('class')) {
3085
                $val = $m->getAttribute('class');
3086
                $m->setAttribute('class', $val . ' ' . $class);
3087
            } else {
3088
                $m->setAttribute('class', $class);
3089
            }
3090
        }
3091
3092
        return $this;
3093
    }
3094
3095
    /**
3096
     * Remove the named class from any element in the DOMQuery that has it.
3097
     *
3098
     * This may result in the entire class attribute being removed. If there
3099
     * are other items in the class attribute, though, they will not be removed.
3100
     *
3101
     * Example:
3102
     * Consider this XML:
3103
     *
3104
     * @code
3105
     * <element class="first second"/>
3106
     * @endcode
3107
     *
3108
     * Executing this fragment of code will remove only the 'first' class:
3109
     * @code
3110
     * qp(document, 'element')->removeClass('first');
3111
     * @endcode
3112
     *
3113
     * The resulting XML will be:
3114
     * @code
3115
     * <element class="second"/>
3116
     * @endcode
3117
     *
3118
     * To remove the entire 'class' attribute, you should use {@see removeAttr()}.
3119
     *
3120
     * @param string $class
3121
     *  The class name to remove.
3122
     * @return \QueryPath\DOMQuery
3123
     *  The modified DOMNode object.
3124
     * @see attr()
3125
     * @see addClass()
3126
     * @see hasClass()
3127
     */
3128
    public function removeClass($class = false)
3129
    {
3130
        if (empty($class)) {
3131
            foreach ($this->matches as $m) {
3132
                $m->removeAttribute('class');
3133
            }
3134
        } else {
3135
            $to_remove = array_filter(explode(' ', $class));
3136
            foreach ($this->matches as $m) {
3137
                if ($m->hasAttribute('class')) {
3138
                    $vals = array_filter(explode(' ', $m->getAttribute('class')));
3139
                    $buf = [];
3140
                    foreach ($vals as $v) {
3141
                        if (!in_array($v, $to_remove)) {
3142
                            $buf[] = $v;
3143
                        }
3144
                    }
3145
                    if (empty($buf)) {
3146
                        $m->removeAttribute('class');
3147
                    } else {
3148
                        $m->setAttribute('class', implode(' ', $buf));
3149
                    }
3150
                }
3151
            }
3152
        }
3153
3154
        return $this;
3155
    }
3156
3157
    /**
3158
     * Returns TRUE if any of the elements in the DOMQuery have the specified class.
3159
     *
3160
     * @param string $class
3161
     *  The name of the class.
3162
     * @return boolean
3163
     *  TRUE if the class exists in one or more of the elements, FALSE otherwise.
3164
     * @see addClass()
3165
     * @see removeClass()
3166
     */
3167
    public function hasClass($class)
3168
    {
3169
        foreach ($this->matches as $m) {
3170
            if ($m->hasAttribute('class')) {
3171
                $vals = explode(' ', $m->getAttribute('class'));
3172
                if (in_array($class, $vals)) {
3173
                    return true;
3174
                }
3175
            }
3176
        }
3177
3178
        return false;
3179
    }
3180
3181
    /**
3182
     * Branch the base DOMQuery into another one with the same matches.
3183
     *
3184
     * This function makes a copy of the DOMQuery object, but keeps the new copy
3185
     * (initially) pointed at the same matches. This object can then be queried without
3186
     * changing the original DOMQuery. However, changes to the elements inside of this
3187
     * DOMQuery will show up in the DOMQuery from which it is branched.
3188
     *
3189
     * Compare this operation with {@link cloneAll()}. The cloneAll() call takes
3190
     * the current DOMNode object and makes a copy of all of its matches. You continue
3191
     * to operate on the same DOMNode object, but the elements inside of the DOMQuery
3192
     * are copies of those before the call to cloneAll().
3193
     *
3194
     * This, on the other hand, copies <i>the DOMQuery</i>, but keeps valid
3195
     * references to the document and the wrapped elements. A new query branch is
3196
     * created, but any changes will be written back to the same document.
3197
     *
3198
     * In practice, this comes in handy when you want to do multiple queries on a part
3199
     * of the document, but then return to a previous set of matches. (see {@link QPTPL}
3200
     * for examples of this in practice).
3201
     *
3202
     * Example:
3203
     *
3204
     * @code
3205
     * <?php
3206
     * $qp = qp( QueryPath::HTML_STUB);
3207
     * $branch = $qp->branch();
3208
     * $branch->find('title')->text('Title');
3209
     * $qp->find('body')->text('This is the body')->writeHTML;
3210
     * ?>
3211
     * @endcode
3212
     *
3213
     * Notice that in the code, each of the DOMQuery objects is doing its own
3214
     * query. However, both are modifying the same document. The result of the above
3215
     * would look something like this:
3216
     *
3217
     * @code
3218
     * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3219
     * <html xmlns="http://www.w3.org/1999/xhtml">
3220
     * <head>
3221
     *    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
3222
     *    <title>Title</title>
3223
     * </head>
3224
     * <body>This is the body</body>
3225
     * </html>
3226
     * @endcode
3227
     *
3228
     * Notice that while $qp and $banch were performing separate queries, they
3229
     * both modified the same document.
3230
     *
3231
     * In jQuery or a browser-based solution, you generally do not need a branching
3232
     * function because there is (implicitly) only one document. In QueryPath, there
3233
     * is no implicit document. Every document must be explicitly specified (and,
3234
     * in most cases, parsed -- which is costly). Branching makes it possible to
3235
     * work on one document with multiple DOMNode objects.
3236
     *
3237
     * @param string $selector
3238
     *  If a selector is passed in, an additional {@link find()} will be executed
3239
     *  on the branch before it is returned. (Added in QueryPath 2.0.)
3240
     * @return \QueryPath\DOMQuery
3241
     *  A copy of the DOMQuery object that points to the same set of elements that
3242
     *  the original DOMQuery was pointing to.
3243
     * @since 1.1
3244
     * @see   cloneAll()
3245
     * @see   find()
3246
     */
3247
    public function branch($selector = NULL)
3248
    {
3249
        $temp = QueryPath::with($this->matches, NULL, $this->options);
3250
        //if (isset($selector)) $temp->find($selector);
3251
        $temp->document = $this->document;
3252
        if (isset($selector)) {
3253
            $temp->findInPlace($selector);
3254
        }
3255
3256
        return $temp;
3257
    }
3258
3259
    protected function inst($matches, $selector, $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

3259
    protected function inst($matches, $selector, /** @scrutinizer ignore-unused */ $options)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
3260
    {
3261
        // https://en.wikipedia.org/wiki/Dolly_(sheep)
3262
        $dolly = clone $this;
3263
        $dolly->setMatches($matches);
3264
3265
        if (isset($selector)) {
3266
            $dolly->findInPlace($selector);
3267
        }
3268
3269
        return $dolly;
3270
    }
3271
3272
    /**
3273
     * Perform a deep clone of each node in the DOMQuery.
3274
     *
3275
     * @attention
3276
     *   This is an in-place modification of the current QueryPath object.
3277
     *
3278
     * This does not clone the DOMQuery object, but instead clones the
3279
     * list of nodes wrapped by the DOMQuery. Every element is deeply
3280
     * cloned.
3281
     *
3282
     * This method is analogous to jQuery's clone() method.
3283
     *
3284
     * This is a destructive operation, which means that end() will revert
3285
     * the list back to the clone's original.
3286
     * @see qp()
3287
     * @return \QueryPath\DOMQuery
3288
     */
3289
    public function cloneAll()
3290
    {
3291
        $found = new \SplObjectStorage();
3292
        foreach ($this->matches as $m) {
3293
            $found->attach($m->cloneNode(true));
3294
        }
3295
        //return $this->inst($found, NULL, $this->options);
3296
        $this->setMatches($found);
3297
3298
        return $this;
3299
    }
3300
3301
    /**
3302
     * Clone the DOMQuery.
3303
     *
3304
     * This makes a deep clone of the elements inside of the DOMQuery.
3305
     *
3306
     * This clones only the QueryPathImpl, not all of the decorators. The
3307
     * clone operator in PHP should handle the cloning of the decorators.
3308
     */
3309
    public function __clone()
3310
    {
3311
        //XXX: Should we clone the document?
3312
3313
        // Make sure we clone the kids.
3314
        $this->cloneAll();
3315
    }
3316
3317
    /**
3318
     * Detach any items from the list if they match the selector.
3319
     *
3320
     * In other words, each item that matches the selector will be removed
3321
     * from the DOM document. The returned DOMQuery wraps the list of
3322
     * removed elements.
3323
     *
3324
     * If no selector is specified, this will remove all current matches from
3325
     * the document.
3326
     *
3327
     * @param string $selector
3328
     *  A CSS Selector.
3329
     * @return \QueryPath\DOMQuery
3330
     *  The Query path wrapping a list of removed items.
3331
     * @see    replaceAll()
3332
     * @see    replaceWith()
3333
     * @see    removeChildren()
3334
     * @since  2.1
3335
     * @author eabrand
3336
     */
3337
    public function detach($selector = NULL)
3338
    {
3339
3340
        if (!empty($selector)) {
3341
            $this->find($selector);
3342
        }
3343
3344
        $found = new \SplObjectStorage();
3345
        $this->last = $this->matches;
3346
        foreach ($this->matches as $item) {
3347
            // The item returned is (according to docs) different from
3348
            // the one passed in, so we have to re-store it.
3349
            $found->attach($item->parentNode->removeChild($item));
3350
        }
3351
3352
        return $this->inst($found, NULL, $this->options);
3353
    }
3354
3355
    /**
3356
     * Attach any items from the list if they match the selector.
3357
     *
3358
     * If no selector is specified, this will remove all current matches from
3359
     * the document.
3360
     *
3361
     * @param DOMQuery $dest
3362
     *  A DOMQuery Selector.
3363
     * @return \QueryPath\DOMQuery
3364
     *  The Query path wrapping a list of removed items.
3365
     * @see    replaceAll()
3366
     * @see    replaceWith()
3367
     * @see    removeChildren()
3368
     * @since  2.1
3369
     * @author eabrand
3370
     */
3371
    public function attach(DOMQuery $dest)
3372
    {
3373
        foreach ($this->last as $m) {
3374
            $dest->append($m);
3375
        }
3376
3377
        return $this;
3378
    }
3379
3380
    /**
3381
     * Reduce the elements matched by DOMQuery to only those which contain the given item.
3382
     *
3383
     * There are two ways in which this is different from jQuery's implementation:
3384
     * - We allow ANY DOMNode, not just DOMElements. That means this will work on
3385
     *   processor instructions, text nodes, comments, etc.
3386
     * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior
3387
     *   and modifies the existing object. It does not create a brand new object.
3388
     *
3389
     * @param mixed $contained
3390
     *     - If $contained is a CSS selector (e.g. '#foo'), this will test to see
3391
     *     if the current DOMQuery has any elements that contain items that match
3392
     *     the selector.
3393
     *     - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode
3394
     *     exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the
3395
     *     same document.)
3396
     * @since  2.1
3397
     * @author eabrand
3398
     * @todo   It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes.
3399
     */
3400
    public function has($contained)
3401
    {
3402
        /*
3403
    if (count($this->matches) == 0) {
3404
      return false;
3405
    }
3406
     */
3407
        $found = new \SplObjectStorage();
3408
3409
        // If it's a selector, we just get all of the DOMNodes that match the selector.
3410
        $nodes = [];
3411
        if (is_string($contained)) {
3412
            // Get the list of nodes.
3413
            $nodes = $this->branch($contained)->get();
3414
        } elseif ($contained instanceof \DOMNode) {
3415
            // Make a list with one node.
3416
            $nodes = [$contained];
3417
        }
3418
3419
        // Now we go through each of the nodes that we are testing. We want to find
3420
        // ALL PARENTS that are in our existing DOMQuery matches. Those are the
3421
        // ones we add to our new matches.
3422
        foreach ($nodes as $original_node) {
3423
            $node = $original_node;
3424
            while (!empty($node)/* && $node != $node->ownerDocument*/) {
3425
                if ($this->matches->contains($node)) {
0 ignored issues
show
Bug introduced by
The method contains() does not exist on Traversable. It seems like you code against a sub-type of Traversable such as QueryPathImpl or QueryPath\DOMQuery or SplObjectStorage. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

3425
                if ($this->matches->/** @scrutinizer ignore-call */ contains($node)) {
Loading history...
3426
                    $found->attach($node);
3427
                }
3428
                $node = $node->parentNode;
3429
            }
3430
        }
3431
3432
        return $this->inst($found, NULL, $this->options);
3433
    }
3434
3435
    /**
3436
     * Empty everything within the specified element.
3437
     *
3438
     * A convenience function for removeChildren(). This is equivalent to jQuery's
3439
     * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a
3440
     * function name.
3441
     *
3442
     * @return \QueryPath\DOMQuery
3443
     *  The DOMQuery object with the newly emptied elements.
3444
     * @see        removeChildren()
3445
     * @since      2.1
3446
     * @author     eabrand
3447
     * @deprecated The removeChildren() function is the preferred method.
3448
     */
3449
    public function emptyElement()
3450
    {
3451
        $this->removeChildren();
3452
3453
        return $this;
3454
    }
3455
3456
    /**
3457
     * Get the even elements, so counter-intuitively 1, 3, 5, etc.
3458
     *
3459
     *
3460
     *
3461
     * @return \QueryPath\DOMQuery
3462
     *  A DOMQuery wrapping all of the children.
3463
     * @see    removeChildren()
3464
     * @see    parent()
3465
     * @see    parents()
3466
     * @see    next()
3467
     * @see    prev()
3468
     * @since  2.1
3469
     * @author eabrand
3470
     */
3471
    public function even()
3472
    {
3473
        $found = new \SplObjectStorage();
3474
        $even = false;
3475
        foreach ($this->matches as $m) {
3476
            if ($even && $m->nodeType == XML_ELEMENT_NODE) {
3477
                $found->attach($m);
3478
            }
3479
            $even = ($even) ? false : true;
3480
        }
3481
3482
        return $this->inst($found, NULL, $this->options);
3483
    }
3484
3485
    /**
3486
     * Get the odd elements, so counter-intuitively 0, 2, 4, etc.
3487
     *
3488
     *
3489
     *
3490
     * @return \QueryPath\DOMQuery
3491
     *  A DOMQuery wrapping all of the children.
3492
     * @see    removeChildren()
3493
     * @see    parent()
3494
     * @see    parents()
3495
     * @see    next()
3496
     * @see    prev()
3497
     * @since  2.1
3498
     * @author eabrand
3499
     */
3500
    public function odd()
3501
    {
3502
        $found = new \SplObjectStorage();
3503
        $odd = true;
3504
        foreach ($this->matches as $m) {
3505
            if ($odd && $m->nodeType == XML_ELEMENT_NODE) {
3506
                $found->attach($m);
3507
            }
3508
            $odd = ($odd) ? false : true;
3509
        }
3510
3511
        return $this->inst($found, NULL, $this->options);
3512
    }
3513
3514
    /**
3515
     * Get the first matching element.
3516
     *
3517
     *
3518
     * @return \QueryPath\DOMQuery
3519
     *  A DOMQuery wrapping all of the children.
3520
     * @see    next()
3521
     * @see    prev()
3522
     * @since  2.1
3523
     * @author eabrand
3524
     */
3525
    public function first()
3526
    {
3527
        $found = new \SplObjectStorage();
3528
        foreach ($this->matches as $m) {
3529
            if ($m->nodeType == XML_ELEMENT_NODE) {
3530
                $found->attach($m);
3531
                break;
3532
            }
3533
        }
3534
3535
        return $this->inst($found, NULL, $this->options);
3536
    }
3537
3538
    /**
3539
     * Get the first child of the matching element.
3540
     *
3541
     *
3542
     * @return \QueryPath\DOMQuery
3543
     *  A DOMQuery wrapping all of the children.
3544
     * @see    next()
3545
     * @see    prev()
3546
     * @since  2.1
3547
     * @author eabrand
3548
     */
3549
    public function firstChild()
3550
    {
3551
        // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php
3552
        $found = new \SplObjectStorage();
3553
        $flag = false;
3554
        foreach ($this->matches as $m) {
3555
            foreach ($m->childNodes as $c) {
3556
                if ($c->nodeType == XML_ELEMENT_NODE) {
3557
                    $found->attach($c);
3558
                    $flag = true;
3559
                    break;
3560
                }
3561
            }
3562
            if ($flag) {
3563
                break;
3564
            }
3565
        }
3566
3567
        return $this->inst($found, NULL, $this->options);
3568
    }
3569
3570
    /**
3571
     * Get the last matching element.
3572
     *
3573
     *
3574
     * @return \QueryPath\DOMQuery
3575
     *  A DOMQuery wrapping all of the children.
3576
     * @see    next()
3577
     * @see    prev()
3578
     * @since  2.1
3579
     * @author eabrand
3580
     */
3581
    public function last()
3582
    {
3583
        $found = new \SplObjectStorage();
3584
        $item = NULL;
3585
        foreach ($this->matches as $m) {
3586
            if ($m->nodeType == XML_ELEMENT_NODE) {
3587
                $item = $m;
3588
            }
3589
        }
3590
        if ($item) {
3591
            $found->attach($item);
3592
        }
3593
3594
        return $this->inst($found, NULL, $this->options);
3595
    }
3596
3597
    /**
3598
     * Get the last child of the matching element.
3599
     *
3600
     *
3601
     * @return \QueryPath\DOMQuery
3602
     *  A DOMQuery wrapping all of the children.
3603
     * @see    next()
3604
     * @see    prev()
3605
     * @since  2.1
3606
     * @author eabrand
3607
     */
3608
    public function lastChild()
3609
    {
3610
        $found = new \SplObjectStorage();
3611
        $item = NULL;
3612
        foreach ($this->matches as $m) {
3613
            foreach ($m->childNodes as $c) {
3614
                if ($c->nodeType == XML_ELEMENT_NODE) {
3615
                    $item = $c;
3616
                }
3617
            }
3618
            if ($item) {
3619
                $found->attach($item);
3620
                $item = NULL;
3621
            }
3622
        }
3623
3624
        return $this->inst($found, NULL, $this->options);
3625
    }
3626
3627
    /**
3628
     * Get all siblings after an element until the selector is reached.
3629
     *
3630
     * For each element in the DOMQuery, get all siblings that appear after
3631
     * it. If a selector is passed in, then only siblings that match the
3632
     * selector will be included.
3633
     *
3634
     * @param string $selector
3635
     *  A valid CSS 3 selector.
3636
     * @return \QueryPath\DOMQuery
3637
     *  The DOMQuery object, now containing the matching siblings.
3638
     * @see    next()
3639
     * @see    prevAll()
3640
     * @see    children()
3641
     * @see    siblings()
3642
     * @since  2.1
3643
     * @author eabrand
3644
     */
3645
    public function nextUntil($selector = NULL)
3646
    {
3647
        $found = new \SplObjectStorage();
3648
        foreach ($this->matches as $m) {
3649
            while (isset($m->nextSibling)) {
3650
                $m = $m->nextSibling;
3651
                if ($m->nodeType === XML_ELEMENT_NODE) {
3652
                    if (!empty($selector) && QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
3653
                        break;
3654
                    }
3655
                    $found->attach($m);
3656
                }
3657
            }
3658
        }
3659
3660
        return $this->inst($found, NULL, $this->options);
3661
    }
3662
3663
    /**
3664
     * Get the previous siblings for each element in the DOMQuery
3665
     * until the selector is reached.
3666
     *
3667
     * For each element in the DOMQuery, get all previous siblings. If a
3668
     * selector is provided, only matching siblings will be retrieved.
3669
     *
3670
     * @param string $selector
3671
     *  A valid CSS 3 selector.
3672
     * @return \QueryPath\DOMQuery
3673
     *  The DOMQuery object, now wrapping previous sibling elements.
3674
     * @see    prev()
3675
     * @see    nextAll()
3676
     * @see    siblings()
3677
     * @see    contents()
3678
     * @see    children()
3679
     * @since  2.1
3680
     * @author eabrand
3681
     */
3682
    public function prevUntil($selector = NULL)
3683
    {
3684
        $found = new \SplObjectStorage();
3685
        foreach ($this->matches as $m) {
3686
            while (isset($m->previousSibling)) {
3687
                $m = $m->previousSibling;
3688
                if ($m->nodeType === XML_ELEMENT_NODE) {
3689
                    if (!empty($selector) && QueryPath::with($m, NULL, $this->options)->is($selector)) {
3690
                        break;
3691
                    }
3692
3693
                    $found->attach($m);
3694
                }
3695
            }
3696
        }
3697
3698
        return $this->inst($found, NULL, $this->options);
3699
    }
3700
3701
    /**
3702
     * Get all ancestors of each element in the DOMQuery until the selector is reached.
3703
     *
3704
     * If a selector is present, only matching ancestors will be retrieved.
3705
     *
3706
     * @see    parent()
3707
     * @param string $selector
3708
     *  A valid CSS 3 Selector.
3709
     * @return \QueryPath\DOMQuery
3710
     *  A DOMNode object containing the matching ancestors.
3711
     * @see    siblings()
3712
     * @see    children()
3713
     * @since  2.1
3714
     * @author eabrand
3715
     */
3716
    public function parentsUntil($selector = NULL)
3717
    {
3718
        $found = new \SplObjectStorage();
3719
        foreach ($this->matches as $m) {
3720
            while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
3721
                $m = $m->parentNode;
3722
                // Is there any case where parent node is not an element?
3723
                if ($m->nodeType === XML_ELEMENT_NODE) {
3724
                    if (!empty($selector)) {
3725
                        if (QueryPath::with($m, NULL, $this->options)->is($selector) > 0) {
3726
                            break;
3727
                        } else {
3728
                            $found->attach($m);
3729
                        }
3730
                    } else {
3731
                        $found->attach($m);
3732
                    }
3733
                }
3734
            }
3735
        }
3736
3737
        return $this->inst($found, NULL, $this->options);
3738
    }
3739
3740
    /////// INTERNAL FUNCTIONS ////////
3741
3742
    /**
3743
     * Call extension methods.
3744
     *
3745
     * This function is used to invoke extension methods. It searches the
3746
     * registered extenstensions for a matching function name. If one is found,
3747
     * it is executed with the arguments in the $arguments array.
3748
     *
3749
     * @throws QueryPath::Exception
3750
     *  An exception is thrown if a non-existent method is called.
3751
     */
3752
    public function __call($name, $arguments)
3753
    {
3754
3755
        if (!ExtensionRegistry::$useRegistry) {
3756
            throw new \QueryPath\Exception("No method named $name found (Extensions disabled).");
3757
        }
3758
3759
        // Loading of extensions is deferred until the first time a
3760
        // non-core method is called. This makes constructing faster, but it
3761
        // may make the first invocation of __call() slower (if there are
3762
        // enough extensions.)
3763
        //
3764
        // The main reason for moving this out of the constructor is that most
3765
        // new DOMQuery instances do not use extensions. Charging qp() calls
3766
        // with the additional hit is not a good idea.
3767
        //
3768
        // Also, this will at least limit the number of circular references.
3769
        if (empty($this->ext)) {
3770
            // Load the registry
3771
            $this->ext = ExtensionRegistry::getExtensions($this);
3772
        }
3773
3774
        // Note that an empty ext registry indicates that extensions are disabled.
3775
        if (!empty($this->ext) && ExtensionRegistry::hasMethod($name)) {
3776
            $owner = ExtensionRegistry::getMethodClass($name);
3777
            $method = new \ReflectionMethod($owner, $name);
3778
3779
            return $method->invokeArgs($this->ext[$owner], $arguments);
3780
        }
3781
        throw new \QueryPath\Exception("No method named $name found. Possibly missing an extension.");
3782
    }
3783
3784
    /**
3785
     * Get an iterator for the matches in this object.
3786
     *
3787
     * @return Iterable
3788
     *  Returns an iterator.
3789
     */
3790
    public function getIterator()
3791
    {
3792
        $i = new QueryPathIterator($this->matches);
0 ignored issues
show
Bug introduced by
It seems like $this->matches can also be of type array; however, parameter $iterator of QueryPath\QueryPathIterator::__construct() does only seem to accept Traversable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3792
        $i = new QueryPathIterator(/** @scrutinizer ignore-type */ $this->matches);
Loading history...
3793
        $i->options = $this->options;
3794
3795
        return $i;
3796
    }
3797
}
3798