Passed
Branch dev/3.0.0 (a78054)
by Gilles
02:10
created

InnerNode::insertAfter()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.0466

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 6
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 14
ccs 6
cts 7
cp 0.8571
crap 4.0466
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PHPHtmlParser\Dom;
6
7
use PHPHtmlParser\Exceptions\ChildNotFoundException;
8
use PHPHtmlParser\Exceptions\CircularException;
9
use PHPHtmlParser\Exceptions\LogicalException;
10
use stringEncode\Encode;
11
12
/**
13
 * Inner node of the html tree, might have children.
14
 */
15
abstract class InnerNode extends ArrayNode
16
{
17
    /**
18
     * An array of all the children.
19
     *
20
     * @var array
21
     */
22
    protected $children = [];
23
24
    /**
25
     * Sets the encoding class to this node and propagates it
26
     * to all its children.
27
     */
28 279
    public function propagateEncoding(Encode $encode): void
29
    {
30 279
        $this->encode = $encode;
31 279
        $this->tag->setEncoding($encode);
32
        // check children
33 279
        foreach ($this->children as $child) {
34
            /** @var AbstractNode $node */
35 279
            $node = $child['node'];
36 279
            $node->propagateEncoding($encode);
37
        }
38 279
    }
39
40
    /**
41
     * Checks if this node has children.
42
     */
43 489
    public function hasChildren(): bool
44
    {
45 489
        return !empty($this->children);
46
    }
47
48
    /**
49
     * Returns the child by id.
50
     *
51
     * @throws ChildNotFoundException
52
     */
53 429
    public function getChild(int $id): AbstractNode
54
    {
55 429
        if (!isset($this->children[$id])) {
56 3
            throw new ChildNotFoundException("Child '$id' not found in this node.");
57
        }
58
59 426
        return $this->children[$id]['node'];
60
    }
61
62
    /**
63
     * Returns a new array of child nodes.
64
     */
65 15
    public function getChildren(): array
66
    {
67 15
        $nodes = [];
68 15
        $childrenIds = [];
69
        try {
70 15
            $child = $this->firstChild();
71
            do {
72 12
                $nodes[] = $child;
73 12
                $childrenIds[] = $child->id;
74 12
                $child = $this->nextChild($child->id());
75 12
                if (\in_array($child->id, $childrenIds, true)) {
76
                    throw new CircularException('Circular sibling referance found. Child with id ' . $child->id() . ' found twice.');
77
                }
78 12
            } while (true);
79 15
        } catch (ChildNotFoundException $e) {
80
            // we are done looking for children
81 15
            unset($e);
82
        }
83
84 15
        return $nodes;
85
    }
86
87
    /**
88
     * Counts children.
89
     */
90 6
    public function countChildren(): int
91
    {
92 6
        return \count($this->children);
93
    }
94
95
    /**
96
     * Adds a child node to this node and returns the id of the child for this
97
     * parent.
98
     *
99
     * @throws ChildNotFoundException
100
     * @throws CircularException
101
     * @throws LogicalException
102
     */
103 480
    public function addChild(AbstractNode $child, int $before = -1): bool
104
    {
105 480
        $key = null;
106
107
        // check integrity
108 480
        if ($this->isAncestor($child->id())) {
109 3
            throw new CircularException('Can not add child. It is my ancestor.');
110
        }
111
112
        // check if child is itself
113 480
        if ($child->id() == $this->id) {
114 3
            throw new CircularException('Can not set itself as a child.');
115
        }
116
117 477
        $next = null;
118
119 477
        if ($this->hasChildren()) {
120 462
            if (isset($this->children[$child->id()])) {
121
                // we already have this child
122 450
                return false;
123
            }
124
125 342
            if ($before >= 0) {
126 9
                if (!isset($this->children[$before])) {
127
                    return false;
128
                }
129
130 9
                $key = $this->children[$before]['prev'];
131
132 9
                if ($key) {
133 6
                    $this->children[$key]['next'] = $child->id();
134
                }
135
136 9
                $this->children[$before]['prev'] = $child->id();
137 9
                $next = $before;
138
            } else {
139 342
                $sibling = $this->lastChild();
140 342
                $key = $sibling->id();
141
142 342
                $this->children[$key]['next'] = $child->id();
143
            }
144
        }
145
146 477
        $keys = \array_keys($this->children);
147
148
        $insert = [
149 477
            'node' => $child,
150 477
            'next' => $next,
151 477
            'prev' => $key,
152
        ];
153
154 477
        $index = $key ? (int) (\array_search($key, $keys, true) + 1) : 0;
155 477
        \array_splice($keys, $index, 0, (string) $child->id());
156
157 477
        $children = \array_values($this->children);
158 477
        \array_splice($children, $index, 0, [$insert]);
159
160
        // add the child
161 477
        $combination = \array_combine($keys, $children);
162 477
        if ($combination === false) {
163
            // The number of elements for each array isn't equal or if the arrays are empty.
164
            throw new LogicalException('array combine failed during add child method call.');
165
        }
166 477
        $this->children = $combination;
167
168
        // tell child I am the new parent
169 477
        $child->setParent($this);
170
171
        //clear any cache
172 477
        $this->clear();
173
174 477
        return true;
175
    }
176
177
    /**
178
     * Insert element before child with provided id.
179
     *
180
     * @throws ChildNotFoundException
181
     * @throws CircularException
182
     */
183 6
    public function insertBefore(AbstractNode $child, int $id): bool
184
    {
185 6
        return $this->addChild($child, $id);
186
    }
187
188
    /**
189
     * Insert element before after with provided id.
190
     *
191
     * @throws ChildNotFoundException
192
     * @throws CircularException
193
     */
194 6
    public function insertAfter(AbstractNode $child, int $id): bool
195
    {
196 6
        if (!isset($this->children[$id])) {
197
            return false;
198
        }
199
200 6
        if (isset($this->children[$id]['next']) && \is_int($this->children[$id]['next'])) {
201 3
            return $this->addChild($child, (int) $this->children[$id]['next']);
202
        }
203
204
        // clear cache
205 3
        $this->clear();
206
207 3
        return $this->addChild($child);
208
    }
209
210
    /**
211
     * Removes the child by id.
212
     *
213
     *
214
     */
215 24
    public function removeChild(int $id): InnerNode
216
    {
217 24
        if (!isset($this->children[$id])) {
218 3
            return $this;
219
        }
220
221
        // handle moving next and previous assignments.
222 21
        $next = $this->children[$id]['next'];
223 21
        $prev = $this->children[$id]['prev'];
224 21
        if (!\is_null($next)) {
225 9
            $this->children[$next]['prev'] = $prev;
226
        }
227 21
        if (!\is_null($prev)) {
228 9
            $this->children[$prev]['next'] = $next;
229
        }
230
231
        // remove the child
232 21
        unset($this->children[$id]);
233
234
        //clear any cache
235 21
        $this->clear();
236
237 21
        return $this;
238
    }
239
240
    /**
241
     * Check if has next Child.
242
     *
243
     * @throws ChildNotFoundException
244
     *
245
     * @return mixed
246
     */
247 6
    public function hasNextChild(int $id)
248
    {
249 6
        $child = $this->getChild($id);
250
251 3
        return $this->children[$child->id()]['next'];
252
    }
253
254
    /**
255
     * Attempts to get the next child.
256
     *
257
     * @throws ChildNotFoundException
258
     *
259
     * @uses $this->getChild()
260
     */
261 387
    public function nextChild(int $id): AbstractNode
262
    {
263 387
        $child = $this->getChild($id);
264 387
        $next = $this->children[$child->id()]['next'];
265 387
        if (\is_null($next) || !\is_int($next)) {
266 366
            throw new ChildNotFoundException("Child '$id' next sibling not found in this node.");
267
        }
268
269 306
        return $this->getChild($next);
270
    }
271
272
    /**
273
     * Attempts to get the previous child.
274
     *
275
     * @throws ChildNotFoundException
276
     *
277
     * @uses $this->getChild()
278
     */
279 12
    public function previousChild(int $id): AbstractNode
280
    {
281 12
        $child = $this->getchild($id);
282 12
        $next = $this->children[$child->id()]['prev'];
283 12
        if (\is_null($next) || !\is_int($next)) {
284 3
            throw new ChildNotFoundException("Child '$id' previous not found in this node.");
285
        }
286
287 9
        return $this->getChild($next);
288
    }
289
290
    /**
291
     * Checks if the given node id is a child of the
292
     * current node.
293
     */
294 462
    public function isChild(int $id): bool
295
    {
296 462
        foreach (\array_keys($this->children) as $childId) {
297 39
            if ($id == $childId) {
298 25
                return true;
299
            }
300
        }
301
302 462
        return false;
303
    }
304
305
    /**
306
     * Removes the child with id $childId and replace it with the new child
307
     * $newChild.
308
     *
309
     * @throws LogicalException
310
     */
311 6
    public function replaceChild(int $childId, AbstractNode $newChild): void
312
    {
313 6
        $oldChild = $this->children[$childId];
314
315 6
        $newChild->prev = (int) $oldChild['prev'];
316 6
        $newChild->next = (int) $oldChild['next'];
317
318 6
        $keys = \array_keys($this->children);
319 6
        $index = \array_search($childId, $keys, true);
320 6
        $keys[$index] = $newChild->id();
321 6
        $combination = \array_combine($keys, $this->children);
322 6
        if ($combination === false) {
323
            // The number of elements for each array isn't equal or if the arrays are empty.
324
            throw new LogicalException('array combine failed during replace child method call.');
325
        }
326 6
        $this->children = $combination;
327 6
        $this->children[$newChild->id()] = [
328 6
            'prev' => $oldChild['prev'],
329 6
            'node' => $newChild,
330 6
            'next' => $oldChild['next'],
331
        ];
332
333
        // change previous child id to new child
334 6
        if ($oldChild['prev'] && isset($this->children[$newChild->prev])) {
335
            $this->children[$oldChild['prev']]['next'] = $newChild->id();
336
        }
337
338
        // change next child id to new child
339 6
        if ($oldChild['next'] && isset($this->children[$newChild->next])) {
340 3
            $this->children[$oldChild['next']]['prev'] = $newChild->id();
341
        }
342
343
        // remove old child
344 6
        unset($this->children[$childId]);
345
346
        // clean out cache
347 6
        $this->clear();
348 6
    }
349
350
    /**
351
     * Shortcut to return the first child.
352
     *
353
     * @throws ChildNotFoundException
354
     *
355
     * @uses $this->getChild()
356
     */
357 378
    public function firstChild(): AbstractNode
358
    {
359 378
        if (\count($this->children) == 0) {
360
            // no children
361 3
            throw new ChildNotFoundException('No children found in node.');
362
        }
363
364 378
        \reset($this->children);
365 378
        $key = (int) \key($this->children);
366
367 378
        return $this->getChild($key);
368
    }
369
370
    /**
371
     * Attempts to get the last child.
372
     *
373
     * @throws ChildNotFoundException
374
     *
375
     * @uses $this->getChild()
376
     */
377 342
    public function lastChild(): AbstractNode
378
    {
379 342
        if (\count($this->children) == 0) {
380
            // no children
381
            throw new ChildNotFoundException('No children found in node.');
382
        }
383
384 342
        \end($this->children);
385 342
        $key = \key($this->children);
386
387 342
        if (!\is_int($key)) {
388
            throw new LogicalException('Children array contain child with a key that is not an int.');
389
        }
390
391 342
        return $this->getChild($key);
392
    }
393
394
    /**
395
     * Checks if the given node id is a descendant of the
396
     * current node.
397
     */
398 462
    public function isDescendant(int $id): bool
399
    {
400 462
        if ($this->isChild($id)) {
401 6
            return true;
402
        }
403
404 462
        foreach ($this->children as $child) {
405
            /** @var InnerNode $node */
406 21
            $node = $child['node'];
407 21
            if ($node instanceof InnerNode
408 21
              && $node->hasChildren()
409 21
              && $node->isDescendant($id)
410
            ) {
411 9
                return true;
412
            }
413
        }
414
415 462
        return false;
416
    }
417
418
    /**
419
     * Sets the parent node.
420
     *
421
     * @throws ChildNotFoundException
422
     * @throws CircularException
423
     */
424 462
    public function setParent(InnerNode $parent): AbstractNode
425
    {
426
        // check integrity
427 462
        if ($this->isDescendant($parent->id())) {
428 3
            throw new CircularException('Can not add descendant "' . $parent->id() . '" as my parent.');
429
        }
430
431
        // clear cache
432 462
        $this->clear();
433
434 462
        return parent::setParent($parent);
435
    }
436
}
437