Passed
Pull Request — master (#12)
by Jake
02:34
created
src/CSS/Traverser.php 1 patch
Indentation   +26 added lines, -26 removed lines patch added patch discarded remove patch
@@ -11,31 +11,31 @@
 block discarded – undo
11 11
  */
12 12
 interface Traverser
13 13
 {
14
-	/**
15
-	 * Process a CSS selector and find matches.
16
-	 *
17
-	 * This specifies a query to be run by the Traverser. A given
18
-	 * Traverser may, in practice, delay the finding until some later time
19
-	 * but must return the found results when getMatches() is called.
20
-	 *
21
-	 * @param string $selector
22
-	 *   A selector. Typically this is a CSS 3 Selector.
23
-	 *
24
-	 * @return \Traverser
25
-	 *  The Traverser that can return matches.
26
-	 */
27
-	public function find($selector);
14
+    /**
15
+     * Process a CSS selector and find matches.
16
+     *
17
+     * This specifies a query to be run by the Traverser. A given
18
+     * Traverser may, in practice, delay the finding until some later time
19
+     * but must return the found results when getMatches() is called.
20
+     *
21
+     * @param string $selector
22
+     *   A selector. Typically this is a CSS 3 Selector.
23
+     *
24
+     * @return \Traverser
25
+     *  The Traverser that can return matches.
26
+     */
27
+    public function find($selector);
28 28
 
29
-	/**
30
-	 * Get the results of a find() operation.
31
-	 *
32
-	 * Return an array of matching items.
33
-	 *
34
-	 * @return array
35
-	 *   An array of matched values. The specific data type in the matches
36
-	 *   will differ depending on the data type searched, but in the core
37
-	 *   QueryPath implementation, this will be an array of DOMNode
38
-	 *   objects.
39
-	 */
40
-	public function matches();
29
+    /**
30
+     * Get the results of a find() operation.
31
+     *
32
+     * Return an array of matching items.
33
+     *
34
+     * @return array
35
+     *   An array of matched values. The specific data type in the matches
36
+     *   will differ depending on the data type searched, but in the core
37
+     *   QueryPath implementation, this will be an array of DOMNode
38
+     *   objects.
39
+     */
40
+    public function matches();
41 41
 }
Please login to merge, or discard this patch.
src/CSS/Token.php 1 patch
Indentation   +64 added lines, -64 removed lines patch added patch discarded remove patch
@@ -14,74 +14,74 @@
 block discarded – undo
14 14
  */
15 15
 final class Token
16 16
 {
17
-	public const CHAR = 0x0;
18
-	public const STAR = 0x1;
19
-	public const RANGLE = 0x2;
20
-	public const DOT = 0x3;
21
-	public const OCTO = 0x4;
22
-	public const RSQUARE = 0x5;
23
-	public const LSQUARE = 0x6;
24
-	public const COLON = 0x7;
25
-	public const RPAREN = 0x8;
26
-	public const LPAREN = 0x9;
27
-	public const PLUS = 0xA;
28
-	public const TILDE = 0xB;
29
-	public const EQ = 0xC;
30
-	public const PIPE = 0xD;
31
-	public const COMMA = 0xE;
32
-	public const WHITE = 0xF;
33
-	public const QUOTE = 0x10;
34
-	public const SQUOTE = 0x11;
35
-	public const BSLASH = 0x12;
36
-	public const CARAT = 0x13;
37
-	public const DOLLAR = 0x14;
38
-	public const AT = 0x15; // This is not in the spec. Apparently, old broken CSS uses it.
17
+    public const CHAR = 0x0;
18
+    public const STAR = 0x1;
19
+    public const RANGLE = 0x2;
20
+    public const DOT = 0x3;
21
+    public const OCTO = 0x4;
22
+    public const RSQUARE = 0x5;
23
+    public const LSQUARE = 0x6;
24
+    public const COLON = 0x7;
25
+    public const RPAREN = 0x8;
26
+    public const LPAREN = 0x9;
27
+    public const PLUS = 0xA;
28
+    public const TILDE = 0xB;
29
+    public const EQ = 0xC;
30
+    public const PIPE = 0xD;
31
+    public const COMMA = 0xE;
32
+    public const WHITE = 0xF;
33
+    public const QUOTE = 0x10;
34
+    public const SQUOTE = 0x11;
35
+    public const BSLASH = 0x12;
36
+    public const CARAT = 0x13;
37
+    public const DOLLAR = 0x14;
38
+    public const AT = 0x15; // This is not in the spec. Apparently, old broken CSS uses it.
39 39
 
40
-	// In legal range for string.
41
-	public const STRING_LEGAL = 0x63;
40
+    // In legal range for string.
41
+    public const STRING_LEGAL = 0x63;
42 42
 
43
-	/**
44
-	 * Get a name for a given constant. Used for error handling.
45
-	 */
46
-	public static function name($const_int)
47
-	{
48
-		$a = [
49
-			'character',
50
-			'star',
51
-			'right angle bracket',
52
-			'dot',
53
-			'octothorp',
54
-			'right square bracket',
55
-			'left square bracket',
56
-			'colon',
57
-			'right parenthesis',
58
-			'left parenthesis',
59
-			'plus',
60
-			'tilde',
61
-			'equals',
62
-			'vertical bar',
63
-			'comma',
64
-			'space',
65
-			'quote',
66
-			'single quote',
67
-			'backslash',
68
-			'carat',
69
-			'dollar',
70
-			'at',
71
-		];
43
+    /**
44
+     * Get a name for a given constant. Used for error handling.
45
+     */
46
+    public static function name($const_int)
47
+    {
48
+        $a = [
49
+            'character',
50
+            'star',
51
+            'right angle bracket',
52
+            'dot',
53
+            'octothorp',
54
+            'right square bracket',
55
+            'left square bracket',
56
+            'colon',
57
+            'right parenthesis',
58
+            'left parenthesis',
59
+            'plus',
60
+            'tilde',
61
+            'equals',
62
+            'vertical bar',
63
+            'comma',
64
+            'space',
65
+            'quote',
66
+            'single quote',
67
+            'backslash',
68
+            'carat',
69
+            'dollar',
70
+            'at',
71
+        ];
72 72
 
73
-		if (isset($a[$const_int]) && is_numeric($const_int)) {
74
-			return $a[$const_int];
75
-		}
73
+        if (isset($a[$const_int]) && is_numeric($const_int)) {
74
+            return $a[$const_int];
75
+        }
76 76
 
77
-		if ($const_int === self::STRING_LEGAL) {
78
-			return 'a legal non-alphanumeric character';
79
-		}
77
+        if ($const_int === self::STRING_LEGAL) {
78
+            return 'a legal non-alphanumeric character';
79
+        }
80 80
 
81
-		if ($const_int === false) {
82
-			return 'end of file';
83
-		}
81
+        if ($const_int === false) {
82
+            return 'end of file';
83
+        }
84 84
 
85
-		return sprintf('illegal character (%s)', $const_int);
86
-	}
85
+        return sprintf('illegal character (%s)', $const_int);
86
+    }
87 87
 }
Please login to merge, or discard this patch.
src/CSS/Selector.php 1 patch
Indentation   +129 added lines, -129 removed lines patch added patch discarded remove patch
@@ -54,133 +54,133 @@
 block discarded – undo
54 54
 class Selector implements EventHandler, IteratorAggregate, Countable
55 55
 {
56 56
 
57
-	protected $selectors = [];
58
-	protected $currSelector;
59
-	protected $selectorGroups = [];
60
-	protected $groupIndex = 0;
61
-
62
-	public function __construct()
63
-	{
64
-		$this->currSelector = new SimpleSelector();
65
-
66
-		$this->selectors[$this->groupIndex][] = $this->currSelector;
67
-	}
68
-
69
-	public function getIterator(): Traversable
70
-	{
71
-		return new ArrayIterator($this->selectors);
72
-	}
73
-
74
-	/**
75
-	 * Get the array of SimpleSelector objects.
76
-	 *
77
-	 * Normally, one iterates over a Selector. However, if it is
78
-	 * necessary to get the selector array and manipulate it, this
79
-	 * method can be used.
80
-	 */
81
-	public function toArray()
82
-	{
83
-		return $this->selectors;
84
-	}
85
-
86
-	public function count(): int
87
-	{
88
-		return count($this->selectors);
89
-	}
90
-
91
-	public function elementID($id)
92
-	{
93
-		$this->currSelector->id = $id;
94
-	}
95
-
96
-	public function element($name)
97
-	{
98
-		$this->currSelector->element = $name;
99
-	}
100
-
101
-	public function elementNS($name, $namespace = null)
102
-	{
103
-		$this->currSelector->ns      = $namespace;
104
-		$this->currSelector->element = $name;
105
-	}
106
-
107
-	public function anyElement()
108
-	{
109
-		$this->currSelector->element = '*';
110
-	}
111
-
112
-	public function anyElementInNS($ns)
113
-	{
114
-		$this->currSelector->ns      = $ns;
115
-		$this->currSelector->element = '*';
116
-	}
117
-
118
-	public function elementClass($name)
119
-	{
120
-		$this->currSelector->classes[] = $name;
121
-	}
122
-
123
-	public function attribute($name, $value = null, $operation = EventHandler::IS_EXACTLY)
124
-	{
125
-		$this->currSelector->attributes[] = [
126
-			'name'  => $name,
127
-			'value' => $value,
128
-			'op'    => $operation,
129
-		];
130
-	}
131
-
132
-	public function attributeNS($name, $ns, $value = null, $operation = EventHandler::IS_EXACTLY)
133
-	{
134
-		$this->currSelector->attributes[] = [
135
-			'name'  => $name,
136
-			'value' => $value,
137
-			'op'    => $operation,
138
-			'ns'    => $ns,
139
-		];
140
-	}
141
-
142
-	public function pseudoClass($name, $value = null)
143
-	{
144
-		$this->currSelector->pseudoClasses[] = ['name' => $name, 'value' => $value];
145
-	}
146
-
147
-	public function pseudoElement($name)
148
-	{
149
-		$this->currSelector->pseudoElements[] = $name;
150
-	}
151
-
152
-	public function combinator($combinatorName)
153
-	{
154
-		$this->currSelector->combinator = $combinatorName;
155
-		$this->currSelector             = new SimpleSelector();
156
-		array_unshift($this->selectors[$this->groupIndex], $this->currSelector);
157
-		//$this->selectors[]= $this->currSelector;
158
-	}
159
-
160
-	public function directDescendant()
161
-	{
162
-		$this->combinator(SimpleSelector::DIRECT_DESCENDANT);
163
-	}
164
-
165
-	public function adjacent()
166
-	{
167
-		$this->combinator(SimpleSelector::ADJACENT);
168
-	}
169
-
170
-	public function anotherSelector()
171
-	{
172
-		$this->groupIndex++;
173
-		$this->currSelector                 = new SimpleSelector();
174
-		$this->selectors[$this->groupIndex] = [$this->currSelector];
175
-	}
176
-
177
-	public function sibling()
178
-	{
179
-		$this->combinator(SimpleSelector::SIBLING);
180
-	}
181
-
182
-	public function anyDescendant()
183
-	{
184
-		$this->combinator(SimpleSelector::ANY_DESCENDANT);
185
-	}
57
+    protected $selectors = [];
58
+    protected $currSelector;
59
+    protected $selectorGroups = [];
60
+    protected $groupIndex = 0;
61
+
62
+    public function __construct()
63
+    {
64
+        $this->currSelector = new SimpleSelector();
65
+
66
+        $this->selectors[$this->groupIndex][] = $this->currSelector;
67
+    }
68
+
69
+    public function getIterator(): Traversable
70
+    {
71
+        return new ArrayIterator($this->selectors);
72
+    }
73
+
74
+    /**
75
+     * Get the array of SimpleSelector objects.
76
+     *
77
+     * Normally, one iterates over a Selector. However, if it is
78
+     * necessary to get the selector array and manipulate it, this
79
+     * method can be used.
80
+     */
81
+    public function toArray()
82
+    {
83
+        return $this->selectors;
84
+    }
85
+
86
+    public function count(): int
87
+    {
88
+        return count($this->selectors);
89
+    }
90
+
91
+    public function elementID($id)
92
+    {
93
+        $this->currSelector->id = $id;
94
+    }
95
+
96
+    public function element($name)
97
+    {
98
+        $this->currSelector->element = $name;
99
+    }
100
+
101
+    public function elementNS($name, $namespace = null)
102
+    {
103
+        $this->currSelector->ns      = $namespace;
104
+        $this->currSelector->element = $name;
105
+    }
106
+
107
+    public function anyElement()
108
+    {
109
+        $this->currSelector->element = '*';
110
+    }
111
+
112
+    public function anyElementInNS($ns)
113
+    {
114
+        $this->currSelector->ns      = $ns;
115
+        $this->currSelector->element = '*';
116
+    }
117
+
118
+    public function elementClass($name)
119
+    {
120
+        $this->currSelector->classes[] = $name;
121
+    }
122
+
123
+    public function attribute($name, $value = null, $operation = EventHandler::IS_EXACTLY)
124
+    {
125
+        $this->currSelector->attributes[] = [
126
+            'name'  => $name,
127
+            'value' => $value,
128
+            'op'    => $operation,
129
+        ];
130
+    }
131
+
132
+    public function attributeNS($name, $ns, $value = null, $operation = EventHandler::IS_EXACTLY)
133
+    {
134
+        $this->currSelector->attributes[] = [
135
+            'name'  => $name,
136
+            'value' => $value,
137
+            'op'    => $operation,
138
+            'ns'    => $ns,
139
+        ];
140
+    }
141
+
142
+    public function pseudoClass($name, $value = null)
143
+    {
144
+        $this->currSelector->pseudoClasses[] = ['name' => $name, 'value' => $value];
145
+    }
146
+
147
+    public function pseudoElement($name)
148
+    {
149
+        $this->currSelector->pseudoElements[] = $name;
150
+    }
151
+
152
+    public function combinator($combinatorName)
153
+    {
154
+        $this->currSelector->combinator = $combinatorName;
155
+        $this->currSelector             = new SimpleSelector();
156
+        array_unshift($this->selectors[$this->groupIndex], $this->currSelector);
157
+        //$this->selectors[]= $this->currSelector;
158
+    }
159
+
160
+    public function directDescendant()
161
+    {
162
+        $this->combinator(SimpleSelector::DIRECT_DESCENDANT);
163
+    }
164
+
165
+    public function adjacent()
166
+    {
167
+        $this->combinator(SimpleSelector::ADJACENT);
168
+    }
169
+
170
+    public function anotherSelector()
171
+    {
172
+        $this->groupIndex++;
173
+        $this->currSelector                 = new SimpleSelector();
174
+        $this->selectors[$this->groupIndex] = [$this->currSelector];
175
+    }
176
+
177
+    public function sibling()
178
+    {
179
+        $this->combinator(SimpleSelector::SIBLING);
180
+    }
181
+
182
+    public function anyDescendant()
183
+    {
184
+        $this->combinator(SimpleSelector::ANY_DESCENDANT);
185
+    }
186 186
 }
Please login to merge, or discard this patch.
src/CSS/DOMTraverser.php 2 patches
Indentation   +814 added lines, -814 removed lines patch added patch discarded remove patch
@@ -60,166 +60,166 @@  discard block
 block discarded – undo
60 60
  */
61 61
 class DOMTraverser implements Traverser
62 62
 {
63
-	protected $matches = [];
64
-	protected $selector;
65
-	protected $dom;
66
-	protected $initialized = true;
67
-	protected $psHandler;
68
-	protected $scopeNode;
69
-
70
-	/**
71
-	 * Build a new DOMTraverser.
72
-	 *
73
-	 * This requires a DOM-like object or collection of DOM nodes.
74
-	 *
75
-	 * @param SplObjectStorage $splos
76
-	 * @param bool             $initialized
77
-	 * @param null             $scopeNode
78
-	 */
79
-	public function __construct(SplObjectStorage $splos, bool $initialized = false, $scopeNode = null)
80
-	{
81
-		$this->psHandler   = new PseudoClass();
82
-		$this->initialized = $initialized;
83
-
84
-		// Re-use the initial splos
85
-		$this->matches = $splos;
86
-
87
-		if (count($splos) !== 0) {
88
-			$splos->rewind();
89
-			$first = $splos->current();
90
-			if ($first instanceof DOMDocument) {
91
-				$this->dom = $first;//->documentElement;
92
-			} else {
93
-				$this->dom = $first->ownerDocument;//->documentElement;
94
-			}
95
-
96
-			$this->scopeNode = $scopeNode;
97
-			if (empty($scopeNode)) {
98
-				$this->scopeNode = $this->dom->documentElement;
99
-			}
100
-		}
101
-
102
-		// This assumes a DOM. Need to also accomodate the case
103
-		// where we get a set of elements.
104
-		/*
63
+    protected $matches = [];
64
+    protected $selector;
65
+    protected $dom;
66
+    protected $initialized = true;
67
+    protected $psHandler;
68
+    protected $scopeNode;
69
+
70
+    /**
71
+     * Build a new DOMTraverser.
72
+     *
73
+     * This requires a DOM-like object or collection of DOM nodes.
74
+     *
75
+     * @param SplObjectStorage $splos
76
+     * @param bool             $initialized
77
+     * @param null             $scopeNode
78
+     */
79
+    public function __construct(SplObjectStorage $splos, bool $initialized = false, $scopeNode = null)
80
+    {
81
+        $this->psHandler   = new PseudoClass();
82
+        $this->initialized = $initialized;
83
+
84
+        // Re-use the initial splos
85
+        $this->matches = $splos;
86
+
87
+        if (count($splos) !== 0) {
88
+            $splos->rewind();
89
+            $first = $splos->current();
90
+            if ($first instanceof DOMDocument) {
91
+                $this->dom = $first;//->documentElement;
92
+            } else {
93
+                $this->dom = $first->ownerDocument;//->documentElement;
94
+            }
95
+
96
+            $this->scopeNode = $scopeNode;
97
+            if (empty($scopeNode)) {
98
+                $this->scopeNode = $this->dom->documentElement;
99
+            }
100
+        }
101
+
102
+        // This assumes a DOM. Need to also accomodate the case
103
+        // where we get a set of elements.
104
+        /*
105 105
 		$this->dom = $dom;
106 106
 		$this->matches = new \SplObjectStorage();
107 107
 		$this->matches->attach($this->dom);
108 108
 		 */
109
-	}
110
-
111
-	public function debug($msg)
112
-	{
113
-		fwrite(STDOUT, PHP_EOL . $msg);
114
-	}
115
-
116
-	/**
117
-	 * Given a selector, find the matches in the given DOM.
118
-	 *
119
-	 * This is the main function for querying the DOM using a CSS
120
-	 * selector.
121
-	 *
122
-	 * @param string $selector
123
-	 *   The selector.
124
-	 *
125
-	 * @return DOMTraverser a list of matched
126
-	 *   DOMNode objects.
127
-	 * @throws ParseException
128
-	 */
129
-	public function find($selector): DOMTraverser
130
-	{
131
-		// Setup
132
-		$handler = new Selector();
133
-		$parser  = new Parser($selector, $handler);
134
-		$parser->parse();
135
-		$this->selector = $handler;
136
-
137
-		//$selector = $handler->toArray();
138
-		$found = $this->newMatches();
139
-		foreach ($handler as $selectorGroup) {
140
-			// Initialize matches if necessary.
141
-			if ($this->initialized) {
142
-				$candidates = $this->matches;
143
-			} else {
144
-				$candidates = $this->initialMatch($selectorGroup[0], $this->matches);
145
-			}
146
-
147
-			/** @var DOMElement $candidate */
148
-			foreach ($candidates as $candidate) {
149
-				// fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]);
150
-				if ($this->matchesSelector($candidate, $selectorGroup)) {
151
-					// $this->debug('Attaching ' . $candidate->nodeName);
152
-					$found->attach($candidate);
153
-				}
154
-			}
155
-		}
156
-		$this->setMatches($found);
157
-
158
-		return $this;
159
-	}
160
-
161
-	public function matches()
162
-	{
163
-		return $this->matches;
164
-	}
165
-
166
-	/**
167
-	 * Check whether the given node matches the given selector.
168
-	 *
169
-	 * A selector is a group of one or more simple selectors combined
170
-	 * by combinators. This determines if a given selector
171
-	 * matches the given node.
172
-	 *
173
-	 * @attention
174
-	 * Evaluation of selectors is done recursively. Thus the length
175
-	 * of the selector is limited to the recursion depth allowed by
176
-	 * the PHP configuration. This should only cause problems for
177
-	 * absolutely huge selectors or for versions of PHP tuned to
178
-	 * strictly limit recursion depth.
179
-	 *
180
-	 * @param DOMElement $node
181
-	 *   The DOMNode to check.
182
-	 * @param             $selector
183
-	 *
184
-	 * @return boolean
185
-	 *   A boolean TRUE if the node matches, false otherwise.
186
-	 */
187
-	public function matchesSelector(DOMElement $node, $selector)
188
-	{
189
-		return $this->matchesSimpleSelector($node, $selector, 0);
190
-	}
191
-
192
-	/**
193
-	 * Performs a match check on a SimpleSelector.
194
-	 *
195
-	 * Where matchesSelector() does a check on an entire selector,
196
-	 * this checks only a simple selector (plus an optional
197
-	 * combinator).
198
-	 *
199
-	 * @param DOMElement $node
200
-	 * @param             $selectors
201
-	 * @param             $index
202
-	 *
203
-	 * @return boolean
204
-	 *   A boolean TRUE if the node matches, false otherwise.
205
-	 * @throws NotImplementedException
206
-	 */
207
-	public function matchesSimpleSelector(DOMElement $node, $selectors, $index)
208
-	{
209
-		$selector = $selectors[$index];
210
-		// Note that this will short circuit as soon as one of these
211
-		// returns FALSE.
212
-		$result = $this->matchElement($node, $selector->element, $selector->ns)
213
-				  && $this->matchAttributes($node, $selector->attributes)
214
-				  && $this->matchId($node, $selector->id)
215
-				  && $this->matchClasses($node, $selector->classes)
216
-				  && $this->matchPseudoClasses($node, $selector->pseudoClasses)
217
-				  && $this->matchPseudoElements($node, $selector->pseudoElements);
218
-
219
-		$isNextRule = isset($selectors[++$index]);
220
-		// If there is another selector, we process that if there a match
221
-		// hasn't been found.
222
-		/*
109
+    }
110
+
111
+    public function debug($msg)
112
+    {
113
+        fwrite(STDOUT, PHP_EOL . $msg);
114
+    }
115
+
116
+    /**
117
+     * Given a selector, find the matches in the given DOM.
118
+     *
119
+     * This is the main function for querying the DOM using a CSS
120
+     * selector.
121
+     *
122
+     * @param string $selector
123
+     *   The selector.
124
+     *
125
+     * @return DOMTraverser a list of matched
126
+     *   DOMNode objects.
127
+     * @throws ParseException
128
+     */
129
+    public function find($selector): DOMTraverser
130
+    {
131
+        // Setup
132
+        $handler = new Selector();
133
+        $parser  = new Parser($selector, $handler);
134
+        $parser->parse();
135
+        $this->selector = $handler;
136
+
137
+        //$selector = $handler->toArray();
138
+        $found = $this->newMatches();
139
+        foreach ($handler as $selectorGroup) {
140
+            // Initialize matches if necessary.
141
+            if ($this->initialized) {
142
+                $candidates = $this->matches;
143
+            } else {
144
+                $candidates = $this->initialMatch($selectorGroup[0], $this->matches);
145
+            }
146
+
147
+            /** @var DOMElement $candidate */
148
+            foreach ($candidates as $candidate) {
149
+                // fprintf(STDOUT, "Testing %s against %s.\n", $candidate->tagName, $selectorGroup[0]);
150
+                if ($this->matchesSelector($candidate, $selectorGroup)) {
151
+                    // $this->debug('Attaching ' . $candidate->nodeName);
152
+                    $found->attach($candidate);
153
+                }
154
+            }
155
+        }
156
+        $this->setMatches($found);
157
+
158
+        return $this;
159
+    }
160
+
161
+    public function matches()
162
+    {
163
+        return $this->matches;
164
+    }
165
+
166
+    /**
167
+     * Check whether the given node matches the given selector.
168
+     *
169
+     * A selector is a group of one or more simple selectors combined
170
+     * by combinators. This determines if a given selector
171
+     * matches the given node.
172
+     *
173
+     * @attention
174
+     * Evaluation of selectors is done recursively. Thus the length
175
+     * of the selector is limited to the recursion depth allowed by
176
+     * the PHP configuration. This should only cause problems for
177
+     * absolutely huge selectors or for versions of PHP tuned to
178
+     * strictly limit recursion depth.
179
+     *
180
+     * @param DOMElement $node
181
+     *   The DOMNode to check.
182
+     * @param             $selector
183
+     *
184
+     * @return boolean
185
+     *   A boolean TRUE if the node matches, false otherwise.
186
+     */
187
+    public function matchesSelector(DOMElement $node, $selector)
188
+    {
189
+        return $this->matchesSimpleSelector($node, $selector, 0);
190
+    }
191
+
192
+    /**
193
+     * Performs a match check on a SimpleSelector.
194
+     *
195
+     * Where matchesSelector() does a check on an entire selector,
196
+     * this checks only a simple selector (plus an optional
197
+     * combinator).
198
+     *
199
+     * @param DOMElement $node
200
+     * @param             $selectors
201
+     * @param             $index
202
+     *
203
+     * @return boolean
204
+     *   A boolean TRUE if the node matches, false otherwise.
205
+     * @throws NotImplementedException
206
+     */
207
+    public function matchesSimpleSelector(DOMElement $node, $selectors, $index)
208
+    {
209
+        $selector = $selectors[$index];
210
+        // Note that this will short circuit as soon as one of these
211
+        // returns FALSE.
212
+        $result = $this->matchElement($node, $selector->element, $selector->ns)
213
+                  && $this->matchAttributes($node, $selector->attributes)
214
+                  && $this->matchId($node, $selector->id)
215
+                  && $this->matchClasses($node, $selector->classes)
216
+                  && $this->matchPseudoClasses($node, $selector->pseudoClasses)
217
+                  && $this->matchPseudoElements($node, $selector->pseudoElements);
218
+
219
+        $isNextRule = isset($selectors[++$index]);
220
+        // If there is another selector, we process that if there a match
221
+        // hasn't been found.
222
+        /*
223 223
 		if ($isNextRule && $selectors[$index]->combinator == SimpleSelector::anotherSelector) {
224 224
 		  // We may need to re-initialize the match set for the next selector.
225 225
 		  if (!$this->initialized) {
@@ -231,457 +231,457 @@  discard block
 block discarded – undo
231 231
 		// If we have a match and we have a combinator, we need to
232 232
 		// recurse up the tree.
233 233
 		else*/
234
-		if ($isNextRule && $result) {
235
-			$result = $this->combine($node, $selectors, $index);
236
-		}
237
-
238
-		return $result;
239
-	}
240
-
241
-	/**
242
-	 * Combine the next selector with the given match
243
-	 * using the next combinator.
244
-	 *
245
-	 * If the next selector is combined with another
246
-	 * selector, that will be evaluated too, and so on.
247
-	 * So if this function returns TRUE, it means that all
248
-	 * child selectors are also matches.
249
-	 *
250
-	 * @param DOMNode $node
251
-	 *   The DOMNode to test.
252
-	 * @param array   $selectors
253
-	 *   The array of simple selectors.
254
-	 * @param int     $index
255
-	 *   The index of the current selector.
256
-	 *
257
-	 * @return boolean
258
-	 *   TRUE if the next selector(s) match.
259
-	 */
260
-	public function combine(DOMElement $node, $selectors, $index)
261
-	{
262
-		$selector = $selectors[$index];
263
-		//$this->debug(implode(' ', $selectors));
264
-		switch ($selector->combinator) {
265
-			case SimpleSelector::ADJACENT:
266
-				return $this->combineAdjacent($node, $selectors, $index);
267
-			case SimpleSelector::SIBLING:
268
-				return $this->combineSibling($node, $selectors, $index);
269
-			case SimpleSelector::DIRECT_DESCENDANT:
270
-				return $this->combineDirectDescendant($node, $selectors, $index);
271
-			case SimpleSelector::ANY_DESCENDANT:
272
-				return $this->combineAnyDescendant($node, $selectors, $index);
273
-			case SimpleSelector::ANOTHER_SELECTOR:
274
-				// fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]);
275
-				return $this->matchesSimpleSelector($node, $selectors, $index);
276
-		}
277
-
278
-		return false;
279
-	}
280
-
281
-	/**
282
-	 * Process an Adjacent Sibling.
283
-	 *
284
-	 * The spec does not indicate whether Adjacent should ignore non-Element
285
-	 * nodes, so we choose to ignore them.
286
-	 *
287
-	 * @param DOMNode $node
288
-	 *   A DOM Node.
289
-	 * @param array   $selectors
290
-	 *   The selectors array.
291
-	 * @param int     $index
292
-	 *   The current index to the operative simple selector in the selectors
293
-	 *   array.
294
-	 *
295
-	 * @return boolean
296
-	 *   TRUE if the combination matches, FALSE otherwise.
297
-	 */
298
-	public function combineAdjacent($node, $selectors, $index)
299
-	{
300
-		while (! empty($node->previousSibling)) {
301
-			$node = $node->previousSibling;
302
-			if ($node->nodeType == XML_ELEMENT_NODE) {
303
-				//$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index]));
304
-				return $this->matchesSimpleSelector($node, $selectors, $index);
305
-			}
306
-		}
307
-
308
-		return false;
309
-	}
310
-
311
-	/**
312
-	 * Check all siblings.
313
-	 *
314
-	 * According to the spec, this only tests elements LEFT of the provided
315
-	 * node.
316
-	 *
317
-	 * @param DOMNode $node
318
-	 *   A DOM Node.
319
-	 * @param array   $selectors
320
-	 *   The selectors array.
321
-	 * @param int     $index
322
-	 *   The current index to the operative simple selector in the selectors
323
-	 *   array.
324
-	 *
325
-	 * @return boolean
326
-	 *   TRUE if the combination matches, FALSE otherwise.
327
-	 */
328
-	public function combineSibling($node, $selectors, $index)
329
-	{
330
-		while (! empty($node->previousSibling)) {
331
-			$node = $node->previousSibling;
332
-			if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) {
333
-				return true;
334
-			}
335
-		}
336
-
337
-		return false;
338
-	}
339
-
340
-	/**
341
-	 * Handle a Direct Descendant combination.
342
-	 *
343
-	 * Check whether the given node is a rightly-related descendant
344
-	 * of its parent node.
345
-	 *
346
-	 * @param DOMNode $node
347
-	 *   A DOM Node.
348
-	 * @param array   $selectors
349
-	 *   The selectors array.
350
-	 * @param int     $index
351
-	 *   The current index to the operative simple selector in the selectors
352
-	 *   array.
353
-	 *
354
-	 * @return boolean
355
-	 *   TRUE if the combination matches, FALSE otherwise.
356
-	 */
357
-	public function combineDirectDescendant($node, $selectors, $index)
358
-	{
359
-		$parent = $node->parentNode;
360
-		if (empty($parent)) {
361
-			return false;
362
-		}
363
-
364
-		return $this->matchesSimpleSelector($parent, $selectors, $index);
365
-	}
366
-
367
-	/**
368
-	 * Handle Any Descendant combinations.
369
-	 *
370
-	 * This checks to see if there are any matching routes from the
371
-	 * selector beginning at the present node.
372
-	 *
373
-	 * @param DOMNode $node
374
-	 *   A DOM Node.
375
-	 * @param array   $selectors
376
-	 *   The selectors array.
377
-	 * @param int     $index
378
-	 *   The current index to the operative simple selector in the selectors
379
-	 *   array.
380
-	 *
381
-	 * @return boolean
382
-	 *   TRUE if the combination matches, FALSE otherwise.
383
-	 */
384
-	public function combineAnyDescendant($node, $selectors, $index)
385
-	{
386
-		while (! empty($node->parentNode)) {
387
-			$node = $node->parentNode;
388
-
389
-			// Catch case where element is child of something
390
-			// else. This should really only happen with a
391
-			// document element.
392
-			if ($node->nodeType != XML_ELEMENT_NODE) {
393
-				continue;
394
-			}
395
-
396
-			if ($this->matchesSimpleSelector($node, $selectors, $index)) {
397
-				return true;
398
-			}
399
-		}
400
-	}
401
-
402
-	/**
403
-	 * Get the intial match set.
404
-	 *
405
-	 * This should only be executed when not working with
406
-	 * an existing match set.
407
-	 *
408
-	 * @param \QueryPath\CSS\SimpleSelector $selector
409
-	 * @param SplObjectStorage              $matches
410
-	 *
411
-	 * @return SplObjectStorage
412
-	 */
413
-	protected function initialMatch(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
414
-	{
415
-		$element = $selector->element;
416
-
417
-		// If no element is specified, we have to start with the
418
-		// entire document.
419
-		if ($element === null) {
420
-			$element = '*';
421
-		}
422
-
423
-		// We try to do some optimization here to reduce the
424
-		// number of matches to the bare minimum. This will
425
-		// reduce the subsequent number of operations that
426
-		// must be performed in the query.
427
-
428
-		// Experimental: ID queries use XPath to match, since
429
-		// this should give us only a single matched element
430
-		// to work with.
431
-		if (/*$element == '*' &&*/
432
-		! empty($selector->id)) {
433
-			$initialMatches = $this->initialMatchOnID($selector, $matches);
434
-		} // If a namespace is set, find the namespace matches.
435
-		elseif (! empty($selector->ns)) {
436
-			$initialMatches = $this->initialMatchOnElementNS($selector, $matches);
437
-		}
438
-		// If the element is a wildcard, using class can
439
-		// substantially reduce the number of elements that
440
-		// we start with.
441
-		elseif ($element === '*' && ! empty($selector->classes)) {
442
-			$initialMatches = $this->initialMatchOnClasses($selector, $matches);
443
-		} else {
444
-			$initialMatches = $this->initialMatchOnElement($selector, $matches);
445
-		}
446
-
447
-		return $initialMatches;
448
-	}
449
-
450
-	/**
451
-	 * Shortcut for finding initial match by ID.
452
-	 *
453
-	 * If the element is set to '*' and an ID is
454
-	 * set, then this should be used to find by ID,
455
-	 * which will drastically reduce the amount of
456
-	 * comparison operations done in PHP.
457
-	 *
458
-	 * @param \QueryPath\CSS\SimpleSelector $selector
459
-	 * @param SplObjectStorage              $matches
460
-	 *
461
-	 * @return SplObjectStorage
462
-	 */
463
-	protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
464
-	{
465
-		$id    = $selector->id;
466
-		$found = $this->newMatches();
467
-
468
-		// Issue #145: DOMXPath will through an exception if the DOM is
469
-		// not set.
470
-		if (! ($this->dom instanceof DOMDocument)) {
471
-			return $found;
472
-		}
473
-		$baseQuery = ".//*[@id='{$id}']";
474
-		$xpath     = new DOMXPath($this->dom);
475
-
476
-		// Now we try to find any matching IDs.
477
-		/** @var DOMElement $node */
478
-		foreach ($matches as $node) {
479
-			if ($node->getAttribute('id') === $id) {
480
-				$found->attach($node);
481
-			}
482
-
483
-			$nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
484
-			if (! empty($nl) && $nl instanceof DOMNodeList) {
485
-				$this->attachNodeList($nl, $found);
486
-			}
487
-		}
488
-		// Unset the ID selector.
489
-		$selector->id = null;
490
-
491
-		return $found;
492
-	}
493
-
494
-	/**
495
-	 * Shortcut for setting the intial match.
496
-	 *
497
-	 * This shortcut should only be used when the initial
498
-	 * element is '*' and there are classes set.
499
-	 *
500
-	 * In any other case, the element finding algo is
501
-	 * faster and should be used instead.
502
-	 *
503
-	 * @param \QueryPath\CSS\SimpleSelector $selector
504
-	 * @param                               $matches
505
-	 *
506
-	 * @return SplObjectStorage
507
-	 */
508
-	protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
509
-	{
510
-		$found = $this->newMatches();
511
-
512
-		// Issue #145: DOMXPath will through an exception if the DOM is
513
-		// not set.
514
-		if (! ($this->dom instanceof DOMDocument)) {
515
-			return $found;
516
-		}
517
-		$baseQuery = './/*[@class]';
518
-		$xpath     = new DOMXPath($this->dom);
519
-
520
-		// Now we try to find any matching IDs.
521
-		/** @var DOMElement $node */
522
-		foreach ($matches as $node) {
523
-			// Refactor me!
524
-			if ($node->hasAttribute('class')) {
525
-				$intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class')));
526
-				if (count($intersect) === count($selector->classes)) {
527
-					$found->attach($node);
528
-				}
529
-			}
530
-
531
-			$nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
532
-			/** @var DOMElement $subNode */
533
-			foreach ($nl as $subNode) {
534
-				$classes    = $subNode->getAttribute('class');
535
-				$classArray = explode(' ', $classes);
536
-
537
-				$intersect = array_intersect($selector->classes, $classArray);
538
-				if (count($intersect) === count($selector->classes)) {
539
-					$found->attach($subNode);
540
-				}
541
-			}
542
-		}
543
-
544
-		// Unset the classes selector.
545
-		$selector->classes = [];
546
-
547
-		return $found;
548
-	}
549
-
550
-	/**
551
-	 * Internal xpath query.
552
-	 *
553
-	 * This is optimized for very specific use, and is not a general
554
-	 * purpose function.
555
-	 *
556
-	 * @param DOMXPath   $xpath
557
-	 * @param DOMElement $node
558
-	 * @param string      $query
559
-	 *
560
-	 * @return DOMNodeList
561
-	 */
562
-	private function initialXpathQuery(DOMXPath $xpath, DOMElement $node, string $query): DOMNodeList
563
-	{
564
-		// This works around a bug in which the document element
565
-		// does not correctly search with the $baseQuery.
566
-		if ($node->isSameNode($this->dom->documentElement)) {
567
-			$query = mb_substr($query, 1);
568
-		}
569
-
570
-		return $xpath->query($query, $node);
571
-	}
572
-
573
-	/**
574
-	 * Shortcut for setting the initial match.
575
-	 *
576
-	 * @param $selector
577
-	 * @param $matches
578
-	 *
579
-	 * @return SplObjectStorage
580
-	 */
581
-	protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
582
-	{
583
-		$element = $selector->element;
584
-		if (null === $element) {
585
-			$element = '*';
586
-		}
587
-		$found = $this->newMatches();
588
-		/** @var DOMDocument $node */
589
-		foreach ($matches as $node) {
590
-			// Capture the case where the initial element is the root element.
591
-			if ($node->tagName === $element
592
-				|| ($element === '*' && $node->parentNode instanceof DOMDocument)) {
593
-				$found->attach($node);
594
-			}
595
-			$nl = $node->getElementsByTagName($element);
596
-			if (! empty($nl) && $nl instanceof DOMNodeList) {
597
-				$this->attachNodeList($nl, $found);
598
-			}
599
-		}
600
-
601
-		$selector->element = null;
602
-
603
-		return $found;
604
-	}
605
-
606
-	/**
607
-	 * Get elements and filter by namespace.
608
-	 *
609
-	 * @param \QueryPath\CSS\SimpleSelector $selector
610
-	 * @param SplObjectStorage              $matches
611
-	 *
612
-	 * @return SplObjectStorage
613
-	 */
614
-	protected function initialMatchOnElementNS(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
615
-	{
616
-		$ns = $selector->ns;
617
-
618
-		$elements = $this->initialMatchOnElement($selector, $matches);
619
-
620
-		// "any namespace" matches anything.
621
-		if ($ns === '*') {
622
-			return $elements;
623
-		}
624
-
625
-		// Loop through and make a list of items that need to be filtered
626
-		// out, then filter them. This is required b/c ObjectStorage iterates
627
-		// wrongly when an item is detached in an access loop.
628
-		$detach = [];
629
-		foreach ($elements as $node) {
630
-			// This lookup must be done PER NODE.
631
-			$nsuri = $node->lookupNamespaceURI($ns);
632
-			if (empty($nsuri) || $node->namespaceURI !== $nsuri) {
633
-				$detach[] = $node;
634
-			}
635
-		}
636
-		foreach ($detach as $rem) {
637
-			$elements->detach($rem);
638
-		}
639
-		$selector->ns = null;
640
-
641
-		return $elements;
642
-	}
643
-
644
-	/**
645
-	 * Checks to see if the DOMNode matches the given element selector.
646
-	 *
647
-	 * This handles the following cases:
648
-	 *
649
-	 * - element (foo)
650
-	 * - namespaced element (ns|foo)
651
-	 * - namespaced wildcard (ns|*)
652
-	 * - wildcard (* or *|*)
653
-	 *
654
-	 * @param DOMElement $node
655
-	 * @param             $element
656
-	 * @param null        $ns
657
-	 *
658
-	 * @return bool
659
-	 */
660
-	protected function matchElement(DOMElement $node, $element, $ns = null): bool
661
-	{
662
-		if (empty($element)) {
663
-			return true;
664
-		}
665
-
666
-		// Handle namespace.
667
-		if (! empty($ns) && $ns !== '*') {
668
-			// Check whether we have a matching NS URI.
669
-			$nsuri = $node->lookupNamespaceURI($ns);
670
-			if (empty($nsuri) || $node->namespaceURI !== $nsuri) {
671
-				return false;
672
-			}
673
-		}
674
-
675
-		// Compare local name to given element name.
676
-		return $element === '*' || $node->localName === $element;
677
-	}
678
-
679
-	/**
680
-	 * Checks to see if the given DOMNode matches an "any element" (*).
681
-	 *
682
-	 * This does not handle namespaced whildcards.
683
-	 */
684
-	/*
234
+        if ($isNextRule && $result) {
235
+            $result = $this->combine($node, $selectors, $index);
236
+        }
237
+
238
+        return $result;
239
+    }
240
+
241
+    /**
242
+     * Combine the next selector with the given match
243
+     * using the next combinator.
244
+     *
245
+     * If the next selector is combined with another
246
+     * selector, that will be evaluated too, and so on.
247
+     * So if this function returns TRUE, it means that all
248
+     * child selectors are also matches.
249
+     *
250
+     * @param DOMNode $node
251
+     *   The DOMNode to test.
252
+     * @param array   $selectors
253
+     *   The array of simple selectors.
254
+     * @param int     $index
255
+     *   The index of the current selector.
256
+     *
257
+     * @return boolean
258
+     *   TRUE if the next selector(s) match.
259
+     */
260
+    public function combine(DOMElement $node, $selectors, $index)
261
+    {
262
+        $selector = $selectors[$index];
263
+        //$this->debug(implode(' ', $selectors));
264
+        switch ($selector->combinator) {
265
+            case SimpleSelector::ADJACENT:
266
+                return $this->combineAdjacent($node, $selectors, $index);
267
+            case SimpleSelector::SIBLING:
268
+                return $this->combineSibling($node, $selectors, $index);
269
+            case SimpleSelector::DIRECT_DESCENDANT:
270
+                return $this->combineDirectDescendant($node, $selectors, $index);
271
+            case SimpleSelector::ANY_DESCENDANT:
272
+                return $this->combineAnyDescendant($node, $selectors, $index);
273
+            case SimpleSelector::ANOTHER_SELECTOR:
274
+                // fprintf(STDOUT, "Next selector: %s\n", $selectors[$index]);
275
+                return $this->matchesSimpleSelector($node, $selectors, $index);
276
+        }
277
+
278
+        return false;
279
+    }
280
+
281
+    /**
282
+     * Process an Adjacent Sibling.
283
+     *
284
+     * The spec does not indicate whether Adjacent should ignore non-Element
285
+     * nodes, so we choose to ignore them.
286
+     *
287
+     * @param DOMNode $node
288
+     *   A DOM Node.
289
+     * @param array   $selectors
290
+     *   The selectors array.
291
+     * @param int     $index
292
+     *   The current index to the operative simple selector in the selectors
293
+     *   array.
294
+     *
295
+     * @return boolean
296
+     *   TRUE if the combination matches, FALSE otherwise.
297
+     */
298
+    public function combineAdjacent($node, $selectors, $index)
299
+    {
300
+        while (! empty($node->previousSibling)) {
301
+            $node = $node->previousSibling;
302
+            if ($node->nodeType == XML_ELEMENT_NODE) {
303
+                //$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index]));
304
+                return $this->matchesSimpleSelector($node, $selectors, $index);
305
+            }
306
+        }
307
+
308
+        return false;
309
+    }
310
+
311
+    /**
312
+     * Check all siblings.
313
+     *
314
+     * According to the spec, this only tests elements LEFT of the provided
315
+     * node.
316
+     *
317
+     * @param DOMNode $node
318
+     *   A DOM Node.
319
+     * @param array   $selectors
320
+     *   The selectors array.
321
+     * @param int     $index
322
+     *   The current index to the operative simple selector in the selectors
323
+     *   array.
324
+     *
325
+     * @return boolean
326
+     *   TRUE if the combination matches, FALSE otherwise.
327
+     */
328
+    public function combineSibling($node, $selectors, $index)
329
+    {
330
+        while (! empty($node->previousSibling)) {
331
+            $node = $node->previousSibling;
332
+            if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) {
333
+                return true;
334
+            }
335
+        }
336
+
337
+        return false;
338
+    }
339
+
340
+    /**
341
+     * Handle a Direct Descendant combination.
342
+     *
343
+     * Check whether the given node is a rightly-related descendant
344
+     * of its parent node.
345
+     *
346
+     * @param DOMNode $node
347
+     *   A DOM Node.
348
+     * @param array   $selectors
349
+     *   The selectors array.
350
+     * @param int     $index
351
+     *   The current index to the operative simple selector in the selectors
352
+     *   array.
353
+     *
354
+     * @return boolean
355
+     *   TRUE if the combination matches, FALSE otherwise.
356
+     */
357
+    public function combineDirectDescendant($node, $selectors, $index)
358
+    {
359
+        $parent = $node->parentNode;
360
+        if (empty($parent)) {
361
+            return false;
362
+        }
363
+
364
+        return $this->matchesSimpleSelector($parent, $selectors, $index);
365
+    }
366
+
367
+    /**
368
+     * Handle Any Descendant combinations.
369
+     *
370
+     * This checks to see if there are any matching routes from the
371
+     * selector beginning at the present node.
372
+     *
373
+     * @param DOMNode $node
374
+     *   A DOM Node.
375
+     * @param array   $selectors
376
+     *   The selectors array.
377
+     * @param int     $index
378
+     *   The current index to the operative simple selector in the selectors
379
+     *   array.
380
+     *
381
+     * @return boolean
382
+     *   TRUE if the combination matches, FALSE otherwise.
383
+     */
384
+    public function combineAnyDescendant($node, $selectors, $index)
385
+    {
386
+        while (! empty($node->parentNode)) {
387
+            $node = $node->parentNode;
388
+
389
+            // Catch case where element is child of something
390
+            // else. This should really only happen with a
391
+            // document element.
392
+            if ($node->nodeType != XML_ELEMENT_NODE) {
393
+                continue;
394
+            }
395
+
396
+            if ($this->matchesSimpleSelector($node, $selectors, $index)) {
397
+                return true;
398
+            }
399
+        }
400
+    }
401
+
402
+    /**
403
+     * Get the intial match set.
404
+     *
405
+     * This should only be executed when not working with
406
+     * an existing match set.
407
+     *
408
+     * @param \QueryPath\CSS\SimpleSelector $selector
409
+     * @param SplObjectStorage              $matches
410
+     *
411
+     * @return SplObjectStorage
412
+     */
413
+    protected function initialMatch(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
414
+    {
415
+        $element = $selector->element;
416
+
417
+        // If no element is specified, we have to start with the
418
+        // entire document.
419
+        if ($element === null) {
420
+            $element = '*';
421
+        }
422
+
423
+        // We try to do some optimization here to reduce the
424
+        // number of matches to the bare minimum. This will
425
+        // reduce the subsequent number of operations that
426
+        // must be performed in the query.
427
+
428
+        // Experimental: ID queries use XPath to match, since
429
+        // this should give us only a single matched element
430
+        // to work with.
431
+        if (/*$element == '*' &&*/
432
+        ! empty($selector->id)) {
433
+            $initialMatches = $this->initialMatchOnID($selector, $matches);
434
+        } // If a namespace is set, find the namespace matches.
435
+        elseif (! empty($selector->ns)) {
436
+            $initialMatches = $this->initialMatchOnElementNS($selector, $matches);
437
+        }
438
+        // If the element is a wildcard, using class can
439
+        // substantially reduce the number of elements that
440
+        // we start with.
441
+        elseif ($element === '*' && ! empty($selector->classes)) {
442
+            $initialMatches = $this->initialMatchOnClasses($selector, $matches);
443
+        } else {
444
+            $initialMatches = $this->initialMatchOnElement($selector, $matches);
445
+        }
446
+
447
+        return $initialMatches;
448
+    }
449
+
450
+    /**
451
+     * Shortcut for finding initial match by ID.
452
+     *
453
+     * If the element is set to '*' and an ID is
454
+     * set, then this should be used to find by ID,
455
+     * which will drastically reduce the amount of
456
+     * comparison operations done in PHP.
457
+     *
458
+     * @param \QueryPath\CSS\SimpleSelector $selector
459
+     * @param SplObjectStorage              $matches
460
+     *
461
+     * @return SplObjectStorage
462
+     */
463
+    protected function initialMatchOnID(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
464
+    {
465
+        $id    = $selector->id;
466
+        $found = $this->newMatches();
467
+
468
+        // Issue #145: DOMXPath will through an exception if the DOM is
469
+        // not set.
470
+        if (! ($this->dom instanceof DOMDocument)) {
471
+            return $found;
472
+        }
473
+        $baseQuery = ".//*[@id='{$id}']";
474
+        $xpath     = new DOMXPath($this->dom);
475
+
476
+        // Now we try to find any matching IDs.
477
+        /** @var DOMElement $node */
478
+        foreach ($matches as $node) {
479
+            if ($node->getAttribute('id') === $id) {
480
+                $found->attach($node);
481
+            }
482
+
483
+            $nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
484
+            if (! empty($nl) && $nl instanceof DOMNodeList) {
485
+                $this->attachNodeList($nl, $found);
486
+            }
487
+        }
488
+        // Unset the ID selector.
489
+        $selector->id = null;
490
+
491
+        return $found;
492
+    }
493
+
494
+    /**
495
+     * Shortcut for setting the intial match.
496
+     *
497
+     * This shortcut should only be used when the initial
498
+     * element is '*' and there are classes set.
499
+     *
500
+     * In any other case, the element finding algo is
501
+     * faster and should be used instead.
502
+     *
503
+     * @param \QueryPath\CSS\SimpleSelector $selector
504
+     * @param                               $matches
505
+     *
506
+     * @return SplObjectStorage
507
+     */
508
+    protected function initialMatchOnClasses(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
509
+    {
510
+        $found = $this->newMatches();
511
+
512
+        // Issue #145: DOMXPath will through an exception if the DOM is
513
+        // not set.
514
+        if (! ($this->dom instanceof DOMDocument)) {
515
+            return $found;
516
+        }
517
+        $baseQuery = './/*[@class]';
518
+        $xpath     = new DOMXPath($this->dom);
519
+
520
+        // Now we try to find any matching IDs.
521
+        /** @var DOMElement $node */
522
+        foreach ($matches as $node) {
523
+            // Refactor me!
524
+            if ($node->hasAttribute('class')) {
525
+                $intersect = array_intersect($selector->classes, explode(' ', $node->getAttribute('class')));
526
+                if (count($intersect) === count($selector->classes)) {
527
+                    $found->attach($node);
528
+                }
529
+            }
530
+
531
+            $nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
532
+            /** @var DOMElement $subNode */
533
+            foreach ($nl as $subNode) {
534
+                $classes    = $subNode->getAttribute('class');
535
+                $classArray = explode(' ', $classes);
536
+
537
+                $intersect = array_intersect($selector->classes, $classArray);
538
+                if (count($intersect) === count($selector->classes)) {
539
+                    $found->attach($subNode);
540
+                }
541
+            }
542
+        }
543
+
544
+        // Unset the classes selector.
545
+        $selector->classes = [];
546
+
547
+        return $found;
548
+    }
549
+
550
+    /**
551
+     * Internal xpath query.
552
+     *
553
+     * This is optimized for very specific use, and is not a general
554
+     * purpose function.
555
+     *
556
+     * @param DOMXPath   $xpath
557
+     * @param DOMElement $node
558
+     * @param string      $query
559
+     *
560
+     * @return DOMNodeList
561
+     */
562
+    private function initialXpathQuery(DOMXPath $xpath, DOMElement $node, string $query): DOMNodeList
563
+    {
564
+        // This works around a bug in which the document element
565
+        // does not correctly search with the $baseQuery.
566
+        if ($node->isSameNode($this->dom->documentElement)) {
567
+            $query = mb_substr($query, 1);
568
+        }
569
+
570
+        return $xpath->query($query, $node);
571
+    }
572
+
573
+    /**
574
+     * Shortcut for setting the initial match.
575
+     *
576
+     * @param $selector
577
+     * @param $matches
578
+     *
579
+     * @return SplObjectStorage
580
+     */
581
+    protected function initialMatchOnElement(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
582
+    {
583
+        $element = $selector->element;
584
+        if (null === $element) {
585
+            $element = '*';
586
+        }
587
+        $found = $this->newMatches();
588
+        /** @var DOMDocument $node */
589
+        foreach ($matches as $node) {
590
+            // Capture the case where the initial element is the root element.
591
+            if ($node->tagName === $element
592
+                || ($element === '*' && $node->parentNode instanceof DOMDocument)) {
593
+                $found->attach($node);
594
+            }
595
+            $nl = $node->getElementsByTagName($element);
596
+            if (! empty($nl) && $nl instanceof DOMNodeList) {
597
+                $this->attachNodeList($nl, $found);
598
+            }
599
+        }
600
+
601
+        $selector->element = null;
602
+
603
+        return $found;
604
+    }
605
+
606
+    /**
607
+     * Get elements and filter by namespace.
608
+     *
609
+     * @param \QueryPath\CSS\SimpleSelector $selector
610
+     * @param SplObjectStorage              $matches
611
+     *
612
+     * @return SplObjectStorage
613
+     */
614
+    protected function initialMatchOnElementNS(SimpleSelector $selector, SplObjectStorage $matches): SplObjectStorage
615
+    {
616
+        $ns = $selector->ns;
617
+
618
+        $elements = $this->initialMatchOnElement($selector, $matches);
619
+
620
+        // "any namespace" matches anything.
621
+        if ($ns === '*') {
622
+            return $elements;
623
+        }
624
+
625
+        // Loop through and make a list of items that need to be filtered
626
+        // out, then filter them. This is required b/c ObjectStorage iterates
627
+        // wrongly when an item is detached in an access loop.
628
+        $detach = [];
629
+        foreach ($elements as $node) {
630
+            // This lookup must be done PER NODE.
631
+            $nsuri = $node->lookupNamespaceURI($ns);
632
+            if (empty($nsuri) || $node->namespaceURI !== $nsuri) {
633
+                $detach[] = $node;
634
+            }
635
+        }
636
+        foreach ($detach as $rem) {
637
+            $elements->detach($rem);
638
+        }
639
+        $selector->ns = null;
640
+
641
+        return $elements;
642
+    }
643
+
644
+    /**
645
+     * Checks to see if the DOMNode matches the given element selector.
646
+     *
647
+     * This handles the following cases:
648
+     *
649
+     * - element (foo)
650
+     * - namespaced element (ns|foo)
651
+     * - namespaced wildcard (ns|*)
652
+     * - wildcard (* or *|*)
653
+     *
654
+     * @param DOMElement $node
655
+     * @param             $element
656
+     * @param null        $ns
657
+     *
658
+     * @return bool
659
+     */
660
+    protected function matchElement(DOMElement $node, $element, $ns = null): bool
661
+    {
662
+        if (empty($element)) {
663
+            return true;
664
+        }
665
+
666
+        // Handle namespace.
667
+        if (! empty($ns) && $ns !== '*') {
668
+            // Check whether we have a matching NS URI.
669
+            $nsuri = $node->lookupNamespaceURI($ns);
670
+            if (empty($nsuri) || $node->namespaceURI !== $nsuri) {
671
+                return false;
672
+            }
673
+        }
674
+
675
+        // Compare local name to given element name.
676
+        return $element === '*' || $node->localName === $element;
677
+    }
678
+
679
+    /**
680
+     * Checks to see if the given DOMNode matches an "any element" (*).
681
+     *
682
+     * This does not handle namespaced whildcards.
683
+     */
684
+    /*
685 685
 	protected function matchAnyElement($node) {
686 686
 	  $ancestors = $this->ancestors($node);
687 687
 
@@ -689,211 +689,211 @@  discard block
 block discarded – undo
689 689
 	}
690 690
 	 */
691 691
 
692
-	/**
693
-	 * Get a list of ancestors to the present node.
694
-	 */
695
-	protected function ancestors($node)
696
-	{
697
-		$buffer = [];
698
-		$parent = $node;
699
-		while (($parent = $parent->parentNode) !== null) {
700
-			$buffer[] = $parent;
701
-		}
702
-
703
-		return $buffer;
704
-	}
705
-
706
-	/**
707
-	 * Check to see if DOMNode has all of the given attributes.
708
-	 *
709
-	 * This can handle namespaced attributes, including namespace
710
-	 * wildcards.
711
-	 *
712
-	 * @param DOMElement $node
713
-	 * @param             $attributes
714
-	 *
715
-	 * @return bool
716
-	 */
717
-	protected function matchAttributes(DOMElement $node, $attributes): bool
718
-	{
719
-		if (empty($attributes)) {
720
-			return true;
721
-		}
722
-
723
-		foreach ($attributes as $attr) {
724
-			$val = isset($attr['value']) ? $attr['value'] : null;
725
-
726
-			// Namespaced attributes.
727
-			if (isset($attr['ns']) && $attr['ns'] !== '*') {
728
-				$nsuri = $node->lookupNamespaceURI($attr['ns']);
729
-				if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) {
730
-					return false;
731
-				}
732
-				$matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']);
733
-			} elseif (isset($attr['ns']) && $attr['ns'] === '*' && $node->hasAttributes()) {
734
-				// Cycle through all of the attributes in the node. Note that
735
-				// these are DOMAttr objects.
736
-				$matches = false;
737
-				$name    = $attr['name'];
738
-				foreach ($node->attributes as $attrNode) {
739
-					if ($attrNode->localName === $name) {
740
-						$nsuri   = $attrNode->namespaceURI;
741
-						$matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']);
742
-					}
743
-				}
744
-			} // No namespace.
745
-			else {
746
-				$matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']);
747
-			}
748
-
749
-			if (! $matches) {
750
-				return false;
751
-			}
752
-		}
753
-
754
-		return true;
755
-	}
756
-
757
-	/**
758
-	 * Check that the given DOMNode has the given ID.
759
-	 *
760
-	 * @param DOMElement $node
761
-	 * @param             $id
762
-	 *
763
-	 * @return bool
764
-	 */
765
-	protected function matchId(DOMElement $node, $id): bool
766
-	{
767
-		if (empty($id)) {
768
-			return true;
769
-		}
770
-
771
-		return $node->hasAttribute('id') && $node->getAttribute('id') === $id;
772
-	}
773
-
774
-	/**
775
-	 * Check that the given DOMNode has all of the given classes.
776
-	 *
777
-	 * @param DOMElement $node
778
-	 * @param             $classes
779
-	 *
780
-	 * @return bool
781
-	 */
782
-	protected function matchClasses(DOMElement $node, $classes): bool
783
-	{
784
-		if (empty($classes)) {
785
-			return true;
786
-		}
787
-
788
-		if (! $node->hasAttribute('class')) {
789
-			return false;
790
-		}
791
-
792
-		$eleClasses = preg_split('/\s+/', $node->getAttribute('class'));
793
-		if (empty($eleClasses)) {
794
-			return false;
795
-		}
796
-
797
-		// The intersection should match the given $classes.
798
-		$missing = array_diff($classes, array_intersect($classes, $eleClasses));
799
-
800
-		return count($missing) === 0;
801
-	}
802
-
803
-	/**
804
-	 * @param DOMElement $node
805
-	 * @param             $pseudoClasses
806
-	 *
807
-	 * @return bool
808
-	 * @throws NotImplementedException
809
-	 * @throws ParseException
810
-	 */
811
-	protected function matchPseudoClasses(DOMElement $node, $pseudoClasses): bool
812
-	{
813
-		$ret = true;
814
-		foreach ($pseudoClasses as $pseudoClass) {
815
-			$name = $pseudoClass['name'];
816
-			// Avoid E_STRICT violation.
817
-			$value = $pseudoClass['value'] ?? null;
818
-			$ret   &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value);
819
-		}
820
-
821
-		return $ret;
822
-	}
823
-
824
-	/**
825
-	 * Test whether the given node matches the pseudoElements.
826
-	 *
827
-	 * If any pseudo-elements are passed, this will test to see
828
-	 * <i>if conditions obtain that would allow the pseudo-element
829
-	 * to be created</i>. This does not modify the match in any way.
830
-	 *
831
-	 * @param DOMElement $node
832
-	 * @param             $pseudoElements
833
-	 *
834
-	 * @return bool
835
-	 * @throws NotImplementedException
836
-	 */
837
-	protected function matchPseudoElements(DOMElement $node, $pseudoElements): bool
838
-	{
839
-		if (empty($pseudoElements)) {
840
-			return true;
841
-		}
842
-
843
-		foreach ($pseudoElements as $pse) {
844
-			switch ($pse) {
845
-				case 'first-line':
846
-				case 'first-letter':
847
-				case 'before':
848
-				case 'after':
849
-					return strlen($node->textContent) > 0;
850
-				case 'selection':
851
-					throw new NotImplementedException("::$pse is not implemented.");
852
-			}
853
-		}
854
-
855
-		return false;
856
-	}
857
-
858
-	protected function newMatches()
859
-	{
860
-		return new SplObjectStorage();
861
-	}
862
-
863
-	/**
864
-	 * Get the internal match set.
865
-	 * Internal utility function.
866
-	 */
867
-	protected function getMatches()
868
-	{
869
-		return $this->matches();
870
-	}
871
-
872
-	/**
873
-	 * Set the internal match set.
874
-	 *
875
-	 * Internal utility function.
876
-	 */
877
-	protected function setMatches($matches)
878
-	{
879
-		$this->matches = $matches;
880
-	}
881
-
882
-	/**
883
-	 * Attach all nodes in a node list to the given \SplObjectStorage.
884
-	 *
885
-	 * @param DOMNodeList     $nodeList
886
-	 * @param SplObjectStorage $splos
887
-	 */
888
-	public function attachNodeList(DOMNodeList $nodeList, SplObjectStorage $splos)
889
-	{
890
-		foreach ($nodeList as $item) {
891
-			$splos->attach($item);
892
-		}
893
-	}
894
-
895
-	public function getDocument()
896
-	{
897
-		return $this->dom;
898
-	}
692
+    /**
693
+     * Get a list of ancestors to the present node.
694
+     */
695
+    protected function ancestors($node)
696
+    {
697
+        $buffer = [];
698
+        $parent = $node;
699
+        while (($parent = $parent->parentNode) !== null) {
700
+            $buffer[] = $parent;
701
+        }
702
+
703
+        return $buffer;
704
+    }
705
+
706
+    /**
707
+     * Check to see if DOMNode has all of the given attributes.
708
+     *
709
+     * This can handle namespaced attributes, including namespace
710
+     * wildcards.
711
+     *
712
+     * @param DOMElement $node
713
+     * @param             $attributes
714
+     *
715
+     * @return bool
716
+     */
717
+    protected function matchAttributes(DOMElement $node, $attributes): bool
718
+    {
719
+        if (empty($attributes)) {
720
+            return true;
721
+        }
722
+
723
+        foreach ($attributes as $attr) {
724
+            $val = isset($attr['value']) ? $attr['value'] : null;
725
+
726
+            // Namespaced attributes.
727
+            if (isset($attr['ns']) && $attr['ns'] !== '*') {
728
+                $nsuri = $node->lookupNamespaceURI($attr['ns']);
729
+                if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) {
730
+                    return false;
731
+                }
732
+                $matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']);
733
+            } elseif (isset($attr['ns']) && $attr['ns'] === '*' && $node->hasAttributes()) {
734
+                // Cycle through all of the attributes in the node. Note that
735
+                // these are DOMAttr objects.
736
+                $matches = false;
737
+                $name    = $attr['name'];
738
+                foreach ($node->attributes as $attrNode) {
739
+                    if ($attrNode->localName === $name) {
740
+                        $nsuri   = $attrNode->namespaceURI;
741
+                        $matches = Util::matchesAttributeNS($node, $name, $nsuri, $val, $attr['op']);
742
+                    }
743
+                }
744
+            } // No namespace.
745
+            else {
746
+                $matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']);
747
+            }
748
+
749
+            if (! $matches) {
750
+                return false;
751
+            }
752
+        }
753
+
754
+        return true;
755
+    }
756
+
757
+    /**
758
+     * Check that the given DOMNode has the given ID.
759
+     *
760
+     * @param DOMElement $node
761
+     * @param             $id
762
+     *
763
+     * @return bool
764
+     */
765
+    protected function matchId(DOMElement $node, $id): bool
766
+    {
767
+        if (empty($id)) {
768
+            return true;
769
+        }
770
+
771
+        return $node->hasAttribute('id') && $node->getAttribute('id') === $id;
772
+    }
773
+
774
+    /**
775
+     * Check that the given DOMNode has all of the given classes.
776
+     *
777
+     * @param DOMElement $node
778
+     * @param             $classes
779
+     *
780
+     * @return bool
781
+     */
782
+    protected function matchClasses(DOMElement $node, $classes): bool
783
+    {
784
+        if (empty($classes)) {
785
+            return true;
786
+        }
787
+
788
+        if (! $node->hasAttribute('class')) {
789
+            return false;
790
+        }
791
+
792
+        $eleClasses = preg_split('/\s+/', $node->getAttribute('class'));
793
+        if (empty($eleClasses)) {
794
+            return false;
795
+        }
796
+
797
+        // The intersection should match the given $classes.
798
+        $missing = array_diff($classes, array_intersect($classes, $eleClasses));
799
+
800
+        return count($missing) === 0;
801
+    }
802
+
803
+    /**
804
+     * @param DOMElement $node
805
+     * @param             $pseudoClasses
806
+     *
807
+     * @return bool
808
+     * @throws NotImplementedException
809
+     * @throws ParseException
810
+     */
811
+    protected function matchPseudoClasses(DOMElement $node, $pseudoClasses): bool
812
+    {
813
+        $ret = true;
814
+        foreach ($pseudoClasses as $pseudoClass) {
815
+            $name = $pseudoClass['name'];
816
+            // Avoid E_STRICT violation.
817
+            $value = $pseudoClass['value'] ?? null;
818
+            $ret   &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value);
819
+        }
820
+
821
+        return $ret;
822
+    }
823
+
824
+    /**
825
+     * Test whether the given node matches the pseudoElements.
826
+     *
827
+     * If any pseudo-elements are passed, this will test to see
828
+     * <i>if conditions obtain that would allow the pseudo-element
829
+     * to be created</i>. This does not modify the match in any way.
830
+     *
831
+     * @param DOMElement $node
832
+     * @param             $pseudoElements
833
+     *
834
+     * @return bool
835
+     * @throws NotImplementedException
836
+     */
837
+    protected function matchPseudoElements(DOMElement $node, $pseudoElements): bool
838
+    {
839
+        if (empty($pseudoElements)) {
840
+            return true;
841
+        }
842
+
843
+        foreach ($pseudoElements as $pse) {
844
+            switch ($pse) {
845
+                case 'first-line':
846
+                case 'first-letter':
847
+                case 'before':
848
+                case 'after':
849
+                    return strlen($node->textContent) > 0;
850
+                case 'selection':
851
+                    throw new NotImplementedException("::$pse is not implemented.");
852
+            }
853
+        }
854
+
855
+        return false;
856
+    }
857
+
858
+    protected function newMatches()
859
+    {
860
+        return new SplObjectStorage();
861
+    }
862
+
863
+    /**
864
+     * Get the internal match set.
865
+     * Internal utility function.
866
+     */
867
+    protected function getMatches()
868
+    {
869
+        return $this->matches();
870
+    }
871
+
872
+    /**
873
+     * Set the internal match set.
874
+     *
875
+     * Internal utility function.
876
+     */
877
+    protected function setMatches($matches)
878
+    {
879
+        $this->matches = $matches;
880
+    }
881
+
882
+    /**
883
+     * Attach all nodes in a node list to the given \SplObjectStorage.
884
+     *
885
+     * @param DOMNodeList     $nodeList
886
+     * @param SplObjectStorage $splos
887
+     */
888
+    public function attachNodeList(DOMNodeList $nodeList, SplObjectStorage $splos)
889
+    {
890
+        foreach ($nodeList as $item) {
891
+            $splos->attach($item);
892
+        }
893
+    }
894
+
895
+    public function getDocument()
896
+    {
897
+        return $this->dom;
898
+    }
899 899
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -88,9 +88,9 @@  discard block
 block discarded – undo
88 88
 			$splos->rewind();
89 89
 			$first = $splos->current();
90 90
 			if ($first instanceof DOMDocument) {
91
-				$this->dom = $first;//->documentElement;
91
+				$this->dom = $first; //->documentElement;
92 92
 			} else {
93
-				$this->dom = $first->ownerDocument;//->documentElement;
93
+				$this->dom = $first->ownerDocument; //->documentElement;
94 94
 			}
95 95
 
96 96
 			$this->scopeNode = $scopeNode;
@@ -297,7 +297,7 @@  discard block
 block discarded – undo
297 297
 	 */
298 298
 	public function combineAdjacent($node, $selectors, $index)
299 299
 	{
300
-		while (! empty($node->previousSibling)) {
300
+		while (!empty($node->previousSibling)) {
301 301
 			$node = $node->previousSibling;
302 302
 			if ($node->nodeType == XML_ELEMENT_NODE) {
303 303
 				//$this->debug(sprintf('Testing %s against "%s"', $node->tagName, $selectors[$index]));
@@ -327,7 +327,7 @@  discard block
 block discarded – undo
327 327
 	 */
328 328
 	public function combineSibling($node, $selectors, $index)
329 329
 	{
330
-		while (! empty($node->previousSibling)) {
330
+		while (!empty($node->previousSibling)) {
331 331
 			$node = $node->previousSibling;
332 332
 			if ($node->nodeType == XML_ELEMENT_NODE && $this->matchesSimpleSelector($node, $selectors, $index)) {
333 333
 				return true;
@@ -383,7 +383,7 @@  discard block
 block discarded – undo
383 383
 	 */
384 384
 	public function combineAnyDescendant($node, $selectors, $index)
385 385
 	{
386
-		while (! empty($node->parentNode)) {
386
+		while (!empty($node->parentNode)) {
387 387
 			$node = $node->parentNode;
388 388
 
389 389
 			// Catch case where element is child of something
@@ -429,16 +429,16 @@  discard block
 block discarded – undo
429 429
 		// this should give us only a single matched element
430 430
 		// to work with.
431 431
 		if (/*$element == '*' &&*/
432
-		! empty($selector->id)) {
432
+		!empty($selector->id)) {
433 433
 			$initialMatches = $this->initialMatchOnID($selector, $matches);
434 434
 		} // If a namespace is set, find the namespace matches.
435
-		elseif (! empty($selector->ns)) {
435
+		elseif (!empty($selector->ns)) {
436 436
 			$initialMatches = $this->initialMatchOnElementNS($selector, $matches);
437 437
 		}
438 438
 		// If the element is a wildcard, using class can
439 439
 		// substantially reduce the number of elements that
440 440
 		// we start with.
441
-		elseif ($element === '*' && ! empty($selector->classes)) {
441
+		elseif ($element === '*' && !empty($selector->classes)) {
442 442
 			$initialMatches = $this->initialMatchOnClasses($selector, $matches);
443 443
 		} else {
444 444
 			$initialMatches = $this->initialMatchOnElement($selector, $matches);
@@ -467,7 +467,7 @@  discard block
 block discarded – undo
467 467
 
468 468
 		// Issue #145: DOMXPath will through an exception if the DOM is
469 469
 		// not set.
470
-		if (! ($this->dom instanceof DOMDocument)) {
470
+		if (!($this->dom instanceof DOMDocument)) {
471 471
 			return $found;
472 472
 		}
473 473
 		$baseQuery = ".//*[@id='{$id}']";
@@ -481,7 +481,7 @@  discard block
 block discarded – undo
481 481
 			}
482 482
 
483 483
 			$nl = $this->initialXpathQuery($xpath, $node, $baseQuery);
484
-			if (! empty($nl) && $nl instanceof DOMNodeList) {
484
+			if (!empty($nl) && $nl instanceof DOMNodeList) {
485 485
 				$this->attachNodeList($nl, $found);
486 486
 			}
487 487
 		}
@@ -511,7 +511,7 @@  discard block
 block discarded – undo
511 511
 
512 512
 		// Issue #145: DOMXPath will through an exception if the DOM is
513 513
 		// not set.
514
-		if (! ($this->dom instanceof DOMDocument)) {
514
+		if (!($this->dom instanceof DOMDocument)) {
515 515
 			return $found;
516 516
 		}
517 517
 		$baseQuery = './/*[@class]';
@@ -593,7 +593,7 @@  discard block
 block discarded – undo
593 593
 				$found->attach($node);
594 594
 			}
595 595
 			$nl = $node->getElementsByTagName($element);
596
-			if (! empty($nl) && $nl instanceof DOMNodeList) {
596
+			if (!empty($nl) && $nl instanceof DOMNodeList) {
597 597
 				$this->attachNodeList($nl, $found);
598 598
 			}
599 599
 		}
@@ -664,7 +664,7 @@  discard block
 block discarded – undo
664 664
 		}
665 665
 
666 666
 		// Handle namespace.
667
-		if (! empty($ns) && $ns !== '*') {
667
+		if (!empty($ns) && $ns !== '*') {
668 668
 			// Check whether we have a matching NS URI.
669 669
 			$nsuri = $node->lookupNamespaceURI($ns);
670 670
 			if (empty($nsuri) || $node->namespaceURI !== $nsuri) {
@@ -726,7 +726,7 @@  discard block
 block discarded – undo
726 726
 			// Namespaced attributes.
727 727
 			if (isset($attr['ns']) && $attr['ns'] !== '*') {
728 728
 				$nsuri = $node->lookupNamespaceURI($attr['ns']);
729
-				if (empty($nsuri) || ! $node->hasAttributeNS($nsuri, $attr['name'])) {
729
+				if (empty($nsuri) || !$node->hasAttributeNS($nsuri, $attr['name'])) {
730 730
 					return false;
731 731
 				}
732 732
 				$matches = Util::matchesAttributeNS($node, $attr['name'], $nsuri, $val, $attr['op']);
@@ -746,7 +746,7 @@  discard block
 block discarded – undo
746 746
 				$matches = Util::matchesAttribute($node, $attr['name'], $val, $attr['op']);
747 747
 			}
748 748
 
749
-			if (! $matches) {
749
+			if (!$matches) {
750 750
 				return false;
751 751
 			}
752 752
 		}
@@ -785,7 +785,7 @@  discard block
 block discarded – undo
785 785
 			return true;
786 786
 		}
787 787
 
788
-		if (! $node->hasAttribute('class')) {
788
+		if (!$node->hasAttribute('class')) {
789 789
 			return false;
790 790
 		}
791 791
 
@@ -815,7 +815,7 @@  discard block
 block discarded – undo
815 815
 			$name = $pseudoClass['name'];
816 816
 			// Avoid E_STRICT violation.
817 817
 			$value = $pseudoClass['value'] ?? null;
818
-			$ret   &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value);
818
+			$ret &= $this->psHandler->elementMatches($name, $node, $this->scopeNode, $value);
819 819
 		}
820 820
 
821 821
 		return $ret;
Please login to merge, or discard this patch.
src/CSS/Parser.php 1 patch
Indentation   +553 added lines, -553 removed lines patch added patch discarded remove patch
@@ -25,72 +25,72 @@  discard block
 block discarded – undo
25 25
  */
26 26
 class Parser
27 27
 {
28
-	protected $scanner;
29
-	protected $buffer = '';
30
-	protected $handler;
31
-	private $strict = false;
32
-
33
-	protected $DEBUG = false;
34
-
35
-	/**
36
-	 * Construct a new CSS parser object. This will attempt to
37
-	 * parse the string as a CSS selector. As it parses, it will
38
-	 * send events to the EventHandler implementation.
39
-	 *
40
-	 * @param string       $string
41
-	 * @param EventHandler $handler
42
-	 */
43
-	public function __construct(string $string, EventHandler $handler)
44
-	{
45
-		$this->originalString = $string;
46
-		$is                   = new InputStream($string);
47
-		$this->scanner        = new Scanner($is);
48
-		$this->handler        = $handler;
49
-	}
50
-
51
-	/**
52
-	 * Parse the selector.
53
-	 *
54
-	 * This begins an event-based parsing process that will
55
-	 * fire events as the selector is handled. A EventHandler
56
-	 * implementation will be responsible for handling the events.
57
-	 *
58
-	 * @throws ParseException
59
-	 * @throws Exception
60
-	 */
61
-	public function parse(): void
62
-	{
63
-		$this->scanner->nextToken();
64
-
65
-		while ($this->scanner->token !== false) {
66
-			// Primitive recursion detection.
67
-			$position = $this->scanner->position();
68
-
69
-			if ($this->DEBUG) {
70
-				echo 'PARSE ' . $this->scanner->token . PHP_EOL;
71
-			}
72
-			$this->selector();
73
-
74
-			$finalPosition = $this->scanner->position();
75
-			if ($this->scanner->token !== false && $finalPosition === $position) {
76
-				// If we get here, then the scanner did not pop a single character
77
-				// off of the input stream during a full run of the parser, which
78
-				// means that the current input does not match any recognizable
79
-				// pattern.
80
-				throw new ParseException('CSS selector is not well formed.');
81
-			}
82
-		}
83
-	}
84
-
85
-	/**
86
-	 * A restricted parser that can only parse simple selectors.
87
-	 * The pseudoClass handler for this parser will throw an
88
-	 * exception if it encounters a pseudo-element or the
89
-	 * negation pseudo-class.
90
-	 *
91
-	 * @deprecated This is not used anywhere in QueryPath and
92
-	 *  may be removed.
93
-	 *//*
28
+    protected $scanner;
29
+    protected $buffer = '';
30
+    protected $handler;
31
+    private $strict = false;
32
+
33
+    protected $DEBUG = false;
34
+
35
+    /**
36
+     * Construct a new CSS parser object. This will attempt to
37
+     * parse the string as a CSS selector. As it parses, it will
38
+     * send events to the EventHandler implementation.
39
+     *
40
+     * @param string       $string
41
+     * @param EventHandler $handler
42
+     */
43
+    public function __construct(string $string, EventHandler $handler)
44
+    {
45
+        $this->originalString = $string;
46
+        $is                   = new InputStream($string);
47
+        $this->scanner        = new Scanner($is);
48
+        $this->handler        = $handler;
49
+    }
50
+
51
+    /**
52
+     * Parse the selector.
53
+     *
54
+     * This begins an event-based parsing process that will
55
+     * fire events as the selector is handled. A EventHandler
56
+     * implementation will be responsible for handling the events.
57
+     *
58
+     * @throws ParseException
59
+     * @throws Exception
60
+     */
61
+    public function parse(): void
62
+    {
63
+        $this->scanner->nextToken();
64
+
65
+        while ($this->scanner->token !== false) {
66
+            // Primitive recursion detection.
67
+            $position = $this->scanner->position();
68
+
69
+            if ($this->DEBUG) {
70
+                echo 'PARSE ' . $this->scanner->token . PHP_EOL;
71
+            }
72
+            $this->selector();
73
+
74
+            $finalPosition = $this->scanner->position();
75
+            if ($this->scanner->token !== false && $finalPosition === $position) {
76
+                // If we get here, then the scanner did not pop a single character
77
+                // off of the input stream during a full run of the parser, which
78
+                // means that the current input does not match any recognizable
79
+                // pattern.
80
+                throw new ParseException('CSS selector is not well formed.');
81
+            }
82
+        }
83
+    }
84
+
85
+    /**
86
+     * A restricted parser that can only parse simple selectors.
87
+     * The pseudoClass handler for this parser will throw an
88
+     * exception if it encounters a pseudo-element or the
89
+     * negation pseudo-class.
90
+     *
91
+     * @deprecated This is not used anywhere in QueryPath and
92
+     *  may be removed.
93
+     *//*
94 94
 	public function parseSimpleSelector() {
95 95
 	while ($this->scanner->token !== FALSE) {
96 96
 	  if ($this->DEBUG) print "SIMPLE SELECTOR\n";
@@ -105,261 +105,261 @@  discard block
 block discarded – undo
105 105
 	}
106 106
 	}*/
107 107
 
108
-	/**
109
-	 * Handle an entire CSS selector.
110
-	 *
111
-	 * @throws ParseException
112
-	 * @throws Exception
113
-	 */
114
-	private function selector(): void
115
-	{
116
-		if ($this->DEBUG) {
117
-			print 'SELECTOR' . $this->scanner->position() . PHP_EOL;
118
-		}
119
-
120
-		$this->consumeWhitespace(); // Remove leading whitespace
121
-		$this->simpleSelectors();
122
-		$this->combinator();
123
-	}
124
-
125
-	/**
126
-	 * Consume whitespace and return a count of the number of whitespace consumed.
127
-	 *
128
-	 * @throws ParseException
129
-	 * @throws Exception
130
-	 */
131
-	private function consumeWhitespace(): int
132
-	{
133
-		if ($this->DEBUG) {
134
-			echo 'CONSUME WHITESPACE' . PHP_EOL;
135
-		}
136
-
137
-		$white = 0;
138
-		while ($this->scanner->token === Token::WHITE) {
139
-			$this->scanner->nextToken();
140
-			++$white;
141
-		}
142
-
143
-		return $white;
144
-	}
145
-
146
-	/**
147
-	 * Handle one of the five combinators: '>', '+', ' ', '~', and ','.
148
-	 * This will call the appropriate event handlers.
149
-	 *
150
-	 * @throws ParseException
151
-	 * @throws Exception
152
-	 * @see EventHandler::anyDescendant(),
153
-	 * @see EventHandler::anotherSelector().
154
-	 * @see EventHandler::directDescendant(),
155
-	 * @see EventHandler::adjacent(),
156
-	 */
157
-	private function combinator(): void
158
-	{
159
-		if ($this->DEBUG) {
160
-			echo 'COMBINATOR' . PHP_EOL;
161
-		}
162
-		/*
108
+    /**
109
+     * Handle an entire CSS selector.
110
+     *
111
+     * @throws ParseException
112
+     * @throws Exception
113
+     */
114
+    private function selector(): void
115
+    {
116
+        if ($this->DEBUG) {
117
+            print 'SELECTOR' . $this->scanner->position() . PHP_EOL;
118
+        }
119
+
120
+        $this->consumeWhitespace(); // Remove leading whitespace
121
+        $this->simpleSelectors();
122
+        $this->combinator();
123
+    }
124
+
125
+    /**
126
+     * Consume whitespace and return a count of the number of whitespace consumed.
127
+     *
128
+     * @throws ParseException
129
+     * @throws Exception
130
+     */
131
+    private function consumeWhitespace(): int
132
+    {
133
+        if ($this->DEBUG) {
134
+            echo 'CONSUME WHITESPACE' . PHP_EOL;
135
+        }
136
+
137
+        $white = 0;
138
+        while ($this->scanner->token === Token::WHITE) {
139
+            $this->scanner->nextToken();
140
+            ++$white;
141
+        }
142
+
143
+        return $white;
144
+    }
145
+
146
+    /**
147
+     * Handle one of the five combinators: '>', '+', ' ', '~', and ','.
148
+     * This will call the appropriate event handlers.
149
+     *
150
+     * @throws ParseException
151
+     * @throws Exception
152
+     * @see EventHandler::anyDescendant(),
153
+     * @see EventHandler::anotherSelector().
154
+     * @see EventHandler::directDescendant(),
155
+     * @see EventHandler::adjacent(),
156
+     */
157
+    private function combinator(): void
158
+    {
159
+        if ($this->DEBUG) {
160
+            echo 'COMBINATOR' . PHP_EOL;
161
+        }
162
+        /*
163 163
 		 * Problem: ' ' and ' > ' are both valid combinators.
164 164
 		 * So we have to track whitespace consumption to see
165 165
 		 * if we are hitting the ' ' combinator or if the
166 166
 		 * selector just has whitespace padding another combinator.
167 167
 		 */
168 168
 
169
-		// Flag to indicate that post-checks need doing
170
-		$inCombinator = false;
171
-		$white        = $this->consumeWhitespace();
172
-		$t            = $this->scanner->token;
173
-
174
-		if ($t === Token::RANGLE) {
175
-			$this->handler->directDescendant();
176
-			$this->scanner->nextToken();
177
-			$inCombinator = true;
178
-			//$this->simpleSelectors();
179
-		} elseif ($t === Token::PLUS) {
180
-			$this->handler->adjacent();
181
-			$this->scanner->nextToken();
182
-			$inCombinator = true;
183
-			//$this->simpleSelectors();
184
-		} elseif ($t === Token::COMMA) {
185
-			$this->handler->anotherSelector();
186
-			$this->scanner->nextToken();
187
-			$inCombinator = true;
188
-			//$this->scanner->selectors();
189
-		} elseif ($t === Token::TILDE) {
190
-			$this->handler->sibling();
191
-			$this->scanner->nextToken();
192
-			$inCombinator = true;
193
-		}
194
-
195
-		// Check that we don't get two combinators in a row.
196
-		if ($inCombinator) {
197
-			if ($this->DEBUG) {
198
-				print 'COMBINATOR: ' . Token::name($t) . "\n";
199
-			}
200
-			$this->consumeWhitespace();
201
-			if ($this->isCombinator($this->scanner->token)) {
202
-				throw new ParseException('Illegal combinator: Cannot have two combinators in sequence.');
203
-			}
204
-		} // Check to see if we have whitespace combinator:
205
-		elseif ($white > 0) {
206
-			if ($this->DEBUG) {
207
-				echo 'COMBINATOR: any descendant' . PHP_EOL;
208
-			}
209
-			$this->handler->anyDescendant();
210
-		} else {
211
-			if ($this->DEBUG) {
212
-				echo 'COMBINATOR: no combinator found.' . PHP_EOL;
213
-			}
214
-		}
215
-	}
216
-
217
-	/**
218
-	 * Check if the token is a combinator.
219
-	 *
220
-	 * @param int $tok
221
-	 *
222
-	 * @return bool
223
-	 */
224
-	private function isCombinator(int $tok): bool
225
-	{
226
-		return in_array($tok, [Token::PLUS, Token::RANGLE, Token::COMMA, Token::TILDE], true);
227
-	}
228
-
229
-	/**
230
-	 * Handle a simple selector.
231
-	 *
232
-	 * @throws ParseException
233
-	 */
234
-	private function simpleSelectors(): void
235
-	{
236
-		if ($this->DEBUG) {
237
-			print 'SIMPLE SELECTOR' . PHP_EOL;
238
-		}
239
-		$this->allElements();
240
-		$this->elementName();
241
-		$this->elementClass();
242
-		$this->elementID();
243
-		$this->pseudoClass();
244
-		$this->attribute();
245
-	}
246
-
247
-	/**
248
-	 * Handles CSS ID selectors.
249
-	 * This will call EventHandler::elementID().
250
-	 *
251
-	 * @throws ParseException
252
-	 * @throws Exception
253
-	 */
254
-	private function elementID(): void
255
-	{
256
-		if ($this->DEBUG) {
257
-			echo 'ELEMENT ID' . PHP_EOL;
258
-		}
259
-
260
-		if ($this->scanner->token === Token::OCTO) {
261
-			$this->scanner->nextToken();
262
-			if ($this->scanner->token !== Token::CHAR) {
263
-				throw new ParseException("Expected string after #");
264
-			}
265
-			$id = $this->scanner->getNameString();
266
-			$this->handler->elementID($id);
267
-		}
268
-	}
269
-
270
-	/**
271
-	 * Handles CSS class selectors.
272
-	 * This will call the EventHandler::elementClass() method.
273
-	 */
274
-	private function elementClass(): void
275
-	{
276
-		if ($this->DEBUG) {
277
-			print 'ELEMENT CLASS' . PHP_EOL;
278
-		}
279
-		if ($this->scanner->token == Token::DOT) {
280
-			$this->scanner->nextToken();
281
-			$this->consumeWhitespace(); // We're very fault tolerent. This should prob through error.
282
-			$cssClass = $this->scanner->getNameString();
283
-			$this->handler->elementClass($cssClass);
284
-		}
285
-	}
286
-
287
-	/**
288
-	 * Handle a pseudo-class and pseudo-element.
289
-	 *
290
-	 * CSS 3 selectors support separate pseudo-elements, using :: instead
291
-	 * of : for separator. This is now supported, and calls the pseudoElement
292
-	 * handler, EventHandler::pseudoElement().
293
-	 *
294
-	 * This will call EventHandler::pseudoClass() when a
295
-	 * pseudo-class is parsed.
296
-	 *
297
-	 * @throws ParseException
298
-	 * @throws Exception
299
-	 */
300
-	private function pseudoClass($restricted = false): void
301
-	{
302
-		if ($this->DEBUG) {
303
-			echo 'PSEUDO-CLASS' . PHP_EOL;
304
-		}
305
-		if ($this->scanner->token === Token::COLON) {
306
-			// Check for CSS 3 pseudo element:
307
-			$isPseudoElement = false;
308
-			if ($this->scanner->nextToken() === Token::COLON) {
309
-				$isPseudoElement = true;
310
-				$this->scanner->nextToken();
311
-			}
312
-
313
-			$name = $this->scanner->getNameString();
314
-			if ($restricted && $name === 'not') {
315
-				throw new ParseException("The 'not' pseudo-class is illegal in this context.");
316
-			}
317
-
318
-			$value = null;
319
-			if ($this->scanner->token === Token::LPAREN) {
320
-				if ($isPseudoElement) {
321
-					throw new ParseException('Illegal left paren. Pseudo-Element cannot have arguments.');
322
-				}
323
-				$value = $this->pseudoClassValue();
324
-			}
325
-
326
-			// FIXME: This should throw errors when pseudo element has values.
327
-			if ($isPseudoElement) {
328
-				if ($restricted) {
329
-					throw new ParseException('Pseudo-Elements are illegal in this context.');
330
-				}
331
-				$this->handler->pseudoElement($name);
332
-				$this->consumeWhitespace();
333
-
334
-				// Per the spec, pseudo-elements must be the last items in a selector, so we
335
-				// check to make sure that we are either at the end of the stream or that a
336
-				// new selector is starting. Only one pseudo-element is allowed per selector.
337
-				if ($this->scanner->token !== false && $this->scanner->token !== Token::COMMA) {
338
-					throw new ParseException('A Pseudo-Element must be the last item in a selector.');
339
-				}
340
-			} else {
341
-				$this->handler->pseudoClass($name, $value);
342
-			}
343
-		}
344
-	}
345
-
346
-	/**
347
-	 * Get the value of a pseudo-classes.
348
-	 *
349
-	 * @return string
350
-	 *  Returns the value found from a pseudo-class.
351
-	 *
352
-	 * @todo Pseudoclasses can be passed pseudo-elements and
353
-	 *  other pseudo-classes as values, which means :pseudo(::pseudo)
354
-	 *  is legal.
355
-	 */
356
-	private function pseudoClassValue()
357
-	{
358
-		if ($this->scanner->token === Token::LPAREN) {
359
-			$buf = '';
360
-
361
-			// For now, just leave pseudoClass value vague.
362
-			/*
169
+        // Flag to indicate that post-checks need doing
170
+        $inCombinator = false;
171
+        $white        = $this->consumeWhitespace();
172
+        $t            = $this->scanner->token;
173
+
174
+        if ($t === Token::RANGLE) {
175
+            $this->handler->directDescendant();
176
+            $this->scanner->nextToken();
177
+            $inCombinator = true;
178
+            //$this->simpleSelectors();
179
+        } elseif ($t === Token::PLUS) {
180
+            $this->handler->adjacent();
181
+            $this->scanner->nextToken();
182
+            $inCombinator = true;
183
+            //$this->simpleSelectors();
184
+        } elseif ($t === Token::COMMA) {
185
+            $this->handler->anotherSelector();
186
+            $this->scanner->nextToken();
187
+            $inCombinator = true;
188
+            //$this->scanner->selectors();
189
+        } elseif ($t === Token::TILDE) {
190
+            $this->handler->sibling();
191
+            $this->scanner->nextToken();
192
+            $inCombinator = true;
193
+        }
194
+
195
+        // Check that we don't get two combinators in a row.
196
+        if ($inCombinator) {
197
+            if ($this->DEBUG) {
198
+                print 'COMBINATOR: ' . Token::name($t) . "\n";
199
+            }
200
+            $this->consumeWhitespace();
201
+            if ($this->isCombinator($this->scanner->token)) {
202
+                throw new ParseException('Illegal combinator: Cannot have two combinators in sequence.');
203
+            }
204
+        } // Check to see if we have whitespace combinator:
205
+        elseif ($white > 0) {
206
+            if ($this->DEBUG) {
207
+                echo 'COMBINATOR: any descendant' . PHP_EOL;
208
+            }
209
+            $this->handler->anyDescendant();
210
+        } else {
211
+            if ($this->DEBUG) {
212
+                echo 'COMBINATOR: no combinator found.' . PHP_EOL;
213
+            }
214
+        }
215
+    }
216
+
217
+    /**
218
+     * Check if the token is a combinator.
219
+     *
220
+     * @param int $tok
221
+     *
222
+     * @return bool
223
+     */
224
+    private function isCombinator(int $tok): bool
225
+    {
226
+        return in_array($tok, [Token::PLUS, Token::RANGLE, Token::COMMA, Token::TILDE], true);
227
+    }
228
+
229
+    /**
230
+     * Handle a simple selector.
231
+     *
232
+     * @throws ParseException
233
+     */
234
+    private function simpleSelectors(): void
235
+    {
236
+        if ($this->DEBUG) {
237
+            print 'SIMPLE SELECTOR' . PHP_EOL;
238
+        }
239
+        $this->allElements();
240
+        $this->elementName();
241
+        $this->elementClass();
242
+        $this->elementID();
243
+        $this->pseudoClass();
244
+        $this->attribute();
245
+    }
246
+
247
+    /**
248
+     * Handles CSS ID selectors.
249
+     * This will call EventHandler::elementID().
250
+     *
251
+     * @throws ParseException
252
+     * @throws Exception
253
+     */
254
+    private function elementID(): void
255
+    {
256
+        if ($this->DEBUG) {
257
+            echo 'ELEMENT ID' . PHP_EOL;
258
+        }
259
+
260
+        if ($this->scanner->token === Token::OCTO) {
261
+            $this->scanner->nextToken();
262
+            if ($this->scanner->token !== Token::CHAR) {
263
+                throw new ParseException("Expected string after #");
264
+            }
265
+            $id = $this->scanner->getNameString();
266
+            $this->handler->elementID($id);
267
+        }
268
+    }
269
+
270
+    /**
271
+     * Handles CSS class selectors.
272
+     * This will call the EventHandler::elementClass() method.
273
+     */
274
+    private function elementClass(): void
275
+    {
276
+        if ($this->DEBUG) {
277
+            print 'ELEMENT CLASS' . PHP_EOL;
278
+        }
279
+        if ($this->scanner->token == Token::DOT) {
280
+            $this->scanner->nextToken();
281
+            $this->consumeWhitespace(); // We're very fault tolerent. This should prob through error.
282
+            $cssClass = $this->scanner->getNameString();
283
+            $this->handler->elementClass($cssClass);
284
+        }
285
+    }
286
+
287
+    /**
288
+     * Handle a pseudo-class and pseudo-element.
289
+     *
290
+     * CSS 3 selectors support separate pseudo-elements, using :: instead
291
+     * of : for separator. This is now supported, and calls the pseudoElement
292
+     * handler, EventHandler::pseudoElement().
293
+     *
294
+     * This will call EventHandler::pseudoClass() when a
295
+     * pseudo-class is parsed.
296
+     *
297
+     * @throws ParseException
298
+     * @throws Exception
299
+     */
300
+    private function pseudoClass($restricted = false): void
301
+    {
302
+        if ($this->DEBUG) {
303
+            echo 'PSEUDO-CLASS' . PHP_EOL;
304
+        }
305
+        if ($this->scanner->token === Token::COLON) {
306
+            // Check for CSS 3 pseudo element:
307
+            $isPseudoElement = false;
308
+            if ($this->scanner->nextToken() === Token::COLON) {
309
+                $isPseudoElement = true;
310
+                $this->scanner->nextToken();
311
+            }
312
+
313
+            $name = $this->scanner->getNameString();
314
+            if ($restricted && $name === 'not') {
315
+                throw new ParseException("The 'not' pseudo-class is illegal in this context.");
316
+            }
317
+
318
+            $value = null;
319
+            if ($this->scanner->token === Token::LPAREN) {
320
+                if ($isPseudoElement) {
321
+                    throw new ParseException('Illegal left paren. Pseudo-Element cannot have arguments.');
322
+                }
323
+                $value = $this->pseudoClassValue();
324
+            }
325
+
326
+            // FIXME: This should throw errors when pseudo element has values.
327
+            if ($isPseudoElement) {
328
+                if ($restricted) {
329
+                    throw new ParseException('Pseudo-Elements are illegal in this context.');
330
+                }
331
+                $this->handler->pseudoElement($name);
332
+                $this->consumeWhitespace();
333
+
334
+                // Per the spec, pseudo-elements must be the last items in a selector, so we
335
+                // check to make sure that we are either at the end of the stream or that a
336
+                // new selector is starting. Only one pseudo-element is allowed per selector.
337
+                if ($this->scanner->token !== false && $this->scanner->token !== Token::COMMA) {
338
+                    throw new ParseException('A Pseudo-Element must be the last item in a selector.');
339
+                }
340
+            } else {
341
+                $this->handler->pseudoClass($name, $value);
342
+            }
343
+        }
344
+    }
345
+
346
+    /**
347
+     * Get the value of a pseudo-classes.
348
+     *
349
+     * @return string
350
+     *  Returns the value found from a pseudo-class.
351
+     *
352
+     * @todo Pseudoclasses can be passed pseudo-elements and
353
+     *  other pseudo-classes as values, which means :pseudo(::pseudo)
354
+     *  is legal.
355
+     */
356
+    private function pseudoClassValue()
357
+    {
358
+        if ($this->scanner->token === Token::LPAREN) {
359
+            $buf = '';
360
+
361
+            // For now, just leave pseudoClass value vague.
362
+            /*
363 363
 			// We have to peek to see if next char is a colon because
364 364
 			// pseudo-classes and pseudo-elements are legal strings here.
365 365
 			print $this->scanner->peek();
@@ -390,242 +390,242 @@  discard block
 block discarded – undo
390 390
 			}
391 391
 			return $buf;
392 392
 			*/
393
-			//$buf .= $this->scanner->getQuotedString();
394
-			$buf .= $this->scanner->getPseudoClassString();
395
-
396
-			return $buf;
397
-		}
398
-	}
399
-
400
-	/**
401
-	 * Handle element names.
402
-	 * This will call the EventHandler::elementName().
403
-	 *
404
-	 * This handles:
405
-	 * <code>
406
-	 *  name (EventHandler::element())
407
-	 *  |name (EventHandler::element())
408
-	 *  ns|name (EventHandler::elementNS())
409
-	 *  ns|* (EventHandler::elementNS())
410
-	 * </code>
411
-	 */
412
-	private function elementName()
413
-	{
414
-		if ($this->DEBUG) {
415
-			print "ELEMENT NAME\n";
416
-		}
417
-		if ($this->scanner->token === Token::PIPE) {
418
-			// We have '|name', which is equiv to 'name'
419
-			$this->scanner->nextToken();
420
-			$this->consumeWhitespace();
421
-			$elementName = $this->scanner->getNameString();
422
-			$this->handler->element($elementName);
423
-		} elseif ($this->scanner->token === Token::CHAR) {
424
-			$elementName = $this->scanner->getNameString();
425
-			if ($this->scanner->token == Token::PIPE) {
426
-				// Get ns|name
427
-				$elementNS = $elementName;
428
-				$this->scanner->nextToken();
429
-				$this->consumeWhitespace();
430
-				if ($this->scanner->token === Token::STAR) {
431
-					// We have ns|*
432
-					$this->handler->anyElementInNS($elementNS);
433
-					$this->scanner->nextToken();
434
-				} elseif ($this->scanner->token !== Token::CHAR) {
435
-					$this->throwError(Token::CHAR, $this->scanner->token);
436
-				} else {
437
-					$elementName = $this->scanner->getNameString();
438
-					// We have ns|name
439
-					$this->handler->elementNS($elementName, $elementNS);
440
-				}
441
-			} else {
442
-				$this->handler->element($elementName);
443
-			}
444
-		}
445
-	}
446
-
447
-	/**
448
-	 * Check for all elements designators. Due to the new CSS 3 namespace
449
-	 * support, this is slightly more complicated, now, as it handles
450
-	 * the *|name and *|* cases as well as *.
451
-	 *
452
-	 * Calls EventHandler::anyElement() or EventHandler::elementName().
453
-	 */
454
-	private function allElements()
455
-	{
456
-		if ($this->scanner->token === Token::STAR) {
457
-			$this->scanner->nextToken();
458
-			if ($this->scanner->token === Token::PIPE) {
459
-				$this->scanner->nextToken();
460
-				if ($this->scanner->token === Token::STAR) {
461
-					// We got *|*. According to spec, this requires
462
-					// that the element has a namespace, so we pass it on
463
-					// to the handler:
464
-					$this->scanner->nextToken();
465
-					$this->handler->anyElementInNS('*');
466
-				} else {
467
-					// We got *|name, which means the name MUST be in a namespce,
468
-					// so we pass this off to elementNameNS().
469
-					$name = $this->scanner->getNameString();
470
-					$this->handler->elementNS($name, '*');
471
-				}
472
-			} else {
473
-				$this->handler->anyElement();
474
-			}
475
-		}
476
-	}
477
-
478
-	/**
479
-	 * Handler an attribute.
480
-	 * An attribute can be in one of two forms:
481
-	 * <code>[attrName]</code>
482
-	 * or
483
-	 * <code>[attrName="AttrValue"]</code>
484
-	 *
485
-	 * This may call the following event handlers: EventHandler::attribute().
486
-	 *
487
-	 * @throws ParseException
488
-	 * @throws Exception
489
-	 */
490
-	private function attribute()
491
-	{
492
-		if ($this->scanner->token === Token::LSQUARE) {
493
-			$attrVal = $op = $ns = null;
494
-
495
-			$this->scanner->nextToken();
496
-			$this->consumeWhitespace();
497
-
498
-			if ($this->scanner->token === Token::AT) {
499
-				if ($this->strict) {
500
-					throw new ParseException('The @ is illegal in attributes.');
501
-				}
502
-
503
-				$this->scanner->nextToken();
504
-				$this->consumeWhitespace();
505
-			}
506
-
507
-			if ($this->scanner->token === Token::STAR) {
508
-				// Global namespace... requires that attr be prefixed,
509
-				// so we pass this on to a namespace handler.
510
-				$ns = '*';
511
-				$this->scanner->nextToken();
512
-			}
513
-			if ($this->scanner->token === Token::PIPE) {
514
-				// Skip this. It's a global namespace.
515
-				$this->scanner->nextToken();
516
-				$this->consumeWhitespace();
517
-			}
518
-
519
-			$attrName = $this->scanner->getNameString();
520
-			$this->consumeWhitespace();
521
-
522
-			// Check for namespace attribute: ns|attr. We have to peek() to make
523
-			// sure that we haven't hit the |= operator, which looks the same.
524
-			if ($this->scanner->token === Token::PIPE && $this->scanner->peek() !== '=') {
525
-				// We have a namespaced attribute.
526
-				$ns = $attrName;
527
-				$this->scanner->nextToken();
528
-				$attrName = $this->scanner->getNameString();
529
-				$this->consumeWhitespace();
530
-			}
531
-
532
-			// Note: We require that operators do not have spaces
533
-			// between characters, e.g. ~= , not ~ =.
534
-
535
-			// Get the operator:
536
-			switch ($this->scanner->token) {
537
-				case Token::EQ:
538
-					$this->consumeWhitespace();
539
-					$op = EventHandler::IS_EXACTLY;
540
-					break;
541
-				case Token::TILDE:
542
-					if ($this->scanner->nextToken() !== Token::EQ) {
543
-						$this->throwError(Token::EQ, $this->scanner->token);
544
-					}
545
-					$op = EventHandler::CONTAINS_WITH_SPACE;
546
-					break;
547
-				case Token::PIPE:
548
-					if ($this->scanner->nextToken() !== Token::EQ) {
549
-						$this->throwError(Token::EQ, $this->scanner->token);
550
-					}
551
-					$op = EventHandler::CONTAINS_WITH_HYPHEN;
552
-					break;
553
-				case Token::STAR:
554
-					if ($this->scanner->nextToken() !== Token::EQ) {
555
-						$this->throwError(Token::EQ, $this->scanner->token);
556
-					}
557
-					$op = EventHandler::CONTAINS_IN_STRING;
558
-					break;
559
-				case Token::DOLLAR:
560
-					if ($this->scanner->nextToken() !== Token::EQ) {
561
-						$this->throwError(Token::EQ, $this->scanner->token);
562
-					}
563
-					$op = EventHandler::ENDS_WITH;
564
-					break;
565
-				case Token::CARAT:
566
-					if ($this->scanner->nextToken() !== Token::EQ) {
567
-						$this->throwError(Token::EQ, $this->scanner->token);
568
-					}
569
-					$op = EventHandler::BEGINS_WITH;
570
-					break;
571
-			}
572
-
573
-			if (isset($op)) {
574
-				// Consume '=' and go on.
575
-				$this->scanner->nextToken();
576
-				$this->consumeWhitespace();
577
-
578
-				// So... here we have a problem. The grammer suggests that the
579
-				// value here is String1 or String2, both of which are enclosed
580
-				// in quotes of some sort, and both of which allow lots of special
581
-				// characters. But the spec itself includes examples like this:
582
-				//   [lang=fr]
583
-				// So some bareword support is assumed. To get around this, we assume
584
-				// that bare words follow the NAME rules, while quoted strings follow
585
-				// the String1/String2 rules.
586
-
587
-				if ($this->scanner->token === Token::QUOTE || $this->scanner->token === Token::SQUOTE) {
588
-					$attrVal = $this->scanner->getQuotedString();
589
-				} else {
590
-					$attrVal = $this->scanner->getNameString();
591
-				}
592
-
593
-				if ($this->DEBUG) {
594
-					print "ATTR: $attrVal AND OP: $op\n";
595
-				}
596
-			}
597
-
598
-			$this->consumeWhitespace();
599
-
600
-			if ($this->scanner->token !== Token::RSQUARE) {
601
-				$this->throwError(Token::RSQUARE, $this->scanner->token);
602
-			}
603
-
604
-			if (isset($ns)) {
605
-				$this->handler->attributeNS($attrName, $ns, $attrVal, $op);
606
-			} elseif (isset($attrVal)) {
607
-				$this->handler->attribute($attrName, $attrVal, $op);
608
-			} else {
609
-				$this->handler->attribute($attrName);
610
-			}
611
-			$this->scanner->nextToken();
612
-		}
613
-	}
614
-
615
-	/**
616
-	 * Utility for throwing a consistantly-formatted parse error.
617
-	 */
618
-	private function throwError($expected, $got)
619
-	{
620
-		$filter = sprintf('Expected %s, got %s', Token::name($expected), Token::name($got));
621
-		throw new ParseException($filter);
622
-	}
623
-
624
-	/**
625
-	 * @return Scanner
626
-	 */
627
-	public function getScanner(): Scanner
628
-	{
629
-		return $this->scanner;
630
-	}
393
+            //$buf .= $this->scanner->getQuotedString();
394
+            $buf .= $this->scanner->getPseudoClassString();
395
+
396
+            return $buf;
397
+        }
398
+    }
399
+
400
+    /**
401
+     * Handle element names.
402
+     * This will call the EventHandler::elementName().
403
+     *
404
+     * This handles:
405
+     * <code>
406
+     *  name (EventHandler::element())
407
+     *  |name (EventHandler::element())
408
+     *  ns|name (EventHandler::elementNS())
409
+     *  ns|* (EventHandler::elementNS())
410
+     * </code>
411
+     */
412
+    private function elementName()
413
+    {
414
+        if ($this->DEBUG) {
415
+            print "ELEMENT NAME\n";
416
+        }
417
+        if ($this->scanner->token === Token::PIPE) {
418
+            // We have '|name', which is equiv to 'name'
419
+            $this->scanner->nextToken();
420
+            $this->consumeWhitespace();
421
+            $elementName = $this->scanner->getNameString();
422
+            $this->handler->element($elementName);
423
+        } elseif ($this->scanner->token === Token::CHAR) {
424
+            $elementName = $this->scanner->getNameString();
425
+            if ($this->scanner->token == Token::PIPE) {
426
+                // Get ns|name
427
+                $elementNS = $elementName;
428
+                $this->scanner->nextToken();
429
+                $this->consumeWhitespace();
430
+                if ($this->scanner->token === Token::STAR) {
431
+                    // We have ns|*
432
+                    $this->handler->anyElementInNS($elementNS);
433
+                    $this->scanner->nextToken();
434
+                } elseif ($this->scanner->token !== Token::CHAR) {
435
+                    $this->throwError(Token::CHAR, $this->scanner->token);
436
+                } else {
437
+                    $elementName = $this->scanner->getNameString();
438
+                    // We have ns|name
439
+                    $this->handler->elementNS($elementName, $elementNS);
440
+                }
441
+            } else {
442
+                $this->handler->element($elementName);
443
+            }
444
+        }
445
+    }
446
+
447
+    /**
448
+     * Check for all elements designators. Due to the new CSS 3 namespace
449
+     * support, this is slightly more complicated, now, as it handles
450
+     * the *|name and *|* cases as well as *.
451
+     *
452
+     * Calls EventHandler::anyElement() or EventHandler::elementName().
453
+     */
454
+    private function allElements()
455
+    {
456
+        if ($this->scanner->token === Token::STAR) {
457
+            $this->scanner->nextToken();
458
+            if ($this->scanner->token === Token::PIPE) {
459
+                $this->scanner->nextToken();
460
+                if ($this->scanner->token === Token::STAR) {
461
+                    // We got *|*. According to spec, this requires
462
+                    // that the element has a namespace, so we pass it on
463
+                    // to the handler:
464
+                    $this->scanner->nextToken();
465
+                    $this->handler->anyElementInNS('*');
466
+                } else {
467
+                    // We got *|name, which means the name MUST be in a namespce,
468
+                    // so we pass this off to elementNameNS().
469
+                    $name = $this->scanner->getNameString();
470
+                    $this->handler->elementNS($name, '*');
471
+                }
472
+            } else {
473
+                $this->handler->anyElement();
474
+            }
475
+        }
476
+    }
477
+
478
+    /**
479
+     * Handler an attribute.
480
+     * An attribute can be in one of two forms:
481
+     * <code>[attrName]</code>
482
+     * or
483
+     * <code>[attrName="AttrValue"]</code>
484
+     *
485
+     * This may call the following event handlers: EventHandler::attribute().
486
+     *
487
+     * @throws ParseException
488
+     * @throws Exception
489
+     */
490
+    private function attribute()
491
+    {
492
+        if ($this->scanner->token === Token::LSQUARE) {
493
+            $attrVal = $op = $ns = null;
494
+
495
+            $this->scanner->nextToken();
496
+            $this->consumeWhitespace();
497
+
498
+            if ($this->scanner->token === Token::AT) {
499
+                if ($this->strict) {
500
+                    throw new ParseException('The @ is illegal in attributes.');
501
+                }
502
+
503
+                $this->scanner->nextToken();
504
+                $this->consumeWhitespace();
505
+            }
506
+
507
+            if ($this->scanner->token === Token::STAR) {
508
+                // Global namespace... requires that attr be prefixed,
509
+                // so we pass this on to a namespace handler.
510
+                $ns = '*';
511
+                $this->scanner->nextToken();
512
+            }
513
+            if ($this->scanner->token === Token::PIPE) {
514
+                // Skip this. It's a global namespace.
515
+                $this->scanner->nextToken();
516
+                $this->consumeWhitespace();
517
+            }
518
+
519
+            $attrName = $this->scanner->getNameString();
520
+            $this->consumeWhitespace();
521
+
522
+            // Check for namespace attribute: ns|attr. We have to peek() to make
523
+            // sure that we haven't hit the |= operator, which looks the same.
524
+            if ($this->scanner->token === Token::PIPE && $this->scanner->peek() !== '=') {
525
+                // We have a namespaced attribute.
526
+                $ns = $attrName;
527
+                $this->scanner->nextToken();
528
+                $attrName = $this->scanner->getNameString();
529
+                $this->consumeWhitespace();
530
+            }
531
+
532
+            // Note: We require that operators do not have spaces
533
+            // between characters, e.g. ~= , not ~ =.
534
+
535
+            // Get the operator:
536
+            switch ($this->scanner->token) {
537
+                case Token::EQ:
538
+                    $this->consumeWhitespace();
539
+                    $op = EventHandler::IS_EXACTLY;
540
+                    break;
541
+                case Token::TILDE:
542
+                    if ($this->scanner->nextToken() !== Token::EQ) {
543
+                        $this->throwError(Token::EQ, $this->scanner->token);
544
+                    }
545
+                    $op = EventHandler::CONTAINS_WITH_SPACE;
546
+                    break;
547
+                case Token::PIPE:
548
+                    if ($this->scanner->nextToken() !== Token::EQ) {
549
+                        $this->throwError(Token::EQ, $this->scanner->token);
550
+                    }
551
+                    $op = EventHandler::CONTAINS_WITH_HYPHEN;
552
+                    break;
553
+                case Token::STAR:
554
+                    if ($this->scanner->nextToken() !== Token::EQ) {
555
+                        $this->throwError(Token::EQ, $this->scanner->token);
556
+                    }
557
+                    $op = EventHandler::CONTAINS_IN_STRING;
558
+                    break;
559
+                case Token::DOLLAR:
560
+                    if ($this->scanner->nextToken() !== Token::EQ) {
561
+                        $this->throwError(Token::EQ, $this->scanner->token);
562
+                    }
563
+                    $op = EventHandler::ENDS_WITH;
564
+                    break;
565
+                case Token::CARAT:
566
+                    if ($this->scanner->nextToken() !== Token::EQ) {
567
+                        $this->throwError(Token::EQ, $this->scanner->token);
568
+                    }
569
+                    $op = EventHandler::BEGINS_WITH;
570
+                    break;
571
+            }
572
+
573
+            if (isset($op)) {
574
+                // Consume '=' and go on.
575
+                $this->scanner->nextToken();
576
+                $this->consumeWhitespace();
577
+
578
+                // So... here we have a problem. The grammer suggests that the
579
+                // value here is String1 or String2, both of which are enclosed
580
+                // in quotes of some sort, and both of which allow lots of special
581
+                // characters. But the spec itself includes examples like this:
582
+                //   [lang=fr]
583
+                // So some bareword support is assumed. To get around this, we assume
584
+                // that bare words follow the NAME rules, while quoted strings follow
585
+                // the String1/String2 rules.
586
+
587
+                if ($this->scanner->token === Token::QUOTE || $this->scanner->token === Token::SQUOTE) {
588
+                    $attrVal = $this->scanner->getQuotedString();
589
+                } else {
590
+                    $attrVal = $this->scanner->getNameString();
591
+                }
592
+
593
+                if ($this->DEBUG) {
594
+                    print "ATTR: $attrVal AND OP: $op\n";
595
+                }
596
+            }
597
+
598
+            $this->consumeWhitespace();
599
+
600
+            if ($this->scanner->token !== Token::RSQUARE) {
601
+                $this->throwError(Token::RSQUARE, $this->scanner->token);
602
+            }
603
+
604
+            if (isset($ns)) {
605
+                $this->handler->attributeNS($attrName, $ns, $attrVal, $op);
606
+            } elseif (isset($attrVal)) {
607
+                $this->handler->attribute($attrName, $attrVal, $op);
608
+            } else {
609
+                $this->handler->attribute($attrName);
610
+            }
611
+            $this->scanner->nextToken();
612
+        }
613
+    }
614
+
615
+    /**
616
+     * Utility for throwing a consistantly-formatted parse error.
617
+     */
618
+    private function throwError($expected, $got)
619
+    {
620
+        $filter = sprintf('Expected %s, got %s', Token::name($expected), Token::name($got));
621
+        throw new ParseException($filter);
622
+    }
623
+
624
+    /**
625
+     * @return Scanner
626
+     */
627
+    public function getScanner(): Scanner
628
+    {
629
+        return $this->scanner;
630
+    }
631 631
 }
Please login to merge, or discard this patch.
src/CSS/InputStream.php 2 patches
Indentation   +46 added lines, -46 removed lines patch added patch discarded remove patch
@@ -14,55 +14,55 @@
 block discarded – undo
14 14
  */
15 15
 class InputStream
16 16
 {
17
-	protected $stream;
18
-	public $position = 0;
17
+    protected $stream;
18
+    public $position = 0;
19 19
 
20
-	/**
21
-	 * Build a new CSS input stream from a string.
22
-	 *
23
-	 * @param string
24
-	 *  String to turn into an input stream.
25
-	 */
26
-	public function __construct($string)
27
-	{
28
-		$this->stream = str_split($string);
29
-	}
20
+    /**
21
+     * Build a new CSS input stream from a string.
22
+     *
23
+     * @param string
24
+     *  String to turn into an input stream.
25
+     */
26
+    public function __construct($string)
27
+    {
28
+        $this->stream = str_split($string);
29
+    }
30 30
 
31
-	/**
32
-	 * Look ahead one character.
33
-	 *
34
-	 * @return char
35
-	 *  Returns the next character, but does not remove it from
36
-	 *  the stream.
37
-	 */
38
-	public function peek()
39
-	{
40
-		return $this->stream[0];
41
-	}
31
+    /**
32
+     * Look ahead one character.
33
+     *
34
+     * @return char
35
+     *  Returns the next character, but does not remove it from
36
+     *  the stream.
37
+     */
38
+    public function peek()
39
+    {
40
+        return $this->stream[0];
41
+    }
42 42
 
43
-	/**
44
-	 * Get the next unconsumed character in the stream.
45
-	 * This will remove that character from the front of the
46
-	 * stream and return it.
47
-	 */
48
-	public function consume()
49
-	{
50
-		$ret = array_shift($this->stream);
51
-		if (! empty($ret)) {
52
-			$this->position++;
53
-		}
43
+    /**
44
+     * Get the next unconsumed character in the stream.
45
+     * This will remove that character from the front of the
46
+     * stream and return it.
47
+     */
48
+    public function consume()
49
+    {
50
+        $ret = array_shift($this->stream);
51
+        if (! empty($ret)) {
52
+            $this->position++;
53
+        }
54 54
 
55
-		return $ret;
56
-	}
55
+        return $ret;
56
+    }
57 57
 
58
-	/**
59
-	 * Check if the stream is empty.
60
-	 *
61
-	 * @return boolean
62
-	 *   Returns TRUE when the stream is empty, FALSE otherwise.
63
-	 */
64
-	public function isEmpty()
65
-	{
66
-		return count($this->stream) === 0;
67
-	}
58
+    /**
59
+     * Check if the stream is empty.
60
+     *
61
+     * @return boolean
62
+     *   Returns TRUE when the stream is empty, FALSE otherwise.
63
+     */
64
+    public function isEmpty()
65
+    {
66
+        return count($this->stream) === 0;
67
+    }
68 68
 }
Please login to merge, or discard this patch.
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -48,7 +48,7 @@
 block discarded – undo
48 48
 	public function consume()
49 49
 	{
50 50
 		$ret = array_shift($this->stream);
51
-		if (! empty($ret)) {
51
+		if (!empty($ret)) {
52 52
 			$this->position++;
53 53
 		}
54 54
 
Please login to merge, or discard this patch.
src/Helpers/QueryMutators.php 2 patches
Indentation   +1061 added lines, -1061 removed lines patch added patch discarded remove patch
@@ -15,1065 +15,1065 @@
 block discarded – undo
15 15
 trait QueryMutators
16 16
 {
17 17
 
18
-	/**
19
-	 * Empty everything within the specified element.
20
-	 *
21
-	 * A convenience function for removeChildren(). This is equivalent to jQuery's
22
-	 * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a
23
-	 * function name.
24
-	 *
25
-	 * @return DOMQuery
26
-	 *  The DOMQuery object with the newly emptied elements.
27
-	 * @see        removeChildren()
28
-	 * @since      2.1
29
-	 * @author     eabrand
30
-	 * @deprecated The removeChildren() function is the preferred method.
31
-	 */
32
-	public function emptyElement(): Query
33
-	{
34
-		$this->removeChildren();
35
-
36
-		return $this;
37
-	}
38
-
39
-	/**
40
-	 * Insert the given markup as the last child.
41
-	 *
42
-	 * The markup will be inserted into each match in the set.
43
-	 *
44
-	 * The same element cannot be inserted multiple times into a document. DOM
45
-	 * documents do not allow a single object to be inserted multiple times
46
-	 * into the DOM. To insert the same XML repeatedly, we must first clone
47
-	 * the object. This has one practical implication: Once you have inserted
48
-	 * an element into the object, you cannot further manipulate the original
49
-	 * element and expect the changes to be replciated in the appended object.
50
-	 * (They are not the same -- there is no shared reference.) Instead, you
51
-	 * will need to retrieve the appended object and operate on that.
52
-	 *
53
-	 * @param mixed $data
54
-	 *  This can be either a string (the usual case), or a DOM Element.
55
-	 *
56
-	 * @return DOMQuery
57
-	 *  The DOMQuery object.
58
-	 * @throws QueryPath::Exception
59
-	 *  Thrown if $data is an unsupported object type.
60
-	 * @throws Exception
61
-	 * @see appendTo()
62
-	 * @see prepend()
63
-	 */
64
-	public function append($data): Query
65
-	{
66
-		$data = $this->prepareInsert($data);
67
-		if (isset($data)) {
68
-			if (empty($this->document->documentElement) && $this->matches->count() === 0) {
69
-				// Then we assume we are writing to the doc root
70
-				$this->document->appendChild($data);
71
-				$found = new SplObjectStorage();
72
-				$found->attach($this->document->documentElement);
73
-				$this->setMatches($found);
74
-			} else {
75
-				// You can only append in item once. So in cases where we
76
-				// need to append multiple times, we have to clone the node.
77
-				foreach ($this->matches as $m) {
78
-					// DOMDocumentFragments are even more troublesome, as they don't
79
-					// always clone correctly. So we have to clone their children.
80
-					if ($data instanceof DOMDocumentFragment) {
81
-						foreach ($data->childNodes as $n) {
82
-							$m->appendChild($n->cloneNode(true));
83
-						}
84
-					} else {
85
-						// Otherwise a standard clone will do.
86
-						$m->appendChild($data->cloneNode(true));
87
-					}
88
-
89
-				}
90
-			}
91
-
92
-		}
93
-
94
-		return $this;
95
-	}
96
-
97
-	/**
98
-	 * Insert the given markup as the first child.
99
-	 *
100
-	 * The markup will be inserted into each match in the set.
101
-	 *
102
-	 * @param mixed $data
103
-	 *  This can be either a string (the usual case), or a DOM Element.
104
-	 *
105
-	 * @return DOMQuery
106
-	 * @throws QueryPath::Exception
107
-	 *  Thrown if $data is an unsupported object type.
108
-	 * @throws Exception
109
-	 * @see after()
110
-	 * @see prependTo()
111
-	 * @see append()
112
-	 * @see before()
113
-	 */
114
-	public function prepend($data): Query
115
-	{
116
-		$data = $this->prepareInsert($data);
117
-		if (isset($data)) {
118
-			foreach ($this->matches as $m) {
119
-				$ins = $data->cloneNode(true);
120
-				if ($m->hasChildNodes()) {
121
-					$m->insertBefore($ins, $m->childNodes->item(0));
122
-				} else {
123
-					$m->appendChild($ins);
124
-				}
125
-			}
126
-		}
127
-
128
-		return $this;
129
-	}
130
-
131
-	/**
132
-	 * Take all nodes in the current object and prepend them to the children nodes of
133
-	 * each matched node in the passed-in DOMQuery object.
134
-	 *
135
-	 * This will iterate through each item in the current DOMQuery object and
136
-	 * add each item to the beginning of the children of each element in the
137
-	 * passed-in DOMQuery object.
138
-	 *
139
-	 * @param DOMQuery $dest
140
-	 *  The destination DOMQuery object.
141
-	 *
142
-	 * @return DOMQuery
143
-	 *  The original DOMQuery, unmodified. NOT the destination DOMQuery.
144
-	 * @throws QueryPath::Exception
145
-	 *  Thrown if $data is an unsupported object type.
146
-	 * @see appendTo()
147
-	 * @see insertBefore()
148
-	 * @see insertAfter()
149
-	 * @see prepend()
150
-	 */
151
-	public function prependTo(Query $dest)
152
-	{
153
-		foreach ($this->matches as $m) {
154
-			$dest->prepend($m);
155
-		}
156
-
157
-		return $this;
158
-	}
159
-
160
-	/**
161
-	 * Insert the given data before each element in the current set of matches.
162
-	 *
163
-	 * This will take the give data (XML or HTML) and put it before each of the items that
164
-	 * the DOMQuery object currently contains. Contrast this with after().
165
-	 *
166
-	 * @param mixed $data
167
-	 *  The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
168
-	 *  or the other usual suspects. (See {@link qp()}).
169
-	 *
170
-	 * @return DOMQuery
171
-	 *  Returns the DOMQuery with the new modifications. The list of elements currently
172
-	 *  selected will remain the same.
173
-	 * @throws QueryPath::Exception
174
-	 *  Thrown if $data is an unsupported object type.
175
-	 * @throws Exception
176
-	 * @see append()
177
-	 * @see prepend()
178
-	 * @see insertBefore()
179
-	 * @see after()
180
-	 */
181
-	public function before($data): Query
182
-	{
183
-		$data = $this->prepareInsert($data);
184
-		foreach ($this->matches as $m) {
185
-			$ins = $data->cloneNode(true);
186
-			$m->parentNode->insertBefore($ins, $m);
187
-		}
188
-
189
-		return $this;
190
-	}
191
-
192
-	/**
193
-	 * Insert the current elements into the destination document.
194
-	 * The items are inserted before each element in the given DOMQuery document.
195
-	 * That is, they will be siblings with the current elements.
196
-	 *
197
-	 * @param Query $dest
198
-	 *  Destination DOMQuery document.
199
-	 *
200
-	 * @return DOMQuery
201
-	 *  The current DOMQuery object, unaltered. Only the destination DOMQuery
202
-	 *  object is altered.
203
-	 * @throws QueryPath::Exception
204
-	 *  Thrown if $data is an unsupported object type.
205
-	 * @see insertAfter()
206
-	 * @see appendTo()
207
-	 * @see before()
208
-	 */
209
-	public function insertBefore(Query $dest): Query
210
-	{
211
-		foreach ($this->matches as $m) {
212
-			$dest->before($m);
213
-		}
214
-
215
-		return $this;
216
-	}
217
-
218
-	/**
219
-	 * Insert the contents of the current DOMQuery after the nodes in the
220
-	 * destination DOMQuery object.
221
-	 *
222
-	 * @param Query $dest
223
-	 *  Destination object where the current elements will be deposited.
224
-	 *
225
-	 * @return DOMQuery
226
-	 *  The present DOMQuery, unaltered. Only the destination object is altered.
227
-	 * @throws QueryPath::Exception
228
-	 *  Thrown if $data is an unsupported object type.
229
-	 * @see insertBefore()
230
-	 * @see append()
231
-	 * @see after()
232
-	 */
233
-	public function insertAfter(Query $dest): Query
234
-	{
235
-		foreach ($this->matches as $m) {
236
-			$dest->after($m);
237
-		}
238
-
239
-		return $this;
240
-	}
241
-
242
-	/**
243
-	 * Insert the given data after each element in the current DOMQuery object.
244
-	 *
245
-	 * This inserts the element as a peer to the currently matched elements.
246
-	 * Contrast this with {@link append()}, which inserts the data as children
247
-	 * of matched elements.
248
-	 *
249
-	 * @param mixed $data
250
-	 *  The data to be appended.
251
-	 *
252
-	 * @return DOMQuery
253
-	 *  The DOMQuery object (with the items inserted).
254
-	 * @throws QueryPath::Exception
255
-	 *  Thrown if $data is an unsupported object type.
256
-	 * @throws Exception
257
-	 * @see before()
258
-	 * @see append()
259
-	 */
260
-	public function after($data): Query
261
-	{
262
-		if (empty($data)) {
263
-			return $this;
264
-		}
265
-		$data = $this->prepareInsert($data);
266
-		foreach ($this->matches as $m) {
267
-			$ins = $data->cloneNode(true);
268
-			if (isset($m->nextSibling)) {
269
-				$m->parentNode->insertBefore($ins, $m->nextSibling);
270
-			} else {
271
-				$m->parentNode->appendChild($ins);
272
-			}
273
-		}
274
-
275
-		return $this;
276
-	}
277
-
278
-	/**
279
-	 * Replace the existing element(s) in the list with a new one.
280
-	 *
281
-	 * @param mixed $new
282
-	 *  A DOMElement or XML in a string. This will replace all elements
283
-	 *  currently wrapped in the DOMQuery object.
284
-	 *
285
-	 * @return DOMQuery
286
-	 *  The DOMQuery object wrapping <b>the items that were removed</b>.
287
-	 *  This remains consistent with the jQuery API.
288
-	 * @throws Exception
289
-	 * @throws ParseException
290
-	 * @throws QueryPath
291
-	 * @see append()
292
-	 * @see prepend()
293
-	 * @see before()
294
-	 * @see after()
295
-	 * @see remove()
296
-	 * @see replaceAll()
297
-	 */
298
-	public function replaceWith($new): Query
299
-	{
300
-		$data  = $this->prepareInsert($new);
301
-		$found = new SplObjectStorage();
302
-		foreach ($this->matches as $m) {
303
-			$parent = $m->parentNode;
304
-			$parent->insertBefore($data->cloneNode(true), $m);
305
-			$found->attach($parent->removeChild($m));
306
-		}
307
-
308
-		return $this->inst($found, null);
309
-	}
310
-
311
-	/**
312
-	 * Remove the parent element from the selected node or nodes.
313
-	 *
314
-	 * This takes the given list of nodes and "unwraps" them, moving them out of their parent
315
-	 * node, and then deleting the parent node.
316
-	 *
317
-	 * For example, consider this:
318
-	 *
319
-	 * @code
320
-	 *   <root><wrapper><content/></wrapper></root>
321
-	 * @endcode
322
-	 *
323
-	 * Now we can run this code:
324
-	 * @code
325
-	 *   qp($xml, 'content')->unwrap();
326
-	 * @endcode
327
-	 *
328
-	 * This will result in:
329
-	 *
330
-	 * @code
331
-	 *   <root><content/></root>
332
-	 * @endcode
333
-	 * This is the opposite of wrap().
334
-	 *
335
-	 * <b>The root element cannot be unwrapped.</b> It has no parents.
336
-	 * If you attempt to use unwrap on a root element, this will throw a
337
-	 * QueryPath::Exception. (You can, however, "Unwrap" a child that is
338
-	 * a direct descendant of the root element. This will remove the root
339
-	 * element, and replace the child as the root element. Be careful, though.
340
-	 * You cannot set more than one child as a root element.)
341
-	 *
342
-	 * @return DOMQuery
343
-	 *  The DOMQuery object, with the same element(s) selected.
344
-	 * @throws Exception
345
-	 * @see    wrap()
346
-	 * @since  2.1
347
-	 * @author mbutcher
348
-	 */
349
-	public function unwrap(): Query
350
-	{
351
-		// We do this in two loops in order to
352
-		// capture the case where two matches are
353
-		// under the same parent. Othwerwise we might
354
-		// remove a match before we can move it.
355
-		$parents = new SplObjectStorage();
356
-		foreach ($this->matches as $m) {
357
-
358
-			// Cannot unwrap the root element.
359
-			if ($m->isSameNode($m->ownerDocument->documentElement)) {
360
-				throw new Exception('Cannot unwrap the root element.');
361
-			}
362
-
363
-			// Move children to peer of parent.
364
-			$parent = $m->parentNode;
365
-			$old    = $parent->removeChild($m);
366
-			$parent->parentNode->insertBefore($old, $parent);
367
-			$parents->attach($parent);
368
-		}
369
-
370
-		// Now that all the children are moved, we
371
-		// remove all of the parents.
372
-		foreach ($parents as $ele) {
373
-			$ele->parentNode->removeChild($ele);
374
-		}
375
-
376
-		return $this;
377
-	}
378
-
379
-	/**
380
-	 * Wrap each element inside of the given markup.
381
-	 *
382
-	 * Markup is usually a string, but it can also be a DOMNode, a document
383
-	 * fragment, a SimpleXMLElement, or another DOMNode object (in which case
384
-	 * the first item in the list will be used.)
385
-	 *
386
-	 * @param mixed $markup
387
-	 *  Markup that will wrap each element in the current list.
388
-	 *
389
-	 * @return DOMQuery
390
-	 *  The DOMQuery object with the wrapping changes made.
391
-	 * @throws Exception
392
-	 * @throws QueryPath
393
-	 * @see wrapAll()
394
-	 * @see wrapInner()
395
-	 */
396
-	public function wrap($markup): Query
397
-	{
398
-		$data = $this->prepareInsert($markup);
399
-
400
-		// If the markup passed in is empty, we don't do any wrapping.
401
-		if (empty($data)) {
402
-			return $this;
403
-		}
404
-
405
-		foreach ($this->matches as $m) {
406
-			if ($data instanceof DOMDocumentFragment) {
407
-				$copy = $data->firstChild->cloneNode(true);
408
-			} else {
409
-				$copy = $data->cloneNode(true);
410
-			}
411
-
412
-			// XXX: Should be able to avoid doing this over and over.
413
-			if ($copy->hasChildNodes()) {
414
-				$deepest = $this->deepestNode($copy);
415
-				// FIXME: Does this need a different data structure?
416
-				$bottom = $deepest[0];
417
-			} else {
418
-				$bottom = $copy;
419
-			}
420
-
421
-			$parent = $m->parentNode;
422
-			$parent->insertBefore($copy, $m);
423
-			$m = $parent->removeChild($m);
424
-			$bottom->appendChild($m);
425
-		}
426
-
427
-		return $this;
428
-	}
429
-
430
-	/**
431
-	 * Wrap all elements inside of the given markup.
432
-	 *
433
-	 * So all elements will be grouped together under this single marked up
434
-	 * item. This works by first determining the parent element of the first item
435
-	 * in the list. It then moves all of the matching elements under the wrapper
436
-	 * and inserts the wrapper where that first element was found. (This is in
437
-	 * accordance with the way jQuery works.)
438
-	 *
439
-	 * Markup is usually XML in a string, but it can also be a DOMNode, a document
440
-	 * fragment, a SimpleXMLElement, or another DOMNode object (in which case
441
-	 * the first item in the list will be used.)
442
-	 *
443
-	 * @param string $markup
444
-	 *  Markup that will wrap all elements in the current list.
445
-	 *
446
-	 * @return DOMQuery
447
-	 *  The DOMQuery object with the wrapping changes made.
448
-	 * @throws Exception
449
-	 * @throws QueryPath
450
-	 * @see wrap()
451
-	 * @see wrapInner()
452
-	 */
453
-	public function wrapAll($markup)
454
-	{
455
-		if ($this->matches->count() === 0) {
456
-			return;
457
-		}
458
-
459
-		$data = $this->prepareInsert($markup);
460
-
461
-		if (empty($data)) {
462
-			return $this;
463
-		}
464
-
465
-		if ($data instanceof DOMDocumentFragment) {
466
-			$data = $data->firstChild->cloneNode(true);
467
-		} else {
468
-			$data = $data->cloneNode(true);
469
-		}
470
-
471
-		if ($data->hasChildNodes()) {
472
-			$deepest = $this->deepestNode($data);
473
-			// FIXME: Does this need fixing?
474
-			$bottom = $deepest[0];
475
-		} else {
476
-			$bottom = $data;
477
-		}
478
-
479
-		$first  = $this->getFirstMatch();
480
-		$parent = $first->parentNode;
481
-		$parent->insertBefore($data, $first);
482
-		foreach ($this->matches as $m) {
483
-			$bottom->appendChild($m->parentNode->removeChild($m));
484
-		}
485
-
486
-		return $this;
487
-	}
488
-
489
-	/**
490
-	 * Wrap the child elements of each item in the list with the given markup.
491
-	 *
492
-	 * Markup is usually a string, but it can also be a DOMNode, a document
493
-	 * fragment, a SimpleXMLElement, or another DOMNode object (in which case
494
-	 * the first item in the list will be used.)
495
-	 *
496
-	 * @param string $markup
497
-	 *  Markup that will wrap children of each element in the current list.
498
-	 *
499
-	 * @return DOMQuery
500
-	 *  The DOMQuery object with the wrapping changes made.
501
-	 * @throws Exception
502
-	 * @throws QueryPath
503
-	 * @see wrap()
504
-	 * @see wrapAll()
505
-	 */
506
-	public function wrapInner($markup)
507
-	{
508
-		$data = $this->prepareInsert($markup);
509
-
510
-		// No data? Short circuit.
511
-		if (empty($data)) {
512
-			return $this;
513
-		}
514
-
515
-		foreach ($this->matches as $m) {
516
-			if ($data instanceof DOMDocumentFragment) {
517
-				$wrapper = $data->firstChild->cloneNode(true);
518
-			} else {
519
-				$wrapper = $data->cloneNode(true);
520
-			}
521
-
522
-			if ($wrapper->hasChildNodes()) {
523
-				$deepest = $this->deepestNode($wrapper);
524
-				// FIXME: ???
525
-				$bottom = $deepest[0];
526
-			} else {
527
-				$bottom = $wrapper;
528
-			}
529
-
530
-			if ($m->hasChildNodes()) {
531
-				while ($m->firstChild) {
532
-					$kid = $m->removeChild($m->firstChild);
533
-					$bottom->appendChild($kid);
534
-				}
535
-			}
536
-
537
-			$m->appendChild($wrapper);
538
-		}
539
-
540
-		return $this;
541
-	}
542
-
543
-	/**
544
-	 * Reduce the set of matches to the deepest child node in the tree.
545
-	 *
546
-	 * This loops through the matches and looks for the deepest child node of all of
547
-	 * the matches. "Deepest", here, is relative to the nodes in the list. It is
548
-	 * calculated as the distance from the starting node to the most distant child
549
-	 * node. In other words, it is not necessarily the farthest node from the root
550
-	 * element, but the farthest note from the matched element.
551
-	 *
552
-	 * In the case where there are multiple nodes at the same depth, all of the
553
-	 * nodes at that depth will be included.
554
-	 *
555
-	 * @return DOMQuery
556
-	 *  The DOMQuery wrapping the single deepest node.
557
-	 * @throws ParseException
558
-	 */
559
-	public function deepest(): Query
560
-	{
561
-		$deepest = 0;
562
-		$winner  = new SplObjectStorage();
563
-		foreach ($this->matches as $m) {
564
-			$local_deepest = 0;
565
-			$local_ele     = $this->deepestNode($m, 0, null, $local_deepest);
566
-
567
-			// Replace with the new deepest.
568
-			if ($local_deepest > $deepest) {
569
-				$winner = new SplObjectStorage();
570
-				foreach ($local_ele as $lele) {
571
-					$winner->attach($lele);
572
-				}
573
-				$deepest = $local_deepest;
574
-			} // Augument with other equally deep elements.
575
-			elseif ($local_deepest === $deepest) {
576
-				foreach ($local_ele as $lele) {
577
-					$winner->attach($lele);
578
-				}
579
-			}
580
-		}
581
-
582
-		return $this->inst($winner, null);
583
-	}
584
-
585
-	/**
586
-	 * Add a class to all elements in the current DOMQuery.
587
-	 *
588
-	 * This searchers for a class attribute on each item wrapped by the current
589
-	 * DOMNode object. If no attribute is found, a new one is added and its value
590
-	 * is set to $class. If a class attribute is found, then the value is appended
591
-	 * on to the end.
592
-	 *
593
-	 * @param string $class
594
-	 *  The name of the class.
595
-	 *
596
-	 * @return DOMQuery
597
-	 *  Returns the DOMQuery object.
598
-	 * @see css()
599
-	 * @see attr()
600
-	 * @see removeClass()
601
-	 * @see hasClass()
602
-	 */
603
-	public function addClass($class)
604
-	{
605
-		foreach ($this->matches as $m) {
606
-			if ($m->hasAttribute('class')) {
607
-				$val = $m->getAttribute('class');
608
-				$m->setAttribute('class', $val . ' ' . $class);
609
-			} else {
610
-				$m->setAttribute('class', $class);
611
-			}
612
-		}
613
-
614
-		return $this;
615
-	}
616
-
617
-	/**
618
-	 * Remove the named class from any element in the DOMQuery that has it.
619
-	 *
620
-	 * This may result in the entire class attribute being removed. If there
621
-	 * are other items in the class attribute, though, they will not be removed.
622
-	 *
623
-	 * Example:
624
-	 * Consider this XML:
625
-	 *
626
-	 * @code
627
-	 * <element class="first second"/>
628
-	 * @endcode
629
-	 *
630
-	 * Executing this fragment of code will remove only the 'first' class:
631
-	 * @code
632
-	 * qp(document, 'element')->removeClass('first');
633
-	 * @endcode
634
-	 *
635
-	 * The resulting XML will be:
636
-	 * @code
637
-	 * <element class="second"/>
638
-	 * @endcode
639
-	 *
640
-	 * To remove the entire 'class' attribute, you should use {@see removeAttr()}.
641
-	 *
642
-	 * @param string $class
643
-	 *  The class name to remove.
644
-	 *
645
-	 * @return DOMQuery
646
-	 *  The modified DOMNode object.
647
-	 * @see attr()
648
-	 * @see addClass()
649
-	 * @see hasClass()
650
-	 */
651
-	public function removeClass($class = false): Query
652
-	{
653
-		if (empty($class)) {
654
-			foreach ($this->matches as $m) {
655
-				$m->removeAttribute('class');
656
-			}
657
-		} else {
658
-			$to_remove = array_filter(explode(' ', $class));
659
-			foreach ($this->matches as $m) {
660
-				if ($m->hasAttribute('class')) {
661
-					$vals = array_filter(explode(' ', $m->getAttribute('class')));
662
-					$buf  = [];
663
-					foreach ($vals as $v) {
664
-						if (! in_array($v, $to_remove)) {
665
-							$buf[] = $v;
666
-						}
667
-					}
668
-					if (empty($buf)) {
669
-						$m->removeAttribute('class');
670
-					} else {
671
-						$m->setAttribute('class', implode(' ', $buf));
672
-					}
673
-				}
674
-			}
675
-		}
676
-
677
-		return $this;
678
-	}
679
-
680
-	/**
681
-	 * Detach any items from the list if they match the selector.
682
-	 *
683
-	 * In other words, each item that matches the selector will be removed
684
-	 * from the DOM document. The returned DOMQuery wraps the list of
685
-	 * removed elements.
686
-	 *
687
-	 * If no selector is specified, this will remove all current matches from
688
-	 * the document.
689
-	 *
690
-	 * @param string $selector
691
-	 *  A CSS Selector.
692
-	 *
693
-	 * @return DOMQuery
694
-	 *  The Query path wrapping a list of removed items.
695
-	 * @throws ParseException
696
-	 * @see    replaceWith()
697
-	 * @see    removeChildren()
698
-	 * @since  2.1
699
-	 * @author eabrand
700
-	 * @see    replaceAll()
701
-	 */
702
-	public function detach($selector = null): Query
703
-	{
704
-		if (null !== $selector) {
705
-			$this->find($selector);
706
-		}
707
-
708
-		$found      = new SplObjectStorage();
709
-		$this->last = $this->matches;
710
-		foreach ($this->matches as $item) {
711
-			// The item returned is (according to docs) different from
712
-			// the one passed in, so we have to re-store it.
713
-			$found->attach($item->parentNode->removeChild($item));
714
-		}
715
-
716
-		return $this->inst($found, null);
717
-	}
718
-
719
-	/**
720
-	 * Attach any items from the list if they match the selector.
721
-	 *
722
-	 * If no selector is specified, this will remove all current matches from
723
-	 * the document.
724
-	 *
725
-	 * @param DOMQuery $dest
726
-	 *  A DOMQuery Selector.
727
-	 *
728
-	 * @return DOMQuery
729
-	 *  The Query path wrapping a list of removed items.
730
-	 * @throws QueryPath
731
-	 * @throws Exception
732
-	 * @see    removeChildren()
733
-	 * @since  2.1
734
-	 * @author eabrand
735
-	 * @see    replaceAll()
736
-	 * @see    replaceWith()
737
-	 */
738
-	public function attach(DOMQuery $dest): Query
739
-	{
740
-		foreach ($this->last as $m) {
741
-			$dest->append($m);
742
-		}
743
-
744
-		return $this;
745
-	}
746
-
747
-	/**
748
-	 * Append the current elements to the destination passed into the function.
749
-	 *
750
-	 * This cycles through all of the current matches and appends them to
751
-	 * the context given in $destination. If a selector is provided then the
752
-	 * $destination is queried (using that selector) prior to the data being
753
-	 * appended. The data is then appended to the found items.
754
-	 *
755
-	 * @param DOMQuery $dest
756
-	 *  A DOMQuery object that will be appended to.
757
-	 *
758
-	 * @return DOMQuery
759
-	 *  The original DOMQuery, unaltered. Only the destination DOMQuery will
760
-	 *  be modified.
761
-	 * @throws QueryPath::Exception
762
-	 *  Thrown if $data is an unsupported object type.
763
-	 * @throws Exception
764
-	 * @see append()
765
-	 * @see prependTo()
766
-	 */
767
-	public function appendTo(DOMQuery $dest): Query
768
-	{
769
-		foreach ($this->matches as $m) {
770
-			$dest->append($m);
771
-		}
772
-
773
-		return $this;
774
-	}
775
-
776
-	/**
777
-	 * Remove any items from the list if they match the selector.
778
-	 *
779
-	 * In other words, each item that matches the selector will be remove
780
-	 * from the DOM document. The returned DOMQuery wraps the list of
781
-	 * removed elements.
782
-	 *
783
-	 * If no selector is specified, this will remove all current matches from
784
-	 * the document.
785
-	 *
786
-	 * @param string $selector
787
-	 *  A CSS Selector.
788
-	 *
789
-	 * @return DOMQuery
790
-	 *  The Query path wrapping a list of removed items.
791
-	 * @throws ParseException
792
-	 * @see replaceWith()
793
-	 * @see removeChildren()
794
-	 * @see replaceAll()
795
-	 */
796
-	public function remove($selector = null): Query
797
-	{
798
-		if (! empty($selector)) {
799
-			// Do a non-destructive find.
800
-			$query = new QueryPathEventHandler($this->matches);
801
-			$query->find($selector);
802
-			$matches = $query->getMatches();
803
-		} else {
804
-			$matches = $this->matches;
805
-		}
806
-
807
-		$found = new SplObjectStorage();
808
-		foreach ($matches as $item) {
809
-			// The item returned is (according to docs) different from
810
-			// the one passed in, so we have to re-store it.
811
-			$found->attach($item->parentNode->removeChild($item));
812
-		}
813
-
814
-		// Return a clone DOMQuery with just the removed items. If
815
-		// no items are found, this will return an empty DOMQuery.
816
-		return count($found) === 0 ? new static() : new static($found);
817
-	}
818
-
819
-	/**
820
-	 * This replaces everything that matches the selector with the first value
821
-	 * in the current list.
822
-	 *
823
-	 * This is the reverse of replaceWith.
824
-	 *
825
-	 * Unlike jQuery, DOMQuery cannot assume a default document. Consequently,
826
-	 * you must specify the intended destination document. If it is omitted, the
827
-	 * present document is assumed to be tthe document. However, that can result
828
-	 * in undefined behavior if the selector and the replacement are not sufficiently
829
-	 * distinct.
830
-	 *
831
-	 * @param string       $selector
832
-	 *  The selector.
833
-	 * @param DOMDocument $document
834
-	 *  The destination document.
835
-	 *
836
-	 * @return DOMQuery
837
-	 *  The DOMQuery wrapping the modified document.
838
-	 * @throws ParseException
839
-	 * @see        remove()
840
-	 * @see        replaceWith()
841
-	 * @deprecated Due to the fact that this is not a particularly friendly method,
842
-	 *             and that it can be easily replicated using {@see replaceWith()}, it is to be
843
-	 *             considered deprecated.
844
-	 */
845
-	public function replaceAll($selector, DOMDocument $document): Query
846
-	{
847
-		$replacement = $this->matches->count() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');
848
-
849
-		$c = new QueryPathEventHandler($document);
850
-		$c->find($selector);
851
-		$temp = $c->getMatches();
852
-		foreach ($temp as $item) {
853
-			$node = $replacement->cloneNode();
854
-			$node = $document->importNode($node);
855
-			$item->parentNode->replaceChild($node, $item);
856
-		}
857
-
858
-		return QueryPath::with($document, null, $this->options);
859
-	}
860
-
861
-	/**
862
-	 * Add more elements to the current set of matches.
863
-	 *
864
-	 * This begins the new query at the top of the DOM again. The results found
865
-	 * when running this selector are then merged into the existing results. In
866
-	 * this way, you can add additional elements to the existing set.
867
-	 *
868
-	 * @param string $selector
869
-	 *  A valid selector.
870
-	 *
871
-	 * @return DOMQuery
872
-	 *  The DOMQuery object with the newly added elements.
873
-	 * @see append()
874
-	 * @see after()
875
-	 * @see andSelf()
876
-	 * @see end()
877
-	 */
878
-	public function add($selector): Query
879
-	{
880
-
881
-		// This is destructive, so we need to set $last:
882
-		$this->last = $this->matches;
883
-
884
-		foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) {
885
-			$this->matches->attach($item);
886
-		}
887
-
888
-		return $this;
889
-	}
890
-
891
-	/**
892
-	 * Remove all child nodes.
893
-	 *
894
-	 * This is equivalent to jQuery's empty() function. (However, empty() is a
895
-	 * PHP built-in, and cannot be used as a method name.)
896
-	 *
897
-	 * @return DOMQuery
898
-	 *  The DOMQuery object with the child nodes removed.
899
-	 * @see replaceWith()
900
-	 * @see replaceAll()
901
-	 * @see remove()
902
-	 */
903
-	public function removeChildren(): Query
904
-	{
905
-		foreach ($this->matches as $m) {
906
-			while ($kid = $m->firstChild) {
907
-				$m->removeChild($kid);
908
-			}
909
-		}
910
-
911
-		return $this;
912
-	}
913
-
914
-	/**
915
-	 * Get/set an attribute.
916
-	 * - If no parameters are specified, this returns an associative array of all
917
-	 *   name/value pairs.
918
-	 * - If both $name and $value are set, then this will set the attribute name/value
919
-	 *   pair for all items in this object.
920
-	 * - If $name is set, and is an array, then
921
-	 *   all attributes in the array will be set for all items in this object.
922
-	 * - If $name is a string and is set, then the attribute value will be returned.
923
-	 *
924
-	 * When an attribute value is retrieved, only the attribute value of the FIRST
925
-	 * match is returned.
926
-	 *
927
-	 * @param mixed  $name
928
-	 *   The name of the attribute or an associative array of name/value pairs.
929
-	 * @param string $value
930
-	 *   A value (used only when setting an individual property).
931
-	 *
932
-	 * @return mixed
933
-	 *   If this was a setter request, return the DOMQuery object. If this was
934
-	 *   an access request (getter), return the string value.
935
-	 * @see removeAttr()
936
-	 * @see tag()
937
-	 * @see hasAttr()
938
-	 * @see hasClass()
939
-	 */
940
-	public function attr($name = null, $value = null)
941
-	{
942
-		// Default case: Return all attributes as an assoc array.
943
-		if (is_null($name)) {
944
-			if ($this->matches->count() === 0) {
945
-				return null;
946
-			}
947
-			$ele    = $this->getFirstMatch();
948
-			$buffer = [];
949
-
950
-			// This does not appear to be part of the DOM
951
-			// spec. Nor is it documented. But it works.
952
-			foreach ($ele->attributes as $name => $attrNode) {
953
-				$buffer[$name] = $attrNode->value;
954
-			}
955
-
956
-			return $buffer;
957
-		}
958
-
959
-		// multi-setter
960
-		if (is_array($name)) {
961
-			foreach ($name as $k => $v) {
962
-				foreach ($this->matches as $m) {
963
-					$m->setAttribute($k, $v);
964
-				}
965
-			}
966
-
967
-			return $this;
968
-		}
969
-		// setter
970
-		if (isset($value)) {
971
-			foreach ($this->matches as $m) {
972
-				$m->setAttribute($name, $value);
973
-			}
974
-
975
-			return $this;
976
-		}
977
-
978
-		//getter
979
-		if ($this->matches->count() === 0) {
980
-			return null;
981
-		}
982
-
983
-		// Special node type handler:
984
-		if ($name === 'nodeType') {
985
-			return $this->getFirstMatch()->nodeType;
986
-		}
987
-
988
-		// Always return first match's attr.
989
-		return $this->getFirstMatch()->getAttribute($name);
990
-	}
991
-
992
-	/**
993
-	 * Set/get a CSS value for the current element(s).
994
-	 * This sets the CSS value for each element in the DOMQuery object.
995
-	 * It does this by setting (or getting) the style attribute (without a namespace).
996
-	 *
997
-	 * For example, consider this code:
998
-	 *
999
-	 * @code
1000
-	 * <?php
1001
-	 * qp(HTML_STUB, 'body')->css('background-color','red')->html();
1002
-	 * ?>
1003
-	 * @endcode
1004
-	 * This will return the following HTML:
1005
-	 * @code
1006
-	 * <body style="background-color: red"/>
1007
-	 * @endcode
1008
-	 *
1009
-	 * If no parameters are passed into this function, then the current style
1010
-	 * element will be returned unparsed. Example:
1011
-	 * @code
1012
-	 * <?php
1013
-	 * qp(HTML_STUB, 'body')->css('background-color','red')->css();
1014
-	 * ?>
1015
-	 * @endcode
1016
-	 * This will return the following:
1017
-	 * @code
1018
-	 * background-color: red
1019
-	 * @endcode
1020
-	 *
1021
-	 * As of QueryPath 2.1, existing style attributes will be merged with new attributes.
1022
-	 * (In previous versions of QueryPath, a call to css() overwrite the existing style
1023
-	 * values).
1024
-	 *
1025
-	 * @param mixed  $name
1026
-	 *  If this is a string, it will be used as a CSS name. If it is an array,
1027
-	 *  this will assume it is an array of name/value pairs of CSS rules. It will
1028
-	 *  apply all rules to all elements in the set.
1029
-	 * @param string $value
1030
-	 *  The value to set. This is only set if $name is a string.
1031
-	 *
1032
-	 * @return DOMQuery
1033
-	 */
1034
-	public function css($name = null, $value = '')
1035
-	{
1036
-		if (empty($name)) {
1037
-			return $this->attr('style');
1038
-		}
1039
-
1040
-		// Get any existing CSS.
1041
-		$css = [];
1042
-		foreach ($this->matches as $match) {
1043
-			$style = $match->getAttribute('style');
1044
-			if (! empty($style)) {
1045
-				// XXX: Is this sufficient?
1046
-				$style_array = explode(';', $style);
1047
-				foreach ($style_array as $item) {
1048
-					$item = trim($item);
1049
-
1050
-					// Skip empty attributes.
1051
-					if ($item === '') {
1052
-						continue;
1053
-					}
1054
-
1055
-					[$css_att, $css_val] = explode(':', $item, 2);
1056
-					$css[$css_att] = trim($css_val);
1057
-				}
1058
-			}
1059
-		}
1060
-
1061
-		if (is_array($name)) {
1062
-			// Use array_merge instead of + to preserve order.
1063
-			$css = array_merge($css, $name);
1064
-		} else {
1065
-			$css[$name] = $value;
1066
-		}
1067
-
1068
-		// Collapse CSS into a string.
1069
-		$format     = '%s: %s;';
1070
-		$css_string = '';
1071
-		foreach ($css as $n => $v) {
1072
-			$css_string .= sprintf($format, $n, trim($v));
1073
-		}
1074
-
1075
-		$this->attr('style', $css_string);
1076
-
1077
-		return $this;
1078
-	}
18
+    /**
19
+     * Empty everything within the specified element.
20
+     *
21
+     * A convenience function for removeChildren(). This is equivalent to jQuery's
22
+     * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a
23
+     * function name.
24
+     *
25
+     * @return DOMQuery
26
+     *  The DOMQuery object with the newly emptied elements.
27
+     * @see        removeChildren()
28
+     * @since      2.1
29
+     * @author     eabrand
30
+     * @deprecated The removeChildren() function is the preferred method.
31
+     */
32
+    public function emptyElement(): Query
33
+    {
34
+        $this->removeChildren();
35
+
36
+        return $this;
37
+    }
38
+
39
+    /**
40
+     * Insert the given markup as the last child.
41
+     *
42
+     * The markup will be inserted into each match in the set.
43
+     *
44
+     * The same element cannot be inserted multiple times into a document. DOM
45
+     * documents do not allow a single object to be inserted multiple times
46
+     * into the DOM. To insert the same XML repeatedly, we must first clone
47
+     * the object. This has one practical implication: Once you have inserted
48
+     * an element into the object, you cannot further manipulate the original
49
+     * element and expect the changes to be replciated in the appended object.
50
+     * (They are not the same -- there is no shared reference.) Instead, you
51
+     * will need to retrieve the appended object and operate on that.
52
+     *
53
+     * @param mixed $data
54
+     *  This can be either a string (the usual case), or a DOM Element.
55
+     *
56
+     * @return DOMQuery
57
+     *  The DOMQuery object.
58
+     * @throws QueryPath::Exception
59
+     *  Thrown if $data is an unsupported object type.
60
+     * @throws Exception
61
+     * @see appendTo()
62
+     * @see prepend()
63
+     */
64
+    public function append($data): Query
65
+    {
66
+        $data = $this->prepareInsert($data);
67
+        if (isset($data)) {
68
+            if (empty($this->document->documentElement) && $this->matches->count() === 0) {
69
+                // Then we assume we are writing to the doc root
70
+                $this->document->appendChild($data);
71
+                $found = new SplObjectStorage();
72
+                $found->attach($this->document->documentElement);
73
+                $this->setMatches($found);
74
+            } else {
75
+                // You can only append in item once. So in cases where we
76
+                // need to append multiple times, we have to clone the node.
77
+                foreach ($this->matches as $m) {
78
+                    // DOMDocumentFragments are even more troublesome, as they don't
79
+                    // always clone correctly. So we have to clone their children.
80
+                    if ($data instanceof DOMDocumentFragment) {
81
+                        foreach ($data->childNodes as $n) {
82
+                            $m->appendChild($n->cloneNode(true));
83
+                        }
84
+                    } else {
85
+                        // Otherwise a standard clone will do.
86
+                        $m->appendChild($data->cloneNode(true));
87
+                    }
88
+
89
+                }
90
+            }
91
+
92
+        }
93
+
94
+        return $this;
95
+    }
96
+
97
+    /**
98
+     * Insert the given markup as the first child.
99
+     *
100
+     * The markup will be inserted into each match in the set.
101
+     *
102
+     * @param mixed $data
103
+     *  This can be either a string (the usual case), or a DOM Element.
104
+     *
105
+     * @return DOMQuery
106
+     * @throws QueryPath::Exception
107
+     *  Thrown if $data is an unsupported object type.
108
+     * @throws Exception
109
+     * @see after()
110
+     * @see prependTo()
111
+     * @see append()
112
+     * @see before()
113
+     */
114
+    public function prepend($data): Query
115
+    {
116
+        $data = $this->prepareInsert($data);
117
+        if (isset($data)) {
118
+            foreach ($this->matches as $m) {
119
+                $ins = $data->cloneNode(true);
120
+                if ($m->hasChildNodes()) {
121
+                    $m->insertBefore($ins, $m->childNodes->item(0));
122
+                } else {
123
+                    $m->appendChild($ins);
124
+                }
125
+            }
126
+        }
127
+
128
+        return $this;
129
+    }
130
+
131
+    /**
132
+     * Take all nodes in the current object and prepend them to the children nodes of
133
+     * each matched node in the passed-in DOMQuery object.
134
+     *
135
+     * This will iterate through each item in the current DOMQuery object and
136
+     * add each item to the beginning of the children of each element in the
137
+     * passed-in DOMQuery object.
138
+     *
139
+     * @param DOMQuery $dest
140
+     *  The destination DOMQuery object.
141
+     *
142
+     * @return DOMQuery
143
+     *  The original DOMQuery, unmodified. NOT the destination DOMQuery.
144
+     * @throws QueryPath::Exception
145
+     *  Thrown if $data is an unsupported object type.
146
+     * @see appendTo()
147
+     * @see insertBefore()
148
+     * @see insertAfter()
149
+     * @see prepend()
150
+     */
151
+    public function prependTo(Query $dest)
152
+    {
153
+        foreach ($this->matches as $m) {
154
+            $dest->prepend($m);
155
+        }
156
+
157
+        return $this;
158
+    }
159
+
160
+    /**
161
+     * Insert the given data before each element in the current set of matches.
162
+     *
163
+     * This will take the give data (XML or HTML) and put it before each of the items that
164
+     * the DOMQuery object currently contains. Contrast this with after().
165
+     *
166
+     * @param mixed $data
167
+     *  The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
168
+     *  or the other usual suspects. (See {@link qp()}).
169
+     *
170
+     * @return DOMQuery
171
+     *  Returns the DOMQuery with the new modifications. The list of elements currently
172
+     *  selected will remain the same.
173
+     * @throws QueryPath::Exception
174
+     *  Thrown if $data is an unsupported object type.
175
+     * @throws Exception
176
+     * @see append()
177
+     * @see prepend()
178
+     * @see insertBefore()
179
+     * @see after()
180
+     */
181
+    public function before($data): Query
182
+    {
183
+        $data = $this->prepareInsert($data);
184
+        foreach ($this->matches as $m) {
185
+            $ins = $data->cloneNode(true);
186
+            $m->parentNode->insertBefore($ins, $m);
187
+        }
188
+
189
+        return $this;
190
+    }
191
+
192
+    /**
193
+     * Insert the current elements into the destination document.
194
+     * The items are inserted before each element in the given DOMQuery document.
195
+     * That is, they will be siblings with the current elements.
196
+     *
197
+     * @param Query $dest
198
+     *  Destination DOMQuery document.
199
+     *
200
+     * @return DOMQuery
201
+     *  The current DOMQuery object, unaltered. Only the destination DOMQuery
202
+     *  object is altered.
203
+     * @throws QueryPath::Exception
204
+     *  Thrown if $data is an unsupported object type.
205
+     * @see insertAfter()
206
+     * @see appendTo()
207
+     * @see before()
208
+     */
209
+    public function insertBefore(Query $dest): Query
210
+    {
211
+        foreach ($this->matches as $m) {
212
+            $dest->before($m);
213
+        }
214
+
215
+        return $this;
216
+    }
217
+
218
+    /**
219
+     * Insert the contents of the current DOMQuery after the nodes in the
220
+     * destination DOMQuery object.
221
+     *
222
+     * @param Query $dest
223
+     *  Destination object where the current elements will be deposited.
224
+     *
225
+     * @return DOMQuery
226
+     *  The present DOMQuery, unaltered. Only the destination object is altered.
227
+     * @throws QueryPath::Exception
228
+     *  Thrown if $data is an unsupported object type.
229
+     * @see insertBefore()
230
+     * @see append()
231
+     * @see after()
232
+     */
233
+    public function insertAfter(Query $dest): Query
234
+    {
235
+        foreach ($this->matches as $m) {
236
+            $dest->after($m);
237
+        }
238
+
239
+        return $this;
240
+    }
241
+
242
+    /**
243
+     * Insert the given data after each element in the current DOMQuery object.
244
+     *
245
+     * This inserts the element as a peer to the currently matched elements.
246
+     * Contrast this with {@link append()}, which inserts the data as children
247
+     * of matched elements.
248
+     *
249
+     * @param mixed $data
250
+     *  The data to be appended.
251
+     *
252
+     * @return DOMQuery
253
+     *  The DOMQuery object (with the items inserted).
254
+     * @throws QueryPath::Exception
255
+     *  Thrown if $data is an unsupported object type.
256
+     * @throws Exception
257
+     * @see before()
258
+     * @see append()
259
+     */
260
+    public function after($data): Query
261
+    {
262
+        if (empty($data)) {
263
+            return $this;
264
+        }
265
+        $data = $this->prepareInsert($data);
266
+        foreach ($this->matches as $m) {
267
+            $ins = $data->cloneNode(true);
268
+            if (isset($m->nextSibling)) {
269
+                $m->parentNode->insertBefore($ins, $m->nextSibling);
270
+            } else {
271
+                $m->parentNode->appendChild($ins);
272
+            }
273
+        }
274
+
275
+        return $this;
276
+    }
277
+
278
+    /**
279
+     * Replace the existing element(s) in the list with a new one.
280
+     *
281
+     * @param mixed $new
282
+     *  A DOMElement or XML in a string. This will replace all elements
283
+     *  currently wrapped in the DOMQuery object.
284
+     *
285
+     * @return DOMQuery
286
+     *  The DOMQuery object wrapping <b>the items that were removed</b>.
287
+     *  This remains consistent with the jQuery API.
288
+     * @throws Exception
289
+     * @throws ParseException
290
+     * @throws QueryPath
291
+     * @see append()
292
+     * @see prepend()
293
+     * @see before()
294
+     * @see after()
295
+     * @see remove()
296
+     * @see replaceAll()
297
+     */
298
+    public function replaceWith($new): Query
299
+    {
300
+        $data  = $this->prepareInsert($new);
301
+        $found = new SplObjectStorage();
302
+        foreach ($this->matches as $m) {
303
+            $parent = $m->parentNode;
304
+            $parent->insertBefore($data->cloneNode(true), $m);
305
+            $found->attach($parent->removeChild($m));
306
+        }
307
+
308
+        return $this->inst($found, null);
309
+    }
310
+
311
+    /**
312
+     * Remove the parent element from the selected node or nodes.
313
+     *
314
+     * This takes the given list of nodes and "unwraps" them, moving them out of their parent
315
+     * node, and then deleting the parent node.
316
+     *
317
+     * For example, consider this:
318
+     *
319
+     * @code
320
+     *   <root><wrapper><content/></wrapper></root>
321
+     * @endcode
322
+     *
323
+     * Now we can run this code:
324
+     * @code
325
+     *   qp($xml, 'content')->unwrap();
326
+     * @endcode
327
+     *
328
+     * This will result in:
329
+     *
330
+     * @code
331
+     *   <root><content/></root>
332
+     * @endcode
333
+     * This is the opposite of wrap().
334
+     *
335
+     * <b>The root element cannot be unwrapped.</b> It has no parents.
336
+     * If you attempt to use unwrap on a root element, this will throw a
337
+     * QueryPath::Exception. (You can, however, "Unwrap" a child that is
338
+     * a direct descendant of the root element. This will remove the root
339
+     * element, and replace the child as the root element. Be careful, though.
340
+     * You cannot set more than one child as a root element.)
341
+     *
342
+     * @return DOMQuery
343
+     *  The DOMQuery object, with the same element(s) selected.
344
+     * @throws Exception
345
+     * @see    wrap()
346
+     * @since  2.1
347
+     * @author mbutcher
348
+     */
349
+    public function unwrap(): Query
350
+    {
351
+        // We do this in two loops in order to
352
+        // capture the case where two matches are
353
+        // under the same parent. Othwerwise we might
354
+        // remove a match before we can move it.
355
+        $parents = new SplObjectStorage();
356
+        foreach ($this->matches as $m) {
357
+
358
+            // Cannot unwrap the root element.
359
+            if ($m->isSameNode($m->ownerDocument->documentElement)) {
360
+                throw new Exception('Cannot unwrap the root element.');
361
+            }
362
+
363
+            // Move children to peer of parent.
364
+            $parent = $m->parentNode;
365
+            $old    = $parent->removeChild($m);
366
+            $parent->parentNode->insertBefore($old, $parent);
367
+            $parents->attach($parent);
368
+        }
369
+
370
+        // Now that all the children are moved, we
371
+        // remove all of the parents.
372
+        foreach ($parents as $ele) {
373
+            $ele->parentNode->removeChild($ele);
374
+        }
375
+
376
+        return $this;
377
+    }
378
+
379
+    /**
380
+     * Wrap each element inside of the given markup.
381
+     *
382
+     * Markup is usually a string, but it can also be a DOMNode, a document
383
+     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
384
+     * the first item in the list will be used.)
385
+     *
386
+     * @param mixed $markup
387
+     *  Markup that will wrap each element in the current list.
388
+     *
389
+     * @return DOMQuery
390
+     *  The DOMQuery object with the wrapping changes made.
391
+     * @throws Exception
392
+     * @throws QueryPath
393
+     * @see wrapAll()
394
+     * @see wrapInner()
395
+     */
396
+    public function wrap($markup): Query
397
+    {
398
+        $data = $this->prepareInsert($markup);
399
+
400
+        // If the markup passed in is empty, we don't do any wrapping.
401
+        if (empty($data)) {
402
+            return $this;
403
+        }
404
+
405
+        foreach ($this->matches as $m) {
406
+            if ($data instanceof DOMDocumentFragment) {
407
+                $copy = $data->firstChild->cloneNode(true);
408
+            } else {
409
+                $copy = $data->cloneNode(true);
410
+            }
411
+
412
+            // XXX: Should be able to avoid doing this over and over.
413
+            if ($copy->hasChildNodes()) {
414
+                $deepest = $this->deepestNode($copy);
415
+                // FIXME: Does this need a different data structure?
416
+                $bottom = $deepest[0];
417
+            } else {
418
+                $bottom = $copy;
419
+            }
420
+
421
+            $parent = $m->parentNode;
422
+            $parent->insertBefore($copy, $m);
423
+            $m = $parent->removeChild($m);
424
+            $bottom->appendChild($m);
425
+        }
426
+
427
+        return $this;
428
+    }
429
+
430
+    /**
431
+     * Wrap all elements inside of the given markup.
432
+     *
433
+     * So all elements will be grouped together under this single marked up
434
+     * item. This works by first determining the parent element of the first item
435
+     * in the list. It then moves all of the matching elements under the wrapper
436
+     * and inserts the wrapper where that first element was found. (This is in
437
+     * accordance with the way jQuery works.)
438
+     *
439
+     * Markup is usually XML in a string, but it can also be a DOMNode, a document
440
+     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
441
+     * the first item in the list will be used.)
442
+     *
443
+     * @param string $markup
444
+     *  Markup that will wrap all elements in the current list.
445
+     *
446
+     * @return DOMQuery
447
+     *  The DOMQuery object with the wrapping changes made.
448
+     * @throws Exception
449
+     * @throws QueryPath
450
+     * @see wrap()
451
+     * @see wrapInner()
452
+     */
453
+    public function wrapAll($markup)
454
+    {
455
+        if ($this->matches->count() === 0) {
456
+            return;
457
+        }
458
+
459
+        $data = $this->prepareInsert($markup);
460
+
461
+        if (empty($data)) {
462
+            return $this;
463
+        }
464
+
465
+        if ($data instanceof DOMDocumentFragment) {
466
+            $data = $data->firstChild->cloneNode(true);
467
+        } else {
468
+            $data = $data->cloneNode(true);
469
+        }
470
+
471
+        if ($data->hasChildNodes()) {
472
+            $deepest = $this->deepestNode($data);
473
+            // FIXME: Does this need fixing?
474
+            $bottom = $deepest[0];
475
+        } else {
476
+            $bottom = $data;
477
+        }
478
+
479
+        $first  = $this->getFirstMatch();
480
+        $parent = $first->parentNode;
481
+        $parent->insertBefore($data, $first);
482
+        foreach ($this->matches as $m) {
483
+            $bottom->appendChild($m->parentNode->removeChild($m));
484
+        }
485
+
486
+        return $this;
487
+    }
488
+
489
+    /**
490
+     * Wrap the child elements of each item in the list with the given markup.
491
+     *
492
+     * Markup is usually a string, but it can also be a DOMNode, a document
493
+     * fragment, a SimpleXMLElement, or another DOMNode object (in which case
494
+     * the first item in the list will be used.)
495
+     *
496
+     * @param string $markup
497
+     *  Markup that will wrap children of each element in the current list.
498
+     *
499
+     * @return DOMQuery
500
+     *  The DOMQuery object with the wrapping changes made.
501
+     * @throws Exception
502
+     * @throws QueryPath
503
+     * @see wrap()
504
+     * @see wrapAll()
505
+     */
506
+    public function wrapInner($markup)
507
+    {
508
+        $data = $this->prepareInsert($markup);
509
+
510
+        // No data? Short circuit.
511
+        if (empty($data)) {
512
+            return $this;
513
+        }
514
+
515
+        foreach ($this->matches as $m) {
516
+            if ($data instanceof DOMDocumentFragment) {
517
+                $wrapper = $data->firstChild->cloneNode(true);
518
+            } else {
519
+                $wrapper = $data->cloneNode(true);
520
+            }
521
+
522
+            if ($wrapper->hasChildNodes()) {
523
+                $deepest = $this->deepestNode($wrapper);
524
+                // FIXME: ???
525
+                $bottom = $deepest[0];
526
+            } else {
527
+                $bottom = $wrapper;
528
+            }
529
+
530
+            if ($m->hasChildNodes()) {
531
+                while ($m->firstChild) {
532
+                    $kid = $m->removeChild($m->firstChild);
533
+                    $bottom->appendChild($kid);
534
+                }
535
+            }
536
+
537
+            $m->appendChild($wrapper);
538
+        }
539
+
540
+        return $this;
541
+    }
542
+
543
+    /**
544
+     * Reduce the set of matches to the deepest child node in the tree.
545
+     *
546
+     * This loops through the matches and looks for the deepest child node of all of
547
+     * the matches. "Deepest", here, is relative to the nodes in the list. It is
548
+     * calculated as the distance from the starting node to the most distant child
549
+     * node. In other words, it is not necessarily the farthest node from the root
550
+     * element, but the farthest note from the matched element.
551
+     *
552
+     * In the case where there are multiple nodes at the same depth, all of the
553
+     * nodes at that depth will be included.
554
+     *
555
+     * @return DOMQuery
556
+     *  The DOMQuery wrapping the single deepest node.
557
+     * @throws ParseException
558
+     */
559
+    public function deepest(): Query
560
+    {
561
+        $deepest = 0;
562
+        $winner  = new SplObjectStorage();
563
+        foreach ($this->matches as $m) {
564
+            $local_deepest = 0;
565
+            $local_ele     = $this->deepestNode($m, 0, null, $local_deepest);
566
+
567
+            // Replace with the new deepest.
568
+            if ($local_deepest > $deepest) {
569
+                $winner = new SplObjectStorage();
570
+                foreach ($local_ele as $lele) {
571
+                    $winner->attach($lele);
572
+                }
573
+                $deepest = $local_deepest;
574
+            } // Augument with other equally deep elements.
575
+            elseif ($local_deepest === $deepest) {
576
+                foreach ($local_ele as $lele) {
577
+                    $winner->attach($lele);
578
+                }
579
+            }
580
+        }
581
+
582
+        return $this->inst($winner, null);
583
+    }
584
+
585
+    /**
586
+     * Add a class to all elements in the current DOMQuery.
587
+     *
588
+     * This searchers for a class attribute on each item wrapped by the current
589
+     * DOMNode object. If no attribute is found, a new one is added and its value
590
+     * is set to $class. If a class attribute is found, then the value is appended
591
+     * on to the end.
592
+     *
593
+     * @param string $class
594
+     *  The name of the class.
595
+     *
596
+     * @return DOMQuery
597
+     *  Returns the DOMQuery object.
598
+     * @see css()
599
+     * @see attr()
600
+     * @see removeClass()
601
+     * @see hasClass()
602
+     */
603
+    public function addClass($class)
604
+    {
605
+        foreach ($this->matches as $m) {
606
+            if ($m->hasAttribute('class')) {
607
+                $val = $m->getAttribute('class');
608
+                $m->setAttribute('class', $val . ' ' . $class);
609
+            } else {
610
+                $m->setAttribute('class', $class);
611
+            }
612
+        }
613
+
614
+        return $this;
615
+    }
616
+
617
+    /**
618
+     * Remove the named class from any element in the DOMQuery that has it.
619
+     *
620
+     * This may result in the entire class attribute being removed. If there
621
+     * are other items in the class attribute, though, they will not be removed.
622
+     *
623
+     * Example:
624
+     * Consider this XML:
625
+     *
626
+     * @code
627
+     * <element class="first second"/>
628
+     * @endcode
629
+     *
630
+     * Executing this fragment of code will remove only the 'first' class:
631
+     * @code
632
+     * qp(document, 'element')->removeClass('first');
633
+     * @endcode
634
+     *
635
+     * The resulting XML will be:
636
+     * @code
637
+     * <element class="second"/>
638
+     * @endcode
639
+     *
640
+     * To remove the entire 'class' attribute, you should use {@see removeAttr()}.
641
+     *
642
+     * @param string $class
643
+     *  The class name to remove.
644
+     *
645
+     * @return DOMQuery
646
+     *  The modified DOMNode object.
647
+     * @see attr()
648
+     * @see addClass()
649
+     * @see hasClass()
650
+     */
651
+    public function removeClass($class = false): Query
652
+    {
653
+        if (empty($class)) {
654
+            foreach ($this->matches as $m) {
655
+                $m->removeAttribute('class');
656
+            }
657
+        } else {
658
+            $to_remove = array_filter(explode(' ', $class));
659
+            foreach ($this->matches as $m) {
660
+                if ($m->hasAttribute('class')) {
661
+                    $vals = array_filter(explode(' ', $m->getAttribute('class')));
662
+                    $buf  = [];
663
+                    foreach ($vals as $v) {
664
+                        if (! in_array($v, $to_remove)) {
665
+                            $buf[] = $v;
666
+                        }
667
+                    }
668
+                    if (empty($buf)) {
669
+                        $m->removeAttribute('class');
670
+                    } else {
671
+                        $m->setAttribute('class', implode(' ', $buf));
672
+                    }
673
+                }
674
+            }
675
+        }
676
+
677
+        return $this;
678
+    }
679
+
680
+    /**
681
+     * Detach any items from the list if they match the selector.
682
+     *
683
+     * In other words, each item that matches the selector will be removed
684
+     * from the DOM document. The returned DOMQuery wraps the list of
685
+     * removed elements.
686
+     *
687
+     * If no selector is specified, this will remove all current matches from
688
+     * the document.
689
+     *
690
+     * @param string $selector
691
+     *  A CSS Selector.
692
+     *
693
+     * @return DOMQuery
694
+     *  The Query path wrapping a list of removed items.
695
+     * @throws ParseException
696
+     * @see    replaceWith()
697
+     * @see    removeChildren()
698
+     * @since  2.1
699
+     * @author eabrand
700
+     * @see    replaceAll()
701
+     */
702
+    public function detach($selector = null): Query
703
+    {
704
+        if (null !== $selector) {
705
+            $this->find($selector);
706
+        }
707
+
708
+        $found      = new SplObjectStorage();
709
+        $this->last = $this->matches;
710
+        foreach ($this->matches as $item) {
711
+            // The item returned is (according to docs) different from
712
+            // the one passed in, so we have to re-store it.
713
+            $found->attach($item->parentNode->removeChild($item));
714
+        }
715
+
716
+        return $this->inst($found, null);
717
+    }
718
+
719
+    /**
720
+     * Attach any items from the list if they match the selector.
721
+     *
722
+     * If no selector is specified, this will remove all current matches from
723
+     * the document.
724
+     *
725
+     * @param DOMQuery $dest
726
+     *  A DOMQuery Selector.
727
+     *
728
+     * @return DOMQuery
729
+     *  The Query path wrapping a list of removed items.
730
+     * @throws QueryPath
731
+     * @throws Exception
732
+     * @see    removeChildren()
733
+     * @since  2.1
734
+     * @author eabrand
735
+     * @see    replaceAll()
736
+     * @see    replaceWith()
737
+     */
738
+    public function attach(DOMQuery $dest): Query
739
+    {
740
+        foreach ($this->last as $m) {
741
+            $dest->append($m);
742
+        }
743
+
744
+        return $this;
745
+    }
746
+
747
+    /**
748
+     * Append the current elements to the destination passed into the function.
749
+     *
750
+     * This cycles through all of the current matches and appends them to
751
+     * the context given in $destination. If a selector is provided then the
752
+     * $destination is queried (using that selector) prior to the data being
753
+     * appended. The data is then appended to the found items.
754
+     *
755
+     * @param DOMQuery $dest
756
+     *  A DOMQuery object that will be appended to.
757
+     *
758
+     * @return DOMQuery
759
+     *  The original DOMQuery, unaltered. Only the destination DOMQuery will
760
+     *  be modified.
761
+     * @throws QueryPath::Exception
762
+     *  Thrown if $data is an unsupported object type.
763
+     * @throws Exception
764
+     * @see append()
765
+     * @see prependTo()
766
+     */
767
+    public function appendTo(DOMQuery $dest): Query
768
+    {
769
+        foreach ($this->matches as $m) {
770
+            $dest->append($m);
771
+        }
772
+
773
+        return $this;
774
+    }
775
+
776
+    /**
777
+     * Remove any items from the list if they match the selector.
778
+     *
779
+     * In other words, each item that matches the selector will be remove
780
+     * from the DOM document. The returned DOMQuery wraps the list of
781
+     * removed elements.
782
+     *
783
+     * If no selector is specified, this will remove all current matches from
784
+     * the document.
785
+     *
786
+     * @param string $selector
787
+     *  A CSS Selector.
788
+     *
789
+     * @return DOMQuery
790
+     *  The Query path wrapping a list of removed items.
791
+     * @throws ParseException
792
+     * @see replaceWith()
793
+     * @see removeChildren()
794
+     * @see replaceAll()
795
+     */
796
+    public function remove($selector = null): Query
797
+    {
798
+        if (! empty($selector)) {
799
+            // Do a non-destructive find.
800
+            $query = new QueryPathEventHandler($this->matches);
801
+            $query->find($selector);
802
+            $matches = $query->getMatches();
803
+        } else {
804
+            $matches = $this->matches;
805
+        }
806
+
807
+        $found = new SplObjectStorage();
808
+        foreach ($matches as $item) {
809
+            // The item returned is (according to docs) different from
810
+            // the one passed in, so we have to re-store it.
811
+            $found->attach($item->parentNode->removeChild($item));
812
+        }
813
+
814
+        // Return a clone DOMQuery with just the removed items. If
815
+        // no items are found, this will return an empty DOMQuery.
816
+        return count($found) === 0 ? new static() : new static($found);
817
+    }
818
+
819
+    /**
820
+     * This replaces everything that matches the selector with the first value
821
+     * in the current list.
822
+     *
823
+     * This is the reverse of replaceWith.
824
+     *
825
+     * Unlike jQuery, DOMQuery cannot assume a default document. Consequently,
826
+     * you must specify the intended destination document. If it is omitted, the
827
+     * present document is assumed to be tthe document. However, that can result
828
+     * in undefined behavior if the selector and the replacement are not sufficiently
829
+     * distinct.
830
+     *
831
+     * @param string       $selector
832
+     *  The selector.
833
+     * @param DOMDocument $document
834
+     *  The destination document.
835
+     *
836
+     * @return DOMQuery
837
+     *  The DOMQuery wrapping the modified document.
838
+     * @throws ParseException
839
+     * @see        remove()
840
+     * @see        replaceWith()
841
+     * @deprecated Due to the fact that this is not a particularly friendly method,
842
+     *             and that it can be easily replicated using {@see replaceWith()}, it is to be
843
+     *             considered deprecated.
844
+     */
845
+    public function replaceAll($selector, DOMDocument $document): Query
846
+    {
847
+        $replacement = $this->matches->count() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');
848
+
849
+        $c = new QueryPathEventHandler($document);
850
+        $c->find($selector);
851
+        $temp = $c->getMatches();
852
+        foreach ($temp as $item) {
853
+            $node = $replacement->cloneNode();
854
+            $node = $document->importNode($node);
855
+            $item->parentNode->replaceChild($node, $item);
856
+        }
857
+
858
+        return QueryPath::with($document, null, $this->options);
859
+    }
860
+
861
+    /**
862
+     * Add more elements to the current set of matches.
863
+     *
864
+     * This begins the new query at the top of the DOM again. The results found
865
+     * when running this selector are then merged into the existing results. In
866
+     * this way, you can add additional elements to the existing set.
867
+     *
868
+     * @param string $selector
869
+     *  A valid selector.
870
+     *
871
+     * @return DOMQuery
872
+     *  The DOMQuery object with the newly added elements.
873
+     * @see append()
874
+     * @see after()
875
+     * @see andSelf()
876
+     * @see end()
877
+     */
878
+    public function add($selector): Query
879
+    {
880
+
881
+        // This is destructive, so we need to set $last:
882
+        $this->last = $this->matches;
883
+
884
+        foreach (QueryPath::with($this->document, $selector, $this->options)->get() as $item) {
885
+            $this->matches->attach($item);
886
+        }
887
+
888
+        return $this;
889
+    }
890
+
891
+    /**
892
+     * Remove all child nodes.
893
+     *
894
+     * This is equivalent to jQuery's empty() function. (However, empty() is a
895
+     * PHP built-in, and cannot be used as a method name.)
896
+     *
897
+     * @return DOMQuery
898
+     *  The DOMQuery object with the child nodes removed.
899
+     * @see replaceWith()
900
+     * @see replaceAll()
901
+     * @see remove()
902
+     */
903
+    public function removeChildren(): Query
904
+    {
905
+        foreach ($this->matches as $m) {
906
+            while ($kid = $m->firstChild) {
907
+                $m->removeChild($kid);
908
+            }
909
+        }
910
+
911
+        return $this;
912
+    }
913
+
914
+    /**
915
+     * Get/set an attribute.
916
+     * - If no parameters are specified, this returns an associative array of all
917
+     *   name/value pairs.
918
+     * - If both $name and $value are set, then this will set the attribute name/value
919
+     *   pair for all items in this object.
920
+     * - If $name is set, and is an array, then
921
+     *   all attributes in the array will be set for all items in this object.
922
+     * - If $name is a string and is set, then the attribute value will be returned.
923
+     *
924
+     * When an attribute value is retrieved, only the attribute value of the FIRST
925
+     * match is returned.
926
+     *
927
+     * @param mixed  $name
928
+     *   The name of the attribute or an associative array of name/value pairs.
929
+     * @param string $value
930
+     *   A value (used only when setting an individual property).
931
+     *
932
+     * @return mixed
933
+     *   If this was a setter request, return the DOMQuery object. If this was
934
+     *   an access request (getter), return the string value.
935
+     * @see removeAttr()
936
+     * @see tag()
937
+     * @see hasAttr()
938
+     * @see hasClass()
939
+     */
940
+    public function attr($name = null, $value = null)
941
+    {
942
+        // Default case: Return all attributes as an assoc array.
943
+        if (is_null($name)) {
944
+            if ($this->matches->count() === 0) {
945
+                return null;
946
+            }
947
+            $ele    = $this->getFirstMatch();
948
+            $buffer = [];
949
+
950
+            // This does not appear to be part of the DOM
951
+            // spec. Nor is it documented. But it works.
952
+            foreach ($ele->attributes as $name => $attrNode) {
953
+                $buffer[$name] = $attrNode->value;
954
+            }
955
+
956
+            return $buffer;
957
+        }
958
+
959
+        // multi-setter
960
+        if (is_array($name)) {
961
+            foreach ($name as $k => $v) {
962
+                foreach ($this->matches as $m) {
963
+                    $m->setAttribute($k, $v);
964
+                }
965
+            }
966
+
967
+            return $this;
968
+        }
969
+        // setter
970
+        if (isset($value)) {
971
+            foreach ($this->matches as $m) {
972
+                $m->setAttribute($name, $value);
973
+            }
974
+
975
+            return $this;
976
+        }
977
+
978
+        //getter
979
+        if ($this->matches->count() === 0) {
980
+            return null;
981
+        }
982
+
983
+        // Special node type handler:
984
+        if ($name === 'nodeType') {
985
+            return $this->getFirstMatch()->nodeType;
986
+        }
987
+
988
+        // Always return first match's attr.
989
+        return $this->getFirstMatch()->getAttribute($name);
990
+    }
991
+
992
+    /**
993
+     * Set/get a CSS value for the current element(s).
994
+     * This sets the CSS value for each element in the DOMQuery object.
995
+     * It does this by setting (or getting) the style attribute (without a namespace).
996
+     *
997
+     * For example, consider this code:
998
+     *
999
+     * @code
1000
+     * <?php
1001
+     * qp(HTML_STUB, 'body')->css('background-color','red')->html();
1002
+     * ?>
1003
+     * @endcode
1004
+     * This will return the following HTML:
1005
+     * @code
1006
+     * <body style="background-color: red"/>
1007
+     * @endcode
1008
+     *
1009
+     * If no parameters are passed into this function, then the current style
1010
+     * element will be returned unparsed. Example:
1011
+     * @code
1012
+     * <?php
1013
+     * qp(HTML_STUB, 'body')->css('background-color','red')->css();
1014
+     * ?>
1015
+     * @endcode
1016
+     * This will return the following:
1017
+     * @code
1018
+     * background-color: red
1019
+     * @endcode
1020
+     *
1021
+     * As of QueryPath 2.1, existing style attributes will be merged with new attributes.
1022
+     * (In previous versions of QueryPath, a call to css() overwrite the existing style
1023
+     * values).
1024
+     *
1025
+     * @param mixed  $name
1026
+     *  If this is a string, it will be used as a CSS name. If it is an array,
1027
+     *  this will assume it is an array of name/value pairs of CSS rules. It will
1028
+     *  apply all rules to all elements in the set.
1029
+     * @param string $value
1030
+     *  The value to set. This is only set if $name is a string.
1031
+     *
1032
+     * @return DOMQuery
1033
+     */
1034
+    public function css($name = null, $value = '')
1035
+    {
1036
+        if (empty($name)) {
1037
+            return $this->attr('style');
1038
+        }
1039
+
1040
+        // Get any existing CSS.
1041
+        $css = [];
1042
+        foreach ($this->matches as $match) {
1043
+            $style = $match->getAttribute('style');
1044
+            if (! empty($style)) {
1045
+                // XXX: Is this sufficient?
1046
+                $style_array = explode(';', $style);
1047
+                foreach ($style_array as $item) {
1048
+                    $item = trim($item);
1049
+
1050
+                    // Skip empty attributes.
1051
+                    if ($item === '') {
1052
+                        continue;
1053
+                    }
1054
+
1055
+                    [$css_att, $css_val] = explode(':', $item, 2);
1056
+                    $css[$css_att] = trim($css_val);
1057
+                }
1058
+            }
1059
+        }
1060
+
1061
+        if (is_array($name)) {
1062
+            // Use array_merge instead of + to preserve order.
1063
+            $css = array_merge($css, $name);
1064
+        } else {
1065
+            $css[$name] = $value;
1066
+        }
1067
+
1068
+        // Collapse CSS into a string.
1069
+        $format     = '%s: %s;';
1070
+        $css_string = '';
1071
+        foreach ($css as $n => $v) {
1072
+            $css_string .= sprintf($format, $n, trim($v));
1073
+        }
1074
+
1075
+        $this->attr('style', $css_string);
1076
+
1077
+        return $this;
1078
+    }
1079 1079
 }
Please login to merge, or discard this patch.
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -661,7 +661,7 @@  discard block
 block discarded – undo
661 661
 					$vals = array_filter(explode(' ', $m->getAttribute('class')));
662 662
 					$buf  = [];
663 663
 					foreach ($vals as $v) {
664
-						if (! in_array($v, $to_remove)) {
664
+						if (!in_array($v, $to_remove)) {
665 665
 							$buf[] = $v;
666 666
 						}
667 667
 					}
@@ -795,7 +795,7 @@  discard block
 block discarded – undo
795 795
 	 */
796 796
 	public function remove($selector = null): Query
797 797
 	{
798
-		if (! empty($selector)) {
798
+		if (!empty($selector)) {
799 799
 			// Do a non-destructive find.
800 800
 			$query = new QueryPathEventHandler($this->matches);
801 801
 			$query->find($selector);
@@ -1041,7 +1041,7 @@  discard block
 block discarded – undo
1041 1041
 		$css = [];
1042 1042
 		foreach ($this->matches as $match) {
1043 1043
 			$style = $match->getAttribute('style');
1044
-			if (! empty($style)) {
1044
+			if (!empty($style)) {
1045 1045
 				// XXX: Is this sufficient?
1046 1046
 				$style_array = explode(';', $style);
1047 1047
 				foreach ($style_array as $item) {
Please login to merge, or discard this patch.
src/Helpers/QueryChecks.php 2 patches
Indentation   +174 added lines, -174 removed lines patch added patch discarded remove patch
@@ -20,182 +20,182 @@
 block discarded – undo
20 20
 trait QueryChecks
21 21
 {
22 22
 
23
-	/**
24
-	 * Given a selector, this checks to see if the current set has one or more matches.
25
-	 *
26
-	 * Unlike jQuery's version, this supports full selectors (not just simple ones).
27
-	 *
28
-	 * @param string|DOMNode $selector
29
-	 *   The selector to search for. As of QueryPath 2.1.1, this also supports passing a
30
-	 *   DOMNode object.
31
-	 *
32
-	 * @return boolean
33
-	 *   TRUE if one or more elements match. FALSE if no match is found.
34
-	 * @throws Exception
35
-	 * @throws Exception
36
-	 * @see get()
37
-	 * @see eq()
38
-	 */
39
-	public function is($selector): bool
40
-	{
41
-		if (is_object($selector)) {
42
-			if ($selector instanceof DOMNode) {
43
-				return count($this->matches) === 1 && $selector->isSameNode($this->get(0));
44
-			}
45
-
46
-			if ($selector instanceof Traversable) {
47
-				if (count($selector) !== count($this->matches)) {
48
-					return false;
49
-				}
50
-				// Without $seen, there is an edge case here if $selector contains the same object
51
-				// more than once, but the counts are equal. For example, [a, a, a, a] will
52
-				// pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this.
53
-				$seen = new SplObjectStorage();
54
-				foreach ($selector as $item) {
55
-					if (! $this->matches->contains($item) || $seen->contains($item)) {
56
-						return false;
57
-					}
58
-					$seen->attach($item);
59
-				}
60
-
61
-				return true;
62
-			}
63
-			throw new Exception('Cannot compare an object to a DOMQuery.');
64
-		}
65
-
66
-		return $this->branch($selector)->count() > 0;
67
-	}
68
-
69
-	/**
70
-	 * Reduce the elements matched by DOMQuery to only those which contain the given item.
71
-	 *
72
-	 * There are two ways in which this is different from jQuery's implementation:
73
-	 * - We allow ANY DOMNode, not just DOMElements. That means this will work on
74
-	 *   processor instructions, text nodes, comments, etc.
75
-	 * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior
76
-	 *   and modifies the existing object. It does not create a brand new object.
77
-	 *
78
-	 * @param mixed $contained
79
-	 *     - If $contained is a CSS selector (e.g. '#foo'), this will test to see
80
-	 *     if the current DOMQuery has any elements that contain items that match
81
-	 *     the selector.
82
-	 *     - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode
83
-	 *     exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the
84
-	 *     same document.)
85
-	 *
86
-	 * @return DOMQuery
87
-	 * @throws ParseException
88
-	 * @todo   It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes.
89
-	 * @since  2.1
90
-	 * @author eabrand
91
-	 */
92
-	public function has($contained): Query
93
-	{
94
-		/*
23
+    /**
24
+     * Given a selector, this checks to see if the current set has one or more matches.
25
+     *
26
+     * Unlike jQuery's version, this supports full selectors (not just simple ones).
27
+     *
28
+     * @param string|DOMNode $selector
29
+     *   The selector to search for. As of QueryPath 2.1.1, this also supports passing a
30
+     *   DOMNode object.
31
+     *
32
+     * @return boolean
33
+     *   TRUE if one or more elements match. FALSE if no match is found.
34
+     * @throws Exception
35
+     * @throws Exception
36
+     * @see get()
37
+     * @see eq()
38
+     */
39
+    public function is($selector): bool
40
+    {
41
+        if (is_object($selector)) {
42
+            if ($selector instanceof DOMNode) {
43
+                return count($this->matches) === 1 && $selector->isSameNode($this->get(0));
44
+            }
45
+
46
+            if ($selector instanceof Traversable) {
47
+                if (count($selector) !== count($this->matches)) {
48
+                    return false;
49
+                }
50
+                // Without $seen, there is an edge case here if $selector contains the same object
51
+                // more than once, but the counts are equal. For example, [a, a, a, a] will
52
+                // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this.
53
+                $seen = new SplObjectStorage();
54
+                foreach ($selector as $item) {
55
+                    if (! $this->matches->contains($item) || $seen->contains($item)) {
56
+                        return false;
57
+                    }
58
+                    $seen->attach($item);
59
+                }
60
+
61
+                return true;
62
+            }
63
+            throw new Exception('Cannot compare an object to a DOMQuery.');
64
+        }
65
+
66
+        return $this->branch($selector)->count() > 0;
67
+    }
68
+
69
+    /**
70
+     * Reduce the elements matched by DOMQuery to only those which contain the given item.
71
+     *
72
+     * There are two ways in which this is different from jQuery's implementation:
73
+     * - We allow ANY DOMNode, not just DOMElements. That means this will work on
74
+     *   processor instructions, text nodes, comments, etc.
75
+     * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior
76
+     *   and modifies the existing object. It does not create a brand new object.
77
+     *
78
+     * @param mixed $contained
79
+     *     - If $contained is a CSS selector (e.g. '#foo'), this will test to see
80
+     *     if the current DOMQuery has any elements that contain items that match
81
+     *     the selector.
82
+     *     - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode
83
+     *     exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the
84
+     *     same document.)
85
+     *
86
+     * @return DOMQuery
87
+     * @throws ParseException
88
+     * @todo   It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes.
89
+     * @since  2.1
90
+     * @author eabrand
91
+     */
92
+    public function has($contained): Query
93
+    {
94
+        /*
95 95
 		if (count($this->matches) == 0) {
96 96
 		return false;
97 97
 		}
98 98
 		*/
99
-		$found = new SplObjectStorage();
100
-
101
-		// If it's a selector, we just get all of the DOMNodes that match the selector.
102
-		$nodes = [];
103
-		if (is_string($contained)) {
104
-			// Get the list of nodes.
105
-			$nodes = $this->branch($contained)->get();
106
-		} elseif ($contained instanceof DOMNode) {
107
-			// Make a list with one node.
108
-			$nodes = [$contained];
109
-		}
110
-
111
-		// Now we go through each of the nodes that we are testing. We want to find
112
-		// ALL PARENTS that are in our existing DOMQuery matches. Those are the
113
-		// ones we add to our new matches.
114
-		foreach ($nodes as $original_node) {
115
-			$node = $original_node;
116
-			while (! empty($node)/* && $node != $node->ownerDocument*/) {
117
-				if ($this->matches->contains($node)) {
118
-					$found->attach($node);
119
-				}
120
-				$node = $node->parentNode;
121
-			}
122
-		}
123
-
124
-		return $this->inst($found, null);
125
-	}
126
-
127
-	/**
128
-	 * Returns TRUE if any of the elements in the DOMQuery have the specified class.
129
-	 *
130
-	 * @param string $class
131
-	 *  The name of the class.
132
-	 *
133
-	 * @return boolean
134
-	 *  TRUE if the class exists in one or more of the elements, FALSE otherwise.
135
-	 * @see addClass()
136
-	 * @see removeClass()
137
-	 */
138
-	public function hasClass($class): bool
139
-	{
140
-		foreach ($this->matches as $m) {
141
-			if ($m->hasAttribute('class')) {
142
-				$vals = explode(' ', $m->getAttribute('class'));
143
-				if (in_array($class, $vals)) {
144
-					return true;
145
-				}
146
-			}
147
-		}
148
-
149
-		return false;
150
-	}
151
-
152
-	/**
153
-	 * Check to see if the given attribute is present.
154
-	 *
155
-	 * This returns TRUE if <em>all</em> selected items have the attribute, or
156
-	 * FALSE if at least one item does not have the attribute.
157
-	 *
158
-	 * @param string $attrName
159
-	 *  The attribute name.
160
-	 *
161
-	 * @return boolean
162
-	 *  TRUE if all matches have the attribute, FALSE otherwise.
163
-	 * @since 2.0
164
-	 * @see   attr()
165
-	 * @see   hasClass()
166
-	 */
167
-	public function hasAttr($attrName): bool
168
-	{
169
-		foreach ($this->matches as $match) {
170
-			if (! $match->hasAttribute($attrName)) {
171
-				return false;
172
-			}
173
-		}
174
-
175
-		return true;
176
-	}
177
-
178
-	/**
179
-	 * Remove the named attribute from all elements in the current DOMQuery.
180
-	 *
181
-	 * This will remove any attribute with the given name. It will do this on each
182
-	 * item currently wrapped by DOMQuery.
183
-	 *
184
-	 * As is the case in jQuery, this operation is not considered destructive.
185
-	 *
186
-	 * @param string $name
187
-	 *  Name of the parameter to remove.
188
-	 *
189
-	 * @return DOMQuery
190
-	 *  The DOMQuery object with the same elements.
191
-	 * @see attr()
192
-	 */
193
-	public function removeAttr($name): Query
194
-	{
195
-		foreach ($this->matches as $m) {
196
-			$m->removeAttribute($name);
197
-		}
198
-
199
-		return $this;
200
-	}
99
+        $found = new SplObjectStorage();
100
+
101
+        // If it's a selector, we just get all of the DOMNodes that match the selector.
102
+        $nodes = [];
103
+        if (is_string($contained)) {
104
+            // Get the list of nodes.
105
+            $nodes = $this->branch($contained)->get();
106
+        } elseif ($contained instanceof DOMNode) {
107
+            // Make a list with one node.
108
+            $nodes = [$contained];
109
+        }
110
+
111
+        // Now we go through each of the nodes that we are testing. We want to find
112
+        // ALL PARENTS that are in our existing DOMQuery matches. Those are the
113
+        // ones we add to our new matches.
114
+        foreach ($nodes as $original_node) {
115
+            $node = $original_node;
116
+            while (! empty($node)/* && $node != $node->ownerDocument*/) {
117
+                if ($this->matches->contains($node)) {
118
+                    $found->attach($node);
119
+                }
120
+                $node = $node->parentNode;
121
+            }
122
+        }
123
+
124
+        return $this->inst($found, null);
125
+    }
126
+
127
+    /**
128
+     * Returns TRUE if any of the elements in the DOMQuery have the specified class.
129
+     *
130
+     * @param string $class
131
+     *  The name of the class.
132
+     *
133
+     * @return boolean
134
+     *  TRUE if the class exists in one or more of the elements, FALSE otherwise.
135
+     * @see addClass()
136
+     * @see removeClass()
137
+     */
138
+    public function hasClass($class): bool
139
+    {
140
+        foreach ($this->matches as $m) {
141
+            if ($m->hasAttribute('class')) {
142
+                $vals = explode(' ', $m->getAttribute('class'));
143
+                if (in_array($class, $vals)) {
144
+                    return true;
145
+                }
146
+            }
147
+        }
148
+
149
+        return false;
150
+    }
151
+
152
+    /**
153
+     * Check to see if the given attribute is present.
154
+     *
155
+     * This returns TRUE if <em>all</em> selected items have the attribute, or
156
+     * FALSE if at least one item does not have the attribute.
157
+     *
158
+     * @param string $attrName
159
+     *  The attribute name.
160
+     *
161
+     * @return boolean
162
+     *  TRUE if all matches have the attribute, FALSE otherwise.
163
+     * @since 2.0
164
+     * @see   attr()
165
+     * @see   hasClass()
166
+     */
167
+    public function hasAttr($attrName): bool
168
+    {
169
+        foreach ($this->matches as $match) {
170
+            if (! $match->hasAttribute($attrName)) {
171
+                return false;
172
+            }
173
+        }
174
+
175
+        return true;
176
+    }
177
+
178
+    /**
179
+     * Remove the named attribute from all elements in the current DOMQuery.
180
+     *
181
+     * This will remove any attribute with the given name. It will do this on each
182
+     * item currently wrapped by DOMQuery.
183
+     *
184
+     * As is the case in jQuery, this operation is not considered destructive.
185
+     *
186
+     * @param string $name
187
+     *  Name of the parameter to remove.
188
+     *
189
+     * @return DOMQuery
190
+     *  The DOMQuery object with the same elements.
191
+     * @see attr()
192
+     */
193
+    public function removeAttr($name): Query
194
+    {
195
+        foreach ($this->matches as $m) {
196
+            $m->removeAttribute($name);
197
+        }
198
+
199
+        return $this;
200
+    }
201 201
 }
Please login to merge, or discard this patch.
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -52,7 +52,7 @@  discard block
 block discarded – undo
52 52
 				// pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this.
53 53
 				$seen = new SplObjectStorage();
54 54
 				foreach ($selector as $item) {
55
-					if (! $this->matches->contains($item) || $seen->contains($item)) {
55
+					if (!$this->matches->contains($item) || $seen->contains($item)) {
56 56
 						return false;
57 57
 					}
58 58
 					$seen->attach($item);
@@ -113,7 +113,7 @@  discard block
 block discarded – undo
113 113
 		// ones we add to our new matches.
114 114
 		foreach ($nodes as $original_node) {
115 115
 			$node = $original_node;
116
-			while (! empty($node)/* && $node != $node->ownerDocument*/) {
116
+			while (!empty($node)/* && $node != $node->ownerDocument*/) {
117 117
 				if ($this->matches->contains($node)) {
118 118
 					$found->attach($node);
119 119
 				}
@@ -167,7 +167,7 @@  discard block
 block discarded – undo
167 167
 	public function hasAttr($attrName): bool
168 168
 	{
169 169
 		foreach ($this->matches as $match) {
170
-			if (! $match->hasAttribute($attrName)) {
170
+			if (!$match->hasAttribute($attrName)) {
171 171
 				return false;
172 172
 			}
173 173
 		}
Please login to merge, or discard this patch.
src/Helpers/QueryFilters.php 2 patches
Indentation   +1124 added lines, -1124 removed lines patch added patch discarded remove patch
@@ -22,1128 +22,1128 @@
 block discarded – undo
22 22
 trait QueryFilters
23 23
 {
24 24
 
25
-	/**
26
-	 * Filter a list down to only elements that match the selector.
27
-	 * Use this, for example, to find all elements with a class, or with
28
-	 * certain children.
29
-	 *
30
-	 * @param string $selector
31
-	 *   The selector to use as a filter.
32
-	 *
33
-	 * @return Query The DOMQuery with non-matching items filtered out.*   The DOMQuery with non-matching items
34
-	 *               filtered out.
35
-	 * @throws ParseException
36
-	 * @see filterCallback()
37
-	 * @see map()
38
-	 * @see find()
39
-	 * @see is()
40
-	 * @see filterLambda()
41
-	 */
42
-	public function filter($selector): Query
43
-	{
44
-		$found = new SplObjectStorage();
45
-		$tmp   = new SplObjectStorage();
46
-
47
-		foreach ($this->matches as $m) {
48
-			$tmp->attach($m);
49
-			// Seems like this should be right... but it fails unit
50
-			// tests. Need to compare to jQuery.
51
-			// $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m);
52
-			$query = new DOMTraverser($tmp);
53
-			$query->find($selector);
54
-			if (count($query->matches())) {
55
-				$found->attach($m);
56
-			}
57
-			$tmp->detach($m);
58
-		}
59
-
60
-		return $this->inst($found, null);
61
-	}
62
-
63
-	/**
64
-	 * Filter based on a lambda function.
65
-	 *
66
-	 * The function string will be executed as if it were the body of a
67
-	 * function. It is passed two arguments:
68
-	 * - $index: The index of the item.
69
-	 * - $item: The current Element.
70
-	 * If the function returns boolean FALSE, the item will be removed from
71
-	 * the list of elements. Otherwise it will be kept.
72
-	 *
73
-	 * Example:
74
-	 *
75
-	 * @code
76
-	 * qp('li')->filterLambda('qp($item)->attr("id") == "test"');
77
-	 * @endcode
78
-	 *
79
-	 * The above would filter down the list to only an item whose ID is
80
-	 * 'text'.
81
-	 *
82
-	 * @param string $fn
83
-	 *  Inline lambda function in a string.
84
-	 *
85
-	 * @return DOMQuery
86
-	 * @throws ParseException
87
-	 *
88
-	 * @see map()
89
-	 * @see mapLambda()
90
-	 * @see filterCallback()
91
-	 * @see filter()
92
-	 * @deprecated
93
-	 *   Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this
94
-	 *   method is not necessary and should be avoided.
95
-	 */
96
-	public function filterLambda($fn): Query
97
-	{
98
-		$function = create_function('$index, $item', $fn);
99
-		$found    = new SplObjectStorage();
100
-		$i        = 0;
101
-		foreach ($this->matches as $item) {
102
-			if ($function($i++, $item) !== false) {
103
-				$found->attach($item);
104
-			}
105
-		}
106
-
107
-		return $this->inst($found, null);
108
-	}
109
-
110
-	/**
111
-	 * Use regular expressions to filter based on the text content of matched elements.
112
-	 *
113
-	 * Only items that match the given regular expression will be kept. All others will
114
-	 * be removed.
115
-	 *
116
-	 * The regular expression is run against the <i>text content</i> (the PCDATA) of the
117
-	 * elements. This is a way of filtering elements based on their content.
118
-	 *
119
-	 * Example:
120
-	 *
121
-	 * @code
122
-	 *  <?xml version="1.0"?>
123
-	 *  <div>Hello <i>World</i></div>
124
-	 * @endcode
125
-	 *
126
-	 * @code
127
-	 *  <?php
128
-	 *    // This will be 1.
129
-	 *    qp($xml, 'div')->filterPreg('/World/')->matches->count();
130
-	 *  ?>
131
-	 * @endcode
132
-	 *
133
-	 * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is
134
-	 * @codeHello World@endcode.
135
-	 *
136
-	 * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class.
137
-	 *
138
-	 * @param string $regex
139
-	 *  A regular expression.
140
-	 *
141
-	 * @return DOMQuery
142
-	 * @throws ParseException
143
-	 * @see       filterCallback()
144
-	 * @see       preg_match()
145
-	 * @see       filter()
146
-	 */
147
-	public function filterPreg($regex): Query
148
-	{
149
-		$found = new SplObjectStorage();
150
-
151
-		foreach ($this->matches as $item) {
152
-			if (preg_match($regex, $item->textContent) > 0) {
153
-				$found->attach($item);
154
-			}
155
-		}
156
-
157
-		return $this->inst($found, null);
158
-	}
159
-
160
-	/**
161
-	 * Filter based on a callback function.
162
-	 *
163
-	 * A callback may be any of the following:
164
-	 *  - a function: 'my_func'.
165
-	 *  - an object/method combo: $obj, 'myMethod'
166
-	 *  - a class/method combo: 'MyClass', 'myMethod'
167
-	 * Note that classes are passed in strings. Objects are not.
168
-	 *
169
-	 * Each callback is passed to arguments:
170
-	 *  - $index: The index position of the object in the array.
171
-	 *  - $item: The item to be operated upon.
172
-	 *
173
-	 * If the callback function returns FALSE, the item will be removed from the
174
-	 * set of matches. Otherwise the item will be considered a match and left alone.
175
-	 *
176
-	 * @param callback $callback .
177
-	 *                           A callback either as a string (function) or an array (object, method OR
178
-	 *                           classname, method).
179
-	 *
180
-	 * @return DOMQuery
181
-	 *                           Query path object augmented according to the function.
182
-	 * @throws ParseException
183
-	 * @throws Exception
184
-	 * @see map()
185
-	 * @see is()
186
-	 * @see find()
187
-	 * @see filter()
188
-	 * @see filterLambda()
189
-	 */
190
-	public function filterCallback($callback): Query
191
-	{
192
-		$found = new SplObjectStorage();
193
-		$i     = 0;
194
-		if (is_callable($callback)) {
195
-			foreach ($this->matches as $item) {
196
-				if ($callback($i++, $item) !== false) {
197
-					$found->attach($item);
198
-				}
199
-			}
200
-		} else {
201
-			throw new Exception('The specified callback is not callable.');
202
-		}
203
-
204
-		return $this->inst($found, null);
205
-	}
206
-
207
-	/**
208
-	 * Run a function on each item in a set.
209
-	 *
210
-	 * The mapping callback can return anything. Whatever it returns will be
211
-	 * stored as a match in the set, though. This means that afer a map call,
212
-	 * there is no guarantee that the elements in the set will behave correctly
213
-	 * with other DOMQuery functions.
214
-	 *
215
-	 * Callback rules:
216
-	 * - If the callback returns NULL, the item will be removed from the array.
217
-	 * - If the callback returns an array, the entire array will be stored in
218
-	 *   the results.
219
-	 * - If the callback returns anything else, it will be appended to the array
220
-	 *   of matches.
221
-	 *
222
-	 * @param callback $callback
223
-	 *  The function or callback to use. The callback will be passed two params:
224
-	 *  - $index: The index position in the list of items wrapped by this object.
225
-	 *  - $item: The current item.
226
-	 *
227
-	 * @return DOMQuery
228
-	 *  The DOMQuery object wrapping a list of whatever values were returned
229
-	 *  by each run of the callback.
230
-	 *
231
-	 * @throws Exception
232
-	 * @throws ParseException
233
-	 * @see find()
234
-	 * @see DOMQuery::get()
235
-	 * @see filter()
236
-	 */
237
-	public function map($callback): Query
238
-	{
239
-		$found = new SplObjectStorage();
240
-
241
-		if (is_callable($callback)) {
242
-			$i = 0;
243
-			foreach ($this->matches as $item) {
244
-				$c = call_user_func($callback, $i, $item);
245
-				if (isset($c)) {
246
-					if (is_array($c) || $c instanceof Iterable) {
247
-						foreach ($c as $retval) {
248
-							if (! is_object($retval)) {
249
-								$tmp              = new stdClass();
250
-								$tmp->textContent = $retval;
251
-								$retval           = $tmp;
252
-							}
253
-							$found->attach($retval);
254
-						}
255
-					} else {
256
-						if (! is_object($c)) {
257
-							$tmp              = new stdClass();
258
-							$tmp->textContent = $c;
259
-							$c                = $tmp;
260
-						}
261
-						$found->attach($c);
262
-					}
263
-				}
264
-				++$i;
265
-			}
266
-		} else {
267
-			throw new Exception('Callback is not callable.');
268
-		}
269
-
270
-		return $this->inst($found, null);
271
-	}
272
-
273
-	/**
274
-	 * Narrow the items in this object down to only a slice of the starting items.
275
-	 *
276
-	 * @param integer $start
277
-	 *  Where in the list of matches to begin the slice.
278
-	 * @param integer $length
279
-	 *  The number of items to include in the slice. If nothing is specified, the
280
-	 *  all remaining matches (from $start onward) will be included in the sliced
281
-	 *  list.
282
-	 *
283
-	 * @return DOMQuery
284
-	 * @throws ParseException
285
-	 * @see array_slice()
286
-	 */
287
-	public function slice($start, $length = 0): Query
288
-	{
289
-		$end   = $length;
290
-		$found = new SplObjectStorage();
291
-		if ($start >= $this->count()) {
292
-			return $this->inst($found, null);
293
-		}
294
-
295
-		$i = $j = 0;
296
-		foreach ($this->matches as $m) {
297
-			if ($i >= $start) {
298
-				if ($end > 0 && $j >= $end) {
299
-					break;
300
-				}
301
-				$found->attach($m);
302
-				++$j;
303
-			}
304
-			++$i;
305
-		}
306
-
307
-		return $this->inst($found, null);
308
-	}
309
-
310
-	/**
311
-	 * Run a callback on each item in the list of items.
312
-	 *
313
-	 * Rules of the callback:
314
-	 * - A callback is passed two variables: $index and $item. (There is no
315
-	 *   special treatment of $this, as there is in jQuery.)
316
-	 *   - You will want to pass $item by reference if it is not an
317
-	 *     object (DOMNodes are all objects).
318
-	 * - A callback that returns FALSE will stop execution of the each() loop. This
319
-	 *   works like break in a standard loop.
320
-	 * - A TRUE return value from the callback is analogous to a continue statement.
321
-	 * - All other return values are ignored.
322
-	 *
323
-	 * @param callback $callback
324
-	 *  The callback to run.
325
-	 *
326
-	 * @return DOMQuery
327
-	 *  The DOMQuery.
328
-	 * @throws Exception
329
-	 * @see filter()
330
-	 * @see map()
331
-	 * @see eachLambda()
332
-	 */
333
-	public function each($callback): Query
334
-	{
335
-		if (is_callable($callback)) {
336
-			$i = 0;
337
-			foreach ($this->matches as $item) {
338
-				if (call_user_func($callback, $i, $item) === false) {
339
-					return $this;
340
-				}
341
-				++$i;
342
-			}
343
-		} else {
344
-			throw new Exception('Callback is not callable.');
345
-		}
346
-
347
-		return $this;
348
-	}
349
-
350
-	/**
351
-	 * An each() iterator that takes a lambda function.
352
-	 *
353
-	 * @param string $lambda
354
-	 *  The lambda function. This will be passed ($index, &$item).
355
-	 *
356
-	 * @return DOMQuery
357
-	 *  The DOMQuery object.
358
-	 * @deprecated
359
-	 *   Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this
360
-	 *   method is not necessary and should be avoided.
361
-	 * @see each()
362
-	 * @see filterLambda()
363
-	 * @see filterCallback()
364
-	 * @see map()
365
-	 */
366
-	public function eachLambda($lambda): Query
367
-	{
368
-		$index = 0;
369
-		foreach ($this->matches as $item) {
370
-			$fn = create_function('$index, &$item', $lambda);
371
-			if ($fn($index, $item) === false) {
372
-				return $this;
373
-			}
374
-			++$index;
375
-		}
376
-
377
-		return $this;
378
-	}
379
-
380
-	/**
381
-	 * Get the even elements, so counter-intuitively 1, 3, 5, etc.
382
-	 *
383
-	 * @return DOMQuery
384
-	 *  A DOMQuery wrapping all of the children.
385
-	 * @throws ParseException
386
-	 * @see    parent()
387
-	 * @see    parents()
388
-	 * @see    next()
389
-	 * @see    prev()
390
-	 * @since  2.1
391
-	 * @author eabrand
392
-	 * @see    removeChildren()
393
-	 */
394
-	public function even(): Query
395
-	{
396
-		$found = new SplObjectStorage();
397
-		$even  = false;
398
-		foreach ($this->matches as $m) {
399
-			if ($even && $m->nodeType === XML_ELEMENT_NODE) {
400
-				$found->attach($m);
401
-			}
402
-			$even = $even ? false : true;
403
-		}
404
-
405
-		return $this->inst($found, null);
406
-	}
407
-
408
-	/**
409
-	 * Get the odd elements, so counter-intuitively 0, 2, 4, etc.
410
-	 *
411
-	 * @return DOMQuery
412
-	 *  A DOMQuery wrapping all of the children.
413
-	 * @throws ParseException
414
-	 * @see    parent()
415
-	 * @see    parents()
416
-	 * @see    next()
417
-	 * @see    prev()
418
-	 * @since  2.1
419
-	 * @author eabrand
420
-	 * @see    removeChildren()
421
-	 */
422
-	public function odd(): Query
423
-	{
424
-		$found = new SplObjectStorage();
425
-		$odd   = true;
426
-		foreach ($this->matches as $m) {
427
-			if ($odd && $m->nodeType === XML_ELEMENT_NODE) {
428
-				$found->attach($m);
429
-			}
430
-			$odd = $odd ? false : true;
431
-		}
432
-
433
-		return $this->inst($found, null);
434
-	}
435
-
436
-	/**
437
-	 * Get the first matching element.
438
-	 *
439
-	 *
440
-	 * @return DOMQuery
441
-	 *  A DOMQuery wrapping all of the children.
442
-	 * @throws ParseException
443
-	 * @see    prev()
444
-	 * @since  2.1
445
-	 * @author eabrand
446
-	 * @see    next()
447
-	 */
448
-	public function first(): Query
449
-	{
450
-		$found = new SplObjectStorage();
451
-		foreach ($this->matches as $m) {
452
-			if ($m->nodeType === XML_ELEMENT_NODE) {
453
-				$found->attach($m);
454
-				break;
455
-			}
456
-		}
457
-
458
-		return $this->inst($found, null);
459
-	}
460
-
461
-	/**
462
-	 * Get the first child of the matching element.
463
-	 *
464
-	 *
465
-	 * @return DOMQuery
466
-	 *  A DOMQuery wrapping all of the children.
467
-	 * @throws ParseException
468
-	 * @see    prev()
469
-	 * @since  2.1
470
-	 * @author eabrand
471
-	 * @see    next()
472
-	 */
473
-	public function firstChild(): Query
474
-	{
475
-		// Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php
476
-		$found = new SplObjectStorage();
477
-		$flag  = false;
478
-		foreach ($this->matches as $m) {
479
-			foreach ($m->childNodes as $c) {
480
-				if ($c->nodeType === XML_ELEMENT_NODE) {
481
-					$found->attach($c);
482
-					$flag = true;
483
-					break;
484
-				}
485
-			}
486
-			if ($flag) {
487
-				break;
488
-			}
489
-		}
490
-
491
-		return $this->inst($found, null);
492
-	}
493
-
494
-	/**
495
-	 * Get the last matching element.
496
-	 *
497
-	 *
498
-	 * @return DOMQuery
499
-	 *  A DOMQuery wrapping all of the children.
500
-	 * @throws ParseException
501
-	 * @see    prev()
502
-	 * @since  2.1
503
-	 * @author eabrand
504
-	 * @see    next()
505
-	 */
506
-	public function last(): Query
507
-	{
508
-		$found = new SplObjectStorage();
509
-		$item  = null;
510
-		foreach ($this->matches as $m) {
511
-			if ($m->nodeType === XML_ELEMENT_NODE) {
512
-				$item = $m;
513
-			}
514
-		}
515
-		if ($item) {
516
-			$found->attach($item);
517
-		}
518
-
519
-		return $this->inst($found, null);
520
-	}
521
-
522
-	/**
523
-	 * Get the last child of the matching element.
524
-	 *
525
-	 *
526
-	 * @return DOMQuery
527
-	 *  A DOMQuery wrapping all of the children.
528
-	 * @throws ParseException
529
-	 * @see    prev()
530
-	 * @since  2.1
531
-	 * @author eabrand
532
-	 * @see    next()
533
-	 */
534
-	public function lastChild(): Query
535
-	{
536
-		$found = new SplObjectStorage();
537
-		$item  = null;
538
-		foreach ($this->matches as $m) {
539
-			foreach ($m->childNodes as $c) {
540
-				if ($c->nodeType === XML_ELEMENT_NODE) {
541
-					$item = $c;
542
-				}
543
-			}
544
-			if ($item) {
545
-				$found->attach($item);
546
-				$item = null;
547
-			}
548
-		}
549
-
550
-		return $this->inst($found, null);
551
-	}
552
-
553
-	/**
554
-	 * Get all siblings after an element until the selector is reached.
555
-	 *
556
-	 * For each element in the DOMQuery, get all siblings that appear after
557
-	 * it. If a selector is passed in, then only siblings that match the
558
-	 * selector will be included.
559
-	 *
560
-	 * @param string $selector
561
-	 *  A valid CSS 3 selector.
562
-	 *
563
-	 * @return DOMQuery
564
-	 *  The DOMQuery object, now containing the matching siblings.
565
-	 * @throws Exception
566
-	 * @see    prevAll()
567
-	 * @see    children()
568
-	 * @see    siblings()
569
-	 * @since  2.1
570
-	 * @author eabrand
571
-	 * @see    next()
572
-	 */
573
-	public function nextUntil($selector = null): Query
574
-	{
575
-		$found = new SplObjectStorage();
576
-		foreach ($this->matches as $m) {
577
-			while (isset($m->nextSibling)) {
578
-				$m = $m->nextSibling;
579
-				if ($m->nodeType === XML_ELEMENT_NODE) {
580
-					if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector) > 0) {
581
-						break;
582
-					}
583
-					$found->attach($m);
584
-				}
585
-			}
586
-		}
587
-
588
-		return $this->inst($found, null);
589
-	}
590
-
591
-	/**
592
-	 * Get the previous siblings for each element in the DOMQuery
593
-	 * until the selector is reached.
594
-	 *
595
-	 * For each element in the DOMQuery, get all previous siblings. If a
596
-	 * selector is provided, only matching siblings will be retrieved.
597
-	 *
598
-	 * @param string $selector
599
-	 *  A valid CSS 3 selector.
600
-	 *
601
-	 * @return DOMQuery
602
-	 *  The DOMQuery object, now wrapping previous sibling elements.
603
-	 * @throws Exception
604
-	 * @see    prev()
605
-	 * @see    nextAll()
606
-	 * @see    siblings()
607
-	 * @see    contents()
608
-	 * @see    children()
609
-	 * @since  2.1
610
-	 * @author eabrand
611
-	 */
612
-	public function prevUntil($selector = null): Query
613
-	{
614
-		$found = new SplObjectStorage();
615
-		foreach ($this->matches as $m) {
616
-			while (isset($m->previousSibling)) {
617
-				$m = $m->previousSibling;
618
-				if ($m->nodeType === XML_ELEMENT_NODE) {
619
-					if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector)) {
620
-						break;
621
-					}
622
-
623
-					$found->attach($m);
624
-				}
625
-			}
626
-		}
627
-
628
-		return $this->inst($found, null);
629
-	}
630
-
631
-	/**
632
-	 * Get all ancestors of each element in the DOMQuery until the selector is reached.
633
-	 *
634
-	 * If a selector is present, only matching ancestors will be retrieved.
635
-	 *
636
-	 * @param string $selector
637
-	 *  A valid CSS 3 Selector.
638
-	 *
639
-	 * @return DOMQuery
640
-	 *  A DOMNode object containing the matching ancestors.
641
-	 * @throws Exception
642
-	 * @see    siblings()
643
-	 * @see    children()
644
-	 * @since  2.1
645
-	 * @author eabrand
646
-	 * @see    parent()
647
-	 */
648
-	public function parentsUntil($selector = null): Query
649
-	{
650
-		$found = new SplObjectStorage();
651
-		foreach ($this->matches as $m) {
652
-			while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
653
-				$m = $m->parentNode;
654
-				// Is there any case where parent node is not an element?
655
-				if ($m->nodeType === XML_ELEMENT_NODE) {
656
-					if (! empty($selector)) {
657
-						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
658
-							break;
659
-						}
660
-						$found->attach($m);
661
-					} else {
662
-						$found->attach($m);
663
-					}
664
-				}
665
-			}
666
-		}
667
-
668
-		return $this->inst($found, null);
669
-	}
670
-
671
-	/**
672
-	 * Reduce the matched set to just one.
673
-	 *
674
-	 * This will take a matched set and reduce it to just one item -- the item
675
-	 * at the index specified. This is a destructive operation, and can be undone
676
-	 * with {@link end()}.
677
-	 *
678
-	 * @param $index
679
-	 *  The index of the element to keep. The rest will be
680
-	 *  discarded.
681
-	 *
682
-	 * @return DOMQuery
683
-	 * @throws ParseException
684
-	 * @see is()
685
-	 * @see end()
686
-	 * @see get()
687
-	 */
688
-	public function eq($index): Query
689
-	{
690
-		return $this->inst($this->getNthMatch($index), null);
691
-	}
692
-
693
-	/**
694
-	 * Filter a list to contain only items that do NOT match.
695
-	 *
696
-	 * @param string $selector
697
-	 *  A selector to use as a negation filter. If the filter is matched, the
698
-	 *  element will be removed from the list.
699
-	 *
700
-	 * @return DOMQuery
701
-	 *  The DOMQuery object with matching items filtered out.
702
-	 * @throws ParseException
703
-	 * @throws Exception
704
-	 * @see find()
705
-	 */
706
-	public function not($selector): Query
707
-	{
708
-		$found = new SplObjectStorage();
709
-		if ($selector instanceof DOMElement) {
710
-			foreach ($this->matches as $m) {
711
-				if ($m !== $selector) {
712
-					$found->attach($m);
713
-				}
714
-			}
715
-		} elseif (is_array($selector)) {
716
-			foreach ($this->matches as $m) {
717
-				if (! in_array($m, $selector, true)) {
718
-					$found->attach($m);
719
-				}
720
-			}
721
-		} elseif ($selector instanceof SplObjectStorage) {
722
-			foreach ($this->matches as $m) {
723
-				if ($selector->contains($m)) {
724
-					$found->attach($m);
725
-				}
726
-			}
727
-		} else {
728
-			foreach ($this->matches as $m) {
729
-				if (! QueryPath::with($m, null, $this->options)->is($selector)) {
730
-					$found->attach($m);
731
-				}
732
-			}
733
-		}
734
-
735
-		return $this->inst($found, null);
736
-	}
737
-
738
-	/**
739
-	 * Find the closest element matching the selector.
740
-	 *
741
-	 * This finds the closest match in the ancestry chain. It first checks the
742
-	 * present element. If the present element does not match, this traverses up
743
-	 * the ancestry chain (e.g. checks each parent) looking for an item that matches.
744
-	 *
745
-	 * It is provided for jQuery 1.3 compatibility.
746
-	 *
747
-	 * @param string $selector
748
-	 *  A CSS Selector to match.
749
-	 *
750
-	 * @return DOMQuery
751
-	 *  The set of matches.
752
-	 * @throws Exception
753
-	 * @since 2.0
754
-	 */
755
-	public function closest($selector): Query
756
-	{
757
-		$found = new SplObjectStorage();
758
-		foreach ($this->matches as $m) {
759
-			if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
760
-				$found->attach($m);
761
-			} else {
762
-				while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
763
-					$m = $m->parentNode;
764
-					// Is there any case where parent node is not an element?
765
-					if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with(
766
-						$m,
767
-						null,
768
-						$this->options
769
-					)->is($selector) > 0) {
770
-						$found->attach($m);
771
-						break;
772
-					}
773
-				}
774
-			}
775
-		}
776
-
777
-		// XXX: Should this be an in-place modification?
778
-		return $this->inst($found, null);
779
-	}
780
-
781
-	/**
782
-	 * Get the immediate parent of each element in the DOMQuery.
783
-	 *
784
-	 * If a selector is passed, this will return the nearest matching parent for
785
-	 * each element in the DOMQuery.
786
-	 *
787
-	 * @param string $selector
788
-	 *  A valid CSS3 selector.
789
-	 *
790
-	 * @return DOMQuery
791
-	 *  A DOMNode object wrapping the matching parents.
792
-	 * @throws Exception
793
-	 * @see siblings()
794
-	 * @see parents()
795
-	 * @see children()
796
-	 */
797
-	public function parent($selector = null): Query
798
-	{
799
-		$found = new SplObjectStorage();
800
-		foreach ($this->matches as $m) {
801
-			while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
802
-				$m = $m->parentNode;
803
-				// Is there any case where parent node is not an element?
804
-				if ($m->nodeType === XML_ELEMENT_NODE) {
805
-					if (! empty($selector)) {
806
-						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
807
-							$found->attach($m);
808
-							break;
809
-						}
810
-					} else {
811
-						$found->attach($m);
812
-						break;
813
-					}
814
-				}
815
-			}
816
-		}
817
-
818
-		return $this->inst($found, null);
819
-	}
820
-
821
-	/**
822
-	 * Get all ancestors of each element in the DOMQuery.
823
-	 *
824
-	 * If a selector is present, only matching ancestors will be retrieved.
825
-	 *
826
-	 * @param string $selector
827
-	 *  A valid CSS 3 Selector.
828
-	 *
829
-	 * @return DOMQuery
830
-	 *  A DOMNode object containing the matching ancestors.
831
-	 * @throws ParseException
832
-	 * @throws Exception
833
-	 * @see children()
834
-	 * @see parent()
835
-	 * @see siblings()
836
-	 */
837
-	public function parents($selector = null): Query
838
-	{
839
-		$found = new SplObjectStorage();
840
-		foreach ($this->matches as $m) {
841
-			while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
842
-				$m = $m->parentNode;
843
-				// Is there any case where parent node is not an element?
844
-				if ($m->nodeType === XML_ELEMENT_NODE) {
845
-					if (! empty($selector)) {
846
-						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
847
-							$found->attach($m);
848
-						}
849
-					} else {
850
-						$found->attach($m);
851
-					}
852
-				}
853
-			}
854
-		}
855
-
856
-		return $this->inst($found, null);
857
-	}
858
-
859
-	/**
860
-	 * Get the next sibling of each element in the DOMQuery.
861
-	 *
862
-	 * If a selector is provided, the next matching sibling will be returned.
863
-	 *
864
-	 * @param string $selector
865
-	 *  A CSS3 selector.
866
-	 *
867
-	 * @return DOMQuery
868
-	 *  The DOMQuery object.
869
-	 * @throws Exception
870
-	 * @throws ParseException
871
-	 * @see nextAll()
872
-	 * @see prev()
873
-	 * @see children()
874
-	 * @see contents()
875
-	 * @see parent()
876
-	 * @see parents()
877
-	 */
878
-	public function next($selector = null): Query
879
-	{
880
-		$found = new SplObjectStorage();
881
-		foreach ($this->matches as $m) {
882
-			while (isset($m->nextSibling)) {
883
-				$m = $m->nextSibling;
884
-				if ($m->nodeType === XML_ELEMENT_NODE) {
885
-					if (! empty($selector)) {
886
-						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
887
-							$found->attach($m);
888
-							break;
889
-						}
890
-					} else {
891
-						$found->attach($m);
892
-						break;
893
-					}
894
-				}
895
-			}
896
-		}
897
-
898
-		return $this->inst($found, null);
899
-	}
900
-
901
-	/**
902
-	 * Get all siblings after an element.
903
-	 *
904
-	 * For each element in the DOMQuery, get all siblings that appear after
905
-	 * it. If a selector is passed in, then only siblings that match the
906
-	 * selector will be included.
907
-	 *
908
-	 * @param string $selector
909
-	 *  A valid CSS 3 selector.
910
-	 *
911
-	 * @return DOMQuery
912
-	 *  The DOMQuery object, now containing the matching siblings.
913
-	 * @throws Exception
914
-	 * @throws ParseException
915
-	 * @see next()
916
-	 * @see prevAll()
917
-	 * @see children()
918
-	 * @see siblings()
919
-	 */
920
-	public function nextAll($selector = null): Query
921
-	{
922
-		$found = new SplObjectStorage();
923
-		foreach ($this->matches as $m) {
924
-			while (isset($m->nextSibling)) {
925
-				$m = $m->nextSibling;
926
-				if ($m->nodeType === XML_ELEMENT_NODE) {
927
-					if (! empty($selector)) {
928
-						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
929
-							$found->attach($m);
930
-						}
931
-					} else {
932
-						$found->attach($m);
933
-					}
934
-				}
935
-			}
936
-		}
937
-
938
-		return $this->inst($found, null);
939
-	}
940
-
941
-	/**
942
-	 * Get the next sibling before each element in the DOMQuery.
943
-	 *
944
-	 * For each element in the DOMQuery, this retrieves the previous sibling
945
-	 * (if any). If a selector is supplied, it retrieves the first matching
946
-	 * sibling (if any is found).
947
-	 *
948
-	 * @param string $selector
949
-	 *  A valid CSS 3 selector.
950
-	 *
951
-	 * @return DOMQuery
952
-	 *  A DOMNode object, now containing any previous siblings that have been
953
-	 *  found.
954
-	 * @throws Exception
955
-	 * @throws ParseException
956
-	 * @see prevAll()
957
-	 * @see next()
958
-	 * @see siblings()
959
-	 * @see children()
960
-	 */
961
-	public function prev($selector = null): Query
962
-	{
963
-		$found = new SplObjectStorage();
964
-		foreach ($this->matches as $m) {
965
-			while (isset($m->previousSibling)) {
966
-				$m = $m->previousSibling;
967
-				if ($m->nodeType === XML_ELEMENT_NODE) {
968
-					if (! empty($selector)) {
969
-						if (QueryPath::with($m, null, $this->options)->is($selector)) {
970
-							$found->attach($m);
971
-							break;
972
-						}
973
-					} else {
974
-						$found->attach($m);
975
-						break;
976
-					}
977
-				}
978
-			}
979
-		}
980
-
981
-		return $this->inst($found, null);
982
-	}
983
-
984
-	/**
985
-	 * Get the previous siblings for each element in the DOMQuery.
986
-	 *
987
-	 * For each element in the DOMQuery, get all previous siblings. If a
988
-	 * selector is provided, only matching siblings will be retrieved.
989
-	 *
990
-	 * @param string $selector
991
-	 *  A valid CSS 3 selector.
992
-	 *
993
-	 * @return DOMQuery
994
-	 *  The DOMQuery object, now wrapping previous sibling elements.
995
-	 * @throws ParseException
996
-	 * @throws Exception
997
-	 * @see siblings()
998
-	 * @see contents()
999
-	 * @see children()
1000
-	 * @see prev()
1001
-	 * @see nextAll()
1002
-	 */
1003
-	public function prevAll($selector = null): Query
1004
-	{
1005
-		$found = new SplObjectStorage();
1006
-		foreach ($this->matches as $m) {
1007
-			while (isset($m->previousSibling)) {
1008
-				$m = $m->previousSibling;
1009
-				if ($m->nodeType === XML_ELEMENT_NODE) {
1010
-					if (! empty($selector)) {
1011
-						if (QueryPath::with($m, null, $this->options)->is($selector)) {
1012
-							$found->attach($m);
1013
-						}
1014
-					} else {
1015
-						$found->attach($m);
1016
-					}
1017
-				}
1018
-			}
1019
-		}
1020
-
1021
-		return $this->inst($found, null);
1022
-	}
1023
-
1024
-	/**
1025
-	 * Get the children of the elements in the DOMQuery object.
1026
-	 *
1027
-	 * If a selector is provided, the list of children will be filtered through
1028
-	 * the selector.
1029
-	 *
1030
-	 * @param string $selector
1031
-	 *  A valid selector.
1032
-	 *
1033
-	 * @return DOMQuery
1034
-	 *  A DOMQuery wrapping all of the children.
1035
-	 * @throws ParseException
1036
-	 * @see parent()
1037
-	 * @see parents()
1038
-	 * @see next()
1039
-	 * @see prev()
1040
-	 * @see removeChildren()
1041
-	 */
1042
-	public function children($selector = null): Query
1043
-	{
1044
-		$found  = new SplObjectStorage();
1045
-		$filter = is_string($selector) && strlen($selector) > 0;
1046
-
1047
-		if ($filter) {
1048
-			$tmp = new SplObjectStorage();
1049
-		}
1050
-		foreach ($this->matches as $m) {
1051
-			foreach ($m->childNodes as $c) {
1052
-				if ($c->nodeType === XML_ELEMENT_NODE) {
1053
-					// This is basically an optimized filter() just for children().
1054
-					if ($filter) {
1055
-						$tmp->attach($c);
1056
-						$query = new DOMTraverser($tmp, true, $c);
1057
-						$query->find($selector);
1058
-						if (count($query->matches()) > 0) {
1059
-							$found->attach($c);
1060
-						}
1061
-						$tmp->detach($c);
1062
-					} // No filter. Just attach it.
1063
-					else {
1064
-						$found->attach($c);
1065
-					}
1066
-				}
1067
-			}
1068
-		}
1069
-
1070
-		return $this->inst($found, null);
1071
-	}
1072
-
1073
-	/**
1074
-	 * Get all child nodes (not just elements) of all items in the matched set.
1075
-	 *
1076
-	 * It gets only the immediate children, not all nodes in the subtree.
1077
-	 *
1078
-	 * This does not process iframes. Xinclude processing is dependent on the
1079
-	 * DOM implementation and configuration.
1080
-	 *
1081
-	 * @return DOMQuery
1082
-	 *  A DOMNode object wrapping all child nodes for all elements in the
1083
-	 *  DOMNode object.
1084
-	 * @throws ParseException
1085
-	 * @see text()
1086
-	 * @see html()
1087
-	 * @see innerHTML()
1088
-	 * @see xml()
1089
-	 * @see innerXML()
1090
-	 * @see find()
1091
-	 */
1092
-	public function contents(): Query
1093
-	{
1094
-		$found = new SplObjectStorage();
1095
-		foreach ($this->matches as $m) {
1096
-			if (empty($m->childNodes)) {
1097
-				continue;
1098
-			}
1099
-			foreach ($m->childNodes as $c) {
1100
-				$found->attach($c);
1101
-			}
1102
-		}
1103
-
1104
-		return $this->inst($found, null);
1105
-	}
1106
-
1107
-	/**
1108
-	 * Get a list of siblings for elements currently wrapped by this object.
1109
-	 *
1110
-	 * This will compile a list of every sibling of every element in the
1111
-	 * current list of elements.
1112
-	 *
1113
-	 * Note that if two siblings are present in the DOMQuery object to begin with,
1114
-	 * then both will be returned in the matched set, since they are siblings of each
1115
-	 * other. In other words,if the matches contain a and b, and a and b are siblings of
1116
-	 * each other, than running siblings will return a set that contains
1117
-	 * both a and b.
1118
-	 *
1119
-	 * @param string $selector
1120
-	 *  If the optional selector is provided, siblings will be filtered through
1121
-	 *  this expression.
1122
-	 *
1123
-	 * @return DOMQuery
1124
-	 *  The DOMQuery containing the matched siblings.
1125
-	 * @throws ParseException
1126
-	 * @throws ParseException
1127
-	 * @see parent()
1128
-	 * @see parents()
1129
-	 * @see contents()
1130
-	 * @see children()
1131
-	 */
1132
-	public function siblings($selector = null): Query
1133
-	{
1134
-		$found = new SplObjectStorage();
1135
-		foreach ($this->matches as $m) {
1136
-			$parent = $m->parentNode;
1137
-			foreach ($parent->childNodes as $n) {
1138
-				if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) {
1139
-					$found->attach($n);
1140
-				}
1141
-			}
1142
-		}
1143
-		if (empty($selector)) {
1144
-			return $this->inst($found, null);
1145
-		}
1146
-
1147
-		return $this->inst($found, null)->filter($selector);
1148
-	}
25
+    /**
26
+     * Filter a list down to only elements that match the selector.
27
+     * Use this, for example, to find all elements with a class, or with
28
+     * certain children.
29
+     *
30
+     * @param string $selector
31
+     *   The selector to use as a filter.
32
+     *
33
+     * @return Query The DOMQuery with non-matching items filtered out.*   The DOMQuery with non-matching items
34
+     *               filtered out.
35
+     * @throws ParseException
36
+     * @see filterCallback()
37
+     * @see map()
38
+     * @see find()
39
+     * @see is()
40
+     * @see filterLambda()
41
+     */
42
+    public function filter($selector): Query
43
+    {
44
+        $found = new SplObjectStorage();
45
+        $tmp   = new SplObjectStorage();
46
+
47
+        foreach ($this->matches as $m) {
48
+            $tmp->attach($m);
49
+            // Seems like this should be right... but it fails unit
50
+            // tests. Need to compare to jQuery.
51
+            // $query = new \QueryPath\CSS\DOMTraverser($tmp, TRUE, $m);
52
+            $query = new DOMTraverser($tmp);
53
+            $query->find($selector);
54
+            if (count($query->matches())) {
55
+                $found->attach($m);
56
+            }
57
+            $tmp->detach($m);
58
+        }
59
+
60
+        return $this->inst($found, null);
61
+    }
62
+
63
+    /**
64
+     * Filter based on a lambda function.
65
+     *
66
+     * The function string will be executed as if it were the body of a
67
+     * function. It is passed two arguments:
68
+     * - $index: The index of the item.
69
+     * - $item: The current Element.
70
+     * If the function returns boolean FALSE, the item will be removed from
71
+     * the list of elements. Otherwise it will be kept.
72
+     *
73
+     * Example:
74
+     *
75
+     * @code
76
+     * qp('li')->filterLambda('qp($item)->attr("id") == "test"');
77
+     * @endcode
78
+     *
79
+     * The above would filter down the list to only an item whose ID is
80
+     * 'text'.
81
+     *
82
+     * @param string $fn
83
+     *  Inline lambda function in a string.
84
+     *
85
+     * @return DOMQuery
86
+     * @throws ParseException
87
+     *
88
+     * @see map()
89
+     * @see mapLambda()
90
+     * @see filterCallback()
91
+     * @see filter()
92
+     * @deprecated
93
+     *   Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this
94
+     *   method is not necessary and should be avoided.
95
+     */
96
+    public function filterLambda($fn): Query
97
+    {
98
+        $function = create_function('$index, $item', $fn);
99
+        $found    = new SplObjectStorage();
100
+        $i        = 0;
101
+        foreach ($this->matches as $item) {
102
+            if ($function($i++, $item) !== false) {
103
+                $found->attach($item);
104
+            }
105
+        }
106
+
107
+        return $this->inst($found, null);
108
+    }
109
+
110
+    /**
111
+     * Use regular expressions to filter based on the text content of matched elements.
112
+     *
113
+     * Only items that match the given regular expression will be kept. All others will
114
+     * be removed.
115
+     *
116
+     * The regular expression is run against the <i>text content</i> (the PCDATA) of the
117
+     * elements. This is a way of filtering elements based on their content.
118
+     *
119
+     * Example:
120
+     *
121
+     * @code
122
+     *  <?xml version="1.0"?>
123
+     *  <div>Hello <i>World</i></div>
124
+     * @endcode
125
+     *
126
+     * @code
127
+     *  <?php
128
+     *    // This will be 1.
129
+     *    qp($xml, 'div')->filterPreg('/World/')->matches->count();
130
+     *  ?>
131
+     * @endcode
132
+     *
133
+     * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is
134
+     * @codeHello World@endcode.
135
+     *
136
+     * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class.
137
+     *
138
+     * @param string $regex
139
+     *  A regular expression.
140
+     *
141
+     * @return DOMQuery
142
+     * @throws ParseException
143
+     * @see       filterCallback()
144
+     * @see       preg_match()
145
+     * @see       filter()
146
+     */
147
+    public function filterPreg($regex): Query
148
+    {
149
+        $found = new SplObjectStorage();
150
+
151
+        foreach ($this->matches as $item) {
152
+            if (preg_match($regex, $item->textContent) > 0) {
153
+                $found->attach($item);
154
+            }
155
+        }
156
+
157
+        return $this->inst($found, null);
158
+    }
159
+
160
+    /**
161
+     * Filter based on a callback function.
162
+     *
163
+     * A callback may be any of the following:
164
+     *  - a function: 'my_func'.
165
+     *  - an object/method combo: $obj, 'myMethod'
166
+     *  - a class/method combo: 'MyClass', 'myMethod'
167
+     * Note that classes are passed in strings. Objects are not.
168
+     *
169
+     * Each callback is passed to arguments:
170
+     *  - $index: The index position of the object in the array.
171
+     *  - $item: The item to be operated upon.
172
+     *
173
+     * If the callback function returns FALSE, the item will be removed from the
174
+     * set of matches. Otherwise the item will be considered a match and left alone.
175
+     *
176
+     * @param callback $callback .
177
+     *                           A callback either as a string (function) or an array (object, method OR
178
+     *                           classname, method).
179
+     *
180
+     * @return DOMQuery
181
+     *                           Query path object augmented according to the function.
182
+     * @throws ParseException
183
+     * @throws Exception
184
+     * @see map()
185
+     * @see is()
186
+     * @see find()
187
+     * @see filter()
188
+     * @see filterLambda()
189
+     */
190
+    public function filterCallback($callback): Query
191
+    {
192
+        $found = new SplObjectStorage();
193
+        $i     = 0;
194
+        if (is_callable($callback)) {
195
+            foreach ($this->matches as $item) {
196
+                if ($callback($i++, $item) !== false) {
197
+                    $found->attach($item);
198
+                }
199
+            }
200
+        } else {
201
+            throw new Exception('The specified callback is not callable.');
202
+        }
203
+
204
+        return $this->inst($found, null);
205
+    }
206
+
207
+    /**
208
+     * Run a function on each item in a set.
209
+     *
210
+     * The mapping callback can return anything. Whatever it returns will be
211
+     * stored as a match in the set, though. This means that afer a map call,
212
+     * there is no guarantee that the elements in the set will behave correctly
213
+     * with other DOMQuery functions.
214
+     *
215
+     * Callback rules:
216
+     * - If the callback returns NULL, the item will be removed from the array.
217
+     * - If the callback returns an array, the entire array will be stored in
218
+     *   the results.
219
+     * - If the callback returns anything else, it will be appended to the array
220
+     *   of matches.
221
+     *
222
+     * @param callback $callback
223
+     *  The function or callback to use. The callback will be passed two params:
224
+     *  - $index: The index position in the list of items wrapped by this object.
225
+     *  - $item: The current item.
226
+     *
227
+     * @return DOMQuery
228
+     *  The DOMQuery object wrapping a list of whatever values were returned
229
+     *  by each run of the callback.
230
+     *
231
+     * @throws Exception
232
+     * @throws ParseException
233
+     * @see find()
234
+     * @see DOMQuery::get()
235
+     * @see filter()
236
+     */
237
+    public function map($callback): Query
238
+    {
239
+        $found = new SplObjectStorage();
240
+
241
+        if (is_callable($callback)) {
242
+            $i = 0;
243
+            foreach ($this->matches as $item) {
244
+                $c = call_user_func($callback, $i, $item);
245
+                if (isset($c)) {
246
+                    if (is_array($c) || $c instanceof Iterable) {
247
+                        foreach ($c as $retval) {
248
+                            if (! is_object($retval)) {
249
+                                $tmp              = new stdClass();
250
+                                $tmp->textContent = $retval;
251
+                                $retval           = $tmp;
252
+                            }
253
+                            $found->attach($retval);
254
+                        }
255
+                    } else {
256
+                        if (! is_object($c)) {
257
+                            $tmp              = new stdClass();
258
+                            $tmp->textContent = $c;
259
+                            $c                = $tmp;
260
+                        }
261
+                        $found->attach($c);
262
+                    }
263
+                }
264
+                ++$i;
265
+            }
266
+        } else {
267
+            throw new Exception('Callback is not callable.');
268
+        }
269
+
270
+        return $this->inst($found, null);
271
+    }
272
+
273
+    /**
274
+     * Narrow the items in this object down to only a slice of the starting items.
275
+     *
276
+     * @param integer $start
277
+     *  Where in the list of matches to begin the slice.
278
+     * @param integer $length
279
+     *  The number of items to include in the slice. If nothing is specified, the
280
+     *  all remaining matches (from $start onward) will be included in the sliced
281
+     *  list.
282
+     *
283
+     * @return DOMQuery
284
+     * @throws ParseException
285
+     * @see array_slice()
286
+     */
287
+    public function slice($start, $length = 0): Query
288
+    {
289
+        $end   = $length;
290
+        $found = new SplObjectStorage();
291
+        if ($start >= $this->count()) {
292
+            return $this->inst($found, null);
293
+        }
294
+
295
+        $i = $j = 0;
296
+        foreach ($this->matches as $m) {
297
+            if ($i >= $start) {
298
+                if ($end > 0 && $j >= $end) {
299
+                    break;
300
+                }
301
+                $found->attach($m);
302
+                ++$j;
303
+            }
304
+            ++$i;
305
+        }
306
+
307
+        return $this->inst($found, null);
308
+    }
309
+
310
+    /**
311
+     * Run a callback on each item in the list of items.
312
+     *
313
+     * Rules of the callback:
314
+     * - A callback is passed two variables: $index and $item. (There is no
315
+     *   special treatment of $this, as there is in jQuery.)
316
+     *   - You will want to pass $item by reference if it is not an
317
+     *     object (DOMNodes are all objects).
318
+     * - A callback that returns FALSE will stop execution of the each() loop. This
319
+     *   works like break in a standard loop.
320
+     * - A TRUE return value from the callback is analogous to a continue statement.
321
+     * - All other return values are ignored.
322
+     *
323
+     * @param callback $callback
324
+     *  The callback to run.
325
+     *
326
+     * @return DOMQuery
327
+     *  The DOMQuery.
328
+     * @throws Exception
329
+     * @see filter()
330
+     * @see map()
331
+     * @see eachLambda()
332
+     */
333
+    public function each($callback): Query
334
+    {
335
+        if (is_callable($callback)) {
336
+            $i = 0;
337
+            foreach ($this->matches as $item) {
338
+                if (call_user_func($callback, $i, $item) === false) {
339
+                    return $this;
340
+                }
341
+                ++$i;
342
+            }
343
+        } else {
344
+            throw new Exception('Callback is not callable.');
345
+        }
346
+
347
+        return $this;
348
+    }
349
+
350
+    /**
351
+     * An each() iterator that takes a lambda function.
352
+     *
353
+     * @param string $lambda
354
+     *  The lambda function. This will be passed ($index, &$item).
355
+     *
356
+     * @return DOMQuery
357
+     *  The DOMQuery object.
358
+     * @deprecated
359
+     *   Since PHP 5.3 supports anonymous functions -- REAL Lambdas -- this
360
+     *   method is not necessary and should be avoided.
361
+     * @see each()
362
+     * @see filterLambda()
363
+     * @see filterCallback()
364
+     * @see map()
365
+     */
366
+    public function eachLambda($lambda): Query
367
+    {
368
+        $index = 0;
369
+        foreach ($this->matches as $item) {
370
+            $fn = create_function('$index, &$item', $lambda);
371
+            if ($fn($index, $item) === false) {
372
+                return $this;
373
+            }
374
+            ++$index;
375
+        }
376
+
377
+        return $this;
378
+    }
379
+
380
+    /**
381
+     * Get the even elements, so counter-intuitively 1, 3, 5, etc.
382
+     *
383
+     * @return DOMQuery
384
+     *  A DOMQuery wrapping all of the children.
385
+     * @throws ParseException
386
+     * @see    parent()
387
+     * @see    parents()
388
+     * @see    next()
389
+     * @see    prev()
390
+     * @since  2.1
391
+     * @author eabrand
392
+     * @see    removeChildren()
393
+     */
394
+    public function even(): Query
395
+    {
396
+        $found = new SplObjectStorage();
397
+        $even  = false;
398
+        foreach ($this->matches as $m) {
399
+            if ($even && $m->nodeType === XML_ELEMENT_NODE) {
400
+                $found->attach($m);
401
+            }
402
+            $even = $even ? false : true;
403
+        }
404
+
405
+        return $this->inst($found, null);
406
+    }
407
+
408
+    /**
409
+     * Get the odd elements, so counter-intuitively 0, 2, 4, etc.
410
+     *
411
+     * @return DOMQuery
412
+     *  A DOMQuery wrapping all of the children.
413
+     * @throws ParseException
414
+     * @see    parent()
415
+     * @see    parents()
416
+     * @see    next()
417
+     * @see    prev()
418
+     * @since  2.1
419
+     * @author eabrand
420
+     * @see    removeChildren()
421
+     */
422
+    public function odd(): Query
423
+    {
424
+        $found = new SplObjectStorage();
425
+        $odd   = true;
426
+        foreach ($this->matches as $m) {
427
+            if ($odd && $m->nodeType === XML_ELEMENT_NODE) {
428
+                $found->attach($m);
429
+            }
430
+            $odd = $odd ? false : true;
431
+        }
432
+
433
+        return $this->inst($found, null);
434
+    }
435
+
436
+    /**
437
+     * Get the first matching element.
438
+     *
439
+     *
440
+     * @return DOMQuery
441
+     *  A DOMQuery wrapping all of the children.
442
+     * @throws ParseException
443
+     * @see    prev()
444
+     * @since  2.1
445
+     * @author eabrand
446
+     * @see    next()
447
+     */
448
+    public function first(): Query
449
+    {
450
+        $found = new SplObjectStorage();
451
+        foreach ($this->matches as $m) {
452
+            if ($m->nodeType === XML_ELEMENT_NODE) {
453
+                $found->attach($m);
454
+                break;
455
+            }
456
+        }
457
+
458
+        return $this->inst($found, null);
459
+    }
460
+
461
+    /**
462
+     * Get the first child of the matching element.
463
+     *
464
+     *
465
+     * @return DOMQuery
466
+     *  A DOMQuery wrapping all of the children.
467
+     * @throws ParseException
468
+     * @see    prev()
469
+     * @since  2.1
470
+     * @author eabrand
471
+     * @see    next()
472
+     */
473
+    public function firstChild(): Query
474
+    {
475
+        // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php
476
+        $found = new SplObjectStorage();
477
+        $flag  = false;
478
+        foreach ($this->matches as $m) {
479
+            foreach ($m->childNodes as $c) {
480
+                if ($c->nodeType === XML_ELEMENT_NODE) {
481
+                    $found->attach($c);
482
+                    $flag = true;
483
+                    break;
484
+                }
485
+            }
486
+            if ($flag) {
487
+                break;
488
+            }
489
+        }
490
+
491
+        return $this->inst($found, null);
492
+    }
493
+
494
+    /**
495
+     * Get the last matching element.
496
+     *
497
+     *
498
+     * @return DOMQuery
499
+     *  A DOMQuery wrapping all of the children.
500
+     * @throws ParseException
501
+     * @see    prev()
502
+     * @since  2.1
503
+     * @author eabrand
504
+     * @see    next()
505
+     */
506
+    public function last(): Query
507
+    {
508
+        $found = new SplObjectStorage();
509
+        $item  = null;
510
+        foreach ($this->matches as $m) {
511
+            if ($m->nodeType === XML_ELEMENT_NODE) {
512
+                $item = $m;
513
+            }
514
+        }
515
+        if ($item) {
516
+            $found->attach($item);
517
+        }
518
+
519
+        return $this->inst($found, null);
520
+    }
521
+
522
+    /**
523
+     * Get the last child of the matching element.
524
+     *
525
+     *
526
+     * @return DOMQuery
527
+     *  A DOMQuery wrapping all of the children.
528
+     * @throws ParseException
529
+     * @see    prev()
530
+     * @since  2.1
531
+     * @author eabrand
532
+     * @see    next()
533
+     */
534
+    public function lastChild(): Query
535
+    {
536
+        $found = new SplObjectStorage();
537
+        $item  = null;
538
+        foreach ($this->matches as $m) {
539
+            foreach ($m->childNodes as $c) {
540
+                if ($c->nodeType === XML_ELEMENT_NODE) {
541
+                    $item = $c;
542
+                }
543
+            }
544
+            if ($item) {
545
+                $found->attach($item);
546
+                $item = null;
547
+            }
548
+        }
549
+
550
+        return $this->inst($found, null);
551
+    }
552
+
553
+    /**
554
+     * Get all siblings after an element until the selector is reached.
555
+     *
556
+     * For each element in the DOMQuery, get all siblings that appear after
557
+     * it. If a selector is passed in, then only siblings that match the
558
+     * selector will be included.
559
+     *
560
+     * @param string $selector
561
+     *  A valid CSS 3 selector.
562
+     *
563
+     * @return DOMQuery
564
+     *  The DOMQuery object, now containing the matching siblings.
565
+     * @throws Exception
566
+     * @see    prevAll()
567
+     * @see    children()
568
+     * @see    siblings()
569
+     * @since  2.1
570
+     * @author eabrand
571
+     * @see    next()
572
+     */
573
+    public function nextUntil($selector = null): Query
574
+    {
575
+        $found = new SplObjectStorage();
576
+        foreach ($this->matches as $m) {
577
+            while (isset($m->nextSibling)) {
578
+                $m = $m->nextSibling;
579
+                if ($m->nodeType === XML_ELEMENT_NODE) {
580
+                    if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector) > 0) {
581
+                        break;
582
+                    }
583
+                    $found->attach($m);
584
+                }
585
+            }
586
+        }
587
+
588
+        return $this->inst($found, null);
589
+    }
590
+
591
+    /**
592
+     * Get the previous siblings for each element in the DOMQuery
593
+     * until the selector is reached.
594
+     *
595
+     * For each element in the DOMQuery, get all previous siblings. If a
596
+     * selector is provided, only matching siblings will be retrieved.
597
+     *
598
+     * @param string $selector
599
+     *  A valid CSS 3 selector.
600
+     *
601
+     * @return DOMQuery
602
+     *  The DOMQuery object, now wrapping previous sibling elements.
603
+     * @throws Exception
604
+     * @see    prev()
605
+     * @see    nextAll()
606
+     * @see    siblings()
607
+     * @see    contents()
608
+     * @see    children()
609
+     * @since  2.1
610
+     * @author eabrand
611
+     */
612
+    public function prevUntil($selector = null): Query
613
+    {
614
+        $found = new SplObjectStorage();
615
+        foreach ($this->matches as $m) {
616
+            while (isset($m->previousSibling)) {
617
+                $m = $m->previousSibling;
618
+                if ($m->nodeType === XML_ELEMENT_NODE) {
619
+                    if (null !== $selector && QueryPath::with($m, null, $this->options)->is($selector)) {
620
+                        break;
621
+                    }
622
+
623
+                    $found->attach($m);
624
+                }
625
+            }
626
+        }
627
+
628
+        return $this->inst($found, null);
629
+    }
630
+
631
+    /**
632
+     * Get all ancestors of each element in the DOMQuery until the selector is reached.
633
+     *
634
+     * If a selector is present, only matching ancestors will be retrieved.
635
+     *
636
+     * @param string $selector
637
+     *  A valid CSS 3 Selector.
638
+     *
639
+     * @return DOMQuery
640
+     *  A DOMNode object containing the matching ancestors.
641
+     * @throws Exception
642
+     * @see    siblings()
643
+     * @see    children()
644
+     * @since  2.1
645
+     * @author eabrand
646
+     * @see    parent()
647
+     */
648
+    public function parentsUntil($selector = null): Query
649
+    {
650
+        $found = new SplObjectStorage();
651
+        foreach ($this->matches as $m) {
652
+            while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
653
+                $m = $m->parentNode;
654
+                // Is there any case where parent node is not an element?
655
+                if ($m->nodeType === XML_ELEMENT_NODE) {
656
+                    if (! empty($selector)) {
657
+                        if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
658
+                            break;
659
+                        }
660
+                        $found->attach($m);
661
+                    } else {
662
+                        $found->attach($m);
663
+                    }
664
+                }
665
+            }
666
+        }
667
+
668
+        return $this->inst($found, null);
669
+    }
670
+
671
+    /**
672
+     * Reduce the matched set to just one.
673
+     *
674
+     * This will take a matched set and reduce it to just one item -- the item
675
+     * at the index specified. This is a destructive operation, and can be undone
676
+     * with {@link end()}.
677
+     *
678
+     * @param $index
679
+     *  The index of the element to keep. The rest will be
680
+     *  discarded.
681
+     *
682
+     * @return DOMQuery
683
+     * @throws ParseException
684
+     * @see is()
685
+     * @see end()
686
+     * @see get()
687
+     */
688
+    public function eq($index): Query
689
+    {
690
+        return $this->inst($this->getNthMatch($index), null);
691
+    }
692
+
693
+    /**
694
+     * Filter a list to contain only items that do NOT match.
695
+     *
696
+     * @param string $selector
697
+     *  A selector to use as a negation filter. If the filter is matched, the
698
+     *  element will be removed from the list.
699
+     *
700
+     * @return DOMQuery
701
+     *  The DOMQuery object with matching items filtered out.
702
+     * @throws ParseException
703
+     * @throws Exception
704
+     * @see find()
705
+     */
706
+    public function not($selector): Query
707
+    {
708
+        $found = new SplObjectStorage();
709
+        if ($selector instanceof DOMElement) {
710
+            foreach ($this->matches as $m) {
711
+                if ($m !== $selector) {
712
+                    $found->attach($m);
713
+                }
714
+            }
715
+        } elseif (is_array($selector)) {
716
+            foreach ($this->matches as $m) {
717
+                if (! in_array($m, $selector, true)) {
718
+                    $found->attach($m);
719
+                }
720
+            }
721
+        } elseif ($selector instanceof SplObjectStorage) {
722
+            foreach ($this->matches as $m) {
723
+                if ($selector->contains($m)) {
724
+                    $found->attach($m);
725
+                }
726
+            }
727
+        } else {
728
+            foreach ($this->matches as $m) {
729
+                if (! QueryPath::with($m, null, $this->options)->is($selector)) {
730
+                    $found->attach($m);
731
+                }
732
+            }
733
+        }
734
+
735
+        return $this->inst($found, null);
736
+    }
737
+
738
+    /**
739
+     * Find the closest element matching the selector.
740
+     *
741
+     * This finds the closest match in the ancestry chain. It first checks the
742
+     * present element. If the present element does not match, this traverses up
743
+     * the ancestry chain (e.g. checks each parent) looking for an item that matches.
744
+     *
745
+     * It is provided for jQuery 1.3 compatibility.
746
+     *
747
+     * @param string $selector
748
+     *  A CSS Selector to match.
749
+     *
750
+     * @return DOMQuery
751
+     *  The set of matches.
752
+     * @throws Exception
753
+     * @since 2.0
754
+     */
755
+    public function closest($selector): Query
756
+    {
757
+        $found = new SplObjectStorage();
758
+        foreach ($this->matches as $m) {
759
+            if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
760
+                $found->attach($m);
761
+            } else {
762
+                while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
763
+                    $m = $m->parentNode;
764
+                    // Is there any case where parent node is not an element?
765
+                    if ($m->nodeType === XML_ELEMENT_NODE && QueryPath::with(
766
+                        $m,
767
+                        null,
768
+                        $this->options
769
+                    )->is($selector) > 0) {
770
+                        $found->attach($m);
771
+                        break;
772
+                    }
773
+                }
774
+            }
775
+        }
776
+
777
+        // XXX: Should this be an in-place modification?
778
+        return $this->inst($found, null);
779
+    }
780
+
781
+    /**
782
+     * Get the immediate parent of each element in the DOMQuery.
783
+     *
784
+     * If a selector is passed, this will return the nearest matching parent for
785
+     * each element in the DOMQuery.
786
+     *
787
+     * @param string $selector
788
+     *  A valid CSS3 selector.
789
+     *
790
+     * @return DOMQuery
791
+     *  A DOMNode object wrapping the matching parents.
792
+     * @throws Exception
793
+     * @see siblings()
794
+     * @see parents()
795
+     * @see children()
796
+     */
797
+    public function parent($selector = null): Query
798
+    {
799
+        $found = new SplObjectStorage();
800
+        foreach ($this->matches as $m) {
801
+            while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
802
+                $m = $m->parentNode;
803
+                // Is there any case where parent node is not an element?
804
+                if ($m->nodeType === XML_ELEMENT_NODE) {
805
+                    if (! empty($selector)) {
806
+                        if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
807
+                            $found->attach($m);
808
+                            break;
809
+                        }
810
+                    } else {
811
+                        $found->attach($m);
812
+                        break;
813
+                    }
814
+                }
815
+            }
816
+        }
817
+
818
+        return $this->inst($found, null);
819
+    }
820
+
821
+    /**
822
+     * Get all ancestors of each element in the DOMQuery.
823
+     *
824
+     * If a selector is present, only matching ancestors will be retrieved.
825
+     *
826
+     * @param string $selector
827
+     *  A valid CSS 3 Selector.
828
+     *
829
+     * @return DOMQuery
830
+     *  A DOMNode object containing the matching ancestors.
831
+     * @throws ParseException
832
+     * @throws Exception
833
+     * @see children()
834
+     * @see parent()
835
+     * @see siblings()
836
+     */
837
+    public function parents($selector = null): Query
838
+    {
839
+        $found = new SplObjectStorage();
840
+        foreach ($this->matches as $m) {
841
+            while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
842
+                $m = $m->parentNode;
843
+                // Is there any case where parent node is not an element?
844
+                if ($m->nodeType === XML_ELEMENT_NODE) {
845
+                    if (! empty($selector)) {
846
+                        if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
847
+                            $found->attach($m);
848
+                        }
849
+                    } else {
850
+                        $found->attach($m);
851
+                    }
852
+                }
853
+            }
854
+        }
855
+
856
+        return $this->inst($found, null);
857
+    }
858
+
859
+    /**
860
+     * Get the next sibling of each element in the DOMQuery.
861
+     *
862
+     * If a selector is provided, the next matching sibling will be returned.
863
+     *
864
+     * @param string $selector
865
+     *  A CSS3 selector.
866
+     *
867
+     * @return DOMQuery
868
+     *  The DOMQuery object.
869
+     * @throws Exception
870
+     * @throws ParseException
871
+     * @see nextAll()
872
+     * @see prev()
873
+     * @see children()
874
+     * @see contents()
875
+     * @see parent()
876
+     * @see parents()
877
+     */
878
+    public function next($selector = null): Query
879
+    {
880
+        $found = new SplObjectStorage();
881
+        foreach ($this->matches as $m) {
882
+            while (isset($m->nextSibling)) {
883
+                $m = $m->nextSibling;
884
+                if ($m->nodeType === XML_ELEMENT_NODE) {
885
+                    if (! empty($selector)) {
886
+                        if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
887
+                            $found->attach($m);
888
+                            break;
889
+                        }
890
+                    } else {
891
+                        $found->attach($m);
892
+                        break;
893
+                    }
894
+                }
895
+            }
896
+        }
897
+
898
+        return $this->inst($found, null);
899
+    }
900
+
901
+    /**
902
+     * Get all siblings after an element.
903
+     *
904
+     * For each element in the DOMQuery, get all siblings that appear after
905
+     * it. If a selector is passed in, then only siblings that match the
906
+     * selector will be included.
907
+     *
908
+     * @param string $selector
909
+     *  A valid CSS 3 selector.
910
+     *
911
+     * @return DOMQuery
912
+     *  The DOMQuery object, now containing the matching siblings.
913
+     * @throws Exception
914
+     * @throws ParseException
915
+     * @see next()
916
+     * @see prevAll()
917
+     * @see children()
918
+     * @see siblings()
919
+     */
920
+    public function nextAll($selector = null): Query
921
+    {
922
+        $found = new SplObjectStorage();
923
+        foreach ($this->matches as $m) {
924
+            while (isset($m->nextSibling)) {
925
+                $m = $m->nextSibling;
926
+                if ($m->nodeType === XML_ELEMENT_NODE) {
927
+                    if (! empty($selector)) {
928
+                        if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
929
+                            $found->attach($m);
930
+                        }
931
+                    } else {
932
+                        $found->attach($m);
933
+                    }
934
+                }
935
+            }
936
+        }
937
+
938
+        return $this->inst($found, null);
939
+    }
940
+
941
+    /**
942
+     * Get the next sibling before each element in the DOMQuery.
943
+     *
944
+     * For each element in the DOMQuery, this retrieves the previous sibling
945
+     * (if any). If a selector is supplied, it retrieves the first matching
946
+     * sibling (if any is found).
947
+     *
948
+     * @param string $selector
949
+     *  A valid CSS 3 selector.
950
+     *
951
+     * @return DOMQuery
952
+     *  A DOMNode object, now containing any previous siblings that have been
953
+     *  found.
954
+     * @throws Exception
955
+     * @throws ParseException
956
+     * @see prevAll()
957
+     * @see next()
958
+     * @see siblings()
959
+     * @see children()
960
+     */
961
+    public function prev($selector = null): Query
962
+    {
963
+        $found = new SplObjectStorage();
964
+        foreach ($this->matches as $m) {
965
+            while (isset($m->previousSibling)) {
966
+                $m = $m->previousSibling;
967
+                if ($m->nodeType === XML_ELEMENT_NODE) {
968
+                    if (! empty($selector)) {
969
+                        if (QueryPath::with($m, null, $this->options)->is($selector)) {
970
+                            $found->attach($m);
971
+                            break;
972
+                        }
973
+                    } else {
974
+                        $found->attach($m);
975
+                        break;
976
+                    }
977
+                }
978
+            }
979
+        }
980
+
981
+        return $this->inst($found, null);
982
+    }
983
+
984
+    /**
985
+     * Get the previous siblings for each element in the DOMQuery.
986
+     *
987
+     * For each element in the DOMQuery, get all previous siblings. If a
988
+     * selector is provided, only matching siblings will be retrieved.
989
+     *
990
+     * @param string $selector
991
+     *  A valid CSS 3 selector.
992
+     *
993
+     * @return DOMQuery
994
+     *  The DOMQuery object, now wrapping previous sibling elements.
995
+     * @throws ParseException
996
+     * @throws Exception
997
+     * @see siblings()
998
+     * @see contents()
999
+     * @see children()
1000
+     * @see prev()
1001
+     * @see nextAll()
1002
+     */
1003
+    public function prevAll($selector = null): Query
1004
+    {
1005
+        $found = new SplObjectStorage();
1006
+        foreach ($this->matches as $m) {
1007
+            while (isset($m->previousSibling)) {
1008
+                $m = $m->previousSibling;
1009
+                if ($m->nodeType === XML_ELEMENT_NODE) {
1010
+                    if (! empty($selector)) {
1011
+                        if (QueryPath::with($m, null, $this->options)->is($selector)) {
1012
+                            $found->attach($m);
1013
+                        }
1014
+                    } else {
1015
+                        $found->attach($m);
1016
+                    }
1017
+                }
1018
+            }
1019
+        }
1020
+
1021
+        return $this->inst($found, null);
1022
+    }
1023
+
1024
+    /**
1025
+     * Get the children of the elements in the DOMQuery object.
1026
+     *
1027
+     * If a selector is provided, the list of children will be filtered through
1028
+     * the selector.
1029
+     *
1030
+     * @param string $selector
1031
+     *  A valid selector.
1032
+     *
1033
+     * @return DOMQuery
1034
+     *  A DOMQuery wrapping all of the children.
1035
+     * @throws ParseException
1036
+     * @see parent()
1037
+     * @see parents()
1038
+     * @see next()
1039
+     * @see prev()
1040
+     * @see removeChildren()
1041
+     */
1042
+    public function children($selector = null): Query
1043
+    {
1044
+        $found  = new SplObjectStorage();
1045
+        $filter = is_string($selector) && strlen($selector) > 0;
1046
+
1047
+        if ($filter) {
1048
+            $tmp = new SplObjectStorage();
1049
+        }
1050
+        foreach ($this->matches as $m) {
1051
+            foreach ($m->childNodes as $c) {
1052
+                if ($c->nodeType === XML_ELEMENT_NODE) {
1053
+                    // This is basically an optimized filter() just for children().
1054
+                    if ($filter) {
1055
+                        $tmp->attach($c);
1056
+                        $query = new DOMTraverser($tmp, true, $c);
1057
+                        $query->find($selector);
1058
+                        if (count($query->matches()) > 0) {
1059
+                            $found->attach($c);
1060
+                        }
1061
+                        $tmp->detach($c);
1062
+                    } // No filter. Just attach it.
1063
+                    else {
1064
+                        $found->attach($c);
1065
+                    }
1066
+                }
1067
+            }
1068
+        }
1069
+
1070
+        return $this->inst($found, null);
1071
+    }
1072
+
1073
+    /**
1074
+     * Get all child nodes (not just elements) of all items in the matched set.
1075
+     *
1076
+     * It gets only the immediate children, not all nodes in the subtree.
1077
+     *
1078
+     * This does not process iframes. Xinclude processing is dependent on the
1079
+     * DOM implementation and configuration.
1080
+     *
1081
+     * @return DOMQuery
1082
+     *  A DOMNode object wrapping all child nodes for all elements in the
1083
+     *  DOMNode object.
1084
+     * @throws ParseException
1085
+     * @see text()
1086
+     * @see html()
1087
+     * @see innerHTML()
1088
+     * @see xml()
1089
+     * @see innerXML()
1090
+     * @see find()
1091
+     */
1092
+    public function contents(): Query
1093
+    {
1094
+        $found = new SplObjectStorage();
1095
+        foreach ($this->matches as $m) {
1096
+            if (empty($m->childNodes)) {
1097
+                continue;
1098
+            }
1099
+            foreach ($m->childNodes as $c) {
1100
+                $found->attach($c);
1101
+            }
1102
+        }
1103
+
1104
+        return $this->inst($found, null);
1105
+    }
1106
+
1107
+    /**
1108
+     * Get a list of siblings for elements currently wrapped by this object.
1109
+     *
1110
+     * This will compile a list of every sibling of every element in the
1111
+     * current list of elements.
1112
+     *
1113
+     * Note that if two siblings are present in the DOMQuery object to begin with,
1114
+     * then both will be returned in the matched set, since they are siblings of each
1115
+     * other. In other words,if the matches contain a and b, and a and b are siblings of
1116
+     * each other, than running siblings will return a set that contains
1117
+     * both a and b.
1118
+     *
1119
+     * @param string $selector
1120
+     *  If the optional selector is provided, siblings will be filtered through
1121
+     *  this expression.
1122
+     *
1123
+     * @return DOMQuery
1124
+     *  The DOMQuery containing the matched siblings.
1125
+     * @throws ParseException
1126
+     * @throws ParseException
1127
+     * @see parent()
1128
+     * @see parents()
1129
+     * @see contents()
1130
+     * @see children()
1131
+     */
1132
+    public function siblings($selector = null): Query
1133
+    {
1134
+        $found = new SplObjectStorage();
1135
+        foreach ($this->matches as $m) {
1136
+            $parent = $m->parentNode;
1137
+            foreach ($parent->childNodes as $n) {
1138
+                if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) {
1139
+                    $found->attach($n);
1140
+                }
1141
+            }
1142
+        }
1143
+        if (empty($selector)) {
1144
+            return $this->inst($found, null);
1145
+        }
1146
+
1147
+        return $this->inst($found, null)->filter($selector);
1148
+    }
1149 1149
 }
Please login to merge, or discard this patch.
Spacing   +11 added lines, -11 removed lines patch added patch discarded remove patch
@@ -245,7 +245,7 @@  discard block
 block discarded – undo
245 245
 				if (isset($c)) {
246 246
 					if (is_array($c) || $c instanceof Iterable) {
247 247
 						foreach ($c as $retval) {
248
-							if (! is_object($retval)) {
248
+							if (!is_object($retval)) {
249 249
 								$tmp              = new stdClass();
250 250
 								$tmp->textContent = $retval;
251 251
 								$retval           = $tmp;
@@ -253,7 +253,7 @@  discard block
 block discarded – undo
253 253
 							$found->attach($retval);
254 254
 						}
255 255
 					} else {
256
-						if (! is_object($c)) {
256
+						if (!is_object($c)) {
257 257
 							$tmp              = new stdClass();
258 258
 							$tmp->textContent = $c;
259 259
 							$c                = $tmp;
@@ -653,7 +653,7 @@  discard block
 block discarded – undo
653 653
 				$m = $m->parentNode;
654 654
 				// Is there any case where parent node is not an element?
655 655
 				if ($m->nodeType === XML_ELEMENT_NODE) {
656
-					if (! empty($selector)) {
656
+					if (!empty($selector)) {
657 657
 						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
658 658
 							break;
659 659
 						}
@@ -714,7 +714,7 @@  discard block
 block discarded – undo
714 714
 			}
715 715
 		} elseif (is_array($selector)) {
716 716
 			foreach ($this->matches as $m) {
717
-				if (! in_array($m, $selector, true)) {
717
+				if (!in_array($m, $selector, true)) {
718 718
 					$found->attach($m);
719 719
 				}
720 720
 			}
@@ -726,7 +726,7 @@  discard block
 block discarded – undo
726 726
 			}
727 727
 		} else {
728 728
 			foreach ($this->matches as $m) {
729
-				if (! QueryPath::with($m, null, $this->options)->is($selector)) {
729
+				if (!QueryPath::with($m, null, $this->options)->is($selector)) {
730 730
 					$found->attach($m);
731 731
 				}
732 732
 			}
@@ -802,7 +802,7 @@  discard block
 block discarded – undo
802 802
 				$m = $m->parentNode;
803 803
 				// Is there any case where parent node is not an element?
804 804
 				if ($m->nodeType === XML_ELEMENT_NODE) {
805
-					if (! empty($selector)) {
805
+					if (!empty($selector)) {
806 806
 						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
807 807
 							$found->attach($m);
808 808
 							break;
@@ -842,7 +842,7 @@  discard block
 block discarded – undo
842 842
 				$m = $m->parentNode;
843 843
 				// Is there any case where parent node is not an element?
844 844
 				if ($m->nodeType === XML_ELEMENT_NODE) {
845
-					if (! empty($selector)) {
845
+					if (!empty($selector)) {
846 846
 						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
847 847
 							$found->attach($m);
848 848
 						}
@@ -882,7 +882,7 @@  discard block
 block discarded – undo
882 882
 			while (isset($m->nextSibling)) {
883 883
 				$m = $m->nextSibling;
884 884
 				if ($m->nodeType === XML_ELEMENT_NODE) {
885
-					if (! empty($selector)) {
885
+					if (!empty($selector)) {
886 886
 						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
887 887
 							$found->attach($m);
888 888
 							break;
@@ -924,7 +924,7 @@  discard block
 block discarded – undo
924 924
 			while (isset($m->nextSibling)) {
925 925
 				$m = $m->nextSibling;
926 926
 				if ($m->nodeType === XML_ELEMENT_NODE) {
927
-					if (! empty($selector)) {
927
+					if (!empty($selector)) {
928 928
 						if (QueryPath::with($m, null, $this->options)->is($selector) > 0) {
929 929
 							$found->attach($m);
930 930
 						}
@@ -965,7 +965,7 @@  discard block
 block discarded – undo
965 965
 			while (isset($m->previousSibling)) {
966 966
 				$m = $m->previousSibling;
967 967
 				if ($m->nodeType === XML_ELEMENT_NODE) {
968
-					if (! empty($selector)) {
968
+					if (!empty($selector)) {
969 969
 						if (QueryPath::with($m, null, $this->options)->is($selector)) {
970 970
 							$found->attach($m);
971 971
 							break;
@@ -1007,7 +1007,7 @@  discard block
 block discarded – undo
1007 1007
 			while (isset($m->previousSibling)) {
1008 1008
 				$m = $m->previousSibling;
1009 1009
 				if ($m->nodeType === XML_ELEMENT_NODE) {
1010
-					if (! empty($selector)) {
1010
+					if (!empty($selector)) {
1011 1011
 						if (QueryPath::with($m, null, $this->options)->is($selector)) {
1012 1012
 							$found->attach($m);
1013 1013
 						}
Please login to merge, or discard this patch.