Passed
Pull Request — master (#31)
by Josh
06:51
created

TableDiff::setInnerHtml()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 17
ccs 0
cts 13
cp 0
rs 9.4285
cc 3
eloc 10
nc 4
nop 2
crap 12
1
<?php
2
3
namespace Caxy\HtmlDiff\Table;
4
5
use Caxy\HtmlDiff\AbstractDiff;
6
use Caxy\HtmlDiff\HtmlDiff;
7
use Caxy\HtmlDiff\Operation;
8
9
/**
10
 * Class TableDiff
11
 * @package Caxy\HtmlDiff\Table
12
 */
13
class TableDiff extends AbstractDiff
14
{
15
    /**
16
     * @var null|Table
17
     */
18
    protected $oldTable = null;
19
20
    /**
21
     * @var null|Table
22
     */
23
    protected $newTable = null;
24
25
    /**
26
     * @var null|Table
27
     */
28
    protected $diffTable = null;
29
30
    /**
31
     * @var null|\DOMDocument
32
     */
33
    protected $diffDom = null;
34
35
    /**
36
     * @var int
37
     */
38
    protected $newRowOffsets = 0;
39
40
    /**
41
     * @var int
42
     */
43
    protected $oldRowOffsets = 0;
44
45
    /**
46
     * @var array
47
     */
48
    protected $cellValues = array();
49
50
    /**
51
     * @var \HTMLPurifier
52
     */
53
    protected $purifier;
54
55
    public function __construct($oldText, $newText, $encoding, $specialCaseTags, $groupDiffs)
56
    {
57
        parent::__construct($oldText, $newText, $encoding, $specialCaseTags, $groupDiffs);
58
59
        $config = \HTMLPurifier_Config::createDefault();
60
        $this->purifier = new \HTMLPurifier($config);
61
    }
62
63
    public function build()
64
    {
65
        $this->buildTableDoms();
66
67
        $this->diffDom = new \DOMDocument();
68
69
        $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...
70
71
        $this->diffTableContent();
72
73
        return $this->content;
74
    }
75
76
    protected function diffTableContent()
77
    {
78
        $this->diffDom = new \DOMDocument();
79
        $this->diffTable = $this->diffDom->importNode($this->newTable->getDomNode()->cloneNode(false), false);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->diffDom->importNo...loneNode(false), false) of type object<DOMNode> is incompatible with the declared type null|object<Caxy\HtmlDiff\Table\Table> of property $diffTable.

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

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

Loading history...
80
        $this->diffDom->appendChild($this->diffTable);
81
82
        $oldRows = $this->oldTable->getRows();
83
        $newRows = $this->newTable->getRows();
84
85
        $oldMatchData = array();
86
        $newMatchData = array();
87
88
        /* @var $oldRow TableRow */
89
        foreach ($oldRows as $oldIndex => $oldRow) {
90
            $oldMatchData[$oldIndex] = array();
91
92
            // Get match percentages
93
            /* @var $newRow TableRow */
94
            foreach ($newRows as $newIndex => $newRow) {
95
                if (!array_key_exists($newIndex, $newMatchData)) {
96
                    $newMatchData[$newIndex] = array();
97
                }
98
99
                // similar_text
100
                $percentage = $this->getMatchPercentage($oldRow, $newRow, $oldIndex, $newIndex);
101
102
                $oldMatchData[$oldIndex][$newIndex] = $percentage;
103
                $newMatchData[$newIndex][$oldIndex] = $percentage;
104
            }
105
        }
106
107
        $matches = $this->getRowMatches($oldMatchData, $newMatchData);
108
        $this->diffTableRowsWithMatches($oldRows, $newRows, $matches);
109
110
        $this->content = $this->htmlFromNode($this->diffTable);
111
    }
112
113
    /**
114
     * @param TableRow[] $oldRows
115
     * @param TableRow[] $newRows
116
     * @param RowMatch[] $matches
117
     */
118
    protected function diffTableRowsWithMatches($oldRows, $newRows, $matches)
119
    {
120
        $operations = array();
121
122
        $indexInOld = 0;
123
        $indexInNew = 0;
124
125
        $oldRowCount = count($oldRows);
126
        $newRowCount = count($newRows);
127
128
        $matches[] = new RowMatch($newRowCount, $oldRowCount, $newRowCount, $oldRowCount);
129
130
        // build operations
131
        foreach ($matches as $match) {
132
            $matchAtIndexInOld = $indexInOld === $match->getStartInOld();
133
            $matchAtIndexInNew = $indexInNew === $match->getStartInNew();
134
135
            $action = 'equal';
136
137
            if (!$matchAtIndexInOld && !$matchAtIndexInNew) {
138
                $action = 'replace';
139
            } elseif ($matchAtIndexInOld && !$matchAtIndexInNew) {
140
                $action = 'insert';
141
            } elseif (!$matchAtIndexInOld && $matchAtIndexInNew) {
142
                $action = 'delete';
143
            }
144
145
            if ($action !== 'equal') {
146
                $operations[] = new Operation($action, $indexInOld, $match->getStartInOld(), $indexInNew, $match->getStartInNew());
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 131 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...
147
            }
148
149
            $operations[] = new Operation('equal', $match->getStartInOld(), $match->getEndInOld(), $match->getStartInNew(), $match->getEndInNew());
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 147 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...
150
151
            $indexInOld = $match->getEndInOld();
152
            $indexInNew = $match->getEndInNew();
153
        }
154
155
        $appliedRowSpans = array();
156
157
        // process operations
158
        foreach ($operations as $operation) {
159
            switch ($operation->action) {
160
                case 'equal':
161
                    $this->processEqualOperation($operation, $oldRows, $newRows, $appliedRowSpans);
162
                    break;
163
164
                case 'delete':
165
                    $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans);
166
                    break;
167
168
                case 'insert':
169
                    $this->processInsertOperation($operation, $newRows, $appliedRowSpans);
170
                    break;
171
172
                case 'replace':
173
                    $this->processReplaceOperation($operation, $oldRows, $newRows, $appliedRowSpans);
174
                    break;
175
            }
176
        }
177
    }
