Passed
Push — master ( 948458...49e3d0 )
by Josh
01:11
created

TableDiff::getNewCellNode()   C

Complexity

Conditions 8
Paths 18

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 6
Bugs 0 Features 1
Metric Value
c 6
b 0
f 1
dl 0
loc 25
ccs 0
cts 15
cp 0
rs 5.3846
cc 8
eloc 16
nc 18
nop 2
crap 72
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
 * @package Caxy\HtmlDiff\Table
13
 */
14
class TableDiff extends AbstractDiff
15
{
16
    /**
17
     * @var null|Table
18
     */
19
    protected $oldTable = null;
20
21
    /**
22
     * @var null|Table
23
     */
24
    protected $newTable = null;
25
26
    /**
27
     * @var null|\DOMElement
28
     */
29
    protected $diffTable = null;
30
31
    /**
32
     * @var null|\DOMDocument
33
     */
34
    protected $diffDom = null;
35
36
    /**
37
     * @var int
38
     */
39
    protected $newRowOffsets = 0;
40
41
    /**
42
     * @var int
43
     */
44
    protected $oldRowOffsets = 0;
45
46
    /**
47
     * @var array
48
     */
49
    protected $cellValues = array();
50
51
    /**
52
     * @var \HTMLPurifier
53
     */
54
    protected $purifier;
55
56
    /**
57
     * @param string              $oldText
58
     * @param string              $newText
59
     * @param HtmlDiffConfig|null $config
60
     *
61
     * @return self
62
     */
63 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...
64
    {
65
        $diff = new self($oldText, $newText);
66
67
        if (null !== $config) {
68
            $diff->setConfig($config);
69
        }
70
71
        return $diff;
72
    }
73
74
    /**
75
     * TableDiff constructor.
76
     *
77
     * @param string     $oldText
78
     * @param string     $newText
79
     * @param string     $encoding
80
     * @param array|null $specialCaseTags
81
     * @param bool|null  $groupDiffs
82
     */
83
    public function __construct($oldText, $newText, $encoding = 'UTF-8', $specialCaseTags = null, $groupDiffs = null)
84
    {
85
        parent::__construct($oldText, $newText, $encoding, $specialCaseTags, $groupDiffs);
86
87
        $this->purifier = new \HTMLPurifier(\HTMLPurifier_Config::createDefault());
88
    }
89
90
    /**
91
     * @return string
92
     */
93
    public function build()
94
    {
95
        $this->buildTableDoms();
96
97
        $this->diffDom = new \DOMDocument();
98
99
        $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...
100
101
        $this->diffTableContent();
102
103
        return $this->content;
104
    }
105
106
    protected function diffTableContent()
107
    {
108
        $this->diffDom = new \DOMDocument();
109
        $this->diffTable = $this->newTable->cloneNode($this->diffDom);
110
        $this->diffDom->appendChild($this->diffTable);
111
112
        $oldRows = $this->oldTable->getRows();
113
        $newRows = $this->newTable->getRows();
114
115
        $oldMatchData = array();
116
        $newMatchData = array();
117
118
        /* @var $oldRow TableRow */
119
        foreach ($oldRows as $oldIndex => $oldRow) {
120
            $oldMatchData[$oldIndex] = array();
121
122
            // Get match percentages
123
            /* @var $newRow TableRow */
124
            foreach ($newRows as $newIndex => $newRow) {
125
                if (!array_key_exists($newIndex, $newMatchData)) {
126
                    $newMatchData[$newIndex] = array();
127
                }
128
129
                // similar_text
130
                $percentage = $this->getMatchPercentage($oldRow, $newRow, $oldIndex, $newIndex);
131
132
                $oldMatchData[$oldIndex][$newIndex] = $percentage;
133
                $newMatchData[$newIndex][$oldIndex] = $percentage;
134
            }
135
        }
136
137
        $matches = $this->getRowMatches($oldMatchData, $newMatchData);
138
        $this->diffTableRowsWithMatches($oldRows, $newRows, $matches);
139
140
        $this->content = $this->htmlFromNode($this->diffTable);
141
    }
142
143
    /**
144
     * @param TableRow[] $oldRows
145
     * @param TableRow[] $newRows
146
     * @param RowMatch[] $matches
147
     */
148
    protected function diffTableRowsWithMatches($oldRows, $newRows, $matches)
149
    {
150
        $operations = array();
151
152
        $indexInOld = 0;
153
        $indexInNew = 0;
154
155
        $oldRowCount = count($oldRows);
156
        $newRowCount = count($newRows);
157
158
        $matches[] = new RowMatch($newRowCount, $oldRowCount, $newRowCount, $oldRowCount);
159
160
        // build operations
161
        foreach ($matches as $match) {
162
            $matchAtIndexInOld = $indexInOld === $match->getStartInOld();
163
            $matchAtIndexInNew = $indexInNew === $match->getStartInNew();
164
165
            $action = 'equal';
166
167
            if (!$matchAtIndexInOld && !$matchAtIndexInNew) {
168
                $action = 'replace';
169
            } elseif ($matchAtIndexInOld && !$matchAtIndexInNew) {
170
                $action = 'insert';
171
            } elseif (!$matchAtIndexInOld && $matchAtIndexInNew) {
172
                $action = 'delete';
173
            }
174
175
            if ($action !== 'equal') {
176
                $operations[] = new Operation(
177
                    $action,
178
                    $indexInOld,
179
                    $match->getStartInOld(),
180
                    $indexInNew,
181
                    $match->getStartInNew()
182
                );
183
            }
184
185
            $operations[] = new Operation(
186
                'equal',
187
                $match->getStartInOld(),
188
                $match->getEndInOld(),
189
                $match->getStartInNew(),
190
                $match->getEndInNew()
191
            );
192
193
            $indexInOld = $match->getEndInOld();
194
            $indexInNew = $match->getEndInNew();
195
        }
196
197
        $appliedRowSpans = array();
198
199
        // process operations
200
        foreach ($operations as $operation) {
201
            switch ($operation->action) {
202
                case 'equal':
203
                    $this->processEqualOperation($operation, $oldRows, $newRows, $appliedRowSpans);
204
                    break;
205
206
                case 'delete':
207
                    $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans);
208
                    break;
209
210
                case 'insert':
211
                    $this->processInsertOperation($operation, $newRows, $appliedRowSpans);
212
                    break;
213
214
                case 'replace':
215
                    $this->processReplaceOperation($operation, $oldRows, $newRows, $appliedRowSpans);
216
                    break;
217
            }
218
        }
219
    }
