Passed
Push — master ( 6f39bc...2ba271 )
by Josh
02:32
created

HtmlDiff   F

Complexity

Total Complexity 166

Size/Duplication

Total Lines 890
Duplicated Lines 0 %

Test Coverage

Coverage 91.11%

Importance

Changes 0
Metric Value
eloc 339
dl 0
loc 890
ccs 338
cts 371
cp 0.9111
rs 2
c 0
b 0
f 0
wmc 166

42 Methods

Rating   Name   Duplication   Size   Complexity  
B createIsolatedDiffTagPlaceholders() 0 43 11
A isImagePlaceholder() 0 3 1
B processInsertOperation() 0 15 7
A diffElements() 0 20 5
A processEqualOperation() 0 13 6
A stripTagAttributes() 0 9 2
A isTablePlaceholder() 0 3 1
A isListPlaceholder() 0 3 1
A wrapText() 0 3 1
A matchingBlocks() 0 6 1
A indexNewWords() 0 11 4
A diffElementsByAttribute() 0 16 2
A isSelfClosingTag() 0 3 1
A findMatchingBlocks() 0 13 6
A findIsolatedDiffTagsInOld() 0 5 1
D insertTag() 0 57 19
A getInsertSpaceInReplace() 0 3 1
A diffIsolatedPlaceholder() 0 16 6
A performOperation() 0 17 5
B operations() 0 36 10
B extractConsecutiveWords() 0 32 11
A isClosingIsolatedDiffTag() 0 13 4
A isOpeningTag() 0 3 1
A diffTables() 0 5 1
B processDeleteOperation() 0 15 7
A isLinkPlaceholder() 0 3 1
A create() 0 9 2
A checkCondition() 0 3 2
A isPlaceholderType() 0 16 4
A getAttributeFromTag() 0 8 2
A setInsertSpaceInReplace() 0 5 1
A diffList() 0 5 1
A processReplaceOperation() 0 4 1
A isOpeningIsolatedDiffTag() 0 13 4
A isClosingTag() 0 3 1
A build() 0 32 6
A replaceIsolatedDiffTags() 0 4 1
A setUseTableDiffing() 0 5 1
A isTag() 0 3 2
C findMatch() 0 55 15
A array_slice_cached() 0 28 5
A isOnlyWhitespace() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like HtmlDiff often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HtmlDiff, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Caxy\HtmlDiff;
4
5
use Caxy\HtmlDiff\Table\TableDiff;
6
7
/**
8
 * Class HtmlDiff.
9
 */
10
class HtmlDiff extends AbstractDiff
11
{
12
    /**
13
     * @var array
14
     */
15
    protected $wordIndices;
16
    /**
17
     * @var array
18
     */
19
    protected $oldTables;
20
    /**
21
     * @var array
22
     */
23
    protected $newTables;
24
    /**
25
     * @var array
26
     */
27
    protected $newIsolatedDiffTags;
28
    /**
29
     * @var array
30
     */
31
    protected $oldIsolatedDiffTags;
32
33
    /**
34
     * @param string              $oldText
35
     * @param string              $newText
36
     * @param HtmlDiffConfig|null $config
37
     *
38
     * @return self
39
     */
40 13
    public static function create($oldText, $newText, HtmlDiffConfig $config = null)
41
    {
42 13
        $diff = new self($oldText, $newText);
43
44 13
        if (null !== $config) {
45 13
            $diff->setConfig($config);
46
        }
47
48 13
        return $diff;
49
    }
50
51
    /**
52
     * @param $bool
53
     *
54
     * @return $this
55
     *
56
     * @deprecated since 0.1.0
57
     */
58
    public function setUseTableDiffing($bool)
59
    {
60
        $this->config->setUseTableDiffing($bool);
61
62
        return $this;
63
    }
64
65
    /**
66
     * @param bool $boolean
67
     *
68
     * @return HtmlDiff
69
     *
70
     * @deprecated since 0.1.0
71
     */
72
    public function setInsertSpaceInReplace($boolean)
73
    {
74
        $this->config->setInsertSpaceInReplace($boolean);
75
76
        return $this;
77
    }
78
79
    /**
80
     * @return bool
81
     *
82
     * @deprecated since 0.1.0
83
     */
84
    public function getInsertSpaceInReplace()
85
    {
86
        return $this->config->isInsertSpaceInReplace();
87
    }
88
89
    /**
90
     * @return string
91
     */
92 16
    public function build()
93
    {
94 16
        $this->prepare();
95
96 16
        if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) {
97
            $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText);
98
99
            return $this->content;
100
        }
