Passed
Push — master ( 4c142a...e9b438 )
by Sven
01:33 queued 10s
created

HtmlDiff::insertTag()   F

Complexity

Conditions 20
Paths 162

Size

Total Lines 72

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 28.6963

Importance

Changes 0
Metric Value
cc 20
nc 162
nop 3
dl 0
loc 72
ccs 31
cts 43
cp 0.7209
crap 28.6963
rs 3.65
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 View Code Duplication
    public static function create($oldText, $newText, HtmlDiffConfig $config = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
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 View Code Duplication
                    if ($openIsolatedDiffTags === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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 View Code Duplication
                if ($openIsolatedDiffTags == 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|false?

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...
202
     */
203 16 View Code Duplication
    protected function isOpeningIsolatedDiffTag($item, $currentIsolatedDiffTag = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|false?

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...
228
     */
229 14 View Code Duplication
    protected function isClosingIsolatedDiffTag($item, $currentIsolatedDiffTag = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
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 View Code Duplication
    protected function processInsertOperation($operation, $cssClass)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
281
    {
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 View Code Duplication
    protected function processDeleteOperation($operation, $cssClass)
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...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|string?

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...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|string?

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...
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 View Code Duplication
    protected function processEqualOperation($operation)
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...
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
578
                if (
579 8
                    isset($workTag[0]) === true &&
580 8
                    $this->isOpeningTag($workTag[0]) === true &&
581 8
                    $this->isClosingTag($workTag[0]) === false
582
                ) {
583 8
                    if ($this->stringUtil->strpos($workTag[0], 'class=')) {
584 2
                        $workTag[0] = str_replace('class="', 'class="diffmod ', $workTag[0]);
585
                    } else {
586 8
                        $isSelfClosing = $this->stringUtil->strpos($workTag[0], '/>') !== false;
587
588 8
                        if ($isSelfClosing === true) {
589 4
                            $workTag[0] = str_replace('/>', ' class="diffmod" />', $workTag[0]);
590
                        } else {
591 7
                            $workTag[0] = str_replace('>', ' class="diffmod">', $workTag[0]);
592
                        }
593
                    }
594
                }
595
596 8
                $appendContent = implode('', $workTag) . $specialCaseTagInjection;
597
598 8
                if (isset($workTag[0]) === true && $this->stringUtil->stripos($workTag[0], '<img') !== false) {
599
                    $appendContent = $this->wrapText($appendContent, $tag, $cssClass);
600
                }
601
602 8
                $this->content .= $appendContent;
603
            }
604
        }
605 13
    }
606
607
    /**
608
     * @param string $word
609
     * @param string $condition
610
     *
611
     * @return bool
612
     */
613 13
    protected function checkCondition($word, $condition)
614
    {
615 13
        return $condition == 'tag' ? $this->isTag($word) : !$this->isTag($word);
616
    }
617
618
    /**
619
     * @param string $text
620
     * @param string $tagName
621
     * @param string $cssClass
622
     *
623
     * @return string
624
     */
625 14
    protected function wrapText($text, $tagName, $cssClass)
626
    {
627 14
        return sprintf('<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text);
628
    }
629
630
    /**
631
     * @param array  $words
632
     * @param string $condition
633
     *
634
     * @return array
635
     */
636 13
    protected function extractConsecutiveWords(&$words, $condition)
637
    {
638 13
        $indexOfFirstTag = null;
639 13
        $words = array_values($words);
640 13
        foreach ($words as $i => $word) {
641 13
            if (!$this->checkCondition($word, $condition)) {
642 8
                $indexOfFirstTag = $i;
643 8
                break;
644
            }
645
        }
646 13
        if ($indexOfFirstTag !== null) {
647 8
            $items = array();
648 8 View Code Duplication
            foreach ($words as $pos => $s) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
649 8
                if ($pos >= 0 && $pos < $indexOfFirstTag) {
650 8
                    $items[] = $s;
651
                }
652
            }
653 8
            if ($indexOfFirstTag > 0) {
654 8
                array_splice($words, 0, $indexOfFirstTag);
655
            }
656
657 8
            return $items;
658
        } else {
659 13
            $items = array();
660 13 View Code Duplication
            foreach ($words as $pos => $s) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
661 13
                if ($pos >= 0 && $pos <= count($words)) {
662 13
                    $items[] = $s;
663
                }
664
            }
665 13
            array_splice($words, 0, count($words));
666
667 13
            return $items;
668
        }
669
    }
670
671
    /**
672
     * @param string $item
673
     *
674
     * @return bool
675
     */
676 16
    protected function isTag($item)
677
    {
678 16
        return $this->isOpeningTag($item) || $this->isClosingTag($item);
679
    }
680
681 16
    protected function isOpeningTag($item) : bool
682
    {
683 16
        return preg_match('#<[^>]+>\\s*#iUu', $item) === 1;
684
    }
685
686 16
    protected function isClosingTag($item) : bool
687
    {
688 16
        return preg_match('#</[^>]+>\\s*#iUu', $item) === 1;
689
    }
690
691
    /**
692
     * @return Operation[]
693
     */
694 16
    protected function operations()
695
    {
696 16
        $positionInOld = 0;
697 16
        $positionInNew = 0;
698 16
        $operations = array();
699
700 16
        $matches   = $this->matchingBlocks();
701 16
        $matches[] = new MatchingBlock(count($this->oldWords), count($this->newWords), 0);
702
703 16
        foreach ($matches as $match) {
704 16
            $matchStartsAtCurrentPositionInOld = ($positionInOld === $match->startInOld);
705 16
            $matchStartsAtCurrentPositionInNew = ($positionInNew === $match->startInNew);
706
707 16
            if ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === false) {
708 9
                $action = 'replace';
709 16
            } elseif ($matchStartsAtCurrentPositionInOld === true && $matchStartsAtCurrentPositionInNew === false) {
710 9
                $action = 'insert';
711 16
            } elseif ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === true) {
712 4
                $action = 'delete';
713
            } else { // This occurs if the first few words are the same in both versions
714 16
                $action = 'none';
715
            }
716
717 16
            if ($action !== 'none') {
718 13
                $operations[] = new Operation($action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew);
719
            }
720
721 16
            if (count($match) !== 0) {
722 16
                $operations[] = new Operation('equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew());
723
            }
724
725 16
            $positionInOld = $match->endInOld();
726 16
            $positionInNew = $match->endInNew();
727
        }
728
729 16
        return $operations;
730
    }
731
732
    /**
733
     * @return MatchingBlock[]
734
     */
735 16
    protected function matchingBlocks()
736
    {
737 16
        $matchingBlocks = array();
738 16
        $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks);
739
740 16
        return $matchingBlocks;
741
    }