220
221
    /**
222
     * @param Operation $operation
223
     * @param array     $newRows
224
     * @param array     $appliedRowSpans
225
     * @param bool      $forceExpansion
226
     */
227 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...
228
        Operation $operation,
229
        $newRows,
230
        &$appliedRowSpans,
231
        $forceExpansion = false
232
    ) {
233
        $targetRows = array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew);
234
        foreach ($targetRows as $row) {
235
            $this->diffAndAppendRows(null, $row, $appliedRowSpans, $forceExpansion);
236
        }
237
    }
238
239
    /**
240
     * @param Operation $operation
241
     * @param array     $oldRows
242
     * @param array     $appliedRowSpans
243
     * @param bool      $forceExpansion
244
     */
245 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...
246
        Operation $operation,
247
        $oldRows,
248
        &$appliedRowSpans,
249
        $forceExpansion = false
250
    ) {
251
        $targetRows = array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld);
252
        foreach ($targetRows as $row) {
253
            $this->diffAndAppendRows($row, null, $appliedRowSpans, $forceExpansion);
254
        }
255
    }
256
257
    /**
258
     * @param Operation $operation
259
     * @param array     $oldRows
260
     * @param array     $newRows
261
     * @param array     $appliedRowSpans
262
     */
263
    protected function processEqualOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
264
    {
265
        $targetOldRows = array_values(
266
            array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld)
267
        );
268
        $targetNewRows = array_values(
269
            array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew)