178
179 View Code Duplication
    protected function processInsertOperation(Operation $operation, $newRows, &$appliedRowSpans, $forceExpansion = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

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

Loading history...
180
    {
181
        $targetRows = array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew);
182
        foreach ($targetRows as $row) {
183
            $this->diffAndAppendRows(null, $row, $appliedRowSpans, $forceExpansion);
184
        }
185
    }
186
187 View Code Duplication
    protected function processDeleteOperation(Operation $operation, $oldRows, &$appliedRowSpans, $forceExpansion = false)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

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

Loading history...
188
    {
189
        $targetRows = array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld);
190
        foreach ($targetRows as $row) {
191
            $this->diffAndAppendRows($row, null, $appliedRowSpans, $forceExpansion);
192
        }
193
    }
194
195
    protected function processEqualOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
196
    {
197
        $targetOldRows = array_values(array_slice($oldRows, $operation->startInOld, $operation->endInOld - $operation->startInOld));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 characters

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

Loading history...
198
        $targetNewRows = array_values(array_slice($newRows, $operation->startInNew, $operation->endInNew - $operation->startInNew));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 132 characters

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

Loading history...
199
200
        foreach ($targetNewRows as $index => $newRow) {
201
            if (!isset($targetOldRows[$index])) {
202
                continue;
203
            }
204
205
            $this->diffAndAppendRows($targetOldRows[$index], $newRow, $appliedRowSpans);
206
        }
207
    }
208
209
    protected function processReplaceOperation(Operation $operation, $oldRows, $newRows, &$appliedRowSpans)
210
    {
211
        $this->processDeleteOperation($operation, $oldRows, $appliedRowSpans, true);
212
        $this->processInsertOperation($operation, $newRows, $appliedRowSpans, true);
213
    }
214
215
    protected function getRowMatches($oldMatchData, $newMatchData)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
216
    {
217
        $matches = array();
218
219
        $startInOld = 0;
220
        $startInNew = 0;
221
        $endInOld = count($oldMatchData);
222
        $endInNew = count($newMatchData);
223
224
        $this->findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, $matches);
225
226
        return $matches;
227
    }
228
229
    protected function findRowMatches($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew, &$matches)
230
    {
231
        $match = $this->findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew);