101
102
        // Pre-processing Optimizations
103
104
        // 1. Equality
105 16
        if ($this->oldText == $this->newText) {
106 10
            return $this->newText;
107
        }
108
109 16
        $this->splitInputsToWords();
110 16
        $this->replaceIsolatedDiffTags();
111 16
        $this->indexNewWords();
112
113 16
        $operations = $this->operations();
114
115 16
        foreach ($operations as $item) {
116 16
            $this->performOperation($item);
117
        }
118
119 16
        if ($this->hasDiffCache()) {
120
            $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
121
        }
122
123 16
        return $this->content;
124
    }
125
126 16
    protected function indexNewWords()
127
    {
128 16
        $this->wordIndices = array();
129 16
        foreach ($this->newWords as $i => $word) {
130 16
            if ($this->isTag($word)) {
131 8
                $word = $this->stripTagAttributes($word);
132
            }
133 16
            if (isset($this->wordIndices[ $word ])) {
134 11
                $this->wordIndices[ $word ][] = $i;
135
            } else {
136 16
                $this->wordIndices[ $word ] = array($i);
137
            }
138
        }
139 16
    }
140
141 16
    protected function replaceIsolatedDiffTags()
142
    {
143 16
        $this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords);
144 16
        $this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords);
145 16
    }
146
147
    /**
148
     * @param array $words
149
     *
150
     * @return array
151
     */
152 16
    protected function createIsolatedDiffTagPlaceholders(&$words)
153
    {
154 16
        $openIsolatedDiffTags = 0;
155 16
        $isolatedDiffTagIndices = array();
156 16
        $isolatedDiffTagStart = 0;
157 16
        $currentIsolatedDiffTag = null;
158 16
        foreach ($words as $index => $word) {
159 16
            $openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag);
160 16
            if ($openIsolatedDiffTag) {
161 14
                if ($this->isSelfClosingTag($word) || $this->stringUtil->stripos($word, '<img') !== false) {
162
                    if ($openIsolatedDiffTags === 0) {
163
                        $isolatedDiffTagIndices[] = array(
164
                            'start' => $index,
165
                            'length' => 1,
166
                            'tagType' => $openIsolatedDiffTag,
167
                        );
168
                        $currentIsolatedDiffTag = null;
169
                    }
170
                } else {
171 14
                    if ($openIsolatedDiffTags === 0) {
172 14
                        $isolatedDiffTagStart = $index;
173
                    }
174 14
                    ++$openIsolatedDiffTags;
175 14
                    $currentIsolatedDiffTag = $openIsolatedDiffTag;
176
                }
177 16
            } elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) {
178 14
                --$openIsolatedDiffTags;
179 14
                if ($openIsolatedDiffTags == 0) {
180 14
                    $isolatedDiffTagIndices[] = array('start' => $isolatedDiffTagStart, 'length' => $index - $isolatedDiffTagStart + 1, 'tagType' => $currentIsolatedDiffTag);
181 14
                    $currentIsolatedDiffTag = null;
182
                }
183
            }
184
        }
185 16
        $isolatedDiffTagScript = array();
186 16
        $offset = 0;
