Passed
Push — master ( 0d07bf...dd4616 )
by Steve
35s
created

TextNodeComparator   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 472
Duplicated Lines 0 %

Test Coverage

Coverage 88.82%

Importance

Changes 0
Metric Value
eloc 170
dl 0
loc 472
ccs 151
cts 170
cp 0.8882
rs 3.52
c 0
b 0
f 0
wmc 61

19 Methods

Rating   Name   Duplication   Size   Complexity  
B markAsNew() 0 34 7
A getRangeCount() 0 3 1
A getBodyNode() 0 3 1
A setLastModified() 0 3 1
F markAsDeleted() 0 143 22
A getNewId() 0 3 1
A skipRangeComparison() 0 3 1
A rangesEqual() 0 7 2
A getLastModified() 0 3 1
A setStartNewId() 0 3 1
A expandWhiteSpace() 0 3 1
A setStartDeletedId() 0 3 1
C handlePossibleChangedPart() 0 79 14
A getIterator() 0 3 1
A getDeletedId() 0 3 1
A setStartChangedId() 0 3 1
A getTextNode() 0 7 2
A getChangedId() 0 3 1
A __construct() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like TextNodeComparator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TextNodeComparator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * (c) Steve Nebes <[email protected]>
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
declare(strict_types=1);
10
11
namespace SN\DaisyDiff\Html;
12
13
use ArrayIterator;
14
use SN\DaisyDiff\Html\Ancestor\AncestorComparator;
15
use SN\DaisyDiff\Html\Ancestor\AncestorComparatorResult;
16
use SN\DaisyDiff\Html\Dom\BodyNode;
17
use SN\DaisyDiff\Html\Dom\DomTreeBuilder;
18
use SN\DaisyDiff\Html\Dom\Helper\LastCommonParentResult;
19
use SN\DaisyDiff\Html\Dom\TagNode;
20
use SN\DaisyDiff\Html\Dom\TextNode;
21
use SN\DaisyDiff\Html\Modification\Modification;
22
use SN\DaisyDiff\Html\Modification\ModificationType;
23
use SN\DaisyDiff\RangeDifferencer\RangeComparatorInterface;
24
use IteratorAggregate;
25
26
/**
27
 * A comparator that generates a DOM tree of sorts from handling SAX events. Then it can be used to compute the
28
 * differences between DOM trees and mark elements accordingly.
29
 */
