Passed
Push — master ( b3f41c...50a57e )
by Josh
01:14
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 3
Bugs 0 Features 0
Metric Value
c 3
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
 * @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
            $this->initPurifier($config->getPurifierCacheLocation());
0 ignored issues
show
Bug introduced by
The variable $this does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

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