187 16
        foreach ($isolatedDiffTagIndices as $isolatedDiffTagIndex) {
188 14
            $start = $isolatedDiffTagIndex['start'] - $offset;
189 14
            $placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']);
190 14
            $isolatedDiffTagScript[$start] = array_splice($words, $start, $isolatedDiffTagIndex['length'], $placeholderString);
191 14
            $offset += $isolatedDiffTagIndex['length'] - 1;
192
        }
193
194 16
        return $isolatedDiffTagScript;
195
    }
196
197
    /**
198
     * @param string      $item
199
     * @param null|string $currentIsolatedDiffTag
200
     *
201
     * @return false|string
202
     */
203 16
    protected function isOpeningIsolatedDiffTag($item, $currentIsolatedDiffTag = null)
204
    {
205 16
        $tagsToMatch = $currentIsolatedDiffTag !== null
206 14
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
207 16
            : $this->config->getIsolatedDiffTags();
208 16
        $pattern = '#<%s(\s+[^>]*)?>#iUu';
209 16
        foreach ($tagsToMatch as $key => $value) {
210 16
            if (preg_match(sprintf($pattern, $key), $item)) {
211 14
                return $key;
212
            }
213
        }
214
215 16
        return false;
216
    }
217
218 14
    protected function isSelfClosingTag($text)
219
    {
220 14
        return (bool) preg_match('/<[^>]+\/\s*>/u', $text);
221
    }
222
223
    /**
224
     * @param string      $item
225
     * @param null|string $currentIsolatedDiffTag
226
     *
227
     * @return false|string
228
     */
229 14
    protected function isClosingIsolatedDiffTag($item, $currentIsolatedDiffTag = null)
230
    {
231 14
        $tagsToMatch = $currentIsolatedDiffTag !== null
232 14
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
233 14
            : $this->config->getIsolatedDiffTags();
234 14
        $pattern = '#</%s(\s+[^>]*)?>#iUu';
235 14
        foreach ($tagsToMatch as $key => $value) {
236 14
            if (preg_match(sprintf($pattern, $key), $item)) {
237 14
                return $key;
238
            }
239
        }
240
241 14
        return false;
242
    }
243
244
    /**
245
     * @param Operation $operation
246
     */
247 16
    protected function performOperation($operation)
248
    {
249 16
        switch ($operation->action) {
250 16
            case 'equal' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
251 16
            $this->processEqualOperation($operation);
252 16
            break;
253 13
            case 'delete' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
254 4
            $this->processDeleteOperation($operation, 'diffdel');
255 4
            break;
256 13
            case 'insert' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
257 9
            $this->processInsertOperation($operation, 'diffins');
258 9
            break;
259 9
            case 'replace':
260 9
            $this->processReplaceOperation($operation);
261 9
            break;
262
            default:
263
            break;
264
        }
265 16
    }
266
267
    /**
268
     * @param Operation $operation
269
     */
270 9
    protected function processReplaceOperation($operation)
271
    {
272 9
        $this->processDeleteOperation($operation, 'diffmod');
273 9
        $this->processInsertOperation($operation, 'diffmod');
274 9
    }
275
276
    /**
277
     * @param Operation $operation
278
     * @param string    $cssClass
279
     */
280 13
    protected function processInsertOperation($operation, $cssClass)
281
    {
282 13
        $text = array();
283 13
        foreach ($this->newWords as $pos => $s) {
284 13
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
285 13
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
286 3
                    foreach ($this->newIsolatedDiffTags[$pos] as $word) {
287 3
                        $text[] = $word;
288
                    }
289
                } else {
290 13
                    $text[] = $s;
291
                }
292
            }
293
        }
294 13
        $this->insertTag('ins', $cssClass, $text);
295 13
    }
296
297
    /**
298
     * @param Operation $operation
299
     * @param string    $cssClass
300
     */
301 11
    protected function processDeleteOperation($operation, $cssClass)