270
        );
271
272
        foreach ($targetNewRows as $index => $newRow) {
273
            if (!isset($targetOldRows[$index])) {
274
                continue;
275
            }
276
277
            $this->diffAndAppendRows($targetOldRows[$index], $newRow, $appliedRowSpans);
278
        }
279
    }
280
281
    /**
282
     * @param Operation $operation
283
     * @param array     $oldRows
284
     * @param array     $newRows
285
     * @param array     $appliedRowSpans
286
     */
287
    protected function processReplaceOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
288
    {
289
        $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans, true);
290
        $this->processInsertOperation($operation, $newRows, $appliedRowSpans, true);
291
    }
292
293
    /**
294
     * @param array $oldMatchData
295
     * @param array $newMatchData
296
     *
297
     * @return array
298
     */
299
    protected function getRowMatches($oldMatchData, $newMatchData)
300
    {
301
        $matches = array();
302
303
        $startInOld = 0;
304
        $startInNew = 0;
305
        $endInOld = count($oldMatchData);
306
        $endInNew = count($newMatchData);
307
308
        $this->findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, $matches);
309
310
        return $matches;
311
    }
312
313
    /**
314
     * @param array $newMatchData
315
     * @param int   $startInOld
316
     * @param int   $endInOld
317
     * @param int   $startInNew
318
     * @param int   $endInNew
319
     * @param array $matches
320
     */
321
    protected function findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, &$matches)
322
    {
323
        $match = $this->findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew);
324
        if ($match !== null) {
325
            if ($startInOld < $match->getStartInOld() &&
326
                $startInNew < $match->getStartInNew()
327
            ) {
328
                $this->findRowMatches(
329
                    $newMatchData,
330
                    $startInOld,
331
                    $match->getStartInOld(),
332
                    $startInNew,
333
                    $match->getStartInNew(),
334
                    $matches
335
                );
336
            }
337
338
            $matches[] = $match;
339
340
            if ($match->getEndInOld() < $endInOld &&
341
                $match->getEndInNew() < $endInNew
342
            ) {
343
                $this->findRowMatches(
344
                    $newMatchData,
345
                    $match->getEndInOld(),
346
                    $endInOld,
347
                    $match->getEndInNew(),
348
                    $endInNew,
349
                    $matches
350
                );
351
            }
352
        }
353
    }
354
355
    /**
356
     * @param array $newMatchData
357
     * @param int   $startInOld
358
     * @param int   $endInOld
359
     * @param int   $startInNew
360
     * @param int   $endInNew
361
     *
362
     * @return RowMatch|null
363
     */
364
    protected function findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew)
365
    {
366
        $bestMatch = null;
367
        $bestPercentage = 0;
368
369
        foreach ($newMatchData as $newIndex => $oldMatches) {
370
            if ($newIndex < $startInNew) {
371
                continue;
372
            }
373
374
            if ($newIndex >= $endInNew) {
375
                break;
376
            }
377
            foreach ($oldMatches as $oldIndex => $percentage) {
378
                if ($oldIndex < $startInOld) {
379
                    continue;
380
                }
381
382
                if ($oldIndex >= $endInOld) {
383
                    break;
384
                }
385
386
                if ($percentage > $bestPercentage) {
387
                    $bestPercentage = $percentage;
388
                    $bestMatch = array(
389
                        'oldIndex' => $oldIndex,
390
                        'newIndex' => $newIndex,
391
                        'percentage' => $percentage,
392
                    );
393
                }
394
            }
395
        }
396
397
        if ($bestMatch !== null) {
398
            return new RowMatch(
399
                $bestMatch['newIndex'],
400
                $bestMatch['oldIndex'],
401
                $bestMatch['newIndex'] + 1,
402
                $bestMatch['oldIndex'] + 1,
403
                $bestMatch['percentage']
404
            );
405
        }
406
407
        return null;
408
    }