232
        if ($match !== null) {
233
            if ($startInOld < $match->getStartInOld() &&
234
                $startInNew < $match->getStartInNew()
235
            ) {
236
                $this->findRowMatches(
237
                    $newMatchData,
238
                    $startInOld,
239
                    $match->getStartInOld(),
240
                    $startInNew,
241
                    $match->getStartInNew(),
242
                    $matches
243
                );
244
            }
245
246
            $matches[] = $match;
247
248
            if ($match->getEndInOld() < $endInOld &&
249
                $match->getEndInNew() < $endInNew
250
            ) {
251
                $this->findRowMatches($newMatchData, $match->getEndInOld(), $endInOld, $match->getEndInNew(), $endInNew, $matches);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 131 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...
252
            }
253
        }
254
    }
255
256
    protected function findRowMatch($newMatchData, $startInOld, $endInOld, $startInNew, $endInNew)
257
    {
258
        $bestMatch = null;
259
        $bestPercentage = 0;
260
261
        foreach ($newMatchData as $newIndex => $oldMatches) {
262
            if ($newIndex < $startInNew) {
263
                continue;
264
            }
265
266
            if ($newIndex >= $endInNew) {
267
                break;
268
            }
269
            foreach ($oldMatches as $oldIndex => $percentage) {
270
                if ($oldIndex < $startInOld) {
271
                    continue;
272
                }
273
274
                if ($oldIndex >= $endInOld) {
275
                    break;
276
                }
277
278
                if ($percentage > $bestPercentage) {
279
                    $bestPercentage = $percentage;
280
                    $bestMatch = array(
281
                        'oldIndex' => $oldIndex,
282
                        'newIndex' => $newIndex,
283
                        'percentage' => $percentage,
284
                    );
285
                }
286
            }
287
        }
288
289
        if ($bestMatch !== null) {
290
            return new RowMatch($bestMatch['newIndex'], $bestMatch['oldIndex'], $bestMatch['newIndex'] + 1, $bestMatch['oldIndex'] + 1, $bestMatch['percentage']);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 162 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...
291
        }
292
293
        return null;
294
    }
295
296
    /**
297
     * @param TableRow|null $oldRow
298
     * @param TableRow|null $newRow
299
     * @param array         $appliedRowSpans
300
     * @param bool          $forceExpansion
301
     *
302
     * @return \DOMNode
0 ignored issues
show
Documentation introduced by
Should the return type not be array?

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

Loading history...
303
     */
304
    protected function diffRows($oldRow, $newRow, array &$appliedRowSpans, $forceExpansion = false)