302
    {
303 11
        $text = array();
304 11
        foreach ($this->oldWords as $pos => $s) {
305 11
            if ($pos >= $operation->startInOld && $pos < $operation->endInOld) {
306 11
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->oldIsolatedDiffTags[$pos])) {
307 6
                    foreach ($this->oldIsolatedDiffTags[$pos] as $word) {
308 6
                        $text[] = $word;
309
                    }
310
                } else {
311 11
                    $text[] = $s;
312
                }
313
            }
314
        }
315 11
        $this->insertTag('del', $cssClass, $text);
316 11
    }
317
318
    /**
319
     * @param Operation $operation
320
     * @param int       $pos
321
     * @param string    $placeholder
322
     * @param bool      $stripWrappingTags
323
     *
324
     * @return string
325
     */
326 13
    protected function diffIsolatedPlaceholder($operation, $pos, $placeholder, $stripWrappingTags = true)
327
    {
328 13
        $oldText = implode('', $this->findIsolatedDiffTagsInOld($operation, $pos));
329 13
        $newText = implode('', $this->newIsolatedDiffTags[$pos]);
330
331 13
        if ($this->isListPlaceholder($placeholder)) {
332 7
            return $this->diffList($oldText, $newText);
333 10
        } elseif ($this->config->isUseTableDiffing() && $this->isTablePlaceholder($placeholder)) {
334 1
            return $this->diffTables($oldText, $newText);
335 9
        } elseif ($this->isLinkPlaceholder($placeholder)) {
336 1
            return $this->diffElementsByAttribute($oldText, $newText, 'href', 'a');
337 8
        } elseif ($this->isImagePlaceholder($placeholder)) {
338
            return $this->diffElementsByAttribute($oldText, $newText, 'src', 'img');
339
        }
340
341 8
        return $this->diffElements($oldText, $newText, $stripWrappingTags);
342
    }
343
344
    /**
345
     * @param string $oldText
346
     * @param string $newText
347
     * @param bool   $stripWrappingTags
348
     *
349
     * @return string
350
     */
351 9
    protected function diffElements($oldText, $newText, $stripWrappingTags = true)
352
    {
353 9
        $wrapStart = '';
354 9
        $wrapEnd = '';
355
356 9
        if ($stripWrappingTags) {
357 9
            $pattern = '/(^<[^>]+>)|(<\/[^>]+>$)/iu';
358 9
            $matches = array();
359
360 9
            if (preg_match_all($pattern, $newText, $matches)) {
361 9
                $wrapStart = isset($matches[0][0]) ? $matches[0][0] : '';
362 9
                $wrapEnd = isset($matches[0][1]) ? $matches[0][1] : '';
363
            }
364 9
            $oldText = preg_replace($pattern, '', $oldText);
365 9
            $newText = preg_replace($pattern, '', $newText);
366
        }
367
368 9
        $diff = self::create($oldText, $newText, $this->config);
369
370 9
        return $wrapStart.$diff->build().$wrapEnd;
371
    }
372
373
    /**
374
     * @param string $oldText
375
     * @param string $newText
376
     *
377
     * @return string
378
     */
379 7
    protected function diffList($oldText, $newText)
380
    {
381 7
        $diff = ListDiffLines::create($oldText, $newText, $this->config);
382
383 7
        return $diff->build();
384
    }
385
386
    /**
387
     * @param string $oldText
388
     * @param string $newText
389
     *
390
     * @return string
391
     */
392 1
    protected function diffTables($oldText, $newText)
393
    {
394 1
        $diff = TableDiff::create($oldText, $newText, $this->config);
395
396 1
        return $diff->build();
397
    }
398
399 1
    protected function diffElementsByAttribute($oldText, $newText, $attribute, $element)
400
    {
401 1
        $oldAttribute = $this->getAttributeFromTag($oldText, $attribute);
402 1
        $newAttribute = $this->getAttributeFromTag($newText, $attribute);
403
404 1
        if ($oldAttribute !== $newAttribute) {
405 1
            $diffClass = sprintf('diffmod diff%s diff%s', $element, $attribute);
406
407 1
            return sprintf(
408 1
                '%s%s',
409 1
                $this->wrapText($oldText, 'del', $diffClass),
410 1
                $this->wrapText($newText, 'ins', $diffClass)
411
            );
412
        }
413
414 1
        return $this->diffElements($oldText, $newText);
415
    }
