Completed
Push — master ( bddea9...637f46 )
by Gilles
03:02
created
src/PHPHtmlParser/Dom/AbstractNode.php 1 patch
Indentation   +389 added lines, -389 removed lines patch added patch discarded remove patch
@@ -18,393 +18,393 @@
 block discarded – undo
18 18
 abstract class AbstractNode
19 19
 {
20 20
 
21
-    /**
22
-     * Contains the tag name/type
23
-     *
24
-     * @var \PHPHtmlParser\Dom\Tag
25
-     */
26
-    protected $tag;
27
-
28
-    /**
29
-     * Contains a list of attributes on this tag.
30
-     *
31
-     * @var array
32
-     */
33
-    protected $attr = [];
34
-
35
-    /**
36
-     * Contains the parent Node.
37
-     *
38
-     * @var InnerNode
39
-     */
40
-    protected $parent = null;
41
-
42
-    /**
43
-     * The unique id of the class. Given by PHP.
44
-     *
45
-     * @var string
46
-     */
47
-    protected $id;
48
-
49
-    /**
50
-     * The encoding class used to encode strings.
51
-     *
52
-     * @var mixed
53
-     */
54
-    protected $encode;
55
-
56
-    /**
57
-     * Creates a unique spl hash for this node.
58
-     */
59
-    public function __construct()
60
-    {
61
-        $this->id = spl_object_hash($this);
62
-    }
63
-
64
-    /**
65
-     * Magic get method for attributes and certain methods.
66
-     *
67
-     * @param string $key
68
-     * @return mixed
69
-     */
70
-    public function __get($key)
71
-    {
72
-        // check attribute first
73
-        if ( ! is_null($this->getAttribute($key))) {
74
-            return $this->getAttribute($key);
75
-        }
76
-        switch (strtolower($key)) {
77
-            case 'outerhtml':
78
-                return $this->outerHtml();
79
-            case 'innerhtml':
80
-                return $this->innerHtml();
81
-            case 'text':
82
-                return $this->text();
83
-            case 'tag':
84
-                return $this->getTag();
85
-            case 'parent':
86
-                $this->getParent();
87
-        }
88
-
89
-        return null;
90
-    }
91
-
92
-    /**
93
-     * Attempts to clear out any object references.
94
-     */
95
-    public function __destruct()
96
-    {
97
-        $this->tag      = null;
98
-        $this->attr     = [];
99
-        $this->parent   = null;
100
-        $this->children = [];
101
-    }
102
-
103
-    /**
104
-     * Simply calls the outer text method.
105
-     *
106
-     * @return string
107
-     */
108
-    public function __toString()
109
-    {
110
-        return $this->outerHtml();
111
-    }
112
-
113
-    /**
114
-     * Returns the id of this object.
115
-     */
116
-    public function id()
117
-    {
118
-        return $this->id;
119
-    }
120
-
121
-    /**
122
-     * Returns the parent of node.
123
-     *
124
-     * @return AbstractNode
125
-     */
126
-    public function getParent()
127
-    {
128
-        return $this->parent;
129
-    }
130
-
131
-    /**
132
-     * Sets the parent node.
133
-     *
134
-     * @param InnerNode $parent
135
-     * @return $this
136
-     * @throws CircularException
137
-     */
138
-    public function setParent(InnerNode $parent)
139
-    {
140
-        // remove from old parent
141
-        if ( ! is_null($this->parent)) {
142
-            if ($this->parent->id() == $parent->id()) {
143
-                // already the parent
144
-                return $this;
145
-            }
146
-
147
-            $this->parent->removeChild($this->id);
148
-        }
149
-
150
-        $this->parent = $parent;
151
-
152
-        // assign child to parent
153
-        $this->parent->addChild($this);
154
-
155
-        //clear any cache
156
-        $this->clear();
157
-
158
-        return $this;
159
-    }
160
-
161
-    /**
162
-     * Removes this node and all its children from the
163
-     * DOM tree.
164
-     *
165
-     * @return void
166
-     */
167
-    public function delete()
168
-    {
169
-        if ( ! is_null($this->parent)) {
170
-            $this->parent->removeChild($this->id);
171
-        }
172
-
173
-        $this->parent = null;
174
-    }
175
-
176
-    /**
177
-     * Sets the encoding class to this node.
178
-     *
179
-     * @param Encode $encode
180
-     * @return void
181
-     */
182
-    public function propagateEncoding(Encode $encode)
183
-    {
184
-        $this->encode = $encode;
185
-        $this->tag->setEncoding($encode);
186
-    }
187
-
188
-    /**
189
-     * Checks if the given node id is an ancestor of
190
-     * the current node.
191
-     *
192
-     * @param int $id
193
-     * @return bool
194
-     */
195
-    public function isAncestor($id)
196
-    {
197
-        if ( ! is_null($this->getAncestor($id))) {
198
-            return true;
199
-        }
200
-
201
-        return false;
202
-    }
203
-
204
-    /**
205
-     * Attempts to get an ancestor node by the given id.
206
-     *
207
-     * @param int $id
208
-     * @return null|AbstractNode
209
-     */
210
-    public function getAncestor($id)
211
-    {
212
-        if ( ! is_null($this->parent)) {
213
-            if ($this->parent->id() == $id) {
214
-                return $this->parent;
215
-            }
216
-
217
-            return $this->parent->getAncestor($id);
218
-        }
219
-
220
-        return null;
221
-    }
222
-
223
-    /**
224
-     * Attempts to get the next sibling.
225
-     *
226
-     * @return AbstractNode
227
-     * @throws ParentNotFoundException
228
-     */
229
-    public function nextSibling()
230
-    {
231
-        if (is_null($this->parent)) {
232
-            throw new ParentNotFoundException('Parent is not set for this node.');
233
-        }
234
-
235
-        return $this->parent->nextChild($this->id);
236
-    }
237
-
238
-    /**
239
-     * Attempts to get the previous sibling
240
-     *
241
-     * @return AbstractNode
242
-     * @throws ParentNotFoundException
243
-     */
244
-    public function previousSibling()
245
-    {
246
-        if (is_null($this->parent)) {
247
-            throw new ParentNotFoundException('Parent is not set for this node.');
248
-        }
249
-
250
-        return $this->parent->previousChild($this->id);
251
-    }
252
-
253
-    /**
254
-     * Gets the tag object of this node.
255
-     *
256
-     * @return Tag
257
-     */
258
-    public function getTag()
259
-    {
260
-        return $this->tag;
261
-    }
262
-
263
-    /**
264
-     * A wrapper method that simply calls the getAttribute method
265
-     * on the tag of this node.
266
-     *
267
-     * @return array
268
-     */
269
-    public function getAttributes()
270
-    {
271
-        $attributes = $this->tag->getAttributes();
272
-        foreach ($attributes as $name => $info) {
273
-            $attributes[$name] = $info['value'];
274
-        }
275
-
276
-        return $attributes;
277
-    }
278
-
279
-    /**
280
-     * A wrapper method that simply calls the getAttribute method
281
-     * on the tag of this node.
282
-     *
283
-     * @param string $key
284
-     * @return mixed
285
-     */
286
-    public function getAttribute($key)
287
-    {
288
-        $attribute = $this->tag->getAttribute($key);
289
-        if ( ! is_null($attribute)) {
290
-            $attribute = $attribute['value'];
291
-        }
292
-
293
-        return $attribute;
294
-    }
295
-
296
-    /**
297
-     * A wrapper method that simply calls the setAttribute method
298
-     * on the tag of this node.
299
-     *
300
-     * @param string $key
301
-     * @param string $value
302
-     * @return $this
303
-     */
304
-    public function setAttribute($key, $value)
305
-    {
306
-        $this->tag->setAttribute($key, $value);
307
-
308
-        return $this;
309
-    }
310
-
311
-    /**
312
-     * A wrapper method that simply calls the removeAttribute method
313
-     * on the tag of this node.
314
-     *
315
-     * @param string $key
316
-     * @return void
317
-     */
318
-    public function removeAttribute($key)
319
-    {
320
-        $this->tag->removeAttribute($key);
321
-    }
322
-
323
-    /**
324
-     * A wrapper method that simply calls the removeAllAttributes
325
-     * method on the tag of this node.
326
-     *
327
-     * @return void
328
-     */
329
-    public function removeAllAttributes()
330
-    {
331
-        $this->tag->removeAllAttributes();
332
-    }
333
-
334
-    /**
335
-     * Function to locate a specific ancestor tag in the path to the root.
336
-     *
337
-     * @param  string $tag
338
-     * @return AbstractNode
339
-     * @throws ParentNotFoundException
340
-     */
341
-    public function ancestorByTag($tag)
342
-    {
343
-        // Start by including ourselves in the comparison.
344
-        $node = $this;
345
-
346
-        while ( ! is_null($node)) {
347
-            if ($node->tag->name() == $tag) {
348
-                return $node;
349
-            }
350
-
351
-            $node = $node->getParent();
352
-        }
353
-
354
-        throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
355
-    }
356
-
357
-    /**
358
-     * Find elements by css selector
359
-     *
360
-     * @param string $selector
361
-     * @param int $nth
362
-     * @return array|AbstractNode
363
-     */
364
-    public function find($selector, $nth = null)
365
-    {
366
-        $selector = new Selector($selector);
367
-        $nodes    = $selector->find($this);
368
-
369
-        if ( ! is_null($nth)) {
370
-            // return nth-element or array
371
-            if (isset($nodes[$nth])) {
372
-                return $nodes[$nth];
373
-            }
374
-
375
-            return null;
376
-        }
377
-
378
-        return $nodes;
379
-    }
380
-
381
-    /**
382
-     * Gets the inner html of this node.
383
-     *
384
-     * @return string
385
-     */
386
-    abstract public function innerHtml();
387
-
388
-    /**
389
-     * Gets the html of this node, including it's own
390
-     * tag.
391
-     *
392
-     * @return string
393
-     */
394
-    abstract public function outerHtml();
395
-
396
-    /**
397
-     * Gets the text of this node (if there is any text).
398
-     *
399
-     * @return string
400
-     */
401
-    abstract public function text();
402
-
403
-    /**
404
-     * Call this when something in the node tree has changed. Like a child has been added
405
-     * or a parent has been changed.
406
-     *
407
-     * @return void
408
-     */
409
-    abstract protected function clear();
21
+	/**
22
+	 * Contains the tag name/type
23
+	 *
24
+	 * @var \PHPHtmlParser\Dom\Tag
25
+	 */
26
+	protected $tag;
27
+
28
+	/**
29
+	 * Contains a list of attributes on this tag.
30
+	 *
31
+	 * @var array
32
+	 */
33
+	protected $attr = [];
34
+
35
+	/**
36
+	 * Contains the parent Node.
37
+	 *
38
+	 * @var InnerNode
39
+	 */
40
+	protected $parent = null;
41
+
42
+	/**
43
+	 * The unique id of the class. Given by PHP.
44
+	 *
45
+	 * @var string
46
+	 */
47
+	protected $id;
48
+
49
+	/**
50
+	 * The encoding class used to encode strings.
51
+	 *
52
+	 * @var mixed
53
+	 */
54
+	protected $encode;
55
+
56
+	/**
57
+	 * Creates a unique spl hash for this node.
58
+	 */
59
+	public function __construct()
60
+	{
61
+		$this->id = spl_object_hash($this);
62
+	}
63
+
64
+	/**
65
+	 * Magic get method for attributes and certain methods.
66
+	 *
67
+	 * @param string $key
68
+	 * @return mixed
69
+	 */
70
+	public function __get($key)
71
+	{
72
+		// check attribute first
73
+		if ( ! is_null($this->getAttribute($key))) {
74
+			return $this->getAttribute($key);
75
+		}
76
+		switch (strtolower($key)) {
77
+			case 'outerhtml':
78
+				return $this->outerHtml();
79
+			case 'innerhtml':
80
+				return $this->innerHtml();
81
+			case 'text':
82
+				return $this->text();
83
+			case 'tag':
84
+				return $this->getTag();
85
+			case 'parent':
86
+				$this->getParent();
87
+		}
88
+
89
+		return null;
90
+	}
91
+
92
+	/**
93
+	 * Attempts to clear out any object references.
94
+	 */
95
+	public function __destruct()
96
+	{
97
+		$this->tag      = null;
98
+		$this->attr     = [];
99
+		$this->parent   = null;
100
+		$this->children = [];
101
+	}
102
+
103
+	/**
104
+	 * Simply calls the outer text method.
105
+	 *
106
+	 * @return string
107
+	 */
108
+	public function __toString()
109
+	{
110
+		return $this->outerHtml();
111
+	}
112
+
113
+	/**
114
+	 * Returns the id of this object.
115
+	 */
116
+	public function id()
117
+	{
118
+		return $this->id;
119
+	}
120
+
121
+	/**
122
+	 * Returns the parent of node.
123
+	 *
124
+	 * @return AbstractNode
125
+	 */
126
+	public function getParent()
127
+	{
128
+		return $this->parent;
129
+	}
130
+
131
+	/**
132
+	 * Sets the parent node.
133
+	 *
134
+	 * @param InnerNode $parent
135
+	 * @return $this
136
+	 * @throws CircularException
137
+	 */
138
+	public function setParent(InnerNode $parent)
139
+	{
140
+		// remove from old parent
141
+		if ( ! is_null($this->parent)) {
142
+			if ($this->parent->id() == $parent->id()) {
143
+				// already the parent
144
+				return $this;
145
+			}
146
+
147
+			$this->parent->removeChild($this->id);
148
+		}
149
+
150
+		$this->parent = $parent;
151
+
152
+		// assign child to parent
153
+		$this->parent->addChild($this);
154
+
155
+		//clear any cache
156
+		$this->clear();
157
+
158
+		return $this;
159
+	}
160
+
161
+	/**
162
+	 * Removes this node and all its children from the
163
+	 * DOM tree.
164
+	 *
165
+	 * @return void
166
+	 */
167
+	public function delete()
168
+	{
169
+		if ( ! is_null($this->parent)) {
170
+			$this->parent->removeChild($this->id);
171
+		}
172
+
173
+		$this->parent = null;
174
+	}
175
+
176
+	/**
177
+	 * Sets the encoding class to this node.
178
+	 *
179
+	 * @param Encode $encode
180
+	 * @return void
181
+	 */
182
+	public function propagateEncoding(Encode $encode)
183
+	{
184
+		$this->encode = $encode;
185
+		$this->tag->setEncoding($encode);
186
+	}
187
+
188
+	/**
189
+	 * Checks if the given node id is an ancestor of
190
+	 * the current node.
191
+	 *
192
+	 * @param int $id
193
+	 * @return bool
194
+	 */
195
+	public function isAncestor($id)
196
+	{
197
+		if ( ! is_null($this->getAncestor($id))) {
198
+			return true;
199
+		}
200
+
201
+		return false;
202
+	}
203
+
204
+	/**
205
+	 * Attempts to get an ancestor node by the given id.
206
+	 *
207
+	 * @param int $id
208
+	 * @return null|AbstractNode
209
+	 */
210
+	public function getAncestor($id)
211
+	{
212
+		if ( ! is_null($this->parent)) {
213
+			if ($this->parent->id() == $id) {
214
+				return $this->parent;
215
+			}
216
+
217
+			return $this->parent->getAncestor($id);
218
+		}
219
+
220
+		return null;
221
+	}
222
+
223
+	/**
224
+	 * Attempts to get the next sibling.
225
+	 *
226
+	 * @return AbstractNode
227
+	 * @throws ParentNotFoundException
228
+	 */
229
+	public function nextSibling()
230
+	{
231
+		if (is_null($this->parent)) {
232
+			throw new ParentNotFoundException('Parent is not set for this node.');
233
+		}
234
+
235
+		return $this->parent->nextChild($this->id);
236
+	}
237
+
238
+	/**
239
+	 * Attempts to get the previous sibling
240
+	 *
241
+	 * @return AbstractNode
242
+	 * @throws ParentNotFoundException
243
+	 */
244
+	public function previousSibling()
245
+	{
246
+		if (is_null($this->parent)) {
247
+			throw new ParentNotFoundException('Parent is not set for this node.');
248
+		}
249
+
250
+		return $this->parent->previousChild($this->id);
251
+	}
252
+
253
+	/**
254
+	 * Gets the tag object of this node.
255
+	 *
256
+	 * @return Tag
257
+	 */
258
+	public function getTag()
259
+	{
260
+		return $this->tag;
261
+	}
262
+
263
+	/**
264
+	 * A wrapper method that simply calls the getAttribute method
265
+	 * on the tag of this node.
266
+	 *
267
+	 * @return array
268
+	 */
269
+	public function getAttributes()
270
+	{
271
+		$attributes = $this->tag->getAttributes();
272
+		foreach ($attributes as $name => $info) {
273
+			$attributes[$name] = $info['value'];
274
+		}
275
+
276
+		return $attributes;
277
+	}
278
+
279
+	/**
280
+	 * A wrapper method that simply calls the getAttribute method
281
+	 * on the tag of this node.
282
+	 *
283
+	 * @param string $key
284
+	 * @return mixed
285
+	 */
286
+	public function getAttribute($key)
287
+	{
288
+		$attribute = $this->tag->getAttribute($key);
289
+		if ( ! is_null($attribute)) {
290
+			$attribute = $attribute['value'];
291
+		}
292
+
293
+		return $attribute;
294
+	}
295
+
296
+	/**
297
+	 * A wrapper method that simply calls the setAttribute method
298
+	 * on the tag of this node.
299
+	 *
300
+	 * @param string $key
301
+	 * @param string $value
302
+	 * @return $this
303
+	 */
304
+	public function setAttribute($key, $value)
305
+	{
306
+		$this->tag->setAttribute($key, $value);
307
+
308
+		return $this;
309
+	}
310
+
311
+	/**
312
+	 * A wrapper method that simply calls the removeAttribute method
313
+	 * on the tag of this node.
314
+	 *
315
+	 * @param string $key
316
+	 * @return void
317
+	 */
318
+	public function removeAttribute($key)
319
+	{
320
+		$this->tag->removeAttribute($key);
321
+	}
322
+
323
+	/**
324
+	 * A wrapper method that simply calls the removeAllAttributes
325
+	 * method on the tag of this node.
326
+	 *
327
+	 * @return void
328
+	 */
329
+	public function removeAllAttributes()
330
+	{
331
+		$this->tag->removeAllAttributes();
332
+	}
333
+
334
+	/**
335
+	 * Function to locate a specific ancestor tag in the path to the root.
336
+	 *
337
+	 * @param  string $tag
338
+	 * @return AbstractNode
339
+	 * @throws ParentNotFoundException
340
+	 */
341
+	public function ancestorByTag($tag)
342
+	{
343
+		// Start by including ourselves in the comparison.
344
+		$node = $this;
345
+
346
+		while ( ! is_null($node)) {
347
+			if ($node->tag->name() == $tag) {
348
+				return $node;
349
+			}
350
+
351
+			$node = $node->getParent();
352
+		}
353
+
354
+		throw new ParentNotFoundException('Could not find an ancestor with "'.$tag.'" tag');
355
+	}
356
+
357
+	/**
358
+	 * Find elements by css selector
359
+	 *
360
+	 * @param string $selector
361
+	 * @param int $nth
362
+	 * @return array|AbstractNode
363
+	 */
364
+	public function find($selector, $nth = null)
365
+	{
366
+		$selector = new Selector($selector);
367
+		$nodes    = $selector->find($this);
368
+
369
+		if ( ! is_null($nth)) {
370
+			// return nth-element or array
371
+			if (isset($nodes[$nth])) {
372
+				return $nodes[$nth];
373
+			}
374
+
375
+			return null;
376
+		}
377
+
378
+		return $nodes;
379
+	}
380
+
381
+	/**
382
+	 * Gets the inner html of this node.
383
+	 *
384
+	 * @return string
385
+	 */
386
+	abstract public function innerHtml();
387
+
388
+	/**
389
+	 * Gets the html of this node, including it's own
390
+	 * tag.
391
+	 *
392
+	 * @return string
393
+	 */
394
+	abstract public function outerHtml();
395
+
396
+	/**
397
+	 * Gets the text of this node (if there is any text).
398
+	 *
399
+	 * @return string
400
+	 */
401
+	abstract public function text();
402
+
403
+	/**
404
+	 * Call this when something in the node tree has changed. Like a child has been added
405
+	 * or a parent has been changed.
406
+	 *
407
+	 * @return void
408
+	 */
409
+	abstract protected function clear();
410 410
 }
