Passed
Push — master ( 48c70a...6b0fb0 )
by Josh
02:34
created

TableDiff   F

Complexity

Total Complexity 128

Size/Duplication

Total Lines 881
Duplicated Lines 0 %

Test Coverage

Coverage 69.05%

Importance

Changes 0
Metric Value
eloc 362
dl 0
loc 881
ccs 270
cts 391
cp 0.6905
rs 2
c 0
b 0
f 0
wmc 128

28 Methods

Rating   Name   Duplication   Size   Complexity  
A create() 0 9 2
A diffAndAppendRows() 0 13 2
A getInnerHtml() 0 10 2
B findRowMatches() 0 29 6
A processReplaceOperation() 0 4 1
A processDeleteOperation() 0 9 2
F diffRows() 0 117 26
C diffTableRowsWithMatches() 0 69 14
A buildTableDoms() 0 4 1
A processInsertOperation() 0 9 2
A createDocumentWithHtml() 0 10 1
A parseTable() 0 18 5
A indexCellValues() 0 11 4
A diffTableContent() 0 35 4
A processEqualOperation() 0 15 3
A diffCells() 0 29 6
B getNewCellNode() 0 24 8
A htmlFromNode() 0 7 1
A getRowMatches() 0 12 1
A syncVirtualColumns() 0 22 5
B findRowMatch() 0 44 9
A parseTableRow() 0 8 3
A setInnerHtml() 0 15 3
A __construct() 0 8 1
A parseTableStructure() 0 11 1
A diffCellsAndIncrementCounters() 0 26 4
A build() 0 23 4
B getMatchPercentage() 0 25 7

How to fix   Complexity   

Complex Class

Complex classes like TableDiff 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 TableDiff, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Caxy\HtmlDiff\Table;
4
5
use Caxy\HtmlDiff\AbstractDiff;
6
use Caxy\HtmlDiff\HtmlDiff;
7
use Caxy\HtmlDiff\HtmlDiffConfig;
8
use Caxy\HtmlDiff\Operation;
9
10
/**
11
 * Class TableDiff.
12
 */
13
class TableDiff extends AbstractDiff
14
{
15
    /**
16
     * @var null|Table
17
     */
18
    protected $oldTable = null;
19
20
    /**
21
     * @var null|Table
22
     */
23
    protected $newTable = null;
24
25
    /**
26
     * @var null|\DOMElement
27
     */
28
    protected $diffTable = null;
29
30
    /**
31
     * @var null|\DOMDocument
32
     */
33
    protected $diffDom = null;
34
35
    /**
36
     * @var int
37
     */
38
    protected $newRowOffsets = 0;
39
40
    /**
41
     * @var int
42
     */
43
    protected $oldRowOffsets = 0;
44
45
    /**
46
     * @var array
47
     */
48
    protected $cellValues = array();
49
50
    /**
51
     * @param string              $oldText
52
     * @param string              $newText
53
     * @param HtmlDiffConfig|null $config
54
     *
55
     * @return self
56
     */
57 1
    public static function create($oldText, $newText, HtmlDiffConfig $config = null)
58
    {
59 1
        $diff = new self($oldText, $newText);
60
61 1
        if (null !== $config) {
62 1
            $diff->setConfig($config);
63
        }
64
65 1
        return $diff;
66
    }
67
68
    /**
69
     * TableDiff constructor.
70
     *
71
     * @param string     $oldText
72
     * @param string     $newText
73
     * @param string     $encoding
74
     * @param array|null $specialCaseTags
75
     * @param bool|null  $groupDiffs
76
     */
77 1
    public function __construct(
78
        $oldText,
79
        $newText,
80
        $encoding = 'UTF-8',
81
        $specialCaseTags = null,
82
        $groupDiffs = null
83
    ) {
84 1
        parent::__construct($oldText, $newText, $encoding, $specialCaseTags, $groupDiffs);
85 1
    }
86
87
    /**
88
     * @return string
89
     */
90 1
    public function build()
91
    {
92 1
        $this->prepare();
93
94 1
        if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) {
95
            $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText);
96
97
            return $this->content;
98
        }
99
100 1
        $this->buildTableDoms();