416
417
    /**
418
     * @param Operation $operation
419
     */
420 16
    protected function processEqualOperation($operation)
421
    {
422 16
        $result = array();
423 16
        foreach ($this->newWords as $pos => $s) {
424 16
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
425 16
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
426 13
                    $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s);
427
                } else {
428 12
                    $result[] = $s;
429
                }
430
            }
431
        }
432 16
        $this->content .= implode('', $result);
433 16
    }
434
435
    /**
436
     * @param string $text
437
     * @param string $attribute
438
     *
439
     * @return null|string
440
     */
441 1
    protected function getAttributeFromTag($text, $attribute)
442
    {
443 1
        $matches = array();
444 1
        if (preg_match(sprintf('/<[^>]*\b%s\s*=\s*([\'"])(.*)\1[^>]*>/iu', $attribute), $text, $matches)) {
445 1
            return htmlspecialchars_decode($matches[2]);
446
        }
447
448
        return;
449
    }
450
451
    /**
452
     * @param string $text
453
     *
454
     * @return bool
455
     */
456 13
    protected function isListPlaceholder($text)
457
    {
458 13
        return $this->isPlaceholderType($text, array('ol', 'dl', 'ul'));
459
    }
460
461
    /**
462
     * @param string $text
463
     *
464
     * @return bool
465
     */
466 9
    public function isLinkPlaceholder($text)
467
    {
468 9
        return $this->isPlaceholderType($text, 'a');
469
    }
470
471
    /**
472
     * @param string $text
473
     *
474
     * @return bool
475
     */
476 8
    public function isImagePlaceholder($text)
477
    {
478 8
        return $this->isPlaceholderType($text, 'img');
479
    }
480
481
    /**
482
     * @param string       $text
483
     * @param array|string $types
484
     * @param bool         $strict
485
     *
486
     * @return bool
487
     */
488 13
    protected function isPlaceholderType($text, $types, $strict = true)
489
    {
490 13
        if (!is_array($types)) {
491 10
            $types = array($types);
492
        }
493
494 13
        $criteria = array();
495 13
        foreach ($types as $type) {
496 13
            if ($this->config->isIsolatedDiffTag($type)) {
497 13
                $criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type);
498
            } else {
499
                $criteria[] = $type;
500
            }
501
        }
502
503 13
        return in_array($text, $criteria, $strict);
504
    }
505
506
    /**
507
     * @param string $text
508
     *
509
     * @return bool
510
     */
511 10
    protected function isTablePlaceholder($text)
512
    {
513 10
        return $this->isPlaceholderType($text, 'table');
514
    }
515
516
    /**
517
     * @param Operation $operation
518
     * @param int       $posInNew
519
     *
520
     * @return array
521
     */
522 13
    protected function findIsolatedDiffTagsInOld($operation, $posInNew)
523
    {
524 13
        $offset = $posInNew - $operation->startInNew;
525
526 13
        return $this->oldIsolatedDiffTags[$operation->startInOld + $offset];
527
    }
528
529
    /**
530
     * @param string $tag
531
     * @param string $cssClass
532
     * @param array  $words
533
     */
534 13
    protected function insertTag($tag, $cssClass, &$words)