305
    {
306
        // create tr dom element
307
        $rowToClone = $newRow ?: $oldRow;
308
        $diffRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
309
310
        $oldCells = $oldRow ? $oldRow->getCells() : array();
311
        $newCells = $newRow ? $newRow->getCells() : array();
312
313
        $position = new DiffRowPosition();
314
315
        $extraRow = null;
316
317
        $expandCells = array();
318
        $cellsWithMultipleRows = array();
319
320
        $newCellCount = count($newCells);
321
        while ($position->getIndexInNew() < $newCellCount) {
322
            if (!$position->areColumnsEqual()) {
323
                $type = $position->getLesserColumnType();
324
                if ($type === 'new') {
325
                    $row = $newRow;
326
                    $targetRow = $extraRow;
327
                } else {
328
                    $row = $oldRow;
329
                    $targetRow = $diffRow;
330
                }
331
                if ($row && (!$type === 'old' || isset($oldCells[$position->getIndexInOld()]))) {
332
                    $this->syncVirtualColumns($row, $position, $cellsWithMultipleRows, $targetRow, $type, true);
333
334
                    continue;
335
                }
336
            }
337
338
            /* @var $newCell TableCell */
339
            $newCell = $newCells[$position->getIndexInNew()];
340
            /* @var $oldCell TableCell */
341
            $oldCell = isset($oldCells[$position->getIndexInOld()]) ? $oldCells[$position->getIndexInOld()] : null;
342
343
            if ($oldCell && $newCell->getColspan() != $oldCell->getColspan()) {
344
                if (null === $extraRow) {
345
                    $extraRow = $this->diffDom->importNode($rowToClone->getDomNode()->cloneNode(false), false);
346
                }
347
348
                if ($oldCell->getColspan() > $newCell->getColspan()) {
349
                    $this->diffCellsAndIncrementCounters(
350
                        $oldCell,
351
                        null,
352
                        $cellsWithMultipleRows,
353
                        $diffRow,
354
                        $position,
355
                        true
356
                    );
357
                    $this->syncVirtualColumns($newRow, $position, $cellsWithMultipleRows, $extraRow, 'new', true);
358
                } else {
359
                    $this->diffCellsAndIncrementCounters(
360
                        null,
361
                        $newCell,
362
                        $cellsWithMultipleRows,
363
                        $extraRow,
364
                        $position,
365
                        true
366
                    );
367
                    $this->syncVirtualColumns($oldRow, $position, $cellsWithMultipleRows, $diffRow, 'old', true);
368
                }
369
            } else {
370
                $diffCell = $this->diffCellsAndIncrementCounters(
371
                    $oldCell,
372
                    $newCell,
373
                    $cellsWithMultipleRows,
374
                    $diffRow,
375
                    $position
376
                );
377
                $expandCells[] = $diffCell;
378
            }
379
        }
380
381
        $oldCellCount = count($oldCells);
382
        while ($position->getIndexInOld() < $oldCellCount) {
383
            $diffCell = $this->diffCellsAndIncrementCounters(
384
                $oldCells[$position->getIndexInOld()],
385
                null,
386
                $cellsWithMultipleRows,
387
                $diffRow,
388
                $position
389
            );
390
            $expandCells[] = $diffCell;
391
        }
392
393
        if ($extraRow) {
394
            foreach ($expandCells as $expandCell) {
395
                $expandCell->setAttribute('rowspan', $expandCell->getAttribute('rowspan') + 1);
396
            }
397
        }
398
399
        if ($extraRow || $forceExpansion) {
400
            foreach ($appliedRowSpans as $rowSpanCells) {
401
                foreach ($rowSpanCells as $extendCell) {
402
                    $extendCell->setAttribute('rowspan', $extendCell->getAttribute('rowspan') + 1);
403
                }
404
            }
405
        }
406
407
        if (!$forceExpansion) {
408
            array_shift($appliedRowSpans);
409
            $appliedRowSpans = array_values($appliedRowSpans);
410
        }
411
        $appliedRowSpans = array_merge($appliedRowSpans, array_values($cellsWithMultipleRows));
412
413
        return array($diffRow, $extraRow);
414
    }
415
416
    /**
417
     * @param TableCell|null $oldCell
418
     * @param TableCell|null $newCell
419
     *
420
     * @return \DOMElement
421
     */
422
    protected function getNewCellNode(TableCell $oldCell = null, TableCell $newCell = null)
423
    {
424
        // If only one cell exists, use it
425
        if (!$oldCell || !$newCell) {
426
            $clone = $newCell
427
                ? $newCell->getDomNode()->cloneNode(false)
428
                : $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...
429
        } else {
430
            $oldNode = $oldCell->getDomNode();
431
            $newNode = $newCell->getDomNode();
432
433
            $clone = $newNode->cloneNode(false);
434
435
            $oldRowspan = $oldNode->getAttribute('rowspan') ?: 1;
436
            $oldColspan = $oldNode->getAttribute('colspan') ?: 1;
437
            $newRowspan = $newNode->getAttribute('rowspan') ?: 1;
438
            $newColspan = $newNode->getAttribute('colspan') ?: 1;
439
440
            $clone->setAttribute('rowspan', max($oldRowspan, $newRowspan));
441
            $clone->setAttribute('colspan', max($oldColspan, $newColspan));
442
        }
443
444
        return $this->diffDom->importNode($clone);
445
    }
446
447
    protected function diffCells($oldCell, $newCell, $usingExtraRow = false)
448
    {
449
        $diffCell = $this->getNewCellNode($oldCell, $newCell);
450
451
        $oldContent = $oldCell ? $this->getInnerHtml($oldCell->getDomNode()) : '';
452
        $newContent = $newCell ? $this->getInnerHtml($newCell->getDomNode()) : '';
453
454
        $htmlDiff = new HtmlDiff(
455
            mb_convert_encoding($oldContent, 'UTF-8', 'HTML-ENTITIES'),
456
            mb_convert_encoding($newContent, 'UTF-8', 'HTML-ENTITIES'),
457
            $this->encoding,
458
            $this->specialCaseTags,
459
            $this->groupDiffs
460
        );
461
        $htmlDiff->setMatchThreshold($this->matchThreshold);
462
        $diff = $htmlDiff->build();
463
464
        $this->setInnerHtml($diffCell, $diff);
465
466
        if (null === $newCell) {
467
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' del'));
468
        }
