QueryPathEventHandler::nthChild()   C
last analyzed

Complexity

Conditions 12
Paths 17

Size

Total Lines 62
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 25
nc 17
nop 3
dl 0
loc 62
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/** @file
3
 * This file contains a full implementation of the EventHandler interface.
4
 *
5
 * The tools in this package initiate a CSS selector parsing routine and then
6
 * handle all of the callbacks.
7
 *
8
 * The implementation provided herein adheres to the CSS 3 Selector specification
9
 * with the following caveats:
10
 *
11
 *  - The negation (:not()) and containment (:has()) pseudo-classes allow *full*
12
 *    selectors and not just simple selectors.
13
 *  - There are a variety of additional pseudo-classes supported by this
14
 *    implementation that are not part of the spec. Most of the jQuery
15
 *    pseudo-classes are supported. The :x-root pseudo-class is also supported.
16
 *  - Pseudo-classes that require a User Agent to function have been disabled.
17
 *    Thus there is no :hover pseudo-class.
18
 *  - All pseudo-elements require the double-colon (::) notation. This breaks
19
 *    backward compatibility with the 2.1 spec, but it makes visible the issue
20
 *    that pseudo-elements cannot be effectively used with most of the present
21
 *    library. They return <b>stdClass objects with a text property</b> (QP > 1.3)
22
 *    instead of elements.
23
 *  - The pseudo-classes first-of-type, nth-of-type and last-of-type may or may
24
 *    not conform to the specification. The spec is unclear.
25
 *  - pseudo-class filters of the form -an+b do not function as described in the
26
 *    specification. However, they do behave the same way here as they do in
27
 *    jQuery.
28
 *  - This library DOES provide XML namespace aware tools. Selectors can use
29
 *    namespaces to increase specificity.
30
 *  - This library does nothing with the CSS 3 Selector specificity rating. Of
31
 *    course specificity is preserved (to the best of our abilities), but there
32
 *    is no calculation done.
33
 *
34
 * For detailed examples of how the code works and what selectors are supported,
35
 * see the CssEventTests file, which contains the unit tests used for
36
 * testing this implementation.
37
 *
38
 * @author  M Butcher <[email protected]>
39
 * @license MIT
40
 */
41
42
namespace QueryPath\CSS;
43
44
/**
45
 * Handler that tracks progress of a query through a DOM.
46
 *
47
 * The main idea is that we keep a copy of the tree, and then use an
48
 * array to keep track of matches. To handle a list of selectors (using
49
 * the comma separator), we have to track both the currently progressing
50
 * match and the previously matched elements.
51
 *
52
 * To use this handler:
53
 *
54
 * @code
55
 * $filter = '#id'; // Some CSS selector
56
 * $handler = new QueryPathEventHandler(DOMNode $dom);
57
 * $parser = new Parser();
58
 * $parser->parse($filter, $handler);
59
 * $matches = $handler->getMatches();
60
 * @endcode
61
 *
62
 * $matches will be an array of zero or more DOMElement objects.
63
 *
64
 * @ingroup querypath_css
65
 */