535
    {
536 13
        while (true) {
537 13
            if (count($words) == 0) {
538 8
                break;
539
            }
540
541 13
            $nonTags = $this->extractConsecutiveWords($words, 'noTag');
542
543 13
            $specialCaseTagInjection = '';
544 13
            $specialCaseTagInjectionIsBefore = false;
545
546 13
            if (count($nonTags) !== 0) {
547 13
                $text = $this->wrapText(implode('', $nonTags), $tag, $cssClass);
548 13
                $this->content .= $text;
549
            } else {
550 6
                $firstOrDefault = false;
551 6
                foreach ($this->config->getSpecialCaseOpeningTags() as $x) {
552
                    if (preg_match($x, $words[ 0 ])) {
553
                        $firstOrDefault = $x;
554
                        break;
555
                    }
556
                }
557 6
                if ($firstOrDefault) {
558
                    $specialCaseTagInjection = '<ins class="mod">';
559
                    if ($tag == 'del') {
560
                        unset($words[ 0 ]);
561
                    }
562 6
                } elseif (array_search($words[ 0 ], $this->config->getSpecialCaseClosingTags()) !== false) {
563
                    $specialCaseTagInjection = '</ins>';
564
                    $specialCaseTagInjectionIsBefore = true;
565
                    if ($tag == 'del') {
566
                        unset($words[ 0 ]);
567
                    }
568
                }
569
            }
570 13
            if (count($words) == 0 && $this->stringUtil->strlen($specialCaseTagInjection) == 0) {
571 12
                break;
572
            }
573 8
            if ($specialCaseTagInjectionIsBefore) {
574
                $this->content .= $specialCaseTagInjection.implode('', $this->extractConsecutiveWords($words, 'tag'));
575
            } else {
576 8
                $workTag = $this->extractConsecutiveWords($words, 'tag');
577 8
                if (isset($workTag[ 0 ]) && $this->isOpeningTag($workTag[ 0 ]) && !$this->isClosingTag($workTag[ 0 ])) {
578 8
                    if ($this->stringUtil->strpos($workTag[ 0 ], 'class=')) {
579 2
                        $workTag[ 0 ] = str_replace('class="', 'class="diffmod ', $workTag[ 0 ]);
580 2
                        $workTag[ 0 ] = str_replace("class='", 'class="diffmod ', $workTag[ 0 ]);
581
                    } else {
582 8
                        $workTag[ 0 ] = str_replace('>', ' class="diffmod">', $workTag[ 0 ]);
583
                    }
584
                }
585
586 8
                $appendContent = implode('', $workTag).$specialCaseTagInjection;
587 8
                if (isset($workTag[0]) && false !== $this->stringUtil->stripos($workTag[0], '<img')) {
588
                    $appendContent = $this->wrapText($appendContent, $tag, $cssClass);
589
                }
590 8
                $this->content .= $appendContent;
591
            }
592
        }
593 13
    }
594
595
    /**
596
     * @param string $word
597
     * @param string $condition
598
     *
599
     * @return bool
600
     */
601 13
    protected function checkCondition($word, $condition)
602
    {
603 13
        return $condition == 'tag' ? $this->isTag($word) : !$this->isTag($word);
604
    }
605
606
    /**
607
     * @param string $text
608
     * @param string $tagName
609
     * @param string $cssClass
610
     *
611
     * @return string
612
     */
613 14
    protected function wrapText($text, $tagName, $cssClass)
