Passed
Pull Request — master (#31)
by Josh
03:22
created

TableDiff::setInnerHtml()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 17
ccs 0
cts 13
cp 0
rs 9.4285
cc 3
eloc 10
nc 4
nop 2
crap 12
1
<?php
2
3
namespace Caxy\HtmlDiff\Table;
4
5
use Caxy\HtmlDiff\AbstractDiff;
6
use Caxy\HtmlDiff\HtmlDiff;
7
use Caxy\HtmlDiff\Operation;
8
9
/**
10
 * Class TableDiff
11
 * @package Caxy\HtmlDiff\Table
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|Table
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
     * @var \HTMLPurifier
52
     */
53
    protected $purifier;
54
55
    /**
56
     * TableDiff constructor.
57
     *
58
     * @param string     $oldText
59
     * @param string     $newText
60
     * @param string     $encoding
61
     * @param array|null $specialCaseTags
62
     * @param bool|null  $groupDiffs
63
     */
64
    public function __construct($oldText, $newText, $encoding = 'UTF-8', $specialCaseTags = null, $groupDiffs = null)
65
    {
66
        parent::__construct($oldText, $newText, $encoding, $specialCaseTags, $groupDiffs);
67
68
        $this->purifier = new \HTMLPurifier(\HTMLPurifier_Config::createDefault());
69
    }
70
71
    /**
72
     * @return string
73
     */
74
    public function build()
75
    {
76
        $this->buildTableDoms();
77
78
        $this->diffDom = new \DOMDocument();
79
80
        $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...
81
82
        $this->diffTableContent();
83
84
        return $this->content;
85
    }
86
87
    protected function diffTableContent()
88
    {
89
        $this->diffDom = new \DOMDocument();
90
        $this->diffTable = $this->diffDom->importNode($this->newTable->getDomNode()->cloneNode(false), false);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->diffDom->importNo...loneNode(false), false) of type object<DOMNode> is incompatible with the declared type null|object<Caxy\HtmlDiff\Table\Table> of property $diffTable.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
91
        $this->diffDom->appendChild($this->diffTable);
92
93
        $oldRows = $this->oldTable->getRows();
94
        $newRows = $this->newTable->getRows();
95
96
        $oldMatchData = array();
97
        $newMatchData = array();
98
99
        /* @var $oldRow TableRow */
100
        foreach ($oldRows as $oldIndex => $oldRow) {
101
            $oldMatchData[$oldIndex] = array();
102
103
            // Get match percentages
104
            /* @var $newRow TableRow */
105
            foreach ($newRows as $newIndex => $newRow) {
106
                if (!array_key_exists($newIndex, $newMatchData)) {
107
                    $newMatchData[$newIndex] = array();
108
                }
109
110
                // similar_text
111
                $percentage = $this->getMatchPercentage($oldRow, $newRow, $oldIndex, $newIndex);
112
113
                $oldMatchData[$oldIndex][$newIndex] = $percentage;
114
                $newMatchData[$newIndex][$oldIndex] = $percentage;
115
            }
116
        }
117
118
        $matches = $this->getRowMatches($oldMatchData, $newMatchData);
119
        $this->diffTableRowsWithMatches($oldRows, $newRows, $matches);
120
121
        $this->content = $this->htmlFromNode($this->diffTable);
122
    }
123
124
    /**
125
     * @param TableRow[] $oldRows
126
     * @param TableRow[] $newRows
127
     * @param RowMatch[] $matches
128
     */
129
    protected function diffTableRowsWithMatches($oldRows, $newRows, $matches)