469
470
        if (null === $oldCell) {
471
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' ins'));
472
        }
473
474
        if ($usingExtraRow) {
475
            $diffCell->setAttribute('class', trim($diffCell->getAttribute('class').' extra-row'));
476
        }
477
478
        return $diffCell;
479
    }
480
481
    protected function buildTableDoms()
482
    {
483
        $this->oldTable = $this->parseTableStructure(mb_convert_encoding($this->oldText, 'HTML-ENTITIES', 'UTF-8'));
484
        $this->newTable = $this->parseTableStructure(mb_convert_encoding($this->newText, 'HTML-ENTITIES', 'UTF-8'));
485
    }
486
487
    protected function parseTableStructure($text)
488
    {
489
        $dom = new \DOMDocument();
490
        $dom->loadHTML($text);
491
492
        $tableNode = $dom->getElementsByTagName('table')->item(0);
493
494
        $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...
495
496
        $this->parseTable($table);
497
498
        return $table;
499
    }
500
501
    protected function parseTable(Table $table, \DOMNode $node = null)
502
    {
503
        if ($node === null) {
504
            $node = $table->getDomNode();
505
        }
506
507
        foreach ($node->childNodes as $child) {
508
            if ($child->nodeName === 'tr') {
509
                $row = new TableRow($child);
510
                $table->addRow($row);
511
512
                $this->parseTableRow($row);
513
            } else {
514
                $this->parseTable($table, $child);
515
            }
516
        }
517
    }
518
519
    protected function parseTableRow(TableRow $row)
520
    {
521
        $node = $row->getDomNode();
522
523
        foreach ($node->childNodes as $child) {
524
            if (in_array($child->nodeName, array('td', 'th'))) {
525
                $cell = new TableCell($child);
526
                $row->addCell($cell);
527
            }
528
        }
529
    }
530
531
    protected function getInnerHtml($node)
532
    {
533
        $innerHtml = '';
534
        $children = $node->childNodes;
535
536
        foreach ($children as $child) {
537
            $innerHtml .= $this->htmlFromNode($child);
538
        }
539
540
        return $innerHtml;
541
    }
542
543 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...
544
    {
545
        $domDocument = new \DOMDocument();
546
        $newNode = $domDocument->importNode($node, true);
547
        $domDocument->appendChild($newNode);
548
549
        return trim($domDocument->saveHTML());
550
    }
551
552
    protected function setInnerHtml($node, $html)
553
    {
554
        // DOMDocument::loadHTML does not allow empty strings.
555
        if (strlen($html) === 0) {
556
            $html = '<span class="empty"></span>';
557
        }
558
559
        $doc = new \DOMDocument();
560
        $doc->loadHTML(mb_convert_encoding($this->purifier->purify($html), 'HTML-ENTITIES', 'UTF-8'));
561
        $fragment = $node->ownerDocument->createDocumentFragment();
562
        $root = $doc->getElementsByTagName('body')->item(0);
563
        foreach ($root->childNodes as $child) {
564
            $fragment->appendChild($node->ownerDocument->importNode($child, true));
565
        }
566
567
        $node->appendChild($fragment);
568
    }
569
570
    protected function indexCellValues(Table $table)
571
    {
572
        foreach ($table->getRows() as $rowIndex => $row) {
573
            foreach ($row->getCells() as $cellIndex => $cell) {
574
                $value = trim($cell->getDomNode()->textContent);
575
576
                if (!isset($this->cellValues[$value])) {
577
                    $this->cellValues[$value] = array();
578
                }
579
580
                $this->cellValues[$value][] = new TablePosition($rowIndex, $cellIndex);
581
            }
582
        }
583
    }
584
585
    /**
586
     * @param        $tableRow
587
     * @param        $currentColumn
588
     * @param        $targetColumn
589
     * @param        $currentCell
590
     * @param        $cellsWithMultipleRows
591
     * @param        $diffRow
592
     * @param        $currentIndex
593
     * @param string $diffType
594
     */