101
102 1
        $this->diffDom = new \DOMDocument();
103
104 1
        $this->indexCellValues($this->newTable);
105
106 1
        $this->diffTableContent();
107
108 1
        if ($this->hasDiffCache()) {
109
            $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
110
        }
111
112 1
        return $this->content;
113
    }
114
115 1
    protected function diffTableContent()
116
    {
117 1
        $this->diffDom = new \DOMDocument();
118 1
        $this->diffTable = $this->newTable->cloneNode($this->diffDom);
0 ignored issues
show
Bug introduced by
The method cloneNode() does not exist on null. ( Ignorable by Annotation )

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

118
        /** @scrutinizer ignore-call */ 
119
        $this->diffTable = $this->newTable->cloneNode($this->diffDom);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
119 1
        $this->diffDom->appendChild($this->diffTable);
120
121 1
        $oldRows = $this->oldTable->getRows();
0 ignored issues
show
Bug introduced by
The method getRows() does not exist on null. ( Ignorable by Annotation )

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

121
        /** @scrutinizer ignore-call */ 
122
        $oldRows = $this->oldTable->getRows();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
122 1
        $newRows = $this->newTable->getRows();
123
124 1
        $oldMatchData = array();
125 1
        $newMatchData = array();
126
127
        /* @var $oldRow TableRow */
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
128 1
        foreach ($oldRows as $oldIndex => $oldRow) {
129 1
            $oldMatchData[$oldIndex] = array();
130
131
            // Get match percentages
132
            /* @var $newRow TableRow */
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
133 1
            foreach ($newRows as $newIndex => $newRow) {
134 1
                if (!array_key_exists($newIndex, $newMatchData)) {
135 1
                    $newMatchData[$newIndex] = array();
136
                }
137
138
                // similar_text
139 1
                $percentage = $this->getMatchPercentage($oldRow, $newRow, $oldIndex, $newIndex);
140
141 1
                $oldMatchData[$oldIndex][$newIndex] = $percentage;
142 1
                $newMatchData[$newIndex][$oldIndex] = $percentage;
143
            }
144
        }
145
146 1
        $matches = $this->getRowMatches($oldMatchData, $newMatchData);
147 1
        $this->diffTableRowsWithMatches($oldRows, $newRows, $matches);
148
149 1
        $this->content = $this->htmlFromNode($this->diffTable);
150 1
    }
151
152
    /**
153
     * @param TableRow[] $oldRows
154
     * @param TableRow[] $newRows
155
     * @param RowMatch[] $matches
156
     */
157 1
    protected function diffTableRowsWithMatches($oldRows, $newRows, $matches)
158
    {
159 1
        $operations = array();
160
161 1
        $indexInOld = 0;
162 1
        $indexInNew = 0;
163
164 1
        $oldRowCount = count($oldRows);
165 1
        $newRowCount = count($newRows);
166
167 1
        $matches[] = new RowMatch($newRowCount, $oldRowCount, $newRowCount, $oldRowCount);
168
169
        // build operations
170 1
        foreach ($matches as $match) {
171 1
            $matchAtIndexInOld = $indexInOld === $match->getStartInOld();
172 1
            $matchAtIndexInNew = $indexInNew === $match->getStartInNew();
173
174 1
            $action = 'equal';
175
176 1
            if (!$matchAtIndexInOld && !$matchAtIndexInNew) {
177
                $action = 'replace';
178 1
            } elseif ($matchAtIndexInOld && !$matchAtIndexInNew) {
179
                $action = 'insert';
180 1
            } elseif (!$matchAtIndexInOld && $matchAtIndexInNew) {
181
                $action = 'delete';
182
            }
183
184 1
            if ($action !== 'equal') {
185
                $operations[] = new Operation(
186
                    $action,
187
                    $indexInOld,
188
                    $match->getStartInOld(),
189
                    $indexInNew,
190
                    $match->getStartInNew()
191
                );
192
            }
193
194 1
            $operations[] = new Operation(
195 1
                'equal',
196 1
                $match->getStartInOld(),
197 1
                $match->getEndInOld(),
198 1
                $match->getStartInNew(),
199 1
                $match->getEndInNew()
200
            );
201
202 1
            $indexInOld = $match->getEndInOld();
203 1
            $indexInNew = $match->getEndInNew();
204
        }
205
206 1
        $appliedRowSpans = array();
207
208
        // process operations
209 1
        foreach ($operations as $operation) {
210 1
            switch ($operation->action) {
211 1
                case 'equal':
212 1
                    $this->processEqualOperation($operation, $oldRows, $newRows, $appliedRowSpans);
213 1
                    break;
214
215
                case 'delete':
216
                    $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans);
217
                    break;
218
219
                case 'insert':
220
                    $this->processInsertOperation($operation, $newRows, $appliedRowSpans);
221
                    break;
222
223
                case 'replace':
224
                    $this->processReplaceOperation($operation, $oldRows, $newRows, $appliedRowSpans);
225 1
                    break;
226
            }
227
        }