130
    {
131
        $operations = array();
132
133
        $indexInOld = 0;
134
        $indexInNew = 0;
135
136
        $oldRowCount = count($oldRows);
137
        $newRowCount = count($newRows);
138
139
        $matches[] = new RowMatch($newRowCount, $oldRowCount, $newRowCount, $oldRowCount);
140
141
        // build operations
142
        foreach ($matches as $match) {
143
            $matchAtIndexInOld = $indexInOld === $match->getStartInOld();
144
            $matchAtIndexInNew = $indexInNew === $match->getStartInNew();
145
146
            $action = 'equal';
147
148
            if (!$matchAtIndexInOld && !$matchAtIndexInNew) {
149
                $action = 'replace';
150
            } elseif ($matchAtIndexInOld && !$matchAtIndexInNew) {
151
                $action = 'insert';
152
            } elseif (!$matchAtIndexInOld && $matchAtIndexInNew) {
153
                $action = 'delete';
154
            }
155
156
            if ($action !== 'equal') {
157
                $operations[] = new Operation(
158
                    $action,
159
                    $indexInOld,
160
                    $match->getStartInOld(),
161
                    $indexInNew,
162
                    $match->getStartInNew()
163
                );
164
            }
165
166
            $operations[] = new Operation(
167
                'equal',
168
                $match->getStartInOld(),
169
                $match->getEndInOld(),
170
                $match->getStartInNew(),
171
                $match->getEndInNew()
172
            );
173
174
            $indexInOld = $match->getEndInOld();
175
            $indexInNew = $match->getEndInNew();
176
        }
177
178
        $appliedRowSpans = array();
179
180
        // process operations
181
        foreach ($operations as $operation) {
182
            switch ($operation->action) {
183
                case 'equal':
184
                    $this->processEqualOperation($operation, $oldRows, $newRows, $appliedRowSpans);
185
                    break;
186
187
                case 'delete':
188
                    $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans);
189
                    break;
190
191
                case 'insert':
192
                    $this->processInsertOperation($operation, $newRows, $appliedRowSpans);
193
                    break;
194
195
                case 'replace':
196
                    $this->processReplaceOperation($operation, $oldRows, $newRows, $appliedRowSpans);
197
                    break;
198
            }
199
        }
200
    }
201
202
    /**
203
     * @param Operation $operation
204
     * @param array     $newRows
205
     * @param array     $appliedRowSpans
206
     * @param bool      $forceExpansion
207
     */
208 View Code Duplication
    protected function processInsertOperation(Operation $operation, $newRows, &$appliedRowSpans, $forceExpansion = false)
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...
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...
209
    {
210
        $targetRows = array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew);
211
        foreach ($targetRows as $row) {
212
            $this->diffAndAppendRows(null, $row, $appliedRowSpans, $forceExpansion);
213
        }
214
    }
215
216
    /**
217
     * @param Operation $operation
218
     * @param array     $oldRows
219
     * @param array     $appliedRowSpans
220
     * @param bool      $forceExpansion
221
     */
222 View Code Duplication
    protected function processDeleteOperation(Operation $operation, $oldRows, &$appliedRowSpans, $forceExpansion = false)
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...
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...
223
    {
224
        $targetRows = array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld);
225
        foreach ($targetRows as $row) {
226
            $this->diffAndAppendRows($row, null, $appliedRowSpans, $forceExpansion);
227
        }
228
    }
229
230
    /**
231
     * @param Operation $operation
232
     * @param array     $oldRows
233
     * @param array     $newRows
234
     * @param array     $appliedRowSpans
235
     */
236
    protected function processEqualOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