Please login to merge, or discard this patch.
src/PHPHtmlParser/Dom/InnerNode.php 1 patch
Indentation   +301 added lines, -301 removed lines patch added patch discarded remove patch
@@ -13,305 +13,305 @@
 block discarded – undo
13 13
 abstract class InnerNode extends ArrayNode
14 14
 {
15 15
 
16
-    /**
17
-     * An array of all the children.
18
-     *
19
-     * @var array
20
-     */
21
-    protected $children = [];
22
-
23
-    /**
24
-     * Sets the encoding class to this node and propagates it
25
-     * to all its children.
26
-     *
27
-     * @param Encode $encode
28
-     * @return void
29
-     */
30
-    public function propagateEncoding(Encode $encode)
31
-    {
32
-        $this->encode = $encode;
33
-        $this->tag->setEncoding($encode);
34
-        // check children
35
-        foreach ($this->children as $id => $child) {
36
-            /** @var AbstractNode $node */
37
-            $node = $child['node'];
38
-            $node->propagateEncoding($encode);
39
-        }
40
-    }
41
-
42
-    /**
43
-     * Checks if this node has children.
44
-     *
45
-     * @return bool
46
-     */
47
-    public function hasChildren()
48
-    {
49
-        return ! empty($this->children);
50
-    }
51
-
52
-    /**
53
-     * Returns the child by id.
54
-     *
55
-     * @param int $id
56
-     * @return AbstractNode
57
-     * @throws ChildNotFoundException
58
-     */
59
-    public function getChild($id)
60
-    {
61
-        if ( ! isset($this->children[$id])) {
62
-            throw new ChildNotFoundException("Child '$id' not found in this node.");
63
-        }
64
-
65
-        return $this->children[$id]['node'];
66
-    }
67
-
68
-    /**
69
-     * Returns a new array of child nodes
70
-     *
71
-     * @return array
72
-     */
73
-    public function getChildren()
74
-    {
75
-        $nodes = [];
76
-        try {
77
-            $child = $this->firstChild();
78
-            do {
79
-                $nodes[] = $child;
80
-                $child   = $this->nextChild($child->id());
81
-            } while ( ! is_null($child));
82
-        } catch (ChildNotFoundException $e) {
83
-            // we are done looking for children
84
-        }
85
-
86
-        return $nodes;
87
-    }
88
-
89
-    /**
90
-     * Counts children
91
-     *
92
-     * @return int
93
-     */
94
-    public function countChildren()
95
-    {
96
-        return count($this->children);
97
-    }
98
-
99
-    /**
100
-     * Adds a child node to this node and returns the id of the child for this
101
-     * parent.
102
-     *
103
-     * @param AbstractNode $child
104
-     * @return bool
105
-     * @throws CircularException
106
-     */
107
-    public function addChild(AbstractNode $child)
108
-    {
109
-        $key = null;
110
-
111
-        // check integrity
112
-        if ($this->isAncestor($child->id())) {
113
-            throw new CircularException('Can not add child. It is my ancestor.');
114
-        }
115
-
116
-        // check if child is itself
117
-        if ($child->id() == $this->id) {
118
-            throw new CircularException('Can not set itself as a child.');
119
-        }
120
-
121
-        if ($this->hasChildren()) {
122
-            if (isset($this->children[$child->id()])) {
123
-                // we already have this child
124
-                return false;
125
-            }
126
-            $sibling                      = $this->lastChild();
127
-            $key                          = $sibling->id();
128
-            $this->children[$key]['next'] = $child->id();
129
-        }
130
-
131
-        // add the child
132
-        $this->children[$child->id()] = [
133
-            'node' => $child,
134
-            'next' => null,
135
-            'prev' => $key,
136
-        ];
137
-
138
-        // tell child I am the new parent
139
-        $child->setParent($this);
140
-
141
-        //clear any cache
142
-        $this->clear();
143
-
144
-        return true;
145
-    }
146
-
147
-    /**
148
-     * Removes the child by id.
149
-     *
150
-     * @param int $id
151
-     * @return $this
152
-     */
153
-    public function removeChild($id)
154
-    {
155
-        if ( ! isset($this->children[$id])) {
156
-            return $this;
157
-        }
158
-
159
-        // handle moving next and previous assignments.
160
-        $next = $this->children[$id]['next'];
161
-        $prev = $this->children[$id]['prev'];
162
-        if ( ! is_null($next)) {
163
-            $this->children[$next]['prev'] = $prev;
164
-        }
165
-        if ( ! is_null($prev)) {
166
-            $this->children[$prev]['next'] = $next;
167
-        }
168
-
169
-        // remove the child
170
-        unset($this->children[$id]);
171
-
172
-        //clear any cache
173
-        $this->clear();
174
-
175
-        return $this;
176
-    }
177
-
178
-    /**
179
-     * Attempts to get the next child.
180
-     *
181
-     * @param int $id
182
-     * @return AbstractNode
183
-     * @uses $this->getChild()
184
-     * @throws ChildNotFoundException
185
-     */
186
-    public function nextChild($id)
187
-    {
188
-        $child = $this->getChild($id);
189
-        $next  = $this->children[$child->id()]['next'];
190
-
191
-        return $this->getChild($next);
192
-    }
193
-
194
-    /**
195
-     * Attempts to get the previous child.
196
-     *
197
-     * @param int $id
198
-     * @return AbstractNode
199
-     * @uses $this->getChild()
200
-     * @throws ChildNotFoundException
201
-     */
202
-    public function previousChild($id)
203
-    {
204
-        $child = $this->getchild($id);
205
-        $next  = $this->children[$child->id()]['prev'];
206
-
207
-        return $this->getChild($next);
208
-    }
209
-
210
-    /**
211
-     * Checks if the given node id is a child of the
212
-     * current node.
213
-     *
214
-     * @param int $id
215
-     * @return bool
216
-     */
217
-    public function isChild($id)
218
-    {
219
-        foreach ($this->children as $childId => $child) {
220
-            if ($id == $childId) {
221
-                return true;
222
-            }
223
-        }
224
-
225
-        return false;
226
-    }
227
-
228
-    /**
229
-     * Removes the child with id $childId and replace it with the new child
230
-     * $newChild.
231
-     *
232
-     * @param int $childId
233
-     * @param AbstractNode $newChild
234
-     * @throws ChildNotFoundException
235
-     */
236
-    public function replaceChild($childId, AbstractNode $newChild)
237
-    {
238
-        $oldChild                        = $this->getChild($childId);
239
-        $keys                            = array_keys($this->children);
240
-        $index                           = array_search($childId, $keys, true);
241
-        $keys[$index]                    = $newChild->id();
242
-        $this->children                  = array_combine($keys, $this->children);
243
-        $this->children[$newChild->id()] = $newChild;
244
-        unset($oldChild);
245
-    }
246
-
247
-    /**
248
-     * Shortcut to return the first child.
249
-     *
250
-     * @return AbstractNode
251
-     * @uses $this->getChild()
252
-     */
253
-    public function firstChild()
254
-    {
255
-        reset($this->children);
256
-        $key = key($this->children);
257
-
258
-        return $this->getChild($key);
259
-    }
260
-
261
-    /**
262
-     * Attempts to get the last child.
263
-     *
264
-     * @return AbstractNode
265
-     */
266
-    public function lastChild()
267
-    {
268
-        end($this->children);
269
-        $key = key($this->children);
270
-
271
-        return $this->getChild($key);
272
-    }
273
-
274
-    /**
275
-     * Checks if the given node id is a descendant of the
276
-     * current node.
277
-     *
278
-     * @param int $id
279
-     * @return bool
280
-     */
281
-    public function isDescendant($id)
282
-    {
283
-        if ($this->isChild($id)) {
284
-            return true;
285
-        }
286
-
287
-        foreach ($this->children as $childId => $child) {
288
-            /** @var InnerNode $node */
289
-            $node = $child['node'];
290
-            if ($node instanceof InnerNode &&
291
-                $node->hasChildren() &&
292
-                $node->isDescendant($id)
293
-            ) {
294
-                return true;
295
-            }
296
-        }
297
-
298
-        return false;
299
-    }
300
-
301
-    /**
302
-     * Sets the parent node.
303
-     *
304
-     * @param InnerNode $parent
305
-     * @return $this
306
-     * @throws CircularException
307
-     */
308
-    public function setParent(InnerNode $parent)
309
-    {
310
-        // check integrity
311
-        if ($this->isDescendant($parent->id())) {
312
-            throw new CircularException('Can not add descendant "'.$parent->id().'" as my parent.');
313
-        }
314
-
315
-        return parent::setParent($parent);
316
-    }
16
+	/**
17
+	 * An array of all the children.
18
+	 *
19
+	 * @var array
20
+	 */
21
+	protected $children = [];
22
+
23
+	/**
24
+	 * Sets the encoding class to this node and propagates it
25
+	 * to all its children.
26
+	 *
27
+	 * @param Encode $encode
28
+	 * @return void
29
+	 */
30
+	public function propagateEncoding(Encode $encode)
31
+	{
32
+		$this->encode = $encode;
33
+		$this->tag->setEncoding($encode);
34
+		// check children
35
+		foreach ($this->children as $id => $child) {
36
+			/** @var AbstractNode $node */
37
+			$node = $child['node'];
38
+			$node->propagateEncoding($encode);
39
+		}
40
+	}
41
+
42
+	/**
43
+	 * Checks if this node has children.
44
+	 *
45
+	 * @return bool
46
+	 */
47
+	public function hasChildren()
48
+	{
49
+		return ! empty($this->children);
50
+	}
51
+
52
+	/**
53
+	 * Returns the child by id.
54
+	 *
55
+	 * @param int $id
56
+	 * @return AbstractNode
57
+	 * @throws ChildNotFoundException
58
+	 */
59
+	public function getChild($id)
60
+	{
61
+		if ( ! isset($this->children[$id])) {
62
+			throw new ChildNotFoundException("Child '$id' not found in this node.");
63
+		}
64
+
65
+		return $this->children[$id]['node'];
66
+	}
67
+
68
+	/**
69
+	 * Returns a new array of child nodes
70
+	 *
71
+	 * @return array
72
+	 */
73
+	public function getChildren()
74
+	{
75
+		$nodes = [];
76
+		try {
77
+			$child = $this->firstChild();
78
+			do {
79
+				$nodes[] = $child;
80
+				$child   = $this->nextChild($child->id());
81
+			} while ( ! is_null($child));
82
+		} catch (ChildNotFoundException $e) {
83
+			// we are done looking for children
84
+		}
85
+
86
+		return $nodes;
87
+	}
88
+
89
+	/**
90
+	 * Counts children
91
+	 *
92
+	 * @return int
93
+	 */
94
+	public function countChildren()
95
+	{
96
+		return count($this->children);
97
+	}
98
+
99
+	/**
100
+	 * Adds a child node to this node and returns the id of the child for this
101
+	 * parent.
102
+	 *
103
+	 * @param AbstractNode $child
104
+	 * @return bool
105
+	 * @throws CircularException
106
+	 */
107
+	public function addChild(AbstractNode $child)
108
+	{
109
+		$key = null;
110
+
111
+		// check integrity
112
+		if ($this->isAncestor($child->id())) {
113
+			throw new CircularException('Can not add child. It is my ancestor.');
114
+		}
115
+
116
+		// check if child is itself
117
+		if ($child->id() == $this->id) {
118
+			throw new CircularException('Can not set itself as a child.');
119
+		}
120
+
121
+		if ($this->hasChildren()) {
122
+			if (isset($this->children[$child->id()])) {
123
+				// we already have this child
124
+				return false;
125
+			}
126
+			$sibling                      = $this->lastChild();
127
+			$key                          = $sibling->id();
128
+			$this->children[$key]['next'] = $child->id();
129
+		}
130
+
131
+		// add the child
132
+		$this->children[$child->id()] = [
133
+			'node' => $child,
134
+			'next' => null,
135
+			'prev' => $key,
136
+		];
137
+
138
+		// tell child I am the new parent
139
+		$child->setParent($this);
140
+
141
+		//clear any cache
142
+		$this->clear();
143
+
144
+		return true;
145
+	}
146
+
147
+	/**
148
+	 * Removes the child by id.
149
+	 *
150
+	 * @param int $id
151
+	 * @return $this
152
+	 */
153
+	public function removeChild($id)
154
+	{
155
+		if ( ! isset($this->children[$id])) {
156
+			return $this;
157
+		}
158
+
159
+		// handle moving next and previous assignments.
160
+		$next = $this->children[$id]['next'];
161
+		$prev = $this->children[$id]['prev'];
162
+		if ( ! is_null($next)) {
163
+			$this->children[$next]['prev'] = $prev;
164
+		}
165
+		if ( ! is_null($prev)) {
166
+			$this->children[$prev]['next'] = $next;
167
+		}
168
+
169
+		// remove the child
170
+		unset($this->children[$id]);
171
+
172
+		//clear any cache
173
+		$this->clear();
174
+
175
+		return $this;
176
+	}
177
+
178
+	/**
179
+	 * Attempts to get the next child.
180
+	 *
181
+	 * @param int $id
182
+	 * @return AbstractNode
183
+	 * @uses $this->getChild()
184
+	 * @throws ChildNotFoundException
185
+	 */
186
+	public function nextChild($id)
187
+	{
188
+		$child = $this->getChild($id);
189
+		$next  = $this->children[$child->id()]['next'];
190
+
191
+		return $this->getChild($next);
192
+	}
193
+
194
+	/**
195
+	 * Attempts to get the previous child.
196
+	 *
197
+	 * @param int $id
198
+	 * @return AbstractNode
199
+	 * @uses $this->getChild()
200
+	 * @throws ChildNotFoundException
201
+	 */
202
+	public function previousChild($id)
203
+	{
204
+		$child = $this->getchild($id);
205
+		$next  = $this->children[$child->id()]['prev'];
206
+
207
+		return $this->getChild($next);
208
+	}
209
+
210
+	/**
211
+	 * Checks if the given node id is a child of the
212
+	 * current node.
213
+	 *
214
+	 * @param int $id
215
+	 * @return bool
216
+	 */
217
+	public function isChild($id)
218
+	{
219
+		foreach ($this->children as $childId => $child) {
220
+			if ($id == $childId) {
221
+				return true;
222
+			}
223
+		}
224
+
225
+		return false;
226
+	}
227
+
228
+	/**
229
+	 * Removes the child with id $childId and replace it with the new child
230
+	 * $newChild.
231
+	 *
232
+	 * @param int $childId
233
+	 * @param AbstractNode $newChild
234
+	 * @throws ChildNotFoundException
235
+	 */
236
+	public function replaceChild($childId, AbstractNode $newChild)
237
+	{
238
+		$oldChild                        = $this->getChild($childId);
239
+		$keys                            = array_keys($this->children);
240
+		$index                           = array_search($childId, $keys, true);
241
+		$keys[$index]                    = $newChild->id();
242
+		$this->children                  = array_combine($keys, $this->children);
243
+		$this->children[$newChild->id()] = $newChild;
244
+		unset($oldChild);
245
+	}
246
+
247
+	/**
248
+	 * Shortcut to return the first child.
249
+	 *
250
+	 * @return AbstractNode
251
+	 * @uses $this->getChild()
252
+	 */
253
+	public function firstChild()
254
+	{
255
+		reset($this->children);
256
+		$key = key($this->children);
257
+
258
+		return $this->getChild($key);
259
+	}
260
+
261
+	/**
262
+	 * Attempts to get the last child.
263
+	 *
264
+	 * @return AbstractNode
265
+	 */
266
+	public function lastChild()
267
+	{
268
+		end($this->children);
269
+		$key = key($this->children);
270
+
271
+		return $this->getChild($key);
272
+	}
273
+
274
+	/**
275
+	 * Checks if the given node id is a descendant of the
276
+	 * current node.
277
+	 *
278
+	 * @param int $id
279
+	 * @return bool
280
+	 */
281
+	public function isDescendant($id)
282
+	{
283
+		if ($this->isChild($id)) {
284
+			return true;
285
+		}
286
+
287
+		foreach ($this->children as $childId => $child) {
288
+			/** @var InnerNode $node */
289
+			$node = $child['node'];
290
+			if ($node instanceof InnerNode &&
291
+				$node->hasChildren() &&
292
+				$node->isDescendant($id)
293
+			) {
294
+				return true;
295
+			}
296
+		}
297
+
298
+		return false;
299
+	}
300
+
301
+	/**
302
+	 * Sets the parent node.
303
+	 *
304
+	 * @param InnerNode $parent
305
+	 * @return $this
306
+	 * @throws CircularException
307
+	 */
308
+	public function setParent(InnerNode $parent)
309
+	{
310
+		// check integrity
311
+		if ($this->isDescendant($parent->id())) {
312
+			throw new CircularException('Can not add descendant "'.$parent->id().'" as my parent.');
313
+		}
314
+
315
+		return parent::setParent($parent);
316
+	}
317 317
 }