614
    {
615 14
        return sprintf('<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text);
616
    }
617
618
    /**
619
     * @param array  $words
620
     * @param string $condition
621
     *
622
     * @return array
623
     */
624 13
    protected function extractConsecutiveWords(&$words, $condition)
625
    {
626 13
        $indexOfFirstTag = null;
627 13
        $words = array_values($words);
628 13
        foreach ($words as $i => $word) {
629 13
            if (!$this->checkCondition($word, $condition)) {
630 8
                $indexOfFirstTag = $i;
631 8
                break;
632
            }
633
        }
634 13
        if ($indexOfFirstTag !== null) {
635 8
            $items = array();
636 8
            foreach ($words as $pos => $s) {
637 8
                if ($pos >= 0 && $pos < $indexOfFirstTag) {
638 8
                    $items[] = $s;
639
                }
640
            }
641 8
            if ($indexOfFirstTag > 0) {
642 8
                array_splice($words, 0, $indexOfFirstTag);
643
            }
644
645 8
            return $items;
646
        } else {
647 13
            $items = array();
648 13
            foreach ($words as $pos => $s) {
649 13
                if ($pos >= 0 && $pos <= count($words)) {
650 13
                    $items[] = $s;
651
                }
652
            }
653 13
            array_splice($words, 0, count($words));
654
655 13
            return $items;
656
        }
657
    }
658
659
    /**
660
     * @param string $item
661
     *
662
     * @return bool
663
     */
664 16
    protected function isTag($item)
665
    {
666 16
        return $this->isOpeningTag($item) || $this->isClosingTag($item);
667
    }
668
669
    /**
670
     * @param string $item
671
     *
672
     * @return bool
673
     */
674 16
    protected function isOpeningTag($item)
675
    {
676 16
        return preg_match('#<[^>]+>\\s*#iUu', $item);
677
    }
678
679
    /**
680
     * @param string $item
681
     *
682
     * @return bool
683
     */
684 16
    protected function isClosingTag($item)
685
    {
686 16
        return preg_match('#</[^>]+>\\s*#iUu', $item);
687
    }
688
689
    /**
690
     * @return Operation[]
691
     */
692 16
    protected function operations()
693
    {
694 16
        $positionInOld = 0;
695 16
        $positionInNew = 0;
696 16
        $operations = array();
697
698 16
        $matches   = $this->matchingBlocks();
699 16
        $matches[] = new Match(count($this->oldWords), count($this->newWords), 0);
700
701 16
        foreach ($matches as $match) {
702 16
            $matchStartsAtCurrentPositionInOld = ($positionInOld === $match->startInOld);
703 16
            $matchStartsAtCurrentPositionInNew = ($positionInNew === $match->startInNew);
704
705 16
            if ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === false) {
706 9
                $action = 'replace';
707 16
            } elseif ($matchStartsAtCurrentPositionInOld === true && $matchStartsAtCurrentPositionInNew === false) {
708 9
                $action = 'insert';
709 16
            } elseif ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === true) {
710 4
                $action = 'delete';
711
            } else { // This occurs if the first few words are the same in both versions
712 16
                $action = 'none';
713
            }
714
715 16
            if ($action !== 'none') {
716 13
                $operations[] = new Operation($action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew);
717
            }
718
719 16
            if (count($match) !== 0) {
720 16
                $operations[] = new Operation('equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew());
721
            }
722
723 16
            $positionInOld = $match->endInOld();
724 16
            $positionInNew = $match->endInNew();
725
        }
726
727 16
        return $operations;
728
    }
729
730
    /**
731
     * @return Match[]
732
     */
733 16
    protected function matchingBlocks()
734
    {
735 16
        $matchingBlocks = array();
736 16
        $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks);
737
738 16
        return $matchingBlocks;
739
    }
740
741
    /**
742
     * @param int   $startInOld
743
     * @param int   $endInOld
744
     * @param int   $startInNew
745
     * @param int   $endInNew
746
     * @param array $matchingBlocks
747
     */
748 16
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
749
    {
750 16
        $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew);
751
752 16
        if ($match !== null) {
753 16
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
754 7
                $this->findMatchingBlocks($startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks);
755
            }
756
757 16
            $matchingBlocks[] = $match;
758
759 16
            if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) {
760 9
                $this->findMatchingBlocks($match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks);
761
            }
762
        }
763 16
    }
764
765
    /**
766
     * @param string $word
767
     *
768
     * @return string
769
     */
770 8
    protected function stripTagAttributes($word)
771
    {
772 8
        $space = $this->stringUtil->strpos($word, ' ', 1);
773
774 8
        if ($space) {
775 5
            return '<' . $this->stringUtil->substr($word, 1, $space) . '>';
776
        }
777
778 6
        return trim($word, '<>');
779
    }
780
781
    /**
782
     * @param int $startInOld
783
     * @param int $endInOld
784
     * @param int $startInNew
785
     * @param int $endInNew
786
     *
787
     * @return Match|null
788
     */