237
    {
238
        $targetOldRows = array_values(array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 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...
239
        $targetNewRows = array_values(array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 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...
240
241
        foreach ($targetNewRows as $index => $newRow) {
242
            if (!isset($targetOldRows[$index])) {
243
                continue;
244
            }
245
246
            $this->diffAndAppendRows($targetOldRows[$index], $newRow, $appliedRowSpans);
247
        }
248
    }
249
250
    /**
251
     * @param Operation $operation
252
     * @param array     $oldRows
253
     * @param array     $newRows
254
     * @param array     $appliedRowSpans
255
     */
256
    protected function processReplaceOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
257
    {
258
        $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans, true);
259
        $this->processInsertOperation($operation, $newRows, $appliedRowSpans, true);
260
    }
261
262
    /**
263
     * @param array $oldMatchData
264
     * @param array $newMatchData
265
     *
266
     * @return array
267
     */
268
    protected function getRowMatches($oldMatchData, $newMatchData)
269
    {
270
        $matches = array();
271
272
        $startInOld = 0;
273
        $startInNew = 0;
274
        $endInOld = count($oldMatchData);
275
        $endInNew = count($newMatchData);
276
277
        $this->findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, $matches);
278
279
        return $matches;
280
    }
281
282
    /**
283
     * @param array $newMatchData
284
     * @param int   $startInOld
285
     * @param int   $endInOld
286
     * @param int   $startInNew
287
     * @param int   $endInNew
288
     * @param array $matches
289
     */
290
    protected function findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, &$matches)
291
    {
292
        $match = $this->findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew);
293
        if ($match !== null) {
294
            if ($startInOld < $match->getStartInOld() &&
295
                $startInNew < $match->getStartInNew()
296
            ) {
297
                $this->findRowMatches(
298
                    $newMatchData,
299
                    $startInOld,
300
                    $match->getStartInOld(),
301
                    $startInNew,
302
                    $match->getStartInNew(),
303
                    $matches
304
                );
305
            }
306
307
            $matches[] = $match;
308
309
            if ($match->getEndInOld() < $endInOld &&
310
                $match->getEndInNew() < $endInNew
311
            ) {
312
                $this->findRowMatches(
313
                    $newMatchData,
314
                    $match->getEndInOld(),
315
                    $endInOld,
316
                    $match->getEndInNew(),
317
                    $endInNew,
318
                    $matches
319
                );
320
            }
321
        }
322
    }
323
324
    /**
325
     * @param array $newMatchData
326
     * @param int   $startInOld
327
     * @param int   $endInOld
328
     * @param int   $startInNew
329
     * @param int   $endInNew
330
     *
331
     * @return RowMatch|null
332
     */
333
    protected function findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew)
334
    {
335
        $bestMatch = null;
336
        $bestPercentage = 0;
337
338
        foreach ($newMatchData as $newIndex => $oldMatches) {
339
            if ($newIndex < $startInNew) {
340
                continue;
341
            }
342
343
            if ($newIndex >= $endInNew) {
344
                break;
345
            }
346
            foreach ($oldMatches as $oldIndex => $percentage) {
347
                if ($oldIndex < $startInOld) {
348
                    continue;
349
                }
350
351
                if ($oldIndex >= $endInOld) {
352
                    break;
353
                }
354
355
                if ($percentage > $bestPercentage) {
356
                    $bestPercentage = $percentage;
357
                    $bestMatch = array(
358
                        'oldIndex' => $oldIndex,
359
                        'newIndex' => $newIndex,
360
                        'percentage' => $percentage,
361
                    );
362
                }
363
            }
364
        }
365
366
        if ($bestMatch !== null) {
367
            return new RowMatch(
368
                $bestMatch['newIndex'],
369
                $bestMatch['oldIndex'],
370
                $bestMatch['newIndex'] + 1,
371
                $bestMatch['oldIndex'] + 1,
372
                $bestMatch['percentage']
373
            );
374
        }
375
376
        return null;
377
    }
378
379
    /**
380
     * @param TableRow|null $oldRow
381
     * @param TableRow|null $newRow
382
     * @param array         $appliedRowSpans
383
     * @param bool          $forceExpansion
384
     *
385
     * @return \DOMNode
0 ignored issues
show
Documentation introduced by
Should the return type not be array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
386
     */
387
    protected function diffRows($oldRow, $newRow, array &$appliedRowSpans, $forceExpansion = false)