228 1
    }
229
230
    /**
231
     * @param Operation $operation
232
     * @param array     $newRows
233
     * @param array     $appliedRowSpans
234
     * @param bool      $forceExpansion
235
     */
236
    protected function processInsertOperation(
237
        Operation $operation,
238
        $newRows,
239
        &$appliedRowSpans,
240
        $forceExpansion = false
241
    ) {
242
        $targetRows = array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew);
243
        foreach ($targetRows as $row) {
244
            $this->diffAndAppendRows(null, $row, $appliedRowSpans, $forceExpansion);
245
        }
246
    }
247
248
    /**
249
     * @param Operation $operation
250
     * @param array     $oldRows
251
     * @param array     $appliedRowSpans
252
     * @param bool      $forceExpansion
253
     */
254
    protected function processDeleteOperation(
255
        Operation $operation,
256
        $oldRows,
257
        &$appliedRowSpans,
258
        $forceExpansion = false
259
    ) {
260
        $targetRows = array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld);
261
        foreach ($targetRows as $row) {
262
            $this->diffAndAppendRows($row, null, $appliedRowSpans, $forceExpansion);
263
        }
264
    }
265
266
    /**
267
     * @param Operation $operation
268
     * @param array     $oldRows
269
     * @param array     $newRows
270
     * @param array     $appliedRowSpans
271
     */
272 1
    protected function processEqualOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
273
    {
274 1
        $targetOldRows = array_values(
275 1
            array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld)
276
        );
277 1
        $targetNewRows = array_values(
278 1
            array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew)
279
        );
280
281 1
        foreach ($targetNewRows as $index => $newRow) {
282 1
            if (!isset($targetOldRows[$index])) {
283
                continue;
284
            }
285
286 1
            $this->diffAndAppendRows($targetOldRows[$index], $newRow, $appliedRowSpans);
287
        }
288 1
    }
289
290
    /**
291
     * @param Operation $operation
292
     * @param array     $oldRows
293
     * @param array     $newRows
294
     * @param array     $appliedRowSpans
295
     */
296
    protected function processReplaceOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
297
    {
298
        $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans, true);
299
        $this->processInsertOperation($operation, $newRows, $appliedRowSpans, true);
300
    }
301
302
    /**
303
     * @param array $oldMatchData
304
     * @param array $newMatchData
305
     *
306
     * @return array
307
     */
308 1
    protected function getRowMatches($oldMatchData, $newMatchData)
309
    {
310 1
        $matches = array();
311
312 1
        $startInOld = 0;
313 1
        $startInNew = 0;
314 1
        $endInOld = count($oldMatchData);
315 1
        $endInNew = count($newMatchData);
316
317 1
        $this->findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, $matches);
318
319 1
        return $matches;
320
    }
321
322
    /**
323
     * @param array $newMatchData
324
     * @param int   $startInOld
325
     * @param int   $endInOld
326
     * @param int   $startInNew
327
     * @param int   $endInNew
328
     * @param array $matches
329
     */
330 1
    protected function findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, &$matches)
331
    {
332 1
        $match = $this->findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew);