409
410
    /**
411
     * @param TableRow|null $oldRow
412
     * @param TableRow|null $newRow
413
     * @param array         $appliedRowSpans
414
     * @param bool          $forceExpansion
415
     *
416
     * @return array
417
     */
418
    protected function diffRows($oldRow, $newRow, array &$appliedRowSpans, $forceExpansion = false)
419
    {
420
        // create tr dom element
421
        $rowToClone = $newRow ?: $oldRow;
422
        /* @var $diffRow \DOMElement */
423
        $diffRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
424
425
        $oldCells = $oldRow ? $oldRow->getCells() : array();
426
        $newCells = $newRow ? $newRow->getCells() : array();
427
428
        $position = new DiffRowPosition();
429
430
        $extraRow = null;
431
432
        /* @var $expandCells \DOMElement[] */
433
        $expandCells = array();
434
        /* @var $cellsWithMultipleRows \DOMElement[] */
435
        $cellsWithMultipleRows = array();
436
437
        $newCellCount = count($newCells);
438
        while ($position->getIndexInNew() < $newCellCount) {
439
            if (!$position->areColumnsEqual()) {
440
                $type = $position->getLesserColumnType();
441
                if ($type === 'new') {
442
                    $row = $newRow;
443
                    $targetRow = $extraRow;
444
                } else {
445
                    $row = $oldRow;
446
                    $targetRow = $diffRow;
447
                }
448
                if ($row && $targetRow && (!$type === 'old' || isset($oldCells[$position->getIndexInOld()]))) {
449
                    $this->syncVirtualColumns($row, $position, $cellsWithMultipleRows, $targetRow, $type, true);
450
451
                    continue;
452
                }
453
            }
454
455
            /* @var $newCell TableCell */
456
            $newCell = $newCells[$position->getIndexInNew()];
457
            /* @var $oldCell TableCell */
458
            $oldCell = isset($oldCells[$position->getIndexInOld()]) ? $oldCells[$position->getIndexInOld()] : null;
459
460
            if ($oldCell && $newCell->getColspan() != $oldCell->getColspan()) {
461
                if (null === $extraRow) {
462
                    /* @var $extraRow \DOMElement */
463
                    $extraRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
464
                }
465
466
                if ($oldCell->getColspan() > $newCell->getColspan()) {
467
                    $this->diffCellsAndIncrementCounters(
468
                        $oldCell,
469
                        null,
470
                        $cellsWithMultipleRows,
471
                        $diffRow,
472
                        $position,
473
                        true
474
                    );
475
                    $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 418 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...
476
                } else {
477
                    $this->diffCellsAndIncrementCounters(
478
                        null,
479
                        $newCell,
480
                        $cellsWithMultipleRows,
481
                        $extraRow,
482
                        $position,
483
                        true
484
                    );
485
                    $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 418 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...
486
                }
487
            } else {
488
                $diffCell = $this->diffCellsAndIncrementCounters(
489
                    $oldCell,
490
                    $newCell,
491
                    $cellsWithMultipleRows,
492
                    $diffRow,
493
                    $position
494
                );
495
                $expandCells[] = $diffCell;
496
            }
497
        }
498
499
        $oldCellCount = count($oldCells);
500
        while ($position->getIndexInOld() < $oldCellCount) {
501
            $diffCell = $this->diffCellsAndIncrementCounters(
502
                $oldCells[$position->getIndexInOld()],
503
                null,
504
                $cellsWithMultipleRows,
505
                $diffRow,
506
                $position
507
            );
508
            $expandCells[] = $diffCell;
509
        }
510
511
        if ($extraRow) {
512
            foreach ($expandCells as $expandCell) {
513
                $rowspan = $expandCell->getAttribute('rowspan') ?: 1;
514
                $expandCell->setAttribute('rowspan', 1 + $rowspan);
515
            }
516
        }
517
518
        if ($extraRow || $forceExpansion) {
519
            foreach ($appliedRowSpans as $rowSpanCells) {
520
                /* @var $rowSpanCells \DOMElement[] */
521
                foreach ($rowSpanCells as $extendCell) {
522
                    $rowspan = $extendCell->getAttribute('rowspan') ?: 1;
523
                    $extendCell->setAttribute('rowspan', 1 + $rowspan);
524
                }
525
            }
526
        }
527
528
        if (!$forceExpansion) {
529
            array_shift($appliedRowSpans);
530
            $appliedRowSpans = array_values($appliedRowSpans);
531
        }
532
        $appliedRowSpans = array_merge($appliedRowSpans, array_values($cellsWithMultipleRows));
533
534
        return array($diffRow, $extraRow);
535
    }