789 16
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
790
    {
791 16
        $groupDiffs     = $this->isGroupDiffs();
0 ignored issues
show
Deprecated Code introduced by
The function Caxy\HtmlDiff\AbstractDiff::isGroupDiffs() has been deprecated: since 0.1.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

791
        $groupDiffs     = /** @scrutinizer ignore-deprecated */ $this->isGroupDiffs();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
792 16
        $bestMatchInOld = $startInOld;
793 16
        $bestMatchInNew = $startInNew;
794 16
        $bestMatchSize = 0;
795 16
        $matchLengthAt = array();
796
797 16
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; ++$indexInOld) {
798 16
            $newMatchLengthAt = array();
799 16
            $index = $this->oldWords[ $indexInOld ];
800 16
            if ($this->isTag($index)) {
801 6
                $index = $this->stripTagAttributes($index);
802
            }
803 16
            if (!isset($this->wordIndices[ $index ])) {
804 11
                $matchLengthAt = $newMatchLengthAt;
805 11
                continue;
806
            }
807 16
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
808 16
                if ($indexInNew < $startInNew) {
809 7
                    continue;
810
                }
811 16
                if ($indexInNew >= $endInNew) {
812 7
                    break;
813
                }
814
815 16
                $newMatchLength = (isset($matchLengthAt[ $indexInNew - 1 ]) ? $matchLengthAt[ $indexInNew - 1 ] : 0) + 1;
816 16
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
817
818 16
                if ($newMatchLength > $bestMatchSize ||
819
                    (
820 11
                        $groupDiffs &&
821 11
                        $bestMatchSize > 0 &&
822 11
                        $this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize))
823
                    )
824
                ) {
825 16
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
826 16
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
827 16
                    $bestMatchSize = $newMatchLength;
828
                }
829
            }
830 16
            $matchLengthAt = $newMatchLengthAt;
831
        }
832
833
        // Skip match if none found or match consists only of whitespace
834 16
        if ($bestMatchSize !== 0 &&
835
            (
836 16
                !$groupDiffs ||
837 16
                !$this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize))
838
            )
839
        ) {
840 16
            return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
841
        }
842
843 9
        return null;
844
    }
845
846
    /**
847
     * @param string $str
848
     *
849
     * @return bool
850
     */
851 16
    protected function isOnlyWhitespace($str)
852
    {
853
        //  Slightly faster then using preg_match
854 16
        return $str !== '' && trim($str) === '';
855
    }
856
857
    /**
858
     * Special array_slice function that caches its last request.
859
     *
860
     * The diff algorithm seems to request the same information many times in a row.
861
     * by returning the previous answer the algorithm preforms way faster.
862
     *
863
     * The result is a string instead of an array, this way we safe on the amount of
864
     * memory intensive implode() calls.
865
     *
866
     * @param array         &$array
867
     * @param integer       $offset
868
     * @param integer|null  $length
869
     *
870
     * @return string
871
     */
872 16
    protected function array_slice_cached(&$array, $offset, $length = null)
0 ignored issues
show
Coding Style introduced by
Method name "HtmlDiff::array_slice_cached" is not in camel caps format
Loading history...
873
    {
874 16
        static $lastOffset = null;
875 16
        static $lastLength = null;
876 16
        static $cache      = null;
877
878
        // PHP has no support for by-reference comparing.
879
        // to prevent false positive hits, reset the cache when the oldWords or newWords is changed.
880 16
        if ($this->resetCache === true) {
881 16
            $cache = null;
882
883 16
            $this->resetCache = false;
884
        }
885
886
        if (
887 16
            $cache !== null &&
888 11
            $lastLength === $length &&
889 11
            $lastOffset === $offset
890
        ) { // Hit
891 11
            return $cache;
892
        } // Miss
893
894 16
        $lastOffset = $offset;
895 16
        $lastLength = $length;
896
897 16
        $cache = implode('', array_slice($array, $offset, $length));
898
899 16
        return $cache;
900
    }
901
}
902