333 1
        if ($match !== null) {
334 1
            if ($startInOld < $match->getStartInOld() &&
335
                $startInNew < $match->getStartInNew()
336
            ) {
337
                $this->findRowMatches(
338
                    $newMatchData,
339
                    $startInOld,
340
                    $match->getStartInOld(),
341
                    $startInNew,
342
                    $match->getStartInNew(),
343
                    $matches
344
                );
345
            }
346
347 1
            $matches[] = $match;
348
349 1
            if ($match->getEndInOld() < $endInOld &&
350
                $match->getEndInNew() < $endInNew
351
            ) {
352
                $this->findRowMatches(
353
                    $newMatchData,
354
                    $match->getEndInOld(),
355
                    $endInOld,
356
                    $match->getEndInNew(),
357
                    $endInNew,
358
                    $matches
359
                );
360
            }
361
        }
362 1
    }
363
364
    /**
365
     * @param array $newMatchData
366
     * @param int   $startInOld
367
     * @param int   $endInOld
368
     * @param int   $startInNew
369
     * @param int   $endInNew
370
     *
371
     * @return RowMatch|null
372
     */
373 1
    protected function findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew)
374
    {
375 1
        $bestMatch = null;
376 1
        $bestPercentage = 0;
377
378 1
        foreach ($newMatchData as $newIndex => $oldMatches) {
379 1
            if ($newIndex < $startInNew) {
380
                continue;
381
            }
382
383 1
            if ($newIndex >= $endInNew) {
384
                break;
385
            }
386 1
            foreach ($oldMatches as $oldIndex => $percentage) {
387 1
                if ($oldIndex < $startInOld) {
388
                    continue;
389
                }
390
391 1
                if ($oldIndex >= $endInOld) {
392
                    break;
393
                }
394
395 1
                if ($percentage > $bestPercentage) {
396 1
                    $bestPercentage = $percentage;
397
                    $bestMatch = array(
398 1
                        'oldIndex' => $oldIndex,
399 1
                        'newIndex' => $newIndex,
400 1
                        'percentage' => $percentage,
401
                    );
402
                }
403
            }
404
        }
405
406 1
        if ($bestMatch !== null) {
407 1
            return new RowMatch(
408 1
                $bestMatch['newIndex'],
409 1
                $bestMatch['oldIndex'],
410 1
                $bestMatch['newIndex'] + 1,
411 1
                $bestMatch['oldIndex'] + 1,
412 1
                $bestMatch['percentage']
413
            );
414
        }
415
416
        return;
417
    }
418
419
    /**
420
     * @param TableRow|null $oldRow
421
     * @param TableRow|null $newRow
422
     * @param array         $appliedRowSpans
423
     * @param bool          $forceExpansion
424
     *
425
     * @return array
426
     */
427 1
    protected function diffRows($oldRow, $newRow, array &$appliedRowSpans, $forceExpansion = false)