742
743
    /**
744
     * @param int   $startInOld
745
     * @param int   $endInOld
746
     * @param int   $startInNew
747
     * @param int   $endInNew
748
     * @param array $matchingBlocks
749
     */
750 16
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
751
    {
752 16
        $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew);
753
754 16
        if ($match !== null) {
755 16
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
756 7
                $this->findMatchingBlocks($startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks);
757
            }
758
759 16
            $matchingBlocks[] = $match;
760
761 16
            if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) {
762 9
                $this->findMatchingBlocks($match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks);
763
            }
764
        }
765 16
    }
766
767
    /**
768
     * @param string $word
769
     *
770
     * @return string
771
     */
772 8
    protected function stripTagAttributes($word)
773
    {
774 8
        $space = $this->stringUtil->strpos($word, ' ', 1);
775
776 8
        if ($space) {
777 5
            return '<' . $this->stringUtil->substr($word, 1, $space) . '>';
778
        }
779
780 6
        return trim($word, '<>');
781
    }
782
783
    /**
784
     * @param int $startInOld
785
     * @param int $endInOld
786
     * @param int $startInNew
787
     * @param int $endInNew
788
     *
789
     * @return MatchingBlock|null
790
     */
791 16
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
792
    {
793 16
        $groupDiffs     = $this->isGroupDiffs();
0 ignored issues
show
Deprecated Code introduced by
The method Caxy\HtmlDiff\AbstractDiff::isGroupDiffs() has been deprecated with message: since 0.1.0

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

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

Loading history...
794 16
        $bestMatchInOld = $startInOld;
795 16
        $bestMatchInNew = $startInNew;
796 16
        $bestMatchSize = 0;
797 16
        $matchLengthAt = array();
798
799 16
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; ++$indexInOld) {
800 16
            $newMatchLengthAt = array();
801 16
            $index = $this->oldWords[ $indexInOld ];
802 16
            if ($this->isTag($index)) {
803 6
                $index = $this->stripTagAttributes($index);
804
            }
805 16
            if (!isset($this->wordIndices[ $index ])) {
806 11
                $matchLengthAt = $newMatchLengthAt;
807 11
                continue;
808
            }
809 16
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
810 16
                if ($indexInNew < $startInNew) {
811 7
                    continue;
812
                }
813 16
                if ($indexInNew >= $endInNew) {
814 7
                    break;
815
                }
816
817 16
                $newMatchLength = (isset($matchLengthAt[ $indexInNew - 1 ]) ? $matchLengthAt[ $indexInNew - 1 ] : 0) + 1;
818 16
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
819
820 16
                if ($newMatchLength > $bestMatchSize ||
821
                    (
822 11
                        $groupDiffs &&
823 11
                        $bestMatchSize > 0 &&
824 16
                        $this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize))
825
                    )