388
    {
389
        // create tr dom element
390
        $rowToClone = $newRow ?: $oldRow;
391
        $diffRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
392
393
        $oldCells = $oldRow ? $oldRow->getCells() : array();
394
        $newCells = $newRow ? $newRow->getCells() : array();
395
396
        $position = new DiffRowPosition();
397
398
        $extraRow = null;
399
400
        $expandCells = array();
401
        $cellsWithMultipleRows = array();
402
403
        $newCellCount = count($newCells);
404
        while ($position->getIndexInNew() < $newCellCount) {
405
            if (!$position->areColumnsEqual()) {
406
                $type = $position->getLesserColumnType();
407
                if ($type === 'new') {
408
                    $row = $newRow;
409
                    $targetRow = $extraRow;
410
                } else {
411
                    $row = $oldRow;
412
                    $targetRow = $diffRow;
413
                }
414
                if ($row && (!$type === 'old' || isset($oldCells[$position->getIndexInOld()]))) {
415
                    $this->syncVirtualColumns($row, $position, $cellsWithMultipleRows, $targetRow, $type, true);
416
417
                    continue;
418
                }
419
            }
420
421
            /* @var $newCell TableCell */
422
            $newCell = $newCells[$position->getIndexInNew()];
423
            /* @var $oldCell TableCell */
424
            $oldCell = isset($oldCells[$position->getIndexInOld()]) ? $oldCells[$position->getIndexInOld()] : null;
425
426
            if ($oldCell && $newCell->getColspan() != $oldCell->getColspan()) {
427
                if (null === $extraRow) {
428
                    $extraRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
429
                }
430
431
                if ($oldCell->getColspan() > $newCell->getColspan()) {
432
                    $this->diffCellsAndIncrementCounters(
433
                        $oldCell,
434
                        null,
435
                        $cellsWithMultipleRows,
436
                        $diffRow,
437
                        $position,
438
                        true
439
                    );
440
                    $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 387 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...
441
                } else {
442
                    $this->diffCellsAndIncrementCounters(
443
                        null,
444
                        $newCell,
445
                        $cellsWithMultipleRows,
446
                        $extraRow,
447
                        $position,
448
                        true
449
                    );
450
                    $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 387 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...
451
                }
452
            } else {
453
                $diffCell = $this->diffCellsAndIncrementCounters(
454
                    $oldCell,
455
                    $newCell,
456
                    $cellsWithMultipleRows,
457
                    $diffRow,
458
                    $position
459
                );
460
                $expandCells[] = $diffCell;
461
            }
462
        }
463
464
        $oldCellCount = count($oldCells);
465
        while ($position->getIndexInOld() < $oldCellCount) {
466
            $diffCell = $this->diffCellsAndIncrementCounters(
467
                $oldCells[$position->getIndexInOld()],
468
                null,
469
                $cellsWithMultipleRows,
470
                $diffRow,
471
                $position
472
            );
473
            $expandCells[] = $diffCell;
474
        }
475
476
        if ($extraRow) {
477
            foreach ($expandCells as $expandCell) {
478
                $expandCell->setAttribute('rowspan', $expandCell->getAttribute('rowspan') + 1);
479
            }
480
        }
481
482
        if ($extraRow || $forceExpansion) {
483
            foreach ($appliedRowSpans as $rowSpanCells) {
484
                foreach ($rowSpanCells as $extendCell) {
485
                    $extendCell->setAttribute('rowspan', $extendCell->getAttribute('rowspan') + 1);
486
                }
487
            }
488
        }
489
490
        if (!$forceExpansion) {
491
            array_shift($appliedRowSpans);
492
            $appliedRowSpans = array_values($appliedRowSpans);
493
        }
494
        $appliedRowSpans = array_merge($appliedRowSpans, array_values($cellsWithMultipleRows));
495
496
        return array($diffRow, $extraRow);
497
    }