428
    {
429
        // create tr dom element
430 1
        $rowToClone = $newRow ?: $oldRow;
431
        /* @var $diffRow \DOMElement */
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
432 1
        $diffRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
0 ignored issues
show
Bug introduced by
The method importNode() does not exist on null. ( Ignorable by Annotation )

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

432
        /** @scrutinizer ignore-call */ 
433
        $diffRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getDomNode() does not exist on null. ( Ignorable by Annotation )

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

432
        $diffRow = $this->diffDom->importNode($rowToClone->/** @scrutinizer ignore-call */ getDomNode()->cloneNode(false), false);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
433
434 1
        $oldCells = $oldRow ? $oldRow->getCells() : array();
435 1
        $newCells = $newRow ? $newRow->getCells() : array();
436
437 1
        $position = new DiffRowPosition();
438
439 1
        $extraRow = null;
440
441
        /* @var $expandCells \DOMElement[] */
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
442 1
        $expandCells = array();
443
        /* @var $cellsWithMultipleRows \DOMElement[] */
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
444 1
        $cellsWithMultipleRows = array();
445
446 1
        $newCellCount = count($newCells);
447 1
        while ($position->getIndexInNew() < $newCellCount) {
448 1
            if (!$position->areColumnsEqual()) {
449
                $type = $position->getLesserColumnType();
450
                if ($type === 'new') {
451
                    $row = $newRow;
452
                    $targetRow = $extraRow;
453
                } else {
454
                    $row = $oldRow;
455
                    $targetRow = $diffRow;
456
                }
457
                if ($row && $targetRow && (!$type === 'old' || isset($oldCells[$position->getIndexInOld()]))) {
458
                    $this->syncVirtualColumns($row, $position, $cellsWithMultipleRows, $targetRow, $type, true);
459
460
                    continue;
461
                }
462
            }
463
464
            /* @var $newCell TableCell */
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
465 1
            $newCell = $newCells[$position->getIndexInNew()];
466
            /* @var $oldCell TableCell */
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
467 1
            $oldCell = isset($oldCells[$position->getIndexInOld()]) ? $oldCells[$position->getIndexInOld()] : null;
468
469 1
            if ($oldCell && $newCell->getColspan() != $oldCell->getColspan()) {
470
                if (null === $extraRow) {
471
                    /* @var $extraRow \DOMElement */
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
472
                    $extraRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
473
                }
474
475
                if ($oldCell->getColspan() > $newCell->getColspan()) {
476
                    $this->diffCellsAndIncrementCounters(
477
                        $oldCell,
478
                        null,
479
                        $cellsWithMultipleRows,
480
                        $diffRow,
481
                        $position,
482
                        true
483
                    );
484
                    $this->syncVirtualColumns($newRow, $position, $cellsWithMultipleRows, $extraRow, 'new', true);
485
                } else {
486
                    $this->diffCellsAndIncrementCounters(
487
                        null,
488
                        $newCell,
489
                        $cellsWithMultipleRows,
490
                        $extraRow,
491
                        $position,
492
                        true
493
                    );
494
                    $this->syncVirtualColumns($oldRow, $position, $cellsWithMultipleRows, $diffRow, 'old', true);
495
                }
496
            } else {
497 1
                $diffCell = $this->diffCellsAndIncrementCounters(
498 1
                    $oldCell,
499 1
                    $newCell,
500 1
                    $cellsWithMultipleRows,
501 1
                    $diffRow,
502 1
                    $position
503
                );
504 1
                $expandCells[] = $diffCell;
505
            }
506
        }
507
508 1
        $oldCellCount = count($oldCells);
509 1
        while ($position->getIndexInOld() < $oldCellCount) {
510
            $diffCell = $this->diffCellsAndIncrementCounters(
511
                $oldCells[$position->getIndexInOld()],
512
                null,
513
                $cellsWithMultipleRows,
514
                $diffRow,
515
                $position
516
            );
517
            $expandCells[] = $diffCell;
518
        }
519
520 1
        if ($extraRow) {
521
            foreach ($expandCells as $expandCell) {
522
                $rowspan = $expandCell->getAttribute('rowspan') ?: 1;
523
                $expandCell->setAttribute('rowspan', 1 + $rowspan);
524
            }
525
        }
526
527 1
        if ($extraRow || $forceExpansion) {
528
            foreach ($appliedRowSpans as $rowSpanCells) {
529
                /* @var $rowSpanCells \DOMElement[] */
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
530
                foreach ($rowSpanCells as $extendCell) {
531
                    $rowspan = $extendCell->getAttribute('rowspan') ?: 1;
532
                    $extendCell->setAttribute('rowspan', 1 + $rowspan);
533
                }
534
            }
535
        }
536
537 1
        if (!$forceExpansion) {
538 1
            array_shift($appliedRowSpans);
539 1
            $appliedRowSpans = array_values($appliedRowSpans);
540
        }
541 1
        $appliedRowSpans = array_merge($appliedRowSpans, array_values($cellsWithMultipleRows));
542
543 1
        return array($diffRow, $extraRow);
544
    }
545
546
    /**
547
     * @param TableCell|null $oldCell
548
     * @param TableCell|null $newCell
549
     *
550
     * @return \DOMElement
551
     */
552 1
    protected function getNewCellNode(TableCell $oldCell = null, TableCell $newCell = null)