66
class QueryPathEventHandler implements EventHandler, Traverser
67
{
68
69
    protected $dom; // Always points to the top level.
70
    protected $matches; // The matches
71
    protected $alreadyMatched; // Matches found before current selector.
72
    protected $findAnyElement = true;
73
74
75
    /**
76
     * Create a new event handler.
77
     */
78
    public function __construct($dom)
79
    {
80
        $this->alreadyMatched = new \SplObjectStorage();
81
        $matches = new \SplObjectStorage();
82
83
        // Array of DOMElements
84
        if (is_array($dom) || $dom instanceof \SplObjectStorage) {
85
            //$matches = array();
86
            foreach ($dom as $item) {
87
                if ($item instanceof \DOMNode && $item->nodeType == XML_ELEMENT_NODE) {
88
                    //$matches[] = $item;
89
                    $matches->attach($item);
90
                }
91
            }
92
            //$this->dom = count($matches) > 0 ? $matches[0] : NULL;
93
            if ($matches->count() > 0) {
94
                $matches->rewind();
95
                $this->dom = $matches->current();
96
            } else {
97
                //throw new Exception("Setting DOM to Null");
98
                $this->dom = NULL;
99
            }
100
            $this->matches = $matches;
101
        } // DOM Document -- we get the root element.
102
        elseif ($dom instanceof \DOMDocument) {
103
            $this->dom = $dom->documentElement;
104
            $matches->attach($dom->documentElement);
105
        } // DOM Element -- we use this directly
106
        elseif ($dom instanceof \DOMElement) {
107
            $this->dom = $dom;
108
            $matches->attach($dom);
109
        } // NodeList -- We turn this into an array
110
        elseif ($dom instanceof \DOMNodeList) {
111
            $a = []; // Not sure why we are doing this....
112
            foreach ($dom as $item) {
113
                if ($item->nodeType == XML_ELEMENT_NODE) {
114
                    $matches->attach($item);
115
                    $a[] = $item;
116
                }
117
            }
118
            $this->dom = $a;
119
        }
120
        // FIXME: Handle SimpleXML!
121
        // Uh-oh... we don't support anything else.
122
        else {
123
            throw new \QueryPath\Exception("Unhandled type: " . get_class($dom));
124
        }
125
        $this->matches = $matches;
126
    }
127
128
    /**
129
     * Generic finding method.
130
     *
131
     * This is the primary searching method used throughout QueryPath.
132
     *
133
     * @param string $filter
134
     *  A valid CSS 3 filter.
135
     * @return QueryPathEventHandler
136
     *  Returns itself.
137
     * @throws ParseException
138
     */
139
    public function find($filter): QueryPathEventHandler
140
    {
141
        $parser = new Parser($filter, $this);
142
        $parser->parse();
143
144
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type QueryPath\CSS\QueryPathEventHandler which is incompatible with the return type mandated by QueryPath\CSS\Traverser::find() of Traverser.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
145
    }
146
147
    /**
148
     * Get the elements that match the evaluated selector.
149
     *
150
     * This should be called after the filter has been parsed.
151
     *
152
     * @return array
153
     *  The matched items. This is almost always an array of
154
     *  {@link DOMElement} objects. It is always an instance of
155
     *  {@link DOMNode} objects.
156
     */
157
    public function getMatches()
158
    {
159
        //$result = array_merge($this->alreadyMatched, $this->matches);
160
        $result = new \SplObjectStorage();
161
        foreach ($this->alreadyMatched as $m) {
162
            $result->attach($m);
163
        }
164
        foreach ($this->matches as $m) {
165
            $result->attach($m);
166
        }
167
168
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type SplObjectStorage which is incompatible with the documented return type array.
Loading history...
169
    }
170
171
    public function matches()
172
    {
173
        return $this->getMatches();
174
    }
175
176
    /**
177
     * Find any element with the ID that matches $id.
178
     *
179
     * If this finds an ID, it will immediately quit. Essentially, it doesn't
180
     * enforce ID uniqueness, but it assumes it.
181
     *
182
     * @param $id
183
     *  String ID for an element.
184
     */
185
    public function elementID($id)
186
    {
187
        $found = new \SplObjectStorage();
188
        $matches = $this->candidateList();
189
        foreach ($matches as $item) {
190
            // Check if any of the current items has the desired ID.
191
            if ($item->hasAttribute('id') && $item->getAttribute('id') === $id) {
192
                $found->attach($item);
193
                break;
194
            }
195
        }
196
        $this->matches = $found;
197
        $this->findAnyElement = false;
198
    }
199
200
    // Inherited
201
    public function element($name)
202
    {
203
        $matches = $this->candidateList();
204
        $this->findAnyElement = false;
205
        $found = new \SplObjectStorage();
206
        foreach ($matches as $item) {
207
            // Should the existing item be included?
208
            // In some cases (e.g. element is root element)
209
            // it definitely should. But what about other cases?
210
            if ($item->tagName == $name) {
211
                $found->attach($item);
212
            }
213
            // Search for matching kids.
214
            //$nl = $item->getElementsByTagName($name);
215
            //$found = array_merge($found, $this->nodeListToArray($nl));
216
        }
217
218
        $this->matches = $found;
219
    }
220
221
    // Inherited
222
    public function elementNS($lname, $namespace = NULL)
223
    {
224
        $this->findAnyElement = false;
225
        $found = new \SplObjectStorage();
226
        $matches = $this->candidateList();
227
        foreach ($matches as $item) {
228
            // Looking up NS URI only works if the XMLNS attributes are declared
229
            // at a level equal to or above the searching doc. Normalizing a doc
230
            // should fix this, but it doesn't. So we have to use a fallback
231
            // detection scheme which basically searches by lname and then
232
            // does a post hoc check on the tagname.
233
234
            //$nsuri = $item->lookupNamespaceURI($namespace);
235
            $nsuri = $this->dom->lookupNamespaceURI($namespace);
236
237
            // XXX: Presumably the base item needs to be checked. Spec isn't
238
            // too clear, but there are three possibilities:
239
            // - base should always be checked (what we do here)
240
            // - base should never be checked (only children)
241
            // - base should only be checked if it is the root node
242
            if ($item instanceof \DOMNode
243
                && $item->namespaceURI == $nsuri
244
                && $lname == $item->localName) {
245
                $found->attach($item);
246
            }
247
248
            if (!empty($nsuri)) {
249
                $nl = $item->getElementsByTagNameNS($nsuri, $lname);
250
                // If something is found, merge them:
251
                //if (!empty($nl)) $found = array_merge($found, $this->nodeListToArray($nl));
252
                if (!empty($nl)) {
253
                    $this->attachNodeList($nl, $found);
254
                }
255
            } else {
256
                //$nl = $item->getElementsByTagName($namespace . ':' . $lname);
257
                $nl = $item->getElementsByTagName($lname);
258
                $tagname = $namespace . ':' . $lname;
259
                $nsmatches = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $nsmatches is dead and can be removed.
Loading history...
260
                foreach ($nl as $node) {
261
                    if ($node->tagName == $tagname) {
262
                        //$nsmatches[] = $node;
263
                        $found->attach($node);
264
                    }
265
                }
266
                // If something is found, merge them:
267
                //if (!empty($nsmatches)) $found = array_merge($found, $nsmatches);
268
            }
269
        }
270
        $this->matches = $found;
271
    }
272
273
    public function anyElement()
274
    {
275
        $found = new \SplObjectStorage();
276
        //$this->findAnyElement = TRUE;
277
        $matches = $this->candidateList();
278
        foreach ($matches as $item) {
279
            $found->attach($item); // Add self
280
            // See issue #20 or section 6.2 of this:
281
            // http://www.w3.org/TR/2009/PR-css3-selectors-20091215/#universal-selector
282
            //$nl = $item->getElementsByTagName('*');
283
            //$this->attachNodeList($nl, $found);
284
        }
285
286
        $this->matches = $found;
287
        $this->findAnyElement = false;
288
    }
289
290
    public function anyElementInNS($ns)
291
    {
292
        //$this->findAnyElement = TRUE;
293
        $nsuri = $this->dom->lookupNamespaceURI($ns);
294
        $found = new \SplObjectStorage();
295
        if (!empty($nsuri)) {
296
            $matches = $this->candidateList();
297
            foreach ($matches as $item) {
298
                if ($item instanceOf \DOMNode && $nsuri == $item->namespaceURI) {
299
                    $found->attach($item);
300
                }
301
            }
302
        }
303
        $this->matches = $found;//UniqueElementList::get($found);
304
        $this->findAnyElement = false;
305
    }
306
307
    public function elementClass($name)
308
    {
309
310
        $found = new \SplObjectStorage();
311
        $matches = $this->candidateList();
312
        foreach ($matches as $item) {
313
            if ($item->hasAttribute('class')) {
314
                $classes = explode(' ', $item->getAttribute('class'));
315
                if (in_array($name, $classes)) {
316
                    $found->attach($item);
317
                }
318
            }
319
        }
320
321
        $this->matches = $found;//UniqueElementList::get($found);
322
        $this->findAnyElement = false;
323
    }
324
325
    public function attribute($name, $value = NULL, $operation = EventHandler::IS_EXACTLY)
326
    {
327
        $found = new \SplObjectStorage();
328
        $matches = $this->candidateList();
329
        foreach ($matches as $item) {
330
            if ($item->hasAttribute($name)) {
331
                if (isset($value)) {
332
                    // If a value exists, then we need a match.
333
                    if ($this->attrValMatches($value, $item->getAttribute($name), $operation)) {
334
                        $found->attach($item);
335
                    }
336
                } else {
337
                    // If no value exists, then we consider it a match.
338
                    $found->attach($item);
339
                }
340
            }
341
        }
342
        $this->matches = $found; //UniqueElementList::get($found);
343
        $this->findAnyElement = false;
344
    }
345
346
    /**
347
     * Helper function to find all elements with exact matches.
348
     *
349
     * @deprecated All use cases seem to be covered by attribute().
350
     */
351
    protected function searchForAttr($name, $value = NULL)
352
    {
353
        $found = new \SplObjectStorage();
354
        $matches = $this->candidateList();
355
        foreach ($matches as $candidate) {
356
            if ($candidate->hasAttribute($name)) {
357
                // If value is required, match that, too.
358
                if (isset($value) && $value == $candidate->getAttribute($name)) {
359
                    $found->attach($candidate);
360
                } // Otherwise, it's a match on name alone.
361
                else {
362
                    $found->attach($candidate);
363
                }
364
            }
365
        }
366
367
        $this->matches = $found;
368
    }
369
370
    public function attributeNS($lname, $ns, $value = NULL, $operation = EventHandler::IS_EXACTLY)
371
    {
372
        $matches = $this->candidateList();
373
        $found = new \SplObjectStorage();
374
        if (count($matches) == 0) {
375
            $this->matches = $found;
376
377
            return;
378
        }
379
380
        // Get the namespace URI for the given label.
381
        //$uri = $matches[0]->lookupNamespaceURI($ns);
382
        $matches->rewind();
383
        $e = $matches->current();
384
        $uri = $e->lookupNamespaceURI($ns);
385
386
        foreach ($matches as $item) {
387
            //foreach ($item->attributes as $attr) {
388
            //  print "$attr->prefix:$attr->localName ($attr->namespaceURI), Value: $attr->nodeValue\n";
389
            //}
390
            if ($item->hasAttributeNS($uri, $lname)) {
391
                if (isset($value)) {
392
                    if ($this->attrValMatches($value, $item->getAttributeNS($uri, $lname), $operation)) {
393
                        $found->attach($item);
394
                    }
395
                } else {
396
                    $found->attach($item);
397
                }
398
            }
399
        }
400
        $this->matches = $found;
401
        $this->findAnyElement = false;
402
    }
403
404
    /**
405
     * This also supports the following nonstandard pseudo classes:
406
     *  - :x-reset/:x-root (reset to the main item passed into the constructor. Less drastic than :root)
407
     *  - :odd/:even (shorthand for :nth-child(odd)/:nth-child(even))
408
     */
409
    public function pseudoClass($name, $value = NULL)
410
    {
411
        $name = strtolower($name);
412
        // Need to handle known pseudoclasses.
413
        switch ($name) {
414
            case 'visited':
415
            case 'hover':
416
            case 'active':
417
            case 'focus':
418
            case 'animated': //  Last 3 are from jQuery
419
            case 'visible':
420
            case 'hidden':
421
                // These require a UA, which we don't have.
422
            case 'target':
423
                // This requires a location URL, which we don't have.
424
                $this->matches = new \SplObjectStorage();
425
                break;
426
            case 'indeterminate':
427
                // The assumption is that there is a UA and the format is HTML.
428
                // I don't know if this should is useful without a UA.
429
                throw new NotImplementedException(":indeterminate is not implemented.");
430
                break;
431
            case 'lang':
432
                // No value = exception.
433
                if (!isset($value)) {
434
                    throw new NotImplementedException("No handler for lang pseudoclass without value.");
435
                }
436
                $this->lang($value);
437
                break;
438
            case 'link':
439
                $this->searchForAttr('href');
0 ignored issues
show
Deprecated Code introduced by
The function QueryPath\CSS\QueryPathE...andler::searchForAttr() has been deprecated: All use cases seem to be covered by attribute(). ( Ignorable by Annotation )

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

439
                /** @scrutinizer ignore-deprecated */ $this->searchForAttr('href');

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...
440
                break;
441
            case 'root':
442
                $found = new \SplObjectStorage();
443
                if (empty($this->dom)) {
444
                    $this->matches = $found;
445
                } elseif (is_array($this->dom)) {
446
                    $found->attach($this->dom[0]->ownerDocument->documentElement);
447
                    $this->matches = $found;
448
                } elseif ($this->dom instanceof \DOMNode) {
449
                    $found->attach($this->dom->ownerDocument->documentElement);
450
                    $this->matches = $found;
451
                } elseif ($this->dom instanceof \DOMNodeList && $this->dom->length > 0) {
452
                    $found->attach($this->dom->item(0)->ownerDocument->documentElement);
453
                    $this->matches = $found;
454
                } else {
455
                    // Hopefully we never get here:
456
                    $found->attach($this->dom);
457
                    $this->matches = $found;
458
                }
459
                break;
460
461
            // NON-STANDARD extensions for reseting to the "top" items set in
462
            // the constructor.
463
            case 'x-root':
464
            case 'x-reset':
465
                $this->matches = new \SplObjectStorage();
466
                $this->matches->attach($this->dom);
467
                break;
468
469
            // NON-STANDARD extensions for simple support of even and odd. These
470
            // are supported by jQuery, FF, and other user agents.
471
            case 'even':
472
                $this->nthChild(2, 0);
473
                break;
474
            case 'odd':
475
                $this->nthChild(2, 1);
476
                break;
477
478
            // Standard child-checking items.
479
            case 'nth-child':
480
                list($aVal, $bVal) = $this->parseAnB($value);
481
                $this->nthChild($aVal, $bVal);
482
                break;
483
            case 'nth-last-child':
484
                list($aVal, $bVal) = $this->parseAnB($value);
485
                $this->nthLastChild($aVal, $bVal);
486
                break;
487
            case 'nth-of-type':
488
                list($aVal, $bVal) = $this->parseAnB($value);
489
                $this->nthOfTypeChild($aVal, $bVal, false);
490
                break;
491
            case 'nth-last-of-type':
492
                list($aVal, $bVal) = $this->parseAnB($value);
493
                $this->nthLastOfTypeChild($aVal, $bVal);
494
                break;
495
            case 'first-child':
496
                $this->nthChild(0, 1);
497
                break;
498
            case 'last-child':
499
                $this->nthLastChild(0, 1);
500
                break;
501
            case 'first-of-type':
502
                $this->firstOfType();
503
                break;
504
            case 'last-of-type':
505
                $this->lastOfType();
506
                break;
507
            case 'only-child':
508
                $this->onlyChild();
509
                break;
510
            case 'only-of-type':
511
                $this->onlyOfType();
512
                break;
513
            case 'empty':
514
                $this->emptyElement();
515
                break;
516
            case 'not':
517
                if (empty($value)) {
518
                    throw new ParseException(":not() requires a value.");
519
                }
520
                $this->not($value);
521
                break;
522
            // Additional pseudo-classes defined in jQuery:
523
            case 'lt':
524
            case 'gt':
525
            case 'nth':
526
            case 'eq':
527
            case 'first':
528
            case 'last':
529
                //case 'even':
530
                //case 'odd':
531
                $this->getByPosition($name, $value);
532
                break;
533
            case 'parent':
534
                $matches = $this->candidateList();
535
                $found = new \SplObjectStorage();
536
                foreach ($matches as $match) {
537
                    if (!empty($match->firstChild)) {
538
                        $found->attach($match);
539
                    }
540
                }
541
                $this->matches = $found;
542
                break;
543
544
            case 'enabled':
545
            case 'disabled':
546
            case 'checked':
547
                $this->attribute($name);
548
                break;
549
            case 'text':
550
            case 'radio':
551
            case 'checkbox':
552
            case 'file':
553
            case 'password':
554
            case 'submit':
555
            case 'image':
556
            case 'reset':
557
            case 'button':
558
                $this->attribute('type', $name);
559
                break;
560
561
            case 'header':
562
                $matches = $this->candidateList();
563
                $found = new \SplObjectStorage();
564
                foreach ($matches as $item) {
565
                    $tag = $item->tagName;
566
                    $f = strtolower(substr($tag, 0, 1));
567
                    if ($f == 'h' && strlen($tag) == 2 && ctype_digit(substr($tag, 1, 1))) {
568
                        $found->attach($item);
569
                    }
570
                }
571
                $this->matches = $found;
572
                break;
573
            case 'has':
574
                $this->has($value);
575
                break;
576
            // Contains == text matches.
577
            // In QP 2.1, this was changed.
578
            case 'contains':
579
                $value = $this->removeQuotes($value);
580
581
                $matches = $this->candidateList();
582
                $found = new \SplObjectStorage();
583
                foreach ($matches as $item) {
584
                    if (strpos($item->textContent, $value) !== false) {
585
                        $found->attach($item);
586
                    }
587
                }
588
                $this->matches = $found;
589
                break;
590
591
            // Since QP 2.1
592
            case 'contains-exactly':
593
                $value = $this->removeQuotes($value);
594
595
                $matches = $this->candidateList();
596
                $found = new \SplObjectStorage();
597
                foreach ($matches as $item) {
598
                    if ($item->textContent == $value) {
599
                        $found->attach($item);
600
                    }
601
                }
602
                $this->matches = $found;
603
                break;
604
            default:
605
                throw new ParseException("Unknown Pseudo-Class: " . $name);
606
        }
607
        $this->findAnyElement = false;
608
    }
609
610
    /**
611
     * Remove leading and trailing quotes.
612
     */
613
    private function removeQuotes($str)
614
    {
615
        $f = substr($str, 0, 1);
616
        $l = substr($str, -1);
617
        if ($f === $l && ($f == '"' || $f == "'")) {
618
            $str = substr($str, 1, -1);
619
        }
620
621
        return $str;
622
    }
623
624
    /**
625
     * Pseudo-class handler for a variety of jQuery pseudo-classes.
626
     * Handles lt, gt, eq, nth, first, last pseudo-classes.
627
     */
628
    private function getByPosition($operator, $pos)
629
    {
630
        $matches = $this->candidateList();
631
        $found = new \SplObjectStorage();
632
        if ($matches->count() == 0) {
633
            return;
634
        }
635
636
        switch ($operator) {
637
            case 'nth':
638
            case 'eq':
639
                if ($matches->count() >= $pos) {
640
                    //$found[] = $matches[$pos -1];
641
                    foreach ($matches as $match) {
642
                        // CSS is 1-based, so we pre-increment.
643
                        if ($matches->key() + 1 == $pos) {
644
                            $found->attach($match);
645
                            break;
646
                        }
647
                    }
648
                }
649
                break;
650
            case 'first':
651
                if ($matches->count() > 0) {
652
                    $matches->rewind(); // This is necessary to init.
653
                    $found->attach($matches->current());
654
                }
655
                break;
656
            case 'last':
657
                if ($matches->count() > 0) {
658
659
                    // Spin through iterator.
660
                    foreach ($matches as $item) {
661
                    };
662
663
                    $found->attach($item);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $item seems to be defined by a foreach iteration on line 660. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
664
                }
665
                break;
666
            // case 'even':
667
            //         for ($i = 1; $i <= count($matches); ++$i) {
668
            //           if ($i % 2 == 0) {
669
            //             $found[] = $matches[$i];
670
            //           }
671
            //         }
672
            //         break;
673
            //       case 'odd':
674
            //         for ($i = 1; $i <= count($matches); ++$i) {
675
            //           if ($i % 2 == 0) {
676
            //             $found[] = $matches[$i];
677
            //           }
678
            //         }
679
            //         break;
680
            case 'lt':
681
                $i = 0;
682
                foreach ($matches as $item) {
683
                    if (++$i < $pos) {
684
                        $found->attach($item);
685
                    }
686
                }
687
                break;
688
            case 'gt':
689
                $i = 0;
690
                foreach ($matches as $item) {
691
                    if (++$i > $pos) {
692
                        $found->attach($item);
693
                    }
694
                }
695
                break;
696
        }
697
698
        $this->matches = $found;
699
    }
700
701
    /**
702
     * Parse an an+b rule for CSS pseudo-classes.
703
     *
704
     * @param $rule
705
     *  Some rule in the an+b format.
706
     * @return
707
     *  Array (list($aVal, $bVal)) of the two values.
708
     * @throws ParseException
709
     *  If the rule does not follow conventions.
710
     */
711
    protected function parseAnB($rule)
712
    {
713
        if ($rule == 'even') {
714
            return [2, 0];
715
        } elseif ($rule == 'odd') {
716
            return [2, 1];
717
        } elseif ($rule == 'n') {
718
            return [1, 0];
719
        } elseif (is_numeric($rule)) {
720
            return [0, (int)$rule];
721
        }
722
723
        $rule = explode('n', $rule);
724
        if (count($rule) == 0) {
725
            throw new ParseException("nth-child value is invalid.");
726
        }
727
728
        // Each of these is legal: 1, -1, and -. '-' is shorthand for -1.
729
        $aVal = trim($rule[0]);
730
        $aVal = ($aVal == '-') ? -1 : (int)$aVal;
731
732
        $bVal = !empty($rule[1]) ? (int)trim($rule[1]) : 0;
733
734
        return [$aVal, $bVal];
735
    }
736
737
    /**
738
     * Pseudo-class handler for nth-child and all related pseudo-classes.
739
     *
740
     * @param int $groupSize
741
     *  The size of the group (in an+b, this is a).
742
     * @param int $elementInGroup
743
     *  The offset in a group. (in an+b this is b).
744
     * @param boolean $lastChild
745
     *  Whether counting should begin with the last child. By default, this is false.
746
     *  Pseudo-classes that start with the last-child can set this to true.
747
     */
748
    protected function nthChild($groupSize, $elementInGroup, $lastChild = false)
749
    {
750
        // EXPERIMENTAL: New in Quark. This should be substantially faster
751
        // than the old (jQuery-ish) version. It still has E_STRICT violations
752
        // though.
753
        $parents = new \SplObjectStorage();
754
        $matches = new \SplObjectStorage();
755
756
        $i = 0;
757
        foreach ($this->matches as $item) {
758
            $parent = $item->parentNode;
759
760
            // Build up an array of all of children of this parent, and store the
761
            // index of each element for reference later. We only need to do this
762
            // once per parent, though.
763
            if (!$parents->contains($parent)) {
764
765
                $c = 0;
766
                foreach ($parent->childNodes as $child) {
767
                    // We only want nodes, and if this call is preceded by an element
768
                    // selector, we only want to match elements with the same tag name.
769
                    // !!! This last part is a grey area in the CSS 3 Selector spec. It seems
770
                    // necessary to make the implementation match the examples in the spec. However,
771
                    // jQuery 1.2 does not do this.
772
                    if ($child->nodeType == XML_ELEMENT_NODE && ($this->findAnyElement || $child->tagName == $item->tagName)) {
773
                        // This may break E_STRICT.
774
                        $child->nodeIndex = ++$c;
775
                    }
776
                }
777
                // This may break E_STRICT.
778
                $parent->numElements = $c;
779
                $parents->attach($parent);
780
            }
781
782
            // If we are looking for the last child, we count from the end of a list.
783
            // Note that we add 1 because CSS indices begin at 1, not 0.
784
            if ($lastChild) {
785
                $indexToMatch = $item->parentNode->numElements - $item->nodeIndex + 1;
786
            } // Otherwise we count from the beginning of the list.
787
            else {
788
                $indexToMatch = $item->nodeIndex;
789
            }
790
791
            // If group size is 0, then we return element at the right index.
792
            if ($groupSize == 0) {
793
                if ($indexToMatch == $elementInGroup) {
794
                    $matches->attach($item);
795
                }
796
            }
797
            // If group size != 0, then we grab nth element from group offset by
798
            // element in group.
799
            else {
800
                if (($indexToMatch - $elementInGroup) % $groupSize == 0
801
                    && ($indexToMatch - $elementInGroup) / $groupSize >= 0) {
802
                    $matches->attach($item);
803
                }
804
            }
805
806
            // Iterate.
807
            ++$i;
808
        }
809
        $this->matches = $matches;
810
    }
811
812
    /**
813
     * Reverse a set of matches.
814
     *
815
     * This is now necessary because internal matches are no longer represented
816
     * as arrays.
817
     *
818
     * @since QueryPath 2.0
819
     *//*
820
  private function reverseMatches() {
821
    // Reverse the candidate list. There must be a better way of doing
822
    // this.
823
    $arr = array();
824
    foreach ($this->matches as $m) array_unshift($arr, $m);
825
826
    $this->found = new \SplObjectStorage();
827
    foreach ($arr as $item) $this->found->attach($item);
828
  }*/
829
830
    /**
831
     * Pseudo-class handler for :nth-last-child and related pseudo-classes.
832
     */
833
    protected function nthLastChild($groupSize, $elementInGroup)
834
    {
835
        // New in Quark.
836
        $this->nthChild($groupSize, $elementInGroup, true);
837
    }
838
839
    /**
840
     * Get a list of peer elements.
841
     * If $requireSameTag is TRUE, then only peer elements with the same
842
     * tagname as the given element will be returned.
843
     *
844
     * @param $element
845
     *  A DomElement.
846
     * @param $requireSameTag
847
     *  Boolean flag indicating whether all matches should have the same
848
     *  element name (tagName) as $element.
849
     * @return
850
     *  Array of peer elements.
851
     *//*
852
  protected function listPeerElements($element, $requireSameTag = FALSE) {
853
    $peers = array();
854
    $parent = $element->parentNode;
855
    foreach ($parent->childNodes as $node) {
856
      if ($node->nodeType == XML_ELEMENT_NODE) {
857
        if ($requireSameTag) {
858
          // Need to make sure that the tag matches:
859
          if ($element->tagName == $node->tagName) {
860
            $peers[] = $node;
861
          }
862
        }
863
        else {
864
          $peers[] = $node;
865
        }
866
      }
867
    }
868
    return $peers;
869
  }
870
  */
871
    /**
872
     * Get the nth child (by index) from matching candidates.
873
     *
874
     * This is used by pseudo-class handlers.
875
     */
876
    /*
877
   protected function childAtIndex($index, $tagName = NULL) {
878
     $restrictToElement = !$this->findAnyElement;
879
     $matches = $this->candidateList();
880
     $defaultTagName = $tagName;
881
882
     // XXX: Added in Quark: I believe this should return an empty
883
     // match set if no child was found tat the index.
884
     $this->matches = new \SplObjectStorage();
885
886
     foreach ($matches as $item) {
887
       $parent = $item->parentNode;
888
889
       // If a default tag name is supplied, we always use it.
890
       if (!empty($defaultTagName)) {
891
         $tagName = $defaultTagName;
892
       }
893
       // If we are inside of an element selector, we use the
894
       // tag name of the given elements.
895
       elseif ($restrictToElement) {
896
         $tagName = $item->tagName;
897
       }
898
       // Otherwise, we skip the tag name match.
899
       else {
900
         $tagName = NULL;
901
       }
902
903
       // Loop through all children looking for matches.
904
       $i = 0;
905
       foreach ($parent->childNodes as $child) {
906
         if ($child->nodeType !== XML_ELEMENT_NODE) {
907
           break; // Skip non-elements
908
         }
909
910
         // If type is set, then we do type comparison
911
         if (!empty($tagName)) {
912
           // Check whether tag name matches the type.
913
           if ($child->tagName == $tagName) {
914
             // See if this is the index we are looking for.
915
             if ($i == $index) {
916
               //$this->matches = new \SplObjectStorage();
917
               $this->matches->attach($child);
918
               return;
919
             }
920
             // If it's not the one we are looking for, increment.
921
             ++$i;
922
           }
923
         }
924
         // We don't care about type. Any tagName will match.
925
         else {
926
           if ($i == $index) {
927
             $this->matches->attach($child);
928
             return;
929
           }
930
           ++$i;
931
         }
932
       } // End foreach
933
     }
934
935
   }*/
936
937
    /**
938
     * Pseudo-class handler for nth-of-type-child.
939
     * Not implemented.
940
     */
941
    protected function nthOfTypeChild($groupSize, $elementInGroup, $lastChild)
942
    {
943
        // EXPERIMENTAL: New in Quark. This should be substantially faster
944
        // than the old (jQuery-ish) version. It still has E_STRICT violations
945
        // though.
946
        $parents = new \SplObjectStorage();
947
        $matches = new \SplObjectStorage();
948
949
        $i = 0;
950
        foreach ($this->matches as $item) {
951
            $parent = $item->parentNode;
952
953
            // Build up an array of all of children of this parent, and store the
954
            // index of each element for reference later. We only need to do this
955
            // once per parent, though.
956
            if (!$parents->contains($parent)) {
957
958
                $c = 0;
959
                foreach ($parent->childNodes as $child) {
960
                    // This doesn't totally make sense, since the CSS 3 spec does not require that
961
                    // this pseudo-class be adjoined to an element (e.g. ' :nth-of-type' is allowed).
962
                    if ($child->nodeType == XML_ELEMENT_NODE && $child->tagName == $item->tagName) {
963
                        // This may break E_STRICT.
964
                        $child->nodeIndex = ++$c;
965
                    }
966
                }
967
                // This may break E_STRICT.
968
                $parent->numElements = $c;
969
                $parents->attach($parent);
970
            }
971
972
            // If we are looking for the last child, we count from the end of a list.
973
            // Note that we add 1 because CSS indices begin at 1, not 0.
974
            if ($lastChild) {
975
                $indexToMatch = $item->parentNode->numElements - $item->nodeIndex + 1;
976
            } // Otherwise we count from the beginning of the list.
977
            else {
978
                $indexToMatch = $item->nodeIndex;
979
            }
980
981
            // If group size is 0, then we return element at the right index.
982
            if ($groupSize == 0) {
983
                if ($indexToMatch == $elementInGroup) {
984
                    $matches->attach($item);
985
                }
986
            }
987
            // If group size != 0, then we grab nth element from group offset by
988
            // element in group.
989
            else {
990
                if (($indexToMatch - $elementInGroup) % $groupSize == 0
991
                    && ($indexToMatch - $elementInGroup) / $groupSize >= 0) {
992
                    $matches->attach($item);
993
                }
994
            }
995
996
            // Iterate.
997
            ++$i;
998
        }
999
        $this->matches = $matches;
1000
    }
1001
1002
    /**
1003
     * Pseudo-class handler for nth-last-of-type-child.
1004
     * Not implemented.
1005
     */
1006
    protected function nthLastOfTypeChild($groupSize, $elementInGroup)
1007
    {
1008
        $this->nthOfTypeChild($groupSize, $elementInGroup, true);
1009
    }
1010
1011
    /**
1012
     * Pseudo-class handler for :lang
1013
     */
1014
    protected function lang($value)
1015
    {
1016
        // TODO: This checks for cases where an explicit language is
1017
        // set. The spec seems to indicate that an element should inherit
1018
        // language from the parent... but this is unclear.
1019
        $operator = (strpos($value, '-') !== false) ? self::IS_EXACTLY : self::CONTAINS_WITH_HYPHEN;
1020
1021
        $orig = $this->matches;
1022
        $origDepth = $this->findAnyElement;
1023
1024
        // Do first pass: attributes in default namespace
1025
        $this->attribute('lang', $value, $operator);
1026
        $lang = $this->matches; // Temp array for merging.
1027
1028
        // Reset
1029
        $this->matches = $orig;
1030
        $this->findAnyElement = $origDepth;
1031
1032
        // Do second pass: attributes in 'xml' namespace.
1033
        $this->attributeNS('lang', 'xml', $value, $operator);
1034
1035
1036
        // Merge results.
1037
        // FIXME: Note that we lose natural ordering in
1038
        // the document because we search for xml:lang separately
1039
        // from lang.
1040
        foreach ($this->matches as $added) {
1041
            $lang->attach($added);
1042
        }
1043
        $this->matches = $lang;
1044
    }
1045
1046
    /**
1047
     * Pseudo-class handler for :not(filter).
1048
     *
1049
     * This does not follow the specification in the following way: The CSS 3
1050
     * selector spec says the value of not() must be a simple selector. This
1051
     * function allows complex selectors.
1052
     *
1053
     * @param string $filter
1054
     *  A CSS selector.
1055
     */
1056
    protected function not($filter)
1057
    {
1058
        $matches = $this->candidateList();
1059
        //$found = array();
1060
        $found = new \SplObjectStorage();
1061
        foreach ($matches as $item) {
1062
            $handler = new QueryPathEventHandler($item);
1063
            $not_these = $handler->find($filter)->getMatches();
1064
            if ($not_these->count() == 0) {
1065
                $found->attach($item);
1066
            }
1067
        }
1068
        // No need to check for unique elements, since the list
1069
        // we began from already had no duplicates.
1070
        $this->matches = $found;
1071
    }
1072
1073
    /**
1074
     * Pseudo-class handler for :has(filter).
1075
     * This can also be used as a general filtering routine.
1076
     */
1077
    public function has($filter)
1078
    {
1079
        $matches = $this->candidateList();
1080
        //$found = array();
1081
        $found = new \SplObjectStorage();
1082
        foreach ($matches as $item) {
1083
            $handler = new QueryPathEventHandler($item);
1084
            $these = $handler->find($filter)->getMatches();
1085
            if (count($these) > 0) {
1086
                $found->attach($item);
1087
            }
1088
        }
1089
        $this->matches = $found;
1090
1091
        return $this;
1092
    }
1093
1094
    /**
1095
     * Pseudo-class handler for :first-of-type.
1096
     */
1097
    protected function firstOfType()
1098
    {
1099
        $matches = $this->candidateList();
1100
        $found = new \SplObjectStorage();
1101
        foreach ($matches as $item) {
1102
            $type = $item->tagName;
1103
            $parent = $item->parentNode;
1104
            foreach ($parent->childNodes as $kid) {
1105
                if ($kid->nodeType == XML_ELEMENT_NODE && $kid->tagName == $type) {
1106
                    if (!$found->contains($kid)) {
1107
                        $found->attach($kid);
1108
                    }
1109
                    break;
1110
                }
1111
            }
1112
        }
1113
        $this->matches = $found;
1114
    }
1115
1116
    /**
1117
     * Pseudo-class handler for :last-of-type.
1118
     */
1119
    protected function lastOfType()
1120
    {
1121
        $matches = $this->candidateList();
1122
        $found = new \SplObjectStorage();
1123
        foreach ($matches as $item) {
1124
            $type = $item->tagName;
1125
            $parent = $item->parentNode;
1126
            for ($i = $parent->childNodes->length - 1; $i >= 0; --$i) {
1127
                $kid = $parent->childNodes->item($i);
1128
                if ($kid->nodeType == XML_ELEMENT_NODE && $kid->tagName == $type) {
1129
                    if (!$found->contains($kid)) {
1130
                        $found->attach($kid);
1131
                    }
1132
                    break;
1133
                }
1134
            }
1135
        }
1136
        $this->matches = $found;
1137
    }
1138
1139
    /**
1140
     * Pseudo-class handler for :only-child.
1141
     */
1142
    protected function onlyChild()
1143
    {
1144
        $matches = $this->candidateList();
1145
        $found = new \SplObjectStorage();
1146
        foreach ($matches as $item) {
1147
            $parent = $item->parentNode;
1148
            $kids = [];
1149
            foreach ($parent->childNodes as $kid) {
1150
                if ($kid->nodeType == XML_ELEMENT_NODE) {
1151
                    $kids[] = $kid;
1152
                }
1153
            }
1154
            // There should be only one child element, and
1155
            // it should be the one being tested.
1156
            if (count($kids) == 1 && $kids[0] === $item) {
1157
                $found->attach($kids[0]);
1158
            }
1159
        }
1160
        $this->matches = $found;
1161
    }
1162
1163
    /**
1164
     * Pseudo-class handler for :empty.
1165
     */
1166
    protected function emptyElement()
1167
    {
1168
        $found = new \SplObjectStorage();
1169
        $matches = $this->candidateList();
1170
        foreach ($matches as $item) {
1171
            $empty = true;
1172
            foreach ($item->childNodes as $kid) {
1173
                // From the spec: Elements and Text nodes are the only ones to
1174
                // affect emptiness.
1175
                if ($kid->nodeType == XML_ELEMENT_NODE || $kid->nodeType == XML_TEXT_NODE) {
1176
                    $empty = false;
1177
                    break;
1178
                }
1179
            }
1180
            if ($empty) {
1181
                $found->attach($item);
1182
            }
1183
        }
1184
        $this->matches = $found;
1185
    }
1186
1187
    /**
1188
     * Pseudo-class handler for :only-of-type.
1189
     */
1190
    protected function onlyOfType()
1191
    {
1192
        $matches = $this->candidateList();
1193
        $found = new \SplObjectStorage();
1194
        foreach ($matches as $item) {
1195
            if (!$item->parentNode) {
1196
                $this->matches = new \SplObjectStorage();
1197
            }
1198
            $parent = $item->parentNode;
1199
            $onlyOfType = true;
1200
1201
            // See if any peers are of the same type
1202
            foreach ($parent->childNodes as $kid) {
1203
                if ($kid->nodeType == XML_ELEMENT_NODE
1204
                    && $kid->tagName == $item->tagName
1205
                    && $kid !== $item) {
1206
                    //$this->matches = new \SplObjectStorage();
1207
                    $onlyOfType = false;
1208
                    break;
1209
                }
1210
            }
1211
1212
            // If no others were found, attach this one.
1213
            if ($onlyOfType) {
1214
                $found->attach($item);
1215
            }
1216
        }
1217
        $this->matches = $found;
1218
    }
1219
1220
    /**
1221
     * Check for attr value matches based on an operation.
1222
     */
1223
    protected function attrValMatches($needle, $haystack, $operation)
1224
    {
1225
1226
        if (strlen($haystack) < strlen($needle)) {
1227
            return false;
1228
        }
1229
1230
        // According to the spec:
1231
        // "The case-sensitivity of attribute names in selectors depends on the document language."
1232
        // (6.3.2)
1233
        // To which I say, "huh?". We assume case sensitivity.
1234
        switch ($operation) {
1235
            case EventHandler::IS_EXACTLY:
1236
                return $needle == $haystack;
1237
            case EventHandler::CONTAINS_WITH_SPACE:
1238
                return in_array($needle, explode(' ', $haystack));
1239
            case EventHandler::CONTAINS_WITH_HYPHEN:
1240
                return in_array($needle, explode('-', $haystack));
1241
            case EventHandler::CONTAINS_IN_STRING:
1242
                return strpos($haystack, $needle) !== false;
1243
            case EventHandler::BEGINS_WITH:
1244
                return strpos($haystack, $needle) === 0;
1245
            case EventHandler::ENDS_WITH:
1246
                //return strrpos($haystack, $needle) === strlen($needle) - 1;
1247
                return preg_match('/' . $needle . '$/', $haystack) == 1;
1248
        }
1249
1250
        return false; // Shouldn't be able to get here.
1251
    }
1252
1253
    /**
1254
     * As the spec mentions, these must be at the end of a selector or
1255
     * else they will cause errors. Most selectors return elements. Pseudo-elements
1256
     * do not.
1257
     */
1258
    public function pseudoElement($name)
1259
    {
1260
        // process the pseudoElement
1261
        switch ($name) {
1262
            // XXX: Should this return an array -- first line of
1263
            // each of the matched elements?
1264
            case 'first-line':
1265
                $matches = $this->candidateList();
1266
                $found = new \SplObjectStorage();
1267
                $o = new \stdClass();
1268
                foreach ($matches as $item) {
1269
                    $str = $item->textContent;
1270
                    $lines = explode("\n", $str);
1271
                    if (!empty($lines)) {
1272
                        $line = trim($lines[0]);
1273
                        if (!empty($line)) {
1274
                            $o->textContent = $line;
1275
                            $found->attach($o);//trim($lines[0]);
1276
                        }
1277
                    }
1278
                }
1279
                $this->matches = $found;
1280
                break;
1281
            // XXX: Should this return an array -- first letter of each
1282
            // of the matched elements?
1283
            case 'first-letter':
1284
                $matches = $this->candidateList();
1285
                $found = new \SplObjectStorage();
1286
                $o = new \stdClass();
1287
                foreach ($matches as $item) {
1288
                    $str = $item->textContent;
1289
                    if (!empty($str)) {
1290
                        $str = substr($str, 0, 1);
1291
                        $o->textContent = $str;
1292
                        $found->attach($o);
1293
                    }
1294
                }
1295
                $this->matches = $found;
1296
                break;
1297
            case 'before':
1298
            case 'after':
1299
                // There is nothing in a DOM to return for the before and after
1300
                // selectors.
1301
            case 'selection':
1302
                // With no user agent, we don't have a concept of user selection.
1303
                throw new NotImplementedException("The $name pseudo-element is not implemented.");
1304
                break;
1305
        }
1306
        $this->findAnyElement = false;
1307
    }
1308
1309
    public function directDescendant()
1310
    {
1311
        $this->findAnyElement = false;
1312
1313
        $kids = new \SplObjectStorage();
1314
        foreach ($this->matches as $item) {
1315
            $kidsNL = $item->childNodes;
1316
            foreach ($kidsNL as $kidNode) {
1317
                if ($kidNode->nodeType == XML_ELEMENT_NODE) {
1318
                    $kids->attach($kidNode);
1319
                }
1320
            }
1321
        }
1322
        $this->matches = $kids;
1323
    }
1324
1325
    /**
1326
     * For an element to be adjacent to another, it must be THE NEXT NODE
1327
     * in the node list. So if an element is surrounded by pcdata, there are
1328
     * no adjacent nodes. E.g. in <a/>FOO<b/>, the a and b elements are not
1329
     * adjacent.
1330
     *
1331
     * In a strict DOM parser, line breaks and empty spaces are nodes. That means
1332
     * nodes like this will not be adjacent: <test/> <test/>. The space between
1333
     * them makes them non-adjacent. If this is not the desired behavior, pass
1334
     * in the appropriate flags to your parser. Example:
1335
     * <code>
1336
     * $doc = new DomDocument();
1337
     * $doc->loadXML('<test/> <test/>', LIBXML_NOBLANKS);
1338
     * </code>
1339
     */
1340
    public function adjacent()
1341
    {
1342
        $this->findAnyElement = false;
1343
        // List of nodes that are immediately adjacent to the current one.
1344
        //$found = array();
1345
        $found = new \SplObjectStorage();
1346
        foreach ($this->matches as $item) {
1347
            while (isset($item->nextSibling)) {
1348
                if (isset($item->nextSibling) && $item->nextSibling->nodeType === XML_ELEMENT_NODE) {
1349
                    $found->attach($item->nextSibling);
1350
                    break;
1351
                }
1352
                $item = $item->nextSibling;
1353
            }
1354
        }
1355
        $this->matches = $found;
1356
    }
1357
1358
    public function anotherSelector()
1359
    {
1360
        $this->findAnyElement = false;
1361
        // Copy old matches into buffer.
1362
        if ($this->matches->count() > 0) {
1363
            //$this->alreadyMatched = array_merge($this->alreadyMatched, $this->matches);
1364
            foreach ($this->matches as $item) {
1365
                $this->alreadyMatched->attach($item);
1366
            }
1367
        }
1368
1369
        // Start over at the top of the tree.
1370
        $this->findAnyElement = true; // Reset depth flag.
1371
        $this->matches = new \SplObjectStorage();
1372
        $this->matches->attach($this->dom);
1373
    }
1374
1375
    /**
1376
     * Get all nodes that are siblings to currently selected nodes.
1377
     *
1378
     * If two passed in items are siblings of each other, neither will
1379
     * be included in the list of siblings. Their status as being candidates
1380
     * excludes them from being considered siblings.
1381
     */
1382
    public function sibling()
1383
    {
1384
        $this->findAnyElement = false;
1385
        // Get the nodes at the same level.
1386
1387
        if ($this->matches->count() > 0) {
1388
            $sibs = new \SplObjectStorage();
1389
            foreach ($this->matches as $item) {
1390
                /*$candidates = $item->parentNode->childNodes;
1391
                foreach ($candidates as $candidate) {
1392
                  if ($candidate->nodeType === XML_ELEMENT_NODE && $candidate !== $item) {
1393
                    $sibs->attach($candidate);
1394
                  }
1395
                }
1396
                */
1397
                while ($item->nextSibling != NULL) {
1398
                    $item = $item->nextSibling;
1399
                    if ($item->nodeType === XML_ELEMENT_NODE) {
1400
                        $sibs->attach($item);
1401
                    }
1402
                }
1403
            }
1404
            $this->matches = $sibs;
1405
        }
1406
    }
1407
1408
    /**
1409
     * Get any descendant.
1410
     */
1411
    public function anyDescendant()
1412
    {
1413
        // Get children:
1414
        $found = new \SplObjectStorage();
1415
        foreach ($this->matches as $item) {
1416
            $kids = $item->getElementsByTagName('*');
1417
            //$found = array_merge($found, $this->nodeListToArray($kids));
1418
            $this->attachNodeList($kids, $found);
1419
        }
1420
        $this->matches = $found;
1421
1422
        // Set depth flag:
1423
        $this->findAnyElement = true;
1424
    }
1425
1426
    /**
1427
     * Determine what candidates are in the current scope.
1428
     *
1429
     * This is a utility method that gets the list of elements
1430
     * that should be evaluated in the context. If $this->findAnyElement
1431
     * is TRUE, this will return a list of every element that appears in
1432
     * the subtree of $this->matches. Otherwise, it will just return
1433
     * $this->matches.
1434
     */
1435
    private function candidateList()
1436
    {
1437
        if ($this->findAnyElement) {
1438
            return $this->getAllCandidates($this->matches);
1439
        }
1440
1441
        return $this->matches;
1442
    }
1443
1444
    /**
1445
     * Get a list of all of the candidate elements.
1446
     *
1447
     * This is used when $this->findAnyElement is TRUE.
1448
     *
1449
     * @param $elements
1450
     *  A list of current elements (usually $this->matches).
1451
     *
1452
     * @return
1453
     *  A list of all candidate elements.
1454
     */
1455
    private function getAllCandidates($elements)
1456
    {
1457
        $found = new \SplObjectStorage();
1458
        foreach ($elements as $item) {
1459
            $found->attach($item); // put self in
1460
            $nl = $item->getElementsByTagName('*');
1461
            //foreach ($nl as $node) $found[] = $node;
1462
            $this->attachNodeList($nl, $found);
1463
        }
1464
1465
        return $found;
1466
    }
1467
    /*
1468
    public function nodeListToArray($nodeList) {
1469
      $array = array();
1470
      foreach ($nodeList as $node) {
1471
        if ($node->nodeType == XML_ELEMENT_NODE) {
1472
          $array[] = $node;
1473
        }
1474
      }
1475
      return $array;
1476
    }
1477
    */
1478
1479
    /**
1480
     * Attach all nodes in a node list to the given \SplObjectStorage.
1481
     *
1482
     * @param \DOMNodeList $nodeList
1483
     * @param \SplObjectStorage $splos
1484
     */
1485
    public function attachNodeList(\DOMNodeList $nodeList, \SplObjectStorage $splos)
1486
    {
1487
        foreach ($nodeList as $item) {
1488
            $splos->attach($item);
1489
        }
1490
    }
1491
1492
}
1493