1
|
|
|
<?php |
2
|
|
|
namespace PHPHtmlParser\Dom; |
3
|
|
|
|
4
|
|
|
use PHPHtmlParser\Exceptions\ChildNotFoundException; |
5
|
|
|
use PHPHtmlParser\Exceptions\CircularException; |
6
|
|
|
use stringEncode\Encode; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Inner node of the html tree, might have children. |
10
|
|
|
* |
11
|
|
|
* @package PHPHtmlParser\Dom |
12
|
|
|
*/ |
13
|
|
|
abstract class InnerNode extends ArrayNode |
14
|
|
|
{ |
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
|
186 |
|
public function propagateEncoding(Encode $encode): void |
31
|
|
|
{ |
32
|
186 |
|
$this->encode = $encode; |
33
|
186 |
|
$this->tag->setEncoding($encode); |
34
|
|
|
// check children |
35
|
186 |
|
foreach ($this->children as $id => $child) { |
36
|
|
|
/** @var AbstractNode $node */ |
37
|
186 |
|
$node = $child['node']; |
38
|
186 |
|
$node->propagateEncoding($encode); |
39
|
|
|
} |
40
|
186 |
|
} |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Checks if this node has children. |
44
|
|
|
* |
45
|
|
|
* @return bool |
46
|
|
|
*/ |
47
|
390 |
|
public function hasChildren(): bool |
48
|
|
|
{ |
49
|
390 |
|
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
|
333 |
|
public function getChild(int $id): AbstractNode |
60
|
|
|
{ |
61
|
333 |
|
if ( ! isset($this->children[$id])) { |
62
|
3 |
|
throw new ChildNotFoundException("Child '$id' not found in this node."); |
63
|
|
|
} |
64
|
|
|
|
65
|
330 |
|
return $this->children[$id]['node']; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Returns a new array of child nodes |
70
|
|
|
* |
71
|
|
|
* @return array |
72
|
|
|
*/ |
73
|
12 |
|
public function getChildren(): array |
74
|
|
|
{ |
75
|
12 |
|
$nodes = []; |
76
|
|
|
try { |
77
|
12 |
|
$child = $this->firstChild(); |
78
|
|
|
do { |
79
|
12 |
|
$nodes[] = $child; |
80
|
12 |
|
$child = $this->nextChild($child->id()); |
81
|
12 |
|
} while ( ! is_null($child)); |
82
|
12 |
|
} catch (ChildNotFoundException $e) { |
83
|
|
|
// we are done looking for children |
84
|
|
|
} |
85
|
|
|
|
86
|
12 |
|
return $nodes; |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* Counts children |
91
|
|
|
* |
92
|
|
|
* @return int |
93
|
|
|
*/ |
94
|
6 |
|
public function countChildren(): int |
95
|
|
|
{ |
96
|
6 |
|
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
|
|
|
* @param Int $before |
105
|
|
|
* @return bool |
106
|
|
|
* @throws CircularException |
107
|
|
|
*/ |
108
|
384 |
|
public function addChild(AbstractNode $child, int $before = -1): bool |
109
|
|
|
{ |
110
|
384 |
|
$key = null; |
111
|
|
|
|
112
|
|
|
// check integrity |
113
|
384 |
|
if ($this->isAncestor($child->id())) { |
114
|
3 |
|
throw new CircularException('Can not add child. It is my ancestor.'); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
// check if child is itself |
118
|
384 |
|
if ($child->id() == $this->id) { |
119
|
3 |
|
throw new CircularException('Can not set itself as a child.'); |
120
|
|
|
} |
121
|
|
|
|
122
|
381 |
|
$next = null; |
123
|
|
|
|
124
|
381 |
|
if ($this->hasChildren()) { |
125
|
366 |
|
if (isset($this->children[$child->id()])) { |
126
|
|
|
// we already have this child |
127
|
354 |
|
return false; |
128
|
|
|
} |
129
|
|
|
|
130
|
297 |
|
if ($before >= 0) { |
131
|
9 |
|
if (!isset($this->children[$before])) { |
132
|
|
|
return false; |
133
|
|
|
} |
134
|
|
|
|
135
|
9 |
|
$key = $this->children[$before]['prev']; |
136
|
|
|
|
137
|
9 |
|
if($key){ |
138
|
6 |
|
$this->children[$key]['next'] = $child->id(); |
139
|
|
|
} |
140
|
|
|
|
141
|
9 |
|
$this->children[$before]['prev'] = $child->id(); |
142
|
9 |
|
$next = $before; |
143
|
|
|
} else { |
144
|
297 |
|
$sibling = $this->lastChild(); |
145
|
297 |
|
$key = $sibling->id(); |
146
|
|
|
|
147
|
297 |
|
$this->children[$key]['next'] = $child->id(); |
148
|
|
|
} |
149
|
|
|
} |
150
|
|
|
|
151
|
381 |
|
$keys = array_keys($this->children); |
152
|
|
|
|
153
|
|
|
$insert = [ |
154
|
381 |
|
'node' => $child, |
155
|
381 |
|
'next' => $next, |
156
|
381 |
|
'prev' => $key, |
157
|
|
|
]; |
158
|
|
|
|
159
|
381 |
|
$index = $key ? (array_search($key, $keys, true) + 1) : 0; |
160
|
381 |
|
array_splice($keys, $index, 0, $child->id()); |
161
|
|
|
|
162
|
381 |
|
$children = array_values($this->children); |
163
|
381 |
|
array_splice($children, $index, 0, [$insert]); |
164
|
|
|
|
165
|
|
|
// add the child |
166
|
381 |
|
$this->children = array_combine($keys, $children); |
167
|
|
|
|
168
|
|
|
// tell child I am the new parent |
169
|
381 |
|
$child->setParent($this); |
170
|
|
|
|
171
|
|
|
//clear any cache |
172
|
381 |
|
$this->clear(); |
173
|
|
|
|
174
|
381 |
|
return true; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Insert element before child with provided id |
179
|
|
|
* |
180
|
|
|
* @param AbstractNode $child |
181
|
|
|
* @param int $id |
182
|
|
|
* @return bool |
183
|
|
|
*/ |
184
|
6 |
|
public function insertBefore(AbstractNode $child, int $id): bool |
185
|
|
|
{ |
186
|
6 |
|
return $this->addChild($child, $id); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Insert element before after with provided id |
191
|
|
|
* |
192
|
|
|
* @param AbstractNode $child |
193
|
|
|
* @param int $id |
194
|
|
|
* @return bool |
195
|
|
|
*/ |
196
|
6 |
|
public function insertAfter(AbstractNode $child, int $id): bool |
197
|
|
|
{ |
198
|
6 |
|
if (!isset($this->children[$id])) { |
199
|
|
|
return false; |
200
|
|
|
} |
201
|
|
|
|
202
|
6 |
|
if ($this->children[$id]['next']) { |
203
|
3 |
|
return $this->addChild($child, $this->children[$id]['next']); |
204
|
|
|
} |
205
|
|
|
|
206
|
3 |
|
return $this->addChild($child); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* Removes the child by id. |
211
|
|
|
* |
212
|
|
|
* @param int $id |
213
|
|
|
* @return InnerNode |
214
|
|
|
* @chainable |
215
|
|
|
*/ |
216
|
21 |
|
public function removeChild(int $id): InnerNode |
217
|
|
|
{ |
218
|
21 |
|
if ( ! isset($this->children[$id])) { |
219
|
3 |
|
return $this; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
// handle moving next and previous assignments. |
223
|
18 |
|
$next = $this->children[$id]['next']; |
224
|
18 |
|
$prev = $this->children[$id]['prev']; |
225
|
18 |
|
if ( ! is_null($next)) { |
226
|
9 |
|
$this->children[$next]['prev'] = $prev; |
227
|
|
|
} |
228
|
18 |
|
if ( ! is_null($prev)) { |
229
|
9 |
|
$this->children[$prev]['next'] = $next; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
// remove the child |
233
|
18 |
|
unset($this->children[$id]); |
234
|
|
|
|
235
|
|
|
//clear any cache |
236
|
18 |
|
$this->clear(); |
237
|
|
|
|
238
|
18 |
|
return $this; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Check if has next Child |
243
|
|
|
* |
244
|
|
|
* @param int $id |
245
|
|
|
* @return mixed |
246
|
|
|
*/ |
247
|
6 |
|
public function hasNextChild(int $id) |
248
|
|
|
{ |
249
|
6 |
|
$child= $this->getChild($id); |
250
|
3 |
|
return $this->children[$child->id()]['next']; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Attempts to get the next child. |
255
|
|
|
* |
256
|
|
|
* @param int $id |
257
|
|
|
* @return AbstractNode |
258
|
|
|
* @uses $this->getChild() |
259
|
|
|
* @throws ChildNotFoundException |
260
|
|
|
*/ |
261
|
291 |
|
public function nextChild(int $id): AbstractNode |
262
|
|
|
{ |
263
|
291 |
|
$child = $this->getChild($id); |
264
|
291 |
|
$next = $this->children[$child->id()]['next']; |
265
|
291 |
|
if (is_null($next)) { |
266
|
270 |
|
throw new ChildNotFoundException("Child '$id' next not found in this node."); |
267
|
|
|
} |
268
|
|
|
|
269
|
261 |
|
return $this->getChild($next); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Attempts to get the previous child. |
274
|
|
|
* |
275
|
|
|
* @param int $id |
276
|
|
|
* @return AbstractNode |
277
|
|
|
* @uses $this->getChild() |
278
|
|
|
* @throws ChildNotFoundException |
279
|
|
|
*/ |
280
|
12 |
|
public function previousChild(int $id): AbstractNode |
281
|
|
|
{ |
282
|
12 |
|
$child = $this->getchild($id); |
283
|
12 |
|
$next = $this->children[$child->id()]['prev']; |
284
|
12 |
|
if (is_null($next)) { |
285
|
3 |
|
throw new ChildNotFoundException("Child '$id' previous not found in this node."); |
286
|
|
|
} |
287
|
|
|
|
288
|
9 |
|
return $this->getChild($next); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Checks if the given node id is a child of the |
293
|
|
|
* current node. |
294
|
|
|
* |
295
|
|
|
* @param int $id |
296
|
|
|
* @return bool |
297
|
|
|
*/ |
298
|
372 |
|
public function isChild(int $id): bool |
299
|
|
|
{ |
300
|
372 |
|
foreach ($this->children as $childId => $child) { |
301
|
36 |
|
if ($id == $childId) { |
302
|
24 |
|
return true; |
303
|
|
|
} |
304
|
|
|
} |
305
|
|
|
|
306
|
372 |
|
return false; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* Removes the child with id $childId and replace it with the new child |
311
|
|
|
* $newChild. |
312
|
|
|
* |
313
|
|
|
* @param int $childId |
314
|
|
|
* @param AbstractNode $newChild |
315
|
|
|
* @throws ChildNotFoundException |
316
|
|
|
* @return void |
317
|
|
|
*/ |
318
|
6 |
|
public function replaceChild(int $childId, AbstractNode $newChild): void |
319
|
|
|
{ |
320
|
6 |
|
$oldChild = $this->children[$childId]; |
321
|
|
|
|
322
|
6 |
|
$newChild->prev = $oldChild['prev']; |
|
|
|
|
323
|
6 |
|
$newChild->next = $oldChild['next']; |
|
|
|
|
324
|
|
|
|
325
|
6 |
|
$keys = array_keys($this->children); |
326
|
6 |
|
$index = array_search($childId, $keys, true); |
327
|
6 |
|
$keys[$index] = $newChild->id(); |
328
|
6 |
|
$this->children = array_combine($keys, $this->children); |
329
|
6 |
|
$this->children[$newChild->id()] = array( |
330
|
6 |
|
'prev' => $oldChild['prev'], |
331
|
6 |
|
'node' => $newChild, |
332
|
6 |
|
'next' => $oldChild['next'] |
333
|
|
|
); |
334
|
|
|
|
335
|
6 |
|
if ($oldChild['prev'] && isset($this->children[$newChild->prev])) { |
|
|
|
|
336
|
|
|
$this->children[$oldChild['prev']]['next'] = $newChild->id(); |
337
|
|
|
} |
338
|
|
|
|
339
|
6 |
|
if ($oldChild['next'] && isset($this->children[$newChild->next])) { |
|
|
|
|
340
|
3 |
|
$this->children[$oldChild['next']]['prev'] = $newChild->id(); |
341
|
|
|
} |
342
|
6 |
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Shortcut to return the first child. |
346
|
|
|
* |
347
|
|
|
* @return AbstractNode |
348
|
|
|
* @uses $this->getChild() |
349
|
|
|
*/ |
350
|
282 |
|
public function firstChild(): AbstractNode |
351
|
|
|
{ |
352
|
282 |
|
reset($this->children); |
353
|
282 |
|
$key = key($this->children); |
354
|
|
|
|
355
|
282 |
|
return $this->getChild($key); |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Attempts to get the last child. |
360
|
|
|
* |
361
|
|
|
* @return AbstractNode |
362
|
|
|
*/ |
363
|
297 |
|
public function lastChild(): AbstractNode |
364
|
|
|
{ |
365
|
297 |
|
end($this->children); |
366
|
297 |
|
$key = key($this->children); |
367
|
|
|
|
368
|
297 |
|
return $this->getChild($key); |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Checks if the given node id is a descendant of the |
373
|
|
|
* current node. |
374
|
|
|
* |
375
|
|
|
* @param int $id |
376
|
|
|
* @return bool |
377
|
|
|
*/ |
378
|
372 |
|
public function isDescendant(int $id): bool |
379
|
|
|
{ |
380
|
372 |
|
if ($this->isChild($id)) { |
381
|
6 |
|
return true; |
382
|
|
|
} |
383
|
|
|
|
384
|
372 |
|
foreach ($this->children as $childId => $child) { |
385
|
|
|
/** @var InnerNode $node */ |
386
|
18 |
|
$node = $child['node']; |
387
|
18 |
|
if ($node instanceof InnerNode && |
388
|
18 |
|
$node->hasChildren() && |
389
|
18 |
|
$node->isDescendant($id) |
390
|
|
|
) { |
391
|
8 |
|
return true; |
392
|
|
|
} |
393
|
|
|
} |
394
|
|
|
|
395
|
372 |
|
return false; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Sets the parent node. |
400
|
|
|
* |
401
|
|
|
* @param InnerNode $parent |
402
|
|
|
* @return AbstractNode |
403
|
|
|
* @throws CircularException |
404
|
|
|
* @chainable |
405
|
|
|
*/ |
406
|
372 |
|
public function setParent(InnerNode $parent): AbstractNode |
407
|
|
|
{ |
408
|
|
|
// check integrity |
409
|
372 |
|
if ($this->isDescendant($parent->id())) { |
410
|
3 |
|
throw new CircularException('Can not add descendant "'.$parent->id().'" as my parent.'); |
411
|
|
|
} |
412
|
|
|
|
413
|
372 |
|
return parent::setParent($parent); |
414
|
|
|
} |
415
|
|
|
} |
416
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.