553
    {
554
        // If only one cell exists, use it
555 1
        if (!$oldCell || !$newCell) {
556
            $clone = $newCell
557
                ? $newCell->getDomNode()->cloneNode(false)
558
                : $oldCell->getDomNode()->cloneNode(false);
0 ignored issues
show
Bug introduced by
The method getDomNode() does not exist on null. ( Ignorable by Annotation )

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

558
                : $oldCell->/** @scrutinizer ignore-call */ getDomNode()->cloneNode(false);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
559
        } else {
560 1
            $oldNode = $oldCell->getDomNode();
561 1
            $newNode = $newCell->getDomNode();
562
563
            /* @var $clone \DOMElement */
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
564 1
            $clone = $newNode->cloneNode(false);
565
566 1
            $oldRowspan = $oldNode->getAttribute('rowspan') ?: 1;
567 1
            $oldColspan = $oldNode->getAttribute('colspan') ?: 1;
568 1
            $newRowspan = $newNode->getAttribute('rowspan') ?: 1;
569 1
            $newColspan = $newNode->getAttribute('colspan') ?: 1;
570
571 1
            $clone->setAttribute('rowspan', max($oldRowspan, $newRowspan));
572 1
            $clone->setAttribute('colspan', max($oldColspan, $newColspan));
573
        }
574
575 1
        return $this->diffDom->importNode($clone);
576
    }
577
578
    /**
579
     * @param TableCell|null $oldCell
580
     * @param TableCell|null $newCell
581
     * @param bool           $usingExtraRow
582
     *
583
     * @return \DOMElement
584
     */
585 1
    protected function diffCells($oldCell, $newCell, $usingExtraRow = false)
586
    {
587 1
        $diffCell = $this->getNewCellNode($oldCell, $newCell);
588
589 1
        $oldContent = $oldCell ? $this->getInnerHtml($oldCell->getDomNode()) : '';
590 1
        $newContent = $newCell ? $this->getInnerHtml($newCell->getDomNode()) : '';
591
592 1
        $htmlDiff = HtmlDiff::create(
593 1
            mb_convert_encoding($oldContent, 'UTF-8', 'HTML-ENTITIES'),
594 1
            mb_convert_encoding($newContent, 'UTF-8', 'HTML-ENTITIES'),
595 1
            $this->config
596
        );
597 1
        $diff = $htmlDiff->build();
598
599 1
        $this->setInnerHtml($diffCell, $diff);
600
601 1
        if (null === $newCell) {
602
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' del'));
603
        }
604
605 1
        if (null === $oldCell) {
606
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' ins'));
607
        }
608
609 1
        if ($usingExtraRow) {
610
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' extra-row'));
611
        }
612
613 1
        return $diffCell;
614
    }
615
616 1
    protected function buildTableDoms()
617
    {
618 1
        $this->oldTable = $this->parseTableStructure($this->oldText);
619 1
        $this->newTable = $this->parseTableStructure($this->newText);
620 1
    }
621
622
    /**
623
     * @param string $text
624
     *
625
     * @return \DOMDocument
626
     */
627 1
    protected function createDocumentWithHtml($text)
628
    {
629 1
        $dom = new \DOMDocument();
630 1
        $dom->loadHTML(mb_convert_encoding(
631 1
            $this->purifier->purify(mb_convert_encoding($text, $this->config->getEncoding(), mb_detect_encoding($text))),
632 1
            'HTML-ENTITIES',
633 1
            $this->config->getEncoding()
634
        ));
635
636 1
        return $dom;
637
    }
638
639
    /**
640
     * @param string $text
641
     *
642
     * @return Table
643
     */
644 1
    protected function parseTableStructure($text)
645
    {
646 1
        $dom = $this->createDocumentWithHtml($text);
647
648 1
        $tableNode = $dom->getElementsByTagName('table')->item(0);
649
650 1
        $table = new Table($tableNode);
651
652 1
        $this->parseTable($table);
653
654 1
        return $table;
655
    }
656
657
    /**
658
     * @param Table         $table
659
     * @param \DOMNode|null $node
660
     */
661 1
    protected function parseTable(Table $table, \DOMNode $node = null)