536
537
    /**
538
     * @param TableCell|null $oldCell
539
     * @param TableCell|null $newCell
540
     *
541
     * @return \DOMElement
542
     */
543
    protected function getNewCellNode(TableCell $oldCell = null, TableCell $newCell = null)
544
    {
545
        // If only one cell exists, use it
546
        if (!$oldCell || !$newCell) {
547
            $clone = $newCell
548
                ? $newCell->getDomNode()->cloneNode(false)
549
                : $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...
550
        } else {
551
            $oldNode = $oldCell->getDomNode();
552
            $newNode = $newCell->getDomNode();
553
554
            /* @var $clone \DOMElement */
555
            $clone = $newNode->cloneNode(false);
556
557
            $oldRowspan = $oldNode->getAttribute('rowspan') ?: 1;
558
            $oldColspan = $oldNode->getAttribute('colspan') ?: 1;
559
            $newRowspan = $newNode->getAttribute('rowspan') ?: 1;
560
            $newColspan = $newNode->getAttribute('colspan') ?: 1;
561
562
            $clone->setAttribute('rowspan', max($oldRowspan, $newRowspan));
563
            $clone->setAttribute('colspan', max($oldColspan, $newColspan));
564
        }
565
566
        return $this->diffDom->importNode($clone);
567
    }
568
569
    /**
570
     * @param TableCell|null $oldCell
571
     * @param TableCell|null $newCell
572
     * @param bool           $usingExtraRow
573
     *
574
     * @return \DOMElement
575
     */
576
    protected function diffCells($oldCell, $newCell, $usingExtraRow = false)
577
    {
578
        $diffCell = $this->getNewCellNode($oldCell, $newCell);
579
580
        $oldContent = $oldCell ? $this->getInnerHtml($oldCell->getDomNode()) : '';
581
        $newContent = $newCell ? $this->getInnerHtml($newCell->getDomNode()) : '';
582
583
        $htmlDiff = HtmlDiff::create(
584
            mb_convert_encoding($oldContent, 'UTF-8', 'HTML-ENTITIES'),
585
            mb_convert_encoding($newContent, 'UTF-8', 'HTML-ENTITIES'),
586
            $this->config
587
        );
588
        $diff = $htmlDiff->build();
589
590
        $this->setInnerHtml($diffCell, $diff);
591
592
        if (null === $newCell) {
593
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' del'));
594
        }
595
596
        if (null === $oldCell) {
597
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' ins'));
598
        }
599
600
        if ($usingExtraRow) {
601
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' extra-row'));
602
        }
603
604
        return $diffCell;
605
    }
606
607
    protected function buildTableDoms()
608
    {
609
        $this->oldTable = $this->parseTableStructure($this->oldText);
610
        $this->newTable = $this->parseTableStructure($this->newText);
611
    }
612
613
    /**
614
     * @param string $text
615
     *
616
     * @return \DOMDocument
617
     */
618
    protected function createDocumentWithHtml($text)
619
    {
620
        $dom = new \DOMDocument();
621
        $dom->loadHTML(mb_convert_encoding(
622
            $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...
623
            'HTML-ENTITIES',
624
            $this->config->getEncoding()
625
        ));
626
627
        return $dom;
628
    }
629
630
    /**
631
     * @param string $text
632
     *
633
     * @return Table
634
     */
635
    protected function parseTableStructure($text)
636
    {
637
        $dom = $this->createDocumentWithHtml($text);
638
639
        $tableNode = $dom->getElementsByTagName('table')->item(0);
640
641
        $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...
642
643
        $this->parseTable($table);
644
645
        return $table;
646
    }