498
499
    /**
500
     * @param TableCell|null $oldCell
501
     * @param TableCell|null $newCell
502
     *
503
     * @return \DOMElement
504
     */
505
    protected function getNewCellNode(TableCell $oldCell = null, TableCell $newCell = null)
506
    {
507
        // If only one cell exists, use it
508
        if (!$oldCell || !$newCell) {
509
            $clone = $newCell
510
                ? $newCell->getDomNode()->cloneNode(false)
511
                : $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...
512
        } else {
513
            $oldNode = $oldCell->getDomNode();
514
            $newNode = $newCell->getDomNode();
515
516
            $clone = $newNode->cloneNode(false);
517
518
            $oldRowspan = $oldNode->getAttribute('rowspan') ?: 1;
519
            $oldColspan = $oldNode->getAttribute('colspan') ?: 1;
520
            $newRowspan = $newNode->getAttribute('rowspan') ?: 1;
521
            $newColspan = $newNode->getAttribute('colspan') ?: 1;
522
523
            $clone->setAttribute('rowspan', max($oldRowspan, $newRowspan));
524
            $clone->setAttribute('colspan', max($oldColspan, $newColspan));
525
        }
526
527
        return $this->diffDom->importNode($clone);
528
    }
529
530
    /**
531
     * @param TableCell|null $oldCell
532
     * @param TableCell|null $newCell
533
     * @param bool           $usingExtraRow
534
     *
535
     * @return \DOMElement
536
     */
537
    protected function diffCells($oldCell, $newCell, $usingExtraRow = false)
538
    {
539
        $diffCell = $this->getNewCellNode($oldCell, $newCell);
540
541
        $oldContent = $oldCell ? $this->getInnerHtml($oldCell->getDomNode()) : '';
542
        $newContent = $newCell ? $this->getInnerHtml($newCell->getDomNode()) : '';
543
544
        $htmlDiff = new HtmlDiff(
545
            mb_convert_encoding($oldContent, 'UTF-8', 'HTML-ENTITIES'),
546
            mb_convert_encoding($newContent, 'UTF-8', 'HTML-ENTITIES'),
547
            $this->encoding,
548
            $this->specialCaseTags,
549
            $this->groupDiffs
550
        );
551
        $htmlDiff->setMatchThreshold($this->matchThreshold);
552
        $diff = $htmlDiff->build();
553
554
        $this->setInnerHtml($diffCell, $diff);
555
556
        if (null === $newCell) {
557
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' del'));
558
        }
559
560
        if (null === $oldCell) {
561
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' ins'));
562
        }
563
564
        if ($usingExtraRow) {
565
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' extra-row'));
566
        }
567
568
        return $diffCell;
569
    }
570
571
    protected function buildTableDoms()
572
    {
573
        $this->oldTable = $this->parseTableStructure(mb_convert_encoding($this->oldText, 'HTML-ENTITIES', 'UTF-8'));
574
        $this->newTable = $this->parseTableStructure(mb_convert_encoding($this->newText, 'HTML-ENTITIES', 'UTF-8'));
575
    }
576
577
    /**
578
     * @param string $text
579
     *
580
     * @return Table
581
     */
582
    protected function parseTableStructure($text)
583
    {
584
        $dom = new \DOMDocument();
585
        $dom->loadHTML($text);
586
587
        $tableNode = $dom->getElementsByTagName('table')->item(0);
588
589
        $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...
590
591
        $this->parseTable($table);
592
593
        return $table;
594
    }
595
596
    /**
597
     * @param Table         $table
598
     * @param \DOMNode|null $node
599
     */
600
    protected function parseTable(Table $table, \DOMNode $node = null)
601
    {
602
        if ($node === null) {
603
            $node = $table->getDomNode();
604
        }
605
606
        foreach ($node->childNodes as $child) {
607
            if ($child->nodeName === 'tr') {
608
                $row = new TableRow($child);
609
                $table->addRow($row);
610
611
                $this->parseTableRow($row);
612
            } else {
613
                $this->parseTable($table, $child);
614
            }
615
        }
616
    }
