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

TableDiff::diffCellsAndIncrementCounters()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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