Completed
Pull Request — master (#10)
by Steve
02:41
created

TextNodeComparator::markAsNew()   B

Complexity

Conditions 7
Paths 9

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 18
nc 9
nop 3
dl 0
loc 34
ccs 19
cts 19
cp 1
crap 7
rs 8.8333
c 0
b 0
f 0
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 DaisyDiff\Html;
12
13
use ArrayIterator;
14
use DaisyDiff\Html\Ancestor\AncestorComparator;
15
use DaisyDiff\Html\Ancestor\AncestorComparatorResult;
16
use DaisyDiff\Html\Dom\BodyNode;
17
use DaisyDiff\Html\Dom\DomTreeBuilder;
18
use DaisyDiff\Html\Dom\Helper\LastCommonParentResult;
19
use DaisyDiff\Html\Dom\TagNode;
20
use DaisyDiff\Html\Dom\TextNode;
21
use DaisyDiff\Html\Modification\Modification;
22
use DaisyDiff\Html\Modification\ModificationType;
23
use 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
    private $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 29
    public function __construct(DomTreeBuilder $domTreeBuilder)
62
    {
63 29
        $this->textNodes = $domTreeBuilder->getTextNodes();
64 29
        $this->bodyNode = $domTreeBuilder->getBodyNode();
65 29
    }
66
67
    /**
68
     * @return BodyNode
69
     */
70 16
    public function getBodyNode(): BodyNode
71
    {
72 16
        return $this->bodyNode;
73
    }
74
75
    /**
76
     * @return int
77
     */
78 15
    public function getRangeCount(): int
79
    {
80 15
        return \count($this->textNodes);
81
    }
82
83
    /**
84
     * @param int $index
85
     * @return TextNode
86
     *
87
     * @throws \OutOfBoundsException
88
     */
89 21
    public function getTextNode(int $index): TextNode
90
    {
91 21
        if (isset($this->textNodes[$index])) {
92 21
            return $this->textNodes[$index];
93
        }
94
95 2
        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 11
    public function markAsNew(int $start, int $end, string $outputFormat = ModificationType::ADDED): void
107
    {
108 11
        if ($end <= $start) {
109 2
            return;
110
        }
111
112 9
        if ($this->whiteAfterLastChangedPart) {
113 2
            $this->getTextNode($start)->setWhiteBefore(false);
114
        }
115
116
        /** @var Modification[] */
117 9
        $nextLastModified = [];
118
119 9
        for ($i = $start; $i < $end; $i++) {
120 9
            $mod = new Modification(ModificationType::ADDED, $outputFormat);
121 9
            $mod->setId($this->newId);
122
123 9
            if (\count($this->lastModified) > 0) {
124 3
                $mod->setPrevious($this->lastModified[0]);
125
126 3
                if (null === $this->lastModified[0]->getNext()) {
127 3
                    foreach ($this->lastModified as $lastMod) {
128 3
                        $lastMod->setNext($mod);
129
                    }
130
                }
131
            }
132
133 9
            $nextLastModified[] = $mod;
134 9
            $this->getTextNode($i)->setModification($mod);
135
        }
136
137 9
        $this->getTextNode($start)->getModification()->setFirstOfId(true);
138 9
        $this->newId++;
139 9
        $this->lastModified = $nextLastModified;
140 9
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 13
    public function rangesEqual(int $thisIndex, RangeComparatorInterface $other, int $otherIndex): bool
146
    {
147 13
        if ($other instanceof TextNodeComparator) {
148 13
            return $this->getTextNode($thisIndex)->isSameText($other->getTextNode($otherIndex));
149
        }
150
151
        return false; // @codeCoverageIgnore
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 8
    public function skipRangeComparison(int $length, int $maxLength, RangeComparatorInterface $other): bool
158
    {
159 8
        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 13
    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 13
        \assert(\is_int($leftEnd));
178
179 13
        $i = $rightStart;
180 13
        $j = $leftStart;
181
182 13
        if ($this->changedIdUsed) {
183
            $this->changedId++;
184
            $this->changedIdUsed = false;
185
        }
186
187
        /** @var Modification[] */
188 13
        $nextLastModified = [];
189 13
        $changes = '';
190
191 13
        while ($i < $rightEnd) {
192 13
            $acThis = new AncestorComparator($this->getTextNode($i)->getParentTree());
193 13
            $acOther = new AncestorComparator($leftComparator->getTextNode($j)->getParentTree());
194
195
            /** @var AncestorComparatorResult */
196 13
            $result = $acThis->getResult($acOther);
197
198 13
            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 2
                } 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 9
            } elseif ($this->changedIdUsed) {
238
                $this->changedId++;
239
                $this->changedIdUsed = false;
240
            }
241
242 13
            $i++;
243 13
            $j++;
244
        }
245
246 13
        if (\count($nextLastModified) > 0) {
247 4
            $this->lastModified = $nextLastModified;
248
        }
249 13
    }
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 int                $after
260
     * @param string             $outputFormat
261
     */
262 6
    public function markAsDeleted(
263
        int $start,
264
        int $end,
265
        TextNodeComparator $oldComp,
266
        int $before,
267
        int $after = 0,
268
        string $outputFormat = ModificationType::REMOVED
269
    ): void
270
    {
271 6
        if ($end <= $start) {
272 1
            return;
273
        }
274
275 5
        if ($before > 0 && $this->getTextNode($before - 1)->isWhiteAfter()) {
276 3
            $this->whiteAfterLastChangedPart = true;
277
        } else {
278 2
            $this->whiteAfterLastChangedPart = false;
279
        }
280
281
        /** @var Modification[] */
282 5
        $nextLastModified = [];
283
284 5
        for ($i = $start; $i < $end; $i++) {
285 5
            $mod = new Modification(ModificationType::REMOVED, $outputFormat);
286 5
            $mod->setId($this->deletedId);
287
288 5
            if (\count($this->lastModified) > 0) {
289 1
                $mod->setPrevious($this->lastModified[0]);
290
291 1
                if (null === $this->lastModified[0]->getNext()) {
292 1
                    foreach ($this->lastModified as $lastMod) {
293 1
                        $lastMod->setNext($mod);
294
                    }
295
                }
296
            }
297
298 5
            $nextLastModified[] = $mod;
299
300
            // $oldComp is used here because we're going to move its deleted elements to this tree.
301 5
            $oldComp->getTextNode($i)->setModification($mod);
302
        }
303
304 5
        $oldComp->getTextNode($start)->getModification()->setFirstOfId(true);
305
306
        /** @var TagNode[] $deletedNodes */
307 5
        $deletedNodes = $oldComp->getBodyNode()->getMinimalDeletedSet($this->deletedId);
308
309
        // Set $prevLeaf to the leaf after which the old HTML needs to be inserted.
310 5
        $prevLeaf = null;
311
312 5
        if ($before > 0) {
313 5
            $prevLeaf = $this->getTextNode($before - 1);
314
        }
315
316
        // Set $nextLeaf to the leaf before which the old HTML needs to be inserted.
317 5
        $nextLeaf = null;
318 5
        $useAfter = false;
319
320 5
        if ($after < $this->getRangeCount()) {
321 5
            $orderResult = $this->getTextNode($before)->getLastCommonParent($this->getTextNode($after));
322 5
            $check = $this->getTextNode($before)->getParentTree();
323 5
            $check = \array_reverse($check);
324
325 5
            foreach ($check as $curr) {
326 5
                if ($curr === $orderResult->getLastCommonParent()) {
327 5
                    break;
328
                } elseif ($curr->isBlockLevel()) {
329
                    $useAfter = true;
330
                    break;
331
                }
332
            }
333
334 5
            if (!$useAfter) {
335 5
                $check = $this->getTextNode($after)->getParentTree();
336 5
                $check = \array_reverse($check);
337
338 5
                foreach ($check as $curr) {
339 5
                    if ($curr === $orderResult->getLastCommonParent()) {
340 3
                        break;
341 2
                    } elseif ($curr->isBlockLevel()) {
342 2
                        $useAfter = true;
343 5
                        break;
344
                    }
345
                }
346
            }
347
        } else {
348
            $useAfter = false;
349
        }
350
351 5
        if ($useAfter) {
352 2
            $nextLeaf = $this->getTextNode($after);
353 3
        } elseif ($before < $this->getRangeCount()) {
354 3
            $nextLeaf = $this->getTextNode($before);
355
        }
356
357 5
        while (\count($deletedNodes) > 0) {
358 3
            $prevResult = null;
359 3
            $nextResult = null;
360
361 3
            if (null !== $prevLeaf) {
362 3
                $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]);
363
            } else {
364
                $prevResult = new LastCommonParentResult();
365
                $prevResult->setLastCommonParent($this->getBodyNode());
366
                $prevResult->setIndexInLastCommonParent(-1);
367
            }
368
369 3
            if (null !== $nextLeaf) {
370 3
                $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[\count($deletedNodes) - 1]);
371
            } else {
372
                $nextResult = new LastCommonParentResult();
373
                $nextResult->setLastCommonParent($this->getBodyNode());
374
                $nextResult->setIndexInLastCommonParent($this->getBodyNode()->getNumChildren());
375
            }
376
377 3
            if ($prevResult->getLastCommonParentDepth() === $nextResult->getLastCommonParentDepth()) {
378
                // We need some metric to choose which way to add...
379 3
                if ($deletedNodes[0]->getParent() === $deletedNodes[\count($deletedNodes) - 1]->getParent() &&
380 3
                    $prevResult->getLastCommonParent() === $nextResult->getLastCommonParent()) {
381
                    // The difference is not in the parent.
382 3
                    $prevResult->setLastCommonParentDepth($prevResult->getLastCommonParentDepth() + 1);
383
                } else {
384
                    // The difference is in the parent, so compare them. now THIS is tricky.
385
                    $distancePrev = $deletedNodes[0]
386
                        ->getParent()
387
                        ->getMatchRatio($prevResult->getLastCommonParent());
388
                    $distanceNext = $deletedNodes[\count($deletedNodes) - 1]
389
                        ->getParent()
390
                        ->getMatchRatio($nextResult->getLastCommonParent());
391
392
                    if ($distancePrev <= $distanceNext) {
393
                        // Insert after the previous node.
394
                        $prevResult->setLastCommonParentDepth($prevResult->getLastCommonParentDepth() + 1);
395
                    } else {
396
                        // Insert before the next node.
397
                        $nextResult->setLastCommonParentDepth($nextResult->getLastCommonParentDepth() + 1);
398
                    }
399
                }
400
            }
401
402 3
            if ($prevResult->getLastCommonParentDepth() > $nextResult->getLastCommonParentDepth()) {
403
                // Inserting at the front.
404 3
                if ($prevResult->isSplittingNeeded()) {
405
                    $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 DaisyDiff\Html\Dom\TagNode::splitUntil() does only seem to accept 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

405
                    $prevLeaf->getParent()->splitUntil($prevResult->getLastCommonParent(), /** @scrutinizer ignore-type */ $prevLeaf, true);
Loading history...
406
                }
407
408
                // array_shift removes first array element, and returns it.
409 3
                $node = \array_shift($deletedNodes);
410 3
                $prevLeaf = $node->copyTree();
411 3
                $prevLeaf->setParent($prevResult->getLastCommonParent());
412 3
                $prevResult->getLastCommonParent()->addChild($prevLeaf, $prevResult->getIndexInLastCommonParent() + 1);
413
            } elseif ($prevResult->getLastCommonParentDepth() < $nextResult->getLastCommonParentDepth()) {
414
                // Inserting at the back.
415
                if ($nextResult->isSplittingNeeded()) {
416
                    $splitOccurred = $nextLeaf
417
                        ->getParent()
418
                        ->splitUntil($nextResult->getLastCommonParent(), $nextLeaf, false);
419
420
                    if ($splitOccurred) {
421
                        // The place where to insert is shifted one place to the right.
422
                        $nextResult->setIndexInLastCommonParent($nextResult->getIndexInLastCommonParent() + 1);
423
                    }
424
                }
425
426
                // array_pop removes last array element, and returns it.
427
                $node = \array_pop($deletedNodes);
428
                $nextLeaf = $node->copyTree();
429
                $nextLeaf->setParent($nextResult->getLastCommonParent());
430
                $nextResult->getLastCommonParent()->addChild($nextLeaf, $nextResult->getIndexInLastCommonParent());
431
            } else {
432
                throw new \RuntimeException();
433
            }
434
        }