647
648
    /**
649
     * @param Table         $table
650
     * @param \DOMNode|null $node
651
     */
652
    protected function parseTable(Table $table, \DOMNode $node = null)
653
    {
654
        if ($node === null) {
655
            $node = $table->getDomNode();
656
        }
657
658
        if (!$node->childNodes) {
659
            return;
660
        }
661
662
        foreach ($node->childNodes as $child) {
663
            if ($child->nodeName === 'tr') {
664
                $row = new TableRow($child);
665
                $table->addRow($row);
666
667
                $this->parseTableRow($row);
668
            } else {
669
                $this->parseTable($table, $child);
670
            }
671
        }
672
    }
673
674
    /**
675
     * @param TableRow $row
676
     */
677
    protected function parseTableRow(TableRow $row)
678
    {
679
        $node = $row->getDomNode();
680
681
        foreach ($node->childNodes as $child) {
682
            if (in_array($child->nodeName, array('td', 'th'))) {
683
                $cell = new TableCell($child);
684
                $row->addCell($cell);
685
            }
686
        }
687
    }
688
689
    /**
690
     * @param \DOMNode $node
691
     *
692
     * @return string
693
     */
694
    protected function getInnerHtml($node)
695
    {
696
        $innerHtml = '';
697
        $children = $node->childNodes;
698
699
        foreach ($children as $child) {
700
            $innerHtml .= $this->htmlFromNode($child);
701
        }
702
703
        return $innerHtml;
704
    }
705
706
    /**
707
     * @param \DOMNode $node
708
     *
709
     * @return string
710
     */
711 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...
712
    {
713
        $domDocument = new \DOMDocument();
714
        $newNode = $domDocument->importNode($node, true);
715
        $domDocument->appendChild($newNode);
716
717
        return $domDocument->saveHTML();
718
    }
719
720
    /**
721
     * @param \DOMNode $node
722
     * @param string   $html
723
     */
724
    protected function setInnerHtml($node, $html)
725
    {
726
        // DOMDocument::loadHTML does not allow empty strings.
727
        if (strlen($html) === 0) {
728
            $html = '<span class="empty"></span>';
729
        }
730
731
        $doc = $this->createDocumentWithHtml($html);
732
        $fragment = $node->ownerDocument->createDocumentFragment();
733
        $root = $doc->getElementsByTagName('body')->item(0);
734
        foreach ($root->childNodes as $child) {
735
            $fragment->appendChild($node->ownerDocument->importNode($child, true));
736
        }
737
738
        $node->appendChild($fragment);
739
    }
740
741
    /**
742
     * @param Table $table
743
     */
744
    protected function indexCellValues(Table $table)
745
    {
746
        foreach ($table->getRows() as $rowIndex => $row) {
747
            foreach ($row->getCells() as $cellIndex => $cell) {
748
                $value = trim($cell->getDomNode()->textContent);
749
750
                if (!isset($this->cellValues[$value])) {
751
                    $this->cellValues[$value] = array();
752
                }
753
754
                $this->cellValues[$value][] = new TablePosition($rowIndex, $cellIndex);
755
            }
756
        }
757
    }
758
759
    /**
760
     * @param TableRow        $tableRow
761
     * @param DiffRowPosition $position
762
     * @param array           $cellsWithMultipleRows
763
     * @param \DOMNode        $diffRow
764
     * @param string          $diffType
765
     * @param bool            $usingExtraRow
766
     */