662
    {
663 1
        if ($node === null) {
664 1
            $node = $table->getDomNode();
665
        }
666
667 1
        if (!$node->childNodes) {
668
            return;
669
        }
670
671 1
        foreach ($node->childNodes as $child) {
672 1
            if ($child->nodeName === 'tr') {
673 1
                $row = new TableRow($child);
674 1
                $table->addRow($row);
675
676 1
                $this->parseTableRow($row);
677
            } else {
678 1
                $this->parseTable($table, $child);
679
            }
680
        }
681 1
    }
682
683
    /**
684
     * @param TableRow $row
685
     */
686 1
    protected function parseTableRow(TableRow $row)
687
    {
688 1
        $node = $row->getDomNode();
689
690 1
        foreach ($node->childNodes as $child) {
691 1
            if (in_array($child->nodeName, array('td', 'th'))) {
692 1
                $cell = new TableCell($child);
693 1
                $row->addCell($cell);
694
            }
695
        }
696 1
    }
697
698
    /**
699
     * @param \DOMNode $node
700
     *
701
     * @return string
702
     */
703 1
    protected function getInnerHtml($node)
704
    {
705 1
        $innerHtml = '';
706 1
        $children = $node->childNodes;
707
708 1
        foreach ($children as $child) {
709 1
            $innerHtml .= $this->htmlFromNode($child);
710
        }
711
712 1
        return $innerHtml;
713
    }
714
715
    /**
716
     * @param \DOMNode $node
717
     *
718
     * @return string
719
     */
720 1
    protected function htmlFromNode($node)
721
    {
722 1
        $domDocument = new \DOMDocument();
723 1
        $newNode = $domDocument->importNode($node, true);
724 1
        $domDocument->appendChild($newNode);
725
726 1
        return $domDocument->saveHTML();
727
    }
728
729
    /**
730
     * @param \DOMNode $node
731
     * @param string   $html
732
     */
733 1
    protected function setInnerHtml($node, $html)
734
    {
735
        // DOMDocument::loadHTML does not allow empty strings.
736 1
        if (mb_strlen(trim($html)) === 0) {
737 1
            $html = '<span class="empty"></span>';
738
        }
739
740 1
        $doc = $this->createDocumentWithHtml($html);
741 1
        $fragment = $node->ownerDocument->createDocumentFragment();
742 1
        $root = $doc->getElementsByTagName('body')->item(0);
743 1
        foreach ($root->childNodes as $child) {
744 1
            $fragment->appendChild($node->ownerDocument->importNode($child, true));
745
        }
746
747 1
        $node->appendChild($fragment);
748 1
    }
749
750
    /**
751
     * @param Table $table
752
     */
753 1
    protected function indexCellValues(Table $table)
754
    {
755 1
        foreach ($table->getRows() as $rowIndex => $row) {
756 1
            foreach ($row->getCells() as $cellIndex => $cell) {
757 1
                $value = trim($cell->getDomNode()->textContent);
758
759 1
                if (!isset($this->cellValues[$value])) {
760 1
                    $this->cellValues[$value] = array();
761
                }
762
763 1
                $this->cellValues[$value][] = new TablePosition($rowIndex, $cellIndex);
764
            }
765
        }
766 1
    }
767
768
    /**
769
     * @param TableRow        $tableRow
770
     * @param DiffRowPosition $position
771
     * @param array           $cellsWithMultipleRows
772
     * @param \DOMNode        $diffRow
773
     * @param string          $diffType
774
     * @param bool            $usingExtraRow
775
     */
776
    protected function syncVirtualColumns(
777
        $tableRow,
778
        DiffRowPosition $position,
779
        &$cellsWithMultipleRows,
780
        $diffRow,
781
        $diffType,
782
        $usingExtraRow = false
783
    ) {
784
        $currentCell = $tableRow->getCell($position->getIndex($diffType));
785
        while ($position->isColumnLessThanOther($diffType) && $currentCell) {
786
            $diffCell = $diffType === 'new' ? $this->diffCells(null, $currentCell, $usingExtraRow) : $this->diffCells(
787
                $currentCell,
788
                null,
789
                $usingExtraRow
790
            );
791
            // Store cell in appliedRowSpans if spans multiple rows
792
            if ($diffCell->getAttribute('rowspan') > 1) {
793
                $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
794
            }
795
            $diffRow->appendChild($diffCell);
796
            $position->incrementColumn($diffType, $currentCell->getColspan());
797
            $currentCell = $tableRow->getCell($position->incrementIndex($diffType));
798
        }
799
    }