30
class TextNodeComparator implements RangeComparatorInterface, IteratorAggregate
31
{
32
    /** @var TextNode[] */
33
    public $textNodes = [];
34
35
    /** @var Modification[] */
36
    private $lastModified = [];
37
38
    /** @var BodyNode */
39
    private $bodyNode;
40
41
    /** @var int */
42
    private $newId = 0;
43
44
    /** @var int */
45
    private $changedId = 0;
46
47
    /** @var int */
48
    private $deletedId = 0;
49
50
    /** @var bool */
51
    private $changedIdUsed = false;
52
53
    /** @var bool */
54
    private $whiteAfterLastChangedPart = false;
55
56
    /**
57
     * Default values.
58
     *
59
     * @param DomTreeBuilder $domTreeBuilder
60
     */
61 33
    public function __construct(DomTreeBuilder $domTreeBuilder)
62
    {
63 33
        $this->textNodes = $domTreeBuilder->getTextNodes();
64 33
        $this->bodyNode = $domTreeBuilder->getBodyNode();
65 33
    }
66
67
    /**
68
     * @return BodyNode
69
     */
70 20
    public function getBodyNode(): BodyNode
71
    {
72 20
        return $this->bodyNode;
73
    }
74
75
    /**
76
     * @return int
77
     */
78 19
    public function getRangeCount(): int
79
    {
80 19
        return \count($this->textNodes);
81
    }
82
83
    /**
84
     * @param int $index
85
     * @return TextNode
86
     *
87
     * @throws \OutOfBoundsException
88
     */
89 25
    public function getTextNode(int $index): TextNode
90
    {
91 25
        if (isset($this->textNodes[$index])) {
92 24
            return $this->textNodes[$index];
93
        }
94
95 1
        throw new \OutOfBoundsException();
96
    }
97
98
    /**
99
     * Marks the given range as new. In the output, the range will be formatted as specified by the anOutputFormat
100
     * parameter.
101
     *
102
     * @param int    $start
103
     * @param int    $end
104
     * @param string $outputFormat
105
     */
106 16
    public function markAsNew(int $start, int $end, string $outputFormat = ModificationType::ADDED): void
107
    {
108 16
        if ($end <= $start) {
109 4
            return;
110
        }
111
112 12
        if ($this->whiteAfterLastChangedPart) {
113 3
            $this->getTextNode($start)->setWhiteBefore(false);
114
        }
115
116
        /** @var Modification[] */
117 12
        $nextLastModified = [];
118
119 12
        for ($i = $start; $i < $end; $i++) {
120 12
            $mod = new Modification(ModificationType::ADDED, $outputFormat);
121 12
            $mod->setId($this->newId);
122
123 12
            if (\count($this->lastModified) > 0) {
124 6
                $mod->setPrevious($this->lastModified[0]);
125
126 6
                if (null === $this->lastModified[0]->getNext()) {
127 6
                    foreach ($this->lastModified as $lastMod) {
128 6
                        $lastMod->setNext($mod);
129
                    }
130
                }
131
            }
132
133 12
            $nextLastModified[] = $mod;
134 12
            $this->getTextNode($i)->setModification($mod);
135
        }
136
137 12
        $this->getTextNode($start)->getModification()->setFirstOfId(true);
138 12
        $this->newId++;
139 12
        $this->lastModified = $nextLastModified;
140 12
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 12
    public function rangesEqual(int $thisIndex, RangeComparatorInterface $other, int $otherIndex): bool
146
    {
147 12
        if ($other instanceof TextNodeComparator) {
148 12
            return $this->getTextNode($thisIndex)->isSameText($other->getTextNode($otherIndex));
149
        }
150
151
        return false; // @codeCoverageIgnore
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 12
    public function skipRangeComparison(int $length, int $maxLength, RangeComparatorInterface $other): bool
158
    {
159 12
        return false;
160
    }
161
162
    /**
163
     * @param int                $leftStart
164
     * @param int                $leftEnd
165
     * @param int                $rightStart
166
     * @param int                $rightEnd
167
     * @param TextNodeComparator $leftComparator
168
     */
169 10
    public function handlePossibleChangedPart(
170
        int $leftStart,
171
        int $leftEnd,
172
        int $rightStart,
173
        int $rightEnd,
174
        TextNodeComparator $leftComparator
175
    ): void {
176
        // $leftEnd is not used below.
177 10
        \assert(\is_int($leftEnd));
178
179 10
        $i = $rightStart;
180 10
        $j = $leftStart;
181
182 10
        if ($this->changedIdUsed) {
183
            $this->changedId++;
184
            $this->changedIdUsed = false;
185
        }
186
187
        /** @var Modification[] */
188 10
        $nextLastModified = [];
189 10
        $changes = '';
190
191 10
        while ($i < $rightEnd) {
192 10
            $acThis = new AncestorComparator($this->getTextNode($i)->getParentTree());
193 10
            $acOther = new AncestorComparator($leftComparator->getTextNode($j)->getParentTree());
194
195
            /** @var AncestorComparatorResult */
196 10
            $result = $acThis->getResult($acOther);
197
198 10
            if ($result->isChanged()) {
199 4
                $mod = new Modification(ModificationType::CHANGED, ModificationType::CHANGED);
200
201 4
                if (!$this->changedIdUsed) {
202 4
                    $mod->setFirstOfId(true);
203
204 4
                    if (\count($nextLastModified) > 0) {
205
                        $this->lastModified = $nextLastModified;
206 4
                        $nextLastModified = [];
207
                    }
208 1
                } elseif (!empty($result->getChanges()) && $changes !== $result->getChanges()) {
209
                    $this->changedId++;
210
                    $mod->setFirstOfId(true);
211
212
                    if (\count($nextLastModified) > 0) {
213
                        $this->lastModified = $nextLastModified;
214
                        $nextLastModified = [];
215
                    }
216
                }
217
218 4
                if (\count($this->lastModified) > 0) {
219 1
                    $mod->setPrevious($this->lastModified[0]);
220
221 1
                    if (null === $this->lastModified[0]->getNext()) {
222 1
                        foreach ($this->lastModified as $lastMod) {
223 1
                            $lastMod->setNext($mod);
224
                        }
225
                    }
226
                }
227
228 4
                $nextLastModified[] = $mod;
229
230 4
                $mod->setChanges($result->getChanges());
231 4
                $mod->setHtmlLayoutChanges($result->getHtmlLayoutChanges());
232 4
                $mod->setId($this->changedId);
233
234 4
                $this->getTextNode($i)->setModification($mod);
235 4
                $changes = $result->getChanges();
236 4
                $this->changedIdUsed = true;
237 7
            } elseif ($this->changedIdUsed) {
238 1
                $this->changedId++;
239 1
                $this->changedIdUsed = false;
240
            }
241
242 10
            $i++;
243 10
            $j++;
244
        }
245
246 10
        if (\count($nextLastModified) > 0) {
247 4
            $this->lastModified = $nextLastModified;
248
        }
249 10
    }
250
251
    /**
252
     * Marks the given range as deleted. In the output, the range will be formatted as specified by the parameter
253
     * anOutputFormat.
254
     *
255
     * @param int                $start
256
     * @param int                $end
257
     * @param TextNodeComparator $oldComp
258
     * @param int                $before
259
     * @param string             $outputFormat
260
     */
261 11
    public function markAsDeleted(
262
        int $start,
263
        int $end,
264
        TextNodeComparator $oldComp,
265
        int $before,
266
        string $outputFormat = ModificationType::REMOVED
267
    ): void
268
    {
269 11
        if ($end <= $start) {
270 1
            return;
271
        }
272
273 10
        if ($before > 0 && $this->getTextNode($before - 1)->isWhiteAfter()) {
274 4
            $this->whiteAfterLastChangedPart = true;
275
        } else {
276 6
            $this->whiteAfterLastChangedPart = false;
277
        }
278
279
        /** @var Modification[] */
280 10
        $nextLastModified = [];
281
282 10
        for ($i = $start; $i < $end; $i++) {
283 10
            $mod = new Modification(ModificationType::REMOVED, $outputFormat);
284 10
            $mod->setId($this->deletedId);
285
286 10
            if (\count($this->lastModified) > 0) {
287 1
                $mod->setPrevious($this->lastModified[0]);
288
289 1
                if (null === $this->lastModified[0]->getNext()) {
290 1
                    foreach ($this->lastModified as $lastMod) {
291 1
                        $lastMod->setNext($mod);
292
                    }
293
                }
294
            }
295
296 10
            $nextLastModified[] = $mod;
297
298
            // $oldComp is used here because we're going to move its deleted elements to this tree.
299 10
            $oldComp->getTextNode($i)->setModification($mod);
300
        }
301
302 10
        $oldComp->getTextNode($start)->getModification()->setFirstOfId(true);
303
304
        /** @var TagNode[] $deletedNodes */
305 10
        $deletedNodes = $oldComp->getBodyNode()->getMinimalDeletedSet($this->deletedId);
306
307
        // Set $prevLeaf to the leaf after which the old HTML needs to be inserted.
308 10
        $prevLeaf = null;
309
310 10
        if ($before > 0) {
311 6
            $prevLeaf = $this->getTextNode($before - 1);
312
        }
313
314
        // Set $nextLeaf to the leaf before which the old HTML needs to be inserted.
315 10
        $nextLeaf = null;
316
317 10
        if ($before < $this->getRangeCount()) {
318 8
            $nextLeaf = $this->getTextNode($before);
319
        }
320
321 10
        while (\count($deletedNodes) > 0) {
322 8
            $prevResult = null;
323 8
            $nextResult = null;
324
325 8
            if (null !== $prevLeaf) {
326 4
                $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]);
327
            } else {
328 4
                $prevResult = new LastCommonParentResult();
329 4
                $prevResult->setLastCommonParent($this->getBodyNode());
330 4
                $prevResult->setIndexInLastCommonParent(-1);
331
            }
332
333 8
            if (null !== $nextLeaf) {
334 6
                $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[\count($deletedNodes) - 1]);
335
            } else {
336 2
                $nextResult = new LastCommonParentResult();
337 2
                $nextResult->setLastCommonParent($this->getBodyNode());
338 2
                $nextResult->setIndexInLastCommonParent($this->getBodyNode()->getNumChildren());
339
            }
340
341 8
            if ($prevResult->getLastCommonParentDepth() === $nextResult->getLastCommonParentDepth()) {
342
                // We need some metric to choose which way to add...
343
                if (
344 6
                    $deletedNodes[0]->getParent() === $deletedNodes[\count($deletedNodes) - 1]->getParent() &&
345 6
                    $prevResult->getLastCommonParent() === $nextResult->getLastCommonParent()
346
                ) {
347
                    // The difference is not in the parent.
348 6
                    $prevResult->setLastCommonParentDepth($prevResult->getLastCommonParentDepth() + 1);
349
                } else {
350
                    // The difference is in the parent, so compare them. now THIS is tricky.
351
                    $distancePrev = $deletedNodes[0]
352
                        ->getParent()
353
                        ->getMatchRatio($prevResult->getLastCommonParent());
354
                    $distanceNext = $deletedNodes[\count($deletedNodes) - 1]
355
                        ->getParent()
356
                        ->getMatchRatio($nextResult->getLastCommonParent());
357
358
                    if ($distancePrev <= $distanceNext) {
359
                        // Insert after the previous node.
360
                        $prevResult->setLastCommonParentDepth($prevResult->getLastCommonParentDepth() + 1);
361
                    } else {
362
                        // Insert before the next node.
363
                        $nextResult->setLastCommonParentDepth($nextResult->getLastCommonParentDepth() + 1);
364
                    }
365
                }
366
            }
367
368 8
            if ($prevResult->getLastCommonParentDepth() > $nextResult->getLastCommonParentDepth()) {
369
                // Inserting at the front.
370 6
                if ($prevResult->isSplittingNeeded()) {
371 1
                    $prevLeaf->getParent()->splitUntil($prevResult->getLastCommonParent(), $prevLeaf, true);
0 ignored issues
show
Bug introduced by
It seems like $prevLeaf can also be of type null; however, parameter $split of SN\DaisyDiff\Html\Dom\TagNode::splitUntil() does only seem to accept SN\DaisyDiff\Html\Dom\Node, maybe add an additional type check? ( Ignorable by Annotation )

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

371
                    $prevLeaf->getParent()->splitUntil($prevResult->getLastCommonParent(), /** @scrutinizer ignore-type */ $prevLeaf, true);
Loading history...
372
                }
373
374
                // array_shift removes first array element, and returns it.
375 6
                $node = \array_shift($deletedNodes);
376 6
                $prevLeaf = $node->copyTree();
377 6
                $prevLeaf->setParent($prevResult->getLastCommonParent());
378 6
                $prevResult->getLastCommonParent()->addChild($prevLeaf, $prevResult->getIndexInLastCommonParent() + 1);
379 2
            } elseif ($prevResult->getLastCommonParentDepth() < $nextResult->getLastCommonParentDepth()) {
380
                // Inserting at the back.
381 2
                if ($nextResult->isSplittingNeeded()) {
382
                    $splitOccurred = $nextLeaf
383 2
                        ->getParent()
384 2
                        ->splitUntil($nextResult->getLastCommonParent(), $nextLeaf, false);
385
386 2
                    if ($splitOccurred) {
387
                        // The place where to insert is shifted one place to the right.
388
                        $nextResult->setIndexInLastCommonParent($nextResult->getIndexInLastCommonParent() + 1);
389
                    }
390
                }
391
392
                // array_pop removes last array element, and returns it.
393 2
                $node = \array_pop($deletedNodes);
394 2
                $nextLeaf = $node->copyTree();
395 2
                $nextLeaf->setParent($nextResult->getLastCommonParent());
396 2
                $nextResult->getLastCommonParent()->addChild($nextLeaf, $nextResult->getIndexInLastCommonParent());
397
            } else {
398
                throw new \RuntimeException();
399
            }
400
        }
401
402 10
        $this->lastModified = $nextLastModified;
403 10
        $this->deletedId++;
404 10
    }
