Passed
Push — master ( 9b95a1...8430aa )
by Josh
01:05
created

TableDiff::initPurifier()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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