800
801
    /**
802
     * @param null|TableCell  $oldCell
803
     * @param null|TableCell  $newCell
804
     * @param array           $cellsWithMultipleRows
805
     * @param \DOMElement     $diffRow
806
     * @param DiffRowPosition $position
807
     * @param bool            $usingExtraRow
808
     *
809
     * @return \DOMElement
810
     */
811 1
    protected function diffCellsAndIncrementCounters(
812
        $oldCell,
813
        $newCell,
814
        &$cellsWithMultipleRows,
815
        $diffRow,
816
        DiffRowPosition $position,
817
        $usingExtraRow = false
818
    ) {
819 1
        $diffCell = $this->diffCells($oldCell, $newCell, $usingExtraRow);
820
        // Store cell in appliedRowSpans if spans multiple rows
821 1
        if ($diffCell->getAttribute('rowspan') > 1) {
822
            $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
823
        }
824 1
        $diffRow->appendChild($diffCell);
825
826 1
        if ($newCell !== null) {
827 1
            $position->incrementIndexInNew();
828 1
            $position->incrementColumnInNew($newCell->getColspan());
829
        }
830
831 1
        if ($oldCell !== null) {
832 1
            $position->incrementIndexInOld();
833 1
            $position->incrementColumnInOld($oldCell->getColspan());
834
        }
835
836 1
        return $diffCell;
837
    }
838
839
    /**
840
     * @param TableRow|null $oldRow
841
     * @param TableRow|null $newRow
842
     * @param array         $appliedRowSpans
843
     * @param bool          $forceExpansion
844
     */
845 1
    protected function diffAndAppendRows($oldRow, $newRow, &$appliedRowSpans, $forceExpansion = false)
846
    {
847 1
        list($rowDom, $extraRow) = $this->diffRows(
848 1
            $oldRow,
849 1
            $newRow,
850 1
            $appliedRowSpans,
851 1
            $forceExpansion
852
        );
853
854 1
        $this->diffTable->appendChild($rowDom);
0 ignored issues
show
Bug introduced by
The method appendChild() does not exist on null. ( Ignorable by Annotation )

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

854
        $this->diffTable->/** @scrutinizer ignore-call */ 
855
                          appendChild($rowDom);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
855
856 1
        if ($extraRow) {
857
            $this->diffTable->appendChild($extraRow);
858
        }
859 1
    }
860
861
    /**
862
     * @param TableRow $oldRow
863
     * @param TableRow $newRow
864
     * @param int      $oldIndex
865
     * @param int      $newIndex
866
     *
867
     * @return float|int
868
     */
869 1
    protected function getMatchPercentage(TableRow $oldRow, TableRow $newRow, $oldIndex, $newIndex)
870
    {
871 1
        $firstCellWeight = 1.5;
872 1
        $indexDeltaWeight = 0.25 * (abs($oldIndex - $newIndex));
873 1
        $thresholdCount = 0;
874 1
        $minCells = min(count($newRow->getCells()), count($oldRow->getCells()));
875 1
        $totalCount = ($minCells + $firstCellWeight + $indexDeltaWeight) * 100;
876 1
        foreach ($newRow->getCells() as $newIndex => $newCell) {
0 ignored issues
show
introduced by
$newIndex is overwriting one of the parameters of this function.
Loading history...
877 1
            $oldCell = $oldRow->getCell($newIndex);
878
879 1
            if ($oldCell) {
880 1
                $percentage = null;
881 1
                similar_text($oldCell->getInnerHtml(), $newCell->getInnerHtml(), $percentage);
882
883 1
                if ($percentage > ($this->config->getMatchThreshold() * 0.50)) {
884 1
                    $increment = $percentage;
885 1
                    if ($newIndex === 0 && $percentage > 95) {
886
                        $increment = $increment * $firstCellWeight;
887
                    }
888 1
                    $thresholdCount += $increment;
889
                }
890
            }
891
        }
892
893 1
        return ($totalCount > 0) ? ($thresholdCount / $totalCount) : 0;
894
    }
895
}
896