405
406
    /**
407
     * @return void
408
     */
409 16
    public function expandWhiteSpace(): void
410
    {
411 16
        $this->getBodyNode()->expandWhiteSpace();
412 16
    }
413
414
    /**
415
     * @return ArrayIterator
416
     */
417 1
    public function getIterator(): ArrayIterator
418
    {
419 1
        return new ArrayIterator($this->textNodes);
420
    }
421
422
    /**
423
     * @codeCoverageIgnore
424
     * @deprecated Not used, and will not be used in the future.
425
     *
426
     * Used for combining multiple comparators in order to create a single output document. The IDs must be successive
427
     * along the different comparators.
428
     *
429
     * @param int $value
430
     */
431
    public function setStartDeletedId(int $value): void
432
    {
433
        $this->deletedId = $value;
434
    }
435
436
    /**
437
     * @codeCoverageIgnore
438
     * @deprecated Not used, and will not be used in the future.
439
     *
440
     * Used for combining multiple comparators in order to create a single output document. The IDs must be successive
441
     * along the different comparators.
442
     *
443
     * @param int $value
444
     */
445
    public function setStartChangedId(int $value): void
446
    {
447
        $this->changedId = $value;
448
    }
449
450
    /**
451
     * @codeCoverageIgnore
452
     * @deprecated Not used, and will not be used in the future.
453
     *
454
     * Used for combining multiple comparators in order to create a single output document. The IDs must be successive
455
     * along the different comparators.
456
     *
457
     * @param int $value
458
     */
459
    public function setStartNewId(int $value): void
460
    {
461
        $this->newId = $value;
462
    }
463
464
    /**
465
     * @return int
466
     */
467 1
    public function getChangedId(): int
468
    {
469 1
        return $this->changedId;
470
    }
471
472
    /**
473
     * @return int
474
     */
475 1
    public function getDeletedId(): int
476
    {
477 1
        return $this->deletedId;
478
    }
479
480
    /**
481
     * @return int
482
     */
483 1
    public function getNewId(): int
484
    {
485 1
        return $this->newId;
486
    }
487
488
    /**
489
     * @return Modification[]
490
     */
491 10
    public function getLastModified(): array
492
    {
493 10
        return $this->lastModified;
494
    }
495
496
    /**
497
     * @param Modification[] $lastModified
498
     */
499 3
    public function setLastModified(array $lastModified): void
500
    {
501 3
        $this->lastModified = $lastModified;
502 3
    }
503
}
504