595
    protected function syncVirtualColumns(
596
        $tableRow,
597
        DiffRowPosition $position,
598
        &$cellsWithMultipleRows,
599
        $diffRow,
600
        $diffType,
601
        $usingExtraRow = false
602
    ) {
603
        $currentCell = $tableRow->getCell($position->getIndex($diffType));
604
        while ($position->isColumnLessThanOther($diffType) && $currentCell) {
605
            $diffCell = $diffType === 'new' ? $this->diffCells(null, $currentCell, $usingExtraRow) : $this->diffCells(
606
                $currentCell,
607
                null,
608
                $usingExtraRow
609
            );
610
            // Store cell in appliedRowSpans if spans multiple rows
611
            if ($diffCell->getAttribute('rowspan') > 1) {
612
                $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
613
            }
614
            $diffRow->appendChild($diffCell);
615
            $position->incrementColumn($diffType, $currentCell->getColspan());
616
            $currentCell = $tableRow->getCell($position->incrementIndex($diffType));
617
        }
618
    }
619
620
    /**
621
     * @param null|TableCell  $oldCell
622
     * @param null|TableCell  $newCell
623
     * @param array           $cellsWithMultipleRows
624
     * @param \DOMElement     $diffRow
625
     * @param DiffRowPosition $position
626
     * @param bool            $usingExtraRow
627
     *
628
     * @return \DOMElement
629
     */
630
    protected function diffCellsAndIncrementCounters(
631
        $oldCell,
632
        $newCell,
633
        &$cellsWithMultipleRows,
634
        $diffRow,
635
        DiffRowPosition $position,
636
        $usingExtraRow = false
637
    ) {
638
        $diffCell = $this->diffCells($oldCell, $newCell, $usingExtraRow);
639
        // Store cell in appliedRowSpans if spans multiple rows
640
        if ($diffCell->getAttribute('rowspan') > 1) {
641
            $cellsWithMultipleRows[$diffCell->getAttribute('rowspan')][] = $diffCell;
642
        }
643
        $diffRow->appendChild($diffCell);
644
645
        if ($newCell !== null) {
646
            $position->incrementIndexInNew();
647
            $position->incrementColumnInNew($newCell->getColspan());
648
        }
649
650
        if ($oldCell !== null) {
651
            $position->incrementIndexInOld();
652
            $position->incrementColumnInOld($oldCell->getColspan());
653
        }
654
655
        return $diffCell;
656
    }
657
658
    /**
659
     * @param      $oldRow
660
     * @param      $newRow
661
     * @param      $appliedRowSpans
662
     * @param bool $forceExpansion
663
     */
664
    protected function diffAndAppendRows($oldRow, $newRow, &$appliedRowSpans, $forceExpansion = false)
665
    {
666
        list($rowDom, $extraRow) = $this->diffRows(
667
            $oldRow,
668
            $newRow,
669
            $appliedRowSpans,
670
            $forceExpansion
671
        );
672
673
        $this->diffTable->appendChild($rowDom);
0 ignored issues
show
Bug introduced by
The method appendChild() does not seem to exist on object<Caxy\HtmlDiff\Table\Table>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
674
675
        if ($extraRow) {
676
            $this->diffTable->appendChild($extraRow);
0 ignored issues
show
Bug introduced by
The method appendChild() does not seem to exist on object<Caxy\HtmlDiff\Table\Table>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
677
        }
678
    }
679
680
    protected function getMatchPercentage(TableRow $oldRow, TableRow $newRow, $oldIndex, $newIndex)
681
    {
682
        $firstCellWeight = 1.5;
683
        $indexDeltaWeight = 0.25 * (abs($oldIndex - $newIndex));
684
        $thresholdCount = 0;
685
        $totalCount = (min(count($newRow->getCells()), count($oldRow->getCells())) + $firstCellWeight + $indexDeltaWeight) * 100;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 129 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...
686
        foreach ($newRow->getCells() as $newIndex => $newCell) {
687
            $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...
688
689
            if ($oldCell) {
690
                $percentage = null;
691
                similar_text($oldCell->getInnerHtml(), $newCell->getInnerHtml(), $percentage);
692
693
                if ($percentage > ($this->matchThreshold * 0.50)) {
694
                    $increment = $percentage;
695
                    if ($newIndex === 0 && $percentage > 95) {
696
                        $increment = $increment * $firstCellWeight;
697
                    }
698
                    $thresholdCount += $increment;
699
                }
700
            }
701
        }
702
703
        $matchPercentage = ($totalCount > 0) ? ($thresholdCount / $totalCount) : 0;
704
705
        return $matchPercentage;
706
    }
707
}
708