617
618
    /**
619
     * @param TableRow $row
620
     */
621
    protected function parseTableRow(TableRow $row)
622
    {
623
        $node = $row->getDomNode();
624
625
        foreach ($node->childNodes as $child) {
626
            if (in_array($child->nodeName, array('td', 'th'))) {
627
                $cell = new TableCell($child);
628
                $row->addCell($cell);
629
            }
630
        }
631
    }
632
633
    /**
634
     * @param \DOMNode $node
635
     *
636
     * @return string
637
     */
638
    protected function getInnerHtml($node)
639
    {
640
        $innerHtml = '';
641
        $children = $node->childNodes;
642
643
        foreach ($children as $child) {
644
            $innerHtml .= $this->htmlFromNode($child);
645
        }
646
647
        return $innerHtml;
648
    }
649
650
    /**
651
     * @param \DOMNode $node
652
     *
653
     * @return string
654
     */
655 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...
656
    {
657
        $domDocument = new \DOMDocument();
658
        $newNode = $domDocument->importNode($node, true);
659
        $domDocument->appendChild($newNode);
660
661
        return $domDocument->saveHTML();
662
    }
663
664
    /**
665
     * @param \DOMNode $node
666
     * @param string   $html
667
     */
668
    protected function setInnerHtml($node, $html)
669
    {
670
        // DOMDocument::loadHTML does not allow empty strings.
671
        if (strlen($html) === 0) {
672
            $html = '<span class="empty"></span>';
673
        }
674
675
        $doc = new \DOMDocument();
676
        $doc->loadHTML(mb_convert_encoding($this->purifier->purify($html), 'HTML-ENTITIES', 'UTF-8'));
677
        $fragment = $node->ownerDocument->createDocumentFragment();
678
        $root = $doc->getElementsByTagName('body')->item(0);
679
        foreach ($root->childNodes as $child) {
680
            $fragment->appendChild($node->ownerDocument->importNode($child, true));
681
        }
682
683
        $node->appendChild($fragment);
684
    }
685
686
    /**
687
     * @param Table $table
688
     */
689
    protected function indexCellValues(Table $table)
690
    {
691
        foreach ($table->getRows() as $rowIndex => $row) {
692
            foreach ($row->getCells() as $cellIndex => $cell) {
693
                $value = trim($cell->getDomNode()->textContent);
694
695
                if (!isset($this->cellValues[$value])) {
696
                    $this->cellValues[$value] = array();
697
                }
698
699
                $this->cellValues[$value][] = new TablePosition($rowIndex, $cellIndex);
700
            }
701
        }
702
    }
703
704
    /**
705
     * @param TableRow        $tableRow
706
     * @param DiffRowPosition $position
707
     * @param array           $cellsWithMultipleRows
708
     * @param \DOMNode        $diffRow
709
     * @param string          $diffType
710
     * @param bool            $usingExtraRow
711
     */
712
    protected function syncVirtualColumns(
713
        $tableRow,
714
        DiffRowPosition $position,
715
        &$cellsWithMultipleRows,
716
        $diffRow,
717
        $diffType,
718
        $usingExtraRow = false
719
    ) {
720
        $currentCell = $tableRow->getCell($position->getIndex($diffType));
721
        while ($position->isColumnLessThanOther($diffType) && $currentCell) {
722
            $diffCell = $diffType === 'new' ? $this->diffCells(null, $currentCell, $usingExtraRow) : $this->diffCells(
723
                $currentCell,
724
                null,
725
                $usingExtraRow
726
            );
727
            // Store cell in appliedRowSpans if spans multiple rows
728
            if ($diffCell->getAttribute('rowspan') > 1) {
729
                $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
730
            }
731
            $diffRow->appendChild($diffCell);
732
            $position->incrementColumn($diffType, $currentCell->getColspan());
733
            $currentCell = $tableRow->getCell($position->incrementIndex($diffType));
734
        }
735
    }
