Passed
Push — master ( d7540c...231250 )
by Josh
05:57
created

TableDiff::diffAndAppendRows()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2.004

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 4
dl 0
loc 15
ccs 9
cts 10
cp 0.9
crap 2.004
rs 9.4285
c 0
b 0
f 0
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 View Code Duplication
    public static function create($oldText, $newText, HtmlDiffConfig $config = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
    public function build()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $this->newTable can be null; however, indexCellValues() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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);
119 1
        $this->diffDom->appendChild($this->diffTable);
120
121 1
        $oldRows = $this->oldTable->getRows();
122 1
        $newRows = $this->newTable->getRows();
123
124 1
        $oldMatchData = array();
125 1
        $newMatchData = array();
126
127
        /* @var $oldRow TableRow */
128 1
        foreach ($oldRows as $oldIndex => $oldRow) {
129 1
            $oldMatchData[$oldIndex] = array();
130
131
            // Get match percentages
132
            /* @var $newRow TableRow */
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 View Code Duplication
    protected function processInsertOperation(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
    protected function processDeleteOperation(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 */
432 1
        $diffRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
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[] */
442 1
        $expandCells = array();
443
        /* @var $cellsWithMultipleRows \DOMElement[] */
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 */
465 1
            $newCell = $newCells[$position->getIndexInNew()];
466
            /* @var $oldCell TableCell */
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 */
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);
0 ignored issues
show
Bug introduced by
It seems like $newRow defined by parameter $newRow on line 427 can be null; however, Caxy\HtmlDiff\Table\Tabl...f::syncVirtualColumns() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
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);
0 ignored issues
show
Bug introduced by
It seems like $oldRow defined by parameter $oldRow on line 427 can be null; however, Caxy\HtmlDiff\Table\Tabl...f::syncVirtualColumns() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
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[] */
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
It seems like $oldCell is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
559
        } else {
560 1
            $oldNode = $oldCell->getDomNode();
561 1
            $newNode = $newCell->getDomNode();
562
563
            /* @var $clone \DOMElement */
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))),
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
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);
0 ignored issues
show
Documentation introduced by
$tableNode is of type object<DOMNode>, but the function expects a null|object<DOMElement>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 View Code Duplication
    protected function htmlFromNode($node)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 (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);
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) {
877 1
            $oldCell = $oldRow->getCell($newIndex);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $oldCell is correct as $oldRow->getCell($newIndex) (which targets Caxy\HtmlDiff\Table\TableRow::getCell()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
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