767
    protected function syncVirtualColumns(
768
        $tableRow,
769
        DiffRowPosition $position,
770
        &$cellsWithMultipleRows,
771
        $diffRow,
772
        $diffType,
773
        $usingExtraRow = false
774
    ) {
775
        $currentCell = $tableRow->getCell($position->getIndex($diffType));
776
        while ($position->isColumnLessThanOther($diffType) && $currentCell) {
777
            $diffCell = $diffType === 'new' ? $this->diffCells(null, $currentCell, $usingExtraRow) : $this->diffCells(
778
                $currentCell,
779
                null,
780
                $usingExtraRow
781
            );
782
            // Store cell in appliedRowSpans if spans multiple rows
783
            if ($diffCell->getAttribute('rowspan') > 1) {
784
                $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
785
            }
786
            $diffRow->appendChild($diffCell);
787
            $position->incrementColumn($diffType, $currentCell->getColspan());
788
            $currentCell = $tableRow->getCell($position->incrementIndex($diffType));
789
        }
790
    }
791
792
    /**
793
     * @param null|TableCell  $oldCell
794
     * @param null|TableCell  $newCell
795
     * @param array           $cellsWithMultipleRows
796
     * @param \DOMElement     $diffRow
797
     * @param DiffRowPosition $position
798
     * @param bool            $usingExtraRow
799
     *
800
     * @return \DOMElement
801
     */
802
    protected function diffCellsAndIncrementCounters(
803
        $oldCell,
804
        $newCell,
805
        &$cellsWithMultipleRows,
806
        $diffRow,
807
        DiffRowPosition $position,
808
        $usingExtraRow = false
809
    ) {
810
        $diffCell = $this->diffCells($oldCell, $newCell, $usingExtraRow);
811
        // Store cell in appliedRowSpans if spans multiple rows
812
        if ($diffCell->getAttribute('rowspan') > 1) {
813
            $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
814
        }
815
        $diffRow->appendChild($diffCell);
816
817
        if ($newCell !== null) {
818
            $position->incrementIndexInNew();
819
            $position->incrementColumnInNew($newCell->getColspan());
820
        }
821
822
        if ($oldCell !== null) {
823
            $position->incrementIndexInOld();
824
            $position->incrementColumnInOld($oldCell->getColspan());
825
        }
826
827
        return $diffCell;
828
    }
829
830
    /**
831
     * @param TableRow|null $oldRow
832
     * @param TableRow|null $newRow
833
     * @param array         $appliedRowSpans
834
     * @param bool          $forceExpansion
835
     */
836
    protected function diffAndAppendRows($oldRow, $newRow, &$appliedRowSpans, $forceExpansion = false)
837
    {
838
        list($rowDom, $extraRow) = $this->diffRows(
839
            $oldRow,
840
            $newRow,
841
            $appliedRowSpans,
842
            $forceExpansion
843
        );
844
845
        $this->diffTable->appendChild($rowDom);
846
847
        if ($extraRow) {
848
            $this->diffTable->appendChild($extraRow);
849
        }
850
    }
851
852
    /**
853
     * @param TableRow $oldRow
854
     * @param TableRow $newRow
855
     * @param int      $oldIndex
856
     * @param int      $newIndex
857
     *
858
     * @return float|int
859
     */
860
    protected function getMatchPercentage(TableRow $oldRow, TableRow $newRow, $oldIndex, $newIndex)
861
    {
862
        $firstCellWeight = 1.5;
863
        $indexDeltaWeight = 0.25 * (abs($oldIndex - $newIndex));
864
        $thresholdCount = 0;
865
        $minCells = min(count($newRow->getCells()), count($oldRow->getCells()));
866
        $totalCount = ($minCells + $firstCellWeight + $indexDeltaWeight) * 100;
867
        foreach ($newRow->getCells() as $newIndex => $newCell) {
868
            $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...
869
870
            if ($oldCell) {
871
                $percentage = null;
872
                similar_text($oldCell->getInnerHtml(), $newCell->getInnerHtml(), $percentage);
873
874
                if ($percentage > ($this->config->getMatchThreshold() * 0.50)) {
875
                    $increment = $percentage;
876
                    if ($newIndex === 0 && $percentage > 95) {
877
                        $increment = $increment * $firstCellWeight;
878
                    }
879
                    $thresholdCount += $increment;
880
                }
881
            }
882
        }
883
884
        return ($totalCount > 0) ? ($thresholdCount / $totalCount) : 0;
885
    }
886
}
887