736
737
    /**
738
     * @param null|TableCell  $oldCell
739
     * @param null|TableCell  $newCell
740
     * @param array           $cellsWithMultipleRows
741
     * @param \DOMElement     $diffRow
742
     * @param DiffRowPosition $position
743
     * @param bool            $usingExtraRow
744
     *
745
     * @return \DOMElement
746
     */
747
    protected function diffCellsAndIncrementCounters(
748
        $oldCell,
749
        $newCell,
750
        &$cellsWithMultipleRows,
751
        $diffRow,
752
        DiffRowPosition $position,
753
        $usingExtraRow = false
754
    ) {
755
        $diffCell = $this->diffCells($oldCell, $newCell, $usingExtraRow);
756
        // Store cell in appliedRowSpans if spans multiple rows
757
        if ($diffCell->getAttribute('rowspan') > 1) {
758
            $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
759
        }
760
        $diffRow->appendChild($diffCell);
761
762
        if ($newCell !== null) {
763
            $position->incrementIndexInNew();
764
            $position->incrementColumnInNew($newCell->getColspan());
765
        }
766
767
        if ($oldCell !== null) {
768
            $position->incrementIndexInOld();
769
            $position->incrementColumnInOld($oldCell->getColspan());
770
        }
771
772
        return $diffCell;
773
    }
774
775
    /**
776
     * @param TableRow|null $oldRow
777
     * @param TableRow|null $newRow
778
     * @param array         $appliedRowSpans
779
     * @param bool          $forceExpansion
780
     */
781
    protected function diffAndAppendRows($oldRow, $newRow, &$appliedRowSpans, $forceExpansion = false)
782
    {
783
        list($rowDom, $extraRow) = $this->diffRows(
784
            $oldRow,
785
            $newRow,
786
            $appliedRowSpans,
787
            $forceExpansion
788
        );
789
790
        $this->diffTable->appendChild($rowDom);
0 ignored issues
show
Bug introduced by
The method appendChild() does not seem to exist on object<Caxy\HtmlDiff\Table\Table>.

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...
791
792
        if ($extraRow) {
793
            $this->diffTable->appendChild($extraRow);
0 ignored issues
show
Bug introduced by
The method appendChild() does not seem to exist on object<Caxy\HtmlDiff\Table\Table>.

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...
794
        }
795
    }
796
797
    /**
798
     * @param TableRow $oldRow
799
     * @param TableRow $newRow
800
     * @param int      $oldIndex
801
     * @param int      $newIndex
802
     *
803
     * @return float|int
804
     */
805
    protected function getMatchPercentage(TableRow $oldRow, TableRow $newRow, $oldIndex, $newIndex)
806
    {
807
        $firstCellWeight = 1.5;
808
        $indexDeltaWeight = 0.25 * (abs($oldIndex - $newIndex));
809
        $thresholdCount = 0;
810
        $minCells = min(count($newRow->getCells()), count($oldRow->getCells()));
811
        $totalCount = ($minCells + $firstCellWeight + $indexDeltaWeight) * 100;
812
        foreach ($newRow->getCells() as $newIndex => $newCell) {
813
            $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...
814
815
            if ($oldCell) {
816
                $percentage = null;
817
                similar_text($oldCell->getInnerHtml(), $newCell->getInnerHtml(), $percentage);
818
819
                if ($percentage > ($this->matchThreshold * 0.50)) {
820
                    $increment = $percentage;
821
                    if ($newIndex === 0 && $percentage > 95) {
822
                        $increment = $increment * $firstCellWeight;
823
                    }
824
                    $thresholdCount += $increment;
825
                }
826
            }
827
        }
828
829
        return ($totalCount > 0) ? ($thresholdCount / $totalCount) : 0;
830
    }
831
}
832