318 318
\ No newline at end of file
Please login to merge, or discard this patch.
src/PHPHtmlParser/Selector.php 1 patch
Indentation   +360 added lines, -360 removed lines patch added patch discarded remove patch
@@ -15,364 +15,364 @@
 block discarded – undo
15 15
 class Selector
16 16
 {
17 17
 
18
-    /**
19
-     * Pattern of CSS selectors, modified from 'mootools'
20
-     *
21
-     * @var string
22
-     */
23
-    protected $pattern = "/([\w-:\*>]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
24
-
25
-    protected $selectors = [];
26
-
27
-    /**
28
-     * Constructs with the selector string
29
-     *
30
-     * @param string $selector
31
-     */
32
-    public function __construct($selector)
33
-    {
34
-        $this->parseSelectorString($selector);
35
-    }
36
-
37
-    /**
38
-     * Returns the selectors that where found in __construct
39
-     *
40
-     * @return array
41
-     */
42
-    public function getSelectors()
43
-    {
44
-        return $this->selectors;
45
-    }
46
-
47
-    /**
48
-     * Attempts to find the selectors starting from the given
49
-     * node object.
50
-     *
51
-     * @param AbstractNode $node
52
-     * @return array|Collection
53
-     */
54
-    public function find(AbstractNode $node)
55
-    {
56
-        $results = new Collection;
57
-        foreach ($this->selectors as $selector) {
58
-            $nodes = [$node];
59
-            if (count($selector) == 0) {
60
-                continue;
61
-            }
62
-
63
-            $options = [];
64
-            foreach ($selector as $rule) {
65
-                if ($rule['alterNext']) {
66
-                    $options[] = $this->alterNext($rule);
67
-                    continue;
68
-                }
69
-                $nodes = $this->seek($nodes, $rule, $options);
70
-                // clear the options
71
-                $options = [];
72
-            }
73
-
74
-            // this is the final set of nodes
75
-            foreach ($nodes as $result) {
76
-                $results[] = $result;
77
-            }
78
-        }
79
-
80
-        return $results;
81
-    }
82
-
83
-    /**
84
-     * Parses the selector string
85
-     *
86
-     * @param string $selector
87
-     */
88
-    protected function parseSelectorString($selector)
89
-    {
90
-        $matches = [];
91
-        preg_match_all($this->pattern, trim($selector).' ', $matches, PREG_SET_ORDER);
92
-
93
-        // skip tbody
94
-        $result = [];
95
-        foreach ($matches as $match) {
96
-            // default values
97
-            $tag       = strtolower(trim($match[1]));
98
-            $operator  = '=';
99
-            $key       = null;
100
-            $value     = null;
101
-            $noKey     = false;
102
-            $alterNext = false;
103
-
104
-            // check for elements that alter the behavior of the next element
105
-            if ($tag == '>') {
106
-                $alterNext = true;
107
-            }
108
-
109
-            // check for id selector
110
-            if ( ! empty($match[2])) {
111
-                $key   = 'id';
112
-                $value = $match[2];
113
-            }
114
-
115
-            // check for class selector
116
-            if ( ! empty($match[3])) {
117
-                $key   = 'class';
118
-                $value = $match[3];
119
-            }
120
-
121
-            // and final attribute selector
122
-            if ( ! empty($match[4])) {
123
-                $key = strtolower($match[4]);
124
-            }
125
-            if ( ! empty($match[5])) {
126
-                $operator = $match[5];
127
-            }
128
-            if ( ! empty($match[6])) {
129
-                $value = $match[6];
130
-            }
131
-
132
-            // check for elements that do not have a specified attribute
133
-            if (isset($key[0]) && $key[0] == '!') {
134
-                $key   = substr($key, 1);
135
-                $noKey = true;
136
-            }
137
-
138
-            $result[] = [
139
-                'tag'       => $tag,
140
-                'key'       => $key,
141
-                'value'     => $value,
142
-                'operator'  => $operator,
143
-                'noKey'     => $noKey,
144
-                'alterNext' => $alterNext,
145
-            ];
146
-            if (trim($match[7]) == ',') {
147
-                $this->selectors[] = $result;
148
-                $result            = [];
149
-            }
150
-        }
151
-
152
-        // save last results
153
-        if (count($result) > 0) {
154
-            $this->selectors[] = $result;
155
-        }
156
-    }
157
-
158
-    /**
159
-     * Attempts to find all children that match the rule
160
-     * given.
161
-     *
162
-     * @param array $nodes
163
-     * @param array $rule
164
-     * @param array $options
165
-     * @return array
166
-     * @recursive
167
-     */
168
-    protected function seek(array $nodes, array $rule, array $options)
169
-    {
170
-        // XPath index
171
-        if (count($rule['tag']) > 0 &&
172
-            count($rule['key']) > 0 &&
173
-            is_numeric($rule['key'])
174
-        ) {
175
-            $count = 0;
176
-            /** @var AbstractNode $node */
177
-            foreach ($nodes as $node) {
178
-                if ($rule['tag'] == '*' ||
179
-                    $rule['tag'] == $node->getTag()->name()
180
-                ) {
181
-                    ++$count;
182
-                    if ($count == $rule['key']) {
183
-                        // found the node we wanted
184
-                        return [$node];
185
-                    }
186
-                }
187
-            }
188
-
189
-            return [];
190
-        }
191
-
192
-        $options = $this->flattenOptions($options);
193
-
194
-        $return = [];
195
-        /** @var InnerNode $node */
196
-        foreach ($nodes as $node) {
197
-            // check if we are a leaf
198
-            if ($node instanceof LeafNode ||
199
-                ! $node->hasChildren()
200
-            ) {
201
-                continue;
202
-            }
203
-
204
-            $children = [];
205
-            $child    = $node->firstChild();
206
-            while ( ! is_null($child)) {
207
-                // wild card, grab all
208
-                if ($rule['tag'] == '*' && is_null($rule['key'])) {
209
-                    $return[] = $child;
210
-                    try {
211
-                        $child = $node->nextChild($child->id());
212
-                    } catch (ChildNotFoundException $e) {
213
-                        // no more children
214
-                        $child = null;
215
-                    }
216
-                    continue;
217
-                }
218
-
219
-                $pass = true;
220
-                // check tag
221
-                if ( ! empty($rule['tag']) && $rule['tag'] != $child->getTag()->name() &&
222
-                    $rule['tag'] != '*'
223
-                ) {
224
-                    // child failed tag check
225
-                    $pass = false;
226
-                }
227
-
228
-                // check key
229
-                if ($pass && ! is_null($rule['key'])) {
230
-                    if ($rule['noKey']) {
231
-                        if ( ! is_null($child->getAttribute($rule['key']))) {
232
-                            $pass = false;
233
-                        }
234
-                    } else {
235
-                        if ($rule['key'] != 'plaintext' &&
236
-                            is_null($child->getAttribute($rule['key']))
237
-                        ) {
238
-                            $pass = false;
239
-                        }
240
-                    }
241
-                }
242
-
243
-                // compare values
244
-                if ($pass && ! is_null($rule['key']) &&
245
-                    ! is_null($rule['value']) && $rule['value'] != '*'
246
-                ) {
247
-                    if ($rule['key'] == 'plaintext') {
248
-                        // plaintext search
249
-                        $nodeValue = $child->text();
250
-                    } else {
251
-                        // normal search
252
-                        $nodeValue = $child->getAttribute($rule['key']);
253
-                    }
254
-
255
-                    $check = $this->match($rule['operator'], $rule['value'], $nodeValue);
256
-
257
-                    // handle multiple classes
258
-                    if ( ! $check && $rule['key'] == 'class') {
259
-                        $childClasses = explode(' ', $child->getAttribute('class'));
260
-                        foreach ($childClasses as $class) {
261
-                            if ( ! empty($class)) {
262
-                                $check = $this->match($rule['operator'], $rule['value'], $class);
263
-                            }
264
-                            if ($check) {
265
-                                break;
266
-                            }
267
-                        }
268
-                    }
269
-
270
-                    if ( ! $check) {
271
-                        $pass = false;
272
-                    }
273
-                }
274
-
275
-                if ($pass) {
276
-                    // it passed all checks
277
-                    $return[] = $child;
278
-                } else {
279
-                    // this child failed to be matched
280
-                    if ($child instanceof InnerNode &&
281
-                        $child->hasChildren()
282
-                    ) {
283
-                        // we still want to check its children
284
-                        $children[] = $child;
285
-                    }
286
-                }
287
-
288
-                try {
289
-                    // get next child
290
-                    $child = $node->nextChild($child->id());
291
-                } catch (ChildNotFoundException $e) {
292
-                    // no more children
293
-                    $child = null;
294
-                }
295
-            }
296
-
297
-            if (( ! isset($options['checkGrandChildren']) ||
298
-                    $options['checkGrandChildren'])
299
-                && count($children) > 0
300
-            ) {
301
-                // we have children that failed but are not leaves.
302
-                $matches = $this->seek($children, $rule, $options);
303
-                foreach ($matches as $match) {
304
-                    $return[] = $match;
305
-                }
306
-            }
307
-        }
308
-
309
-        return $return;
310
-    }
311
-
312
-    /**
313
-     * Attempts to match the given arguments with the given operator.
314
-     *
315
-     * @param string $operator
316
-     * @param string $pattern
317
-     * @param string $value
318
-     * @return bool
319
-     */
320
-    protected function match($operator, $pattern, $value)
321
-    {
322
-        $value   = strtolower($value);
323
-        $pattern = strtolower($pattern);
324
-        switch ($operator) {
325
-            case '=':
326
-                return $value === $pattern;
327
-            case '!=':
328
-                return $value !== $pattern;
329
-            case '^=':
330
-                return preg_match('/^'.preg_quote($pattern, '/').'/', $value);
331
-            case '$=':
332
-                return preg_match('/'.preg_quote($pattern, '/').'$/', $value);
333
-            case '*=':
334
-                if ($pattern[0] == '/') {
335
-                    return preg_match($pattern, $value);
336
-                }
337
-
338
-                return preg_match("/".$pattern."/i", $value);
339
-        }
340
-
341
-        return false;
342
-    }
343
-
344
-    /**
345
-     * Attempts to figure out what the alteration will be for
346
-     * the next element.
347
-     *
348
-     * @param array $rule
349
-     * @return array
350
-     */
351
-    protected function alterNext($rule)
352
-    {
353
-        $options = [];
354
-        if ($rule['tag'] == '>') {
355
-            $options['checkGrandChildren'] = false;
356
-        }
357
-
358
-        return $options;
359
-    }
360
-
361
-    /**
362
-     * Flattens the option array.
363
-     *
364
-     * @param array $optionsArray
365
-     * @return array
366
-     */
367
-    protected function flattenOptions(array $optionsArray)
368
-    {
369
-        $options = [];
370
-        foreach ($optionsArray as $optionArray) {
371
-            foreach ($optionArray as $key => $option) {
372
-                $options[$key] = $option;
373
-            }
374
-        }
375
-
376
-        return $options;
377
-    }
18
+	/**
19
+	 * Pattern of CSS selectors, modified from 'mootools'
20
+	 *
21
+	 * @var string
22
+	 */
23
+	protected $pattern = "/([\w-:\*>]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is";
24
+
25
+	protected $selectors = [];
26
+
27
+	/**
28
+	 * Constructs with the selector string
29
+	 *
30
+	 * @param string $selector
31
+	 */
32
+	public function __construct($selector)
33
+	{
34
+		$this->parseSelectorString($selector);
35
+	}
36
+
37
+	/**
38
+	 * Returns the selectors that where found in __construct
39
+	 *
40
+	 * @return array
41
+	 */
42
+	public function getSelectors()
43
+	{
44
+		return $this->selectors;
45
+	}
46
+
47
+	/**
48
+	 * Attempts to find the selectors starting from the given
49
+	 * node object.
50
+	 *
51
+	 * @param AbstractNode $node
52
+	 * @return array|Collection
53
+	 */
54
+	public function find(AbstractNode $node)
55
+	{
56
+		$results = new Collection;
57
+		foreach ($this->selectors as $selector) {
58
+			$nodes = [$node];
59
+			if (count($selector) == 0) {
60
+				continue;
61
+			}
62
+
63
+			$options = [];
64
+			foreach ($selector as $rule) {
65
+				if ($rule['alterNext']) {
66
+					$options[] = $this->alterNext($rule);
67
+					continue;
68
+				}
69
+				$nodes = $this->seek($nodes, $rule, $options);
70
+				// clear the options
71
+				$options = [];
72
+			}
73
+
74
+			// this is the final set of nodes
75
+			foreach ($nodes as $result) {
76
+				$results[] = $result;
77
+			}
78
+		}
79
+
80
+		return $results;
81
+	}
82
+
83
+	/**
84
+	 * Parses the selector string
85
+	 *
86
+	 * @param string $selector
87
+	 */
88
+	protected function parseSelectorString($selector)
89
+	{
90
+		$matches = [];
91
+		preg_match_all($this->pattern, trim($selector).' ', $matches, PREG_SET_ORDER);
92
+
93
+		// skip tbody
94
+		$result = [];
95
+		foreach ($matches as $match) {
96
+			// default values
97
+			$tag       = strtolower(trim($match[1]));
98
+			$operator  = '=';
99
+			$key       = null;
100
+			$value     = null;
101
+			$noKey     = false;
102
+			$alterNext = false;
103
+
104
+			// check for elements that alter the behavior of the next element
105
+			if ($tag == '>') {
106
+				$alterNext = true;
107
+			}
108
+
109
+			// check for id selector
110
+			if ( ! empty($match[2])) {
111
+				$key   = 'id';
112
+				$value = $match[2];
113
+			}
114
+
115
+			// check for class selector
116
+			if ( ! empty($match[3])) {
117
+				$key   = 'class';
118
+				$value = $match[3];
119
+			}
120
+
121
+			// and final attribute selector
122
+			if ( ! empty($match[4])) {
123
+				$key = strtolower($match[4]);
124
+			}
125
+			if ( ! empty($match[5])) {
126
+				$operator = $match[5];
127
+			}
128
+			if ( ! empty($match[6])) {
129
+				$value = $match[6];
130
+			}
131
+
132
+			// check for elements that do not have a specified attribute
133
+			if (isset($key[0]) && $key[0] == '!') {
134
+				$key   = substr($key, 1);
135
+				$noKey = true;
136
+			}
137
+
138
+			$result[] = [
139
+				'tag'       => $tag,
140
+				'key'       => $key,
141
+				'value'     => $value,
142
+				'operator'  => $operator,
143
+				'noKey'     => $noKey,
144
+				'alterNext' => $alterNext,
145
+			];
146
+			if (trim($match[7]) == ',') {
147
+				$this->selectors[] = $result;
148
+				$result            = [];
149
+			}
150
+		}
151
+
152
+		// save last results
153
+		if (count($result) > 0) {
154
+			$this->selectors[] = $result;
155
+		}
156
+	}
157
+
158
+	/**
159
+	 * Attempts to find all children that match the rule
160
+	 * given.
161
+	 *
162
+	 * @param array $nodes
163
+	 * @param array $rule
164
+	 * @param array $options
165
+	 * @return array
166
+	 * @recursive
167
+	 */
168
+	protected function seek(array $nodes, array $rule, array $options)
169
+	{
170
+		// XPath index
171
+		if (count($rule['tag']) > 0 &&
172
+			count($rule['key']) > 0 &&
173
+			is_numeric($rule['key'])
174
+		) {
175
+			$count = 0;
176
+			/** @var AbstractNode $node */
177
+			foreach ($nodes as $node) {
178
+				if ($rule['tag'] == '*' ||
179
+					$rule['tag'] == $node->getTag()->name()
180
+				) {
181
+					++$count;
182
+					if ($count == $rule['key']) {
183
+						// found the node we wanted
184
+						return [$node];
185
+					}
186
+				}
187
+			}
188
+
189
+			return [];
190
+		}
191
+
192
+		$options = $this->flattenOptions($options);
193
+
194
+		$return = [];
195
+		/** @var InnerNode $node */
196
+		foreach ($nodes as $node) {
197
+			// check if we are a leaf
198
+			if ($node instanceof LeafNode ||
199
+				! $node->hasChildren()
200
+			) {
201
+				continue;
202
+			}
203
+
204
+			$children = [];
205
+			$child    = $node->firstChild();
206
+			while ( ! is_null($child)) {
207
+				// wild card, grab all
208
+				if ($rule['tag'] == '*' && is_null($rule['key'])) {
209
+					$return[] = $child;
210
+					try {
211
+						$child = $node->nextChild($child->id());
212
+					} catch (ChildNotFoundException $e) {
213
+						// no more children
214
+						$child = null;
215
+					}
216
+					continue;
217
+				}
218
+
219
+				$pass = true;
220
+				// check tag
221
+				if ( ! empty($rule['tag']) && $rule['tag'] != $child->getTag()->name() &&
222
+					$rule['tag'] != '*'
223
+				) {
224
+					// child failed tag check
225
+					$pass = false;
226
+				}
227
+
228
+				// check key
229
+				if ($pass && ! is_null($rule['key'])) {
230
+					if ($rule['noKey']) {
231
+						if ( ! is_null($child->getAttribute($rule['key']))) {
232
+							$pass = false;
233
+						}
234
+					} else {
235
+						if ($rule['key'] != 'plaintext' &&
236
+							is_null($child->getAttribute($rule['key']))
237
+						) {
238
+							$pass = false;
239
+						}
240
+					}
241
+				}
242
+
243
+				// compare values
244
+				if ($pass && ! is_null($rule['key']) &&
245
+					! is_null($rule['value']) && $rule['value'] != '*'
246
+				) {
247
+					if ($rule['key'] == 'plaintext') {
248
+						// plaintext search
249
+						$nodeValue = $child->text();
250
+					} else {
251
+						// normal search
252
+						$nodeValue = $child->getAttribute($rule['key']);
253
+					}
254
+
255
+					$check = $this->match($rule['operator'], $rule['value'], $nodeValue);
256
+
257
+					// handle multiple classes
258
+					if ( ! $check && $rule['key'] == 'class') {
259
+						$childClasses = explode(' ', $child->getAttribute('class'));
260
+						foreach ($childClasses as $class) {
261
+							if ( ! empty($class)) {
262
+								$check = $this->match($rule['operator'], $rule['value'], $class);
263
+							}
264
+							if ($check) {
265
+								break;
266
+							}
267
+						}
268
+					}
269
+
270
+					if ( ! $check) {
271
+						$pass = false;
272
+					}
273
+				}
274
+
275
+				if ($pass) {
276
+					// it passed all checks
277
+					$return[] = $child;
278
+				} else {
279
+					// this child failed to be matched
280
+					if ($child instanceof InnerNode &&
281
+						$child->hasChildren()
282
+					) {
283
+						// we still want to check its children
284
+						$children[] = $child;
285
+					}
286
+				}
287
+
288
+				try {
289
+					// get next child
290
+					$child = $node->nextChild($child->id());
291
+				} catch (ChildNotFoundException $e) {
292
+					// no more children
293
+					$child = null;
294
+				}
295
+			}
296
+
297
+			if (( ! isset($options['checkGrandChildren']) ||
298
+					$options['checkGrandChildren'])
299
+				&& count($children) > 0
300
+			) {
301
+				// we have children that failed but are not leaves.
302
+				$matches = $this->seek($children, $rule, $options);
303
+				foreach ($matches as $match) {
304
+					$return[] = $match;
305
+				}
306
+			}
307
+		}
308
+
309
+		return $return;
310
+	}
311
+
312
+	/**
313
+	 * Attempts to match the given arguments with the given operator.
314
+	 *
315
+	 * @param string $operator
316
+	 * @param string $pattern
317
+	 * @param string $value
318
+	 * @return bool
319
+	 */
320
+	protected function match($operator, $pattern, $value)
321
+	{
322
+		$value   = strtolower($value);
323
+		$pattern = strtolower($pattern);
324
+		switch ($operator) {
325
+			case '=':
326
+				return $value === $pattern;
327
+			case '!=':
328
+				return $value !== $pattern;
329
+			case '^=':
330
+				return preg_match('/^'.preg_quote($pattern, '/').'/', $value);
331
+			case '$=':
332
+				return preg_match('/'.preg_quote($pattern, '/').'$/', $value);
333
+			case '*=':
334
+				if ($pattern[0] == '/') {
335
+					return preg_match($pattern, $value);
336
+				}
337
+
338
+				return preg_match("/".$pattern."/i", $value);
339
+		}
340
+
341
+		return false;
342
+	}
343
+
344
+	/**
345
+	 * Attempts to figure out what the alteration will be for
346
+	 * the next element.
347
+	 *
348
+	 * @param array $rule
349
+	 * @return array
350
+	 */
351
+	protected function alterNext($rule)
352
+	{
353
+		$options = [];
354
+		if ($rule['tag'] == '>') {
355
+			$options['checkGrandChildren'] = false;
356
+		}
357
+
358
+		return $options;
359
+	}
360
+
361
+	/**
362
+	 * Flattens the option array.
363
+	 *
364
+	 * @param array $optionsArray
365
+	 * @return array
366
+	 */
367
+	protected function flattenOptions(array $optionsArray)
368
+	{
369
+		$options = [];
370
+		foreach ($optionsArray as $optionArray) {
371
+			foreach ($optionArray as $key => $option) {
372
+				$options[$key] = $option;
373
+			}
374
+		}
375
+
376
+		return $options;
377
+	}
378 378
 }
Please login to merge, or discard this patch.