826
                ) {
827 16
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
828 16
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
829 16
                    $bestMatchSize = $newMatchLength;
830
                }
831
            }
832 16
            $matchLengthAt = $newMatchLengthAt;
833
        }
834
835
        // Skip match if none found or match consists only of whitespace
836 16
        if ($bestMatchSize !== 0 &&
837
            (
838 16
                !$groupDiffs ||
839 16
                !$this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize))
840
            )
841
        ) {
842 16
            return new MatchingBlock($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
843
        }
844
845 9
        return null;
846
    }
847
848
    /**
849
     * @param string $str
850
     *
851
     * @return bool
852
     */
853 16
    protected function isOnlyWhitespace($str)
854
    {
855
        //  Slightly faster then using preg_match
856 16
        return $str !== '' && trim($str) === '';
857
    }
858
859
    /**
860
     * Special array_slice function that caches its last request.
861
     *
862
     * The diff algorithm seems to request the same information many times in a row.
863
     * by returning the previous answer the algorithm preforms way faster.
864
     *
865
     * The result is a string instead of an array, this way we safe on the amount of
866
     * memory intensive implode() calls.
867
     *
868
     * @param array         &$array
869
     * @param integer       $offset
870
     * @param integer|null  $length
871
     *
872
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be null|string?

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...
873
     */
874 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...
875
    {
876 16
        static $lastOffset = null;
877 16
        static $lastLength = null;
878 16
        static $cache      = null;
879
880
        // PHP has no support for by-reference comparing.
881
        // to prevent false positive hits, reset the cache when the oldWords or newWords is changed.
882 16
        if ($this->resetCache === true) {
883 16
            $cache = null;
884
885 16
            $this->resetCache = false;
886
        }
887
888
        if (
889 16
            $cache !== null &&
890 16
            $lastLength === $length &&
891 16
            $lastOffset === $offset
892
        ) { // Hit
893 11
            return $cache;
894
        } // Miss
895
896 16
        $lastOffset = $offset;
897 16
        $lastLength = $length;
898
899 16
        $cache = implode('', array_slice($array, $offset, $length));
900
901 16
        return $cache;
902
    }
903
}
904