435
436 5
        $this->lastModified = $nextLastModified;
437 5
        $this->deletedId++;
438 5
    }
439
440
    /**
441
     * @return void
442
     */
443 12
    public function expandWhiteSpace(): void
444
    {
445 12
        $this->getBodyNode()->expandWhiteSpace();
446 12
    }
447
448
    /**
449
     * @return ArrayIterator
450
     */
451 1
    public function getIterator(): ArrayIterator
452
    {
453 1
        return new ArrayIterator($this->textNodes);
454
    }
455
456
    /**
457
     * @deprecated Not used, and will not be used in the future.
458
     *
459
     * Used for combining multiple comparators in order to create a single output document. The IDs must be successive
460
     * along the different comparators.
461
     *
462
     * @param int $value
463
     */
464 1
    public function setStartDeletedId(int $value): void
465
    {
466 1
        $this->deletedId = $value;
467 1
    }
468
469
    /**
470
     * @deprecated Not used, and will not be used in the future.
471
     *
472
     * Used for combining multiple comparators in order to create a single output document. The IDs must be successive
473
     * along the different comparators.
474
     *
475
     * @param int $value
476
     */
477 1
    public function setStartChangedId(int $value): void
478
    {
479 1
        $this->changedId = $value;
480 1
    }
481
482
    /**
483
     * @deprecated Not used, and will not be used in the future.
484
     *
485
     * Used for combining multiple comparators in order to create a single output document. The IDs must be successive
486
     * along the different comparators.
487
     *
488
     * @param int $value
489
     */
490 1
    public function setStartNewId(int $value): void
491
    {
492 1
        $this->newId = $value;
493 1
    }
494
495
    /**
496
     * @return int
497
     */
498 1
    public function getChangedId(): int
499
    {
500 1
        return $this->changedId;
501
    }
502
503
    /**
504
     * @return int
505
     */
506 1
    public function getDeletedId(): int
507
    {
508 1
        return $this->deletedId;
509
    }
510
511
    /**
512
     * @return int
513
     */
514 1
    public function getNewId(): int
515
    {
516 1
        return $this->newId;
517
    }
518
519
    /**
520
     * @return Modification[]
521
     */
522 9
    public function getLastModified(): array
523
    {
524 9
        return $this->lastModified;
525
    }
526
527
    /**
528
     * @param Modification[] $lastModified
529
     */
530 3
    public function setLastModified(array $lastModified): void
531
    {
532 3
        $this->lastModified = $lastModified;
533 3
    }
534
}
535