Passed
Push — master ( 8430aa...11d349 )
by Josh
01:24
created

HtmlDiff   D

Complexity

Total Complexity 158

Size/Duplication

Total Lines 824
Duplicated Lines 13.11 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 90.49%

Importance

Changes 48
Bugs 6 Features 13
Metric Value
c 48
b 6
f 13
dl 108
loc 824
ccs 371
cts 410
cp 0.9049
rs 4.4444
wmc 158
lcom 1
cbo 6

40 Methods

Rating   Name   Duplication   Size   Complexity  
A setInsertSpaceInReplace() 0 6 1
A getInsertSpaceInReplace() 0 4 1
A create() 10 10 2
A setUseTableDiffing() 0 6 1
B build() 0 33 6
A indexNewWords() 0 14 4
A replaceIsolatedDiffTags() 0 5 1
C createIsolatedDiffTagPlaceholders() 12 44 11
A isOpeningIsolatedDiffTag() 14 14 4
A isSelfClosingTag() 0 4 1
A isClosingIsolatedDiffTag() 14 14 4
B performOperation() 0 19 5
A processReplaceOperation() 0 5 1
B processInsertOperation() 16 16 7
B processDeleteOperation() 16 16 7
B diffIsolatedPlaceholder() 0 17 6
B diffElements() 0 21 5
A diffList() 0 6 1
A diffTables() 0 6 1
A diffElementsByAttribute() 0 17 2
B processEqualOperation() 14 14 6
A getAttributeFromTag() 0 9 2
A isListPlaceholder() 0 4 1
A isLinkPlaceholder() 0 4 1
A isImagePlaceholder() 0 4 1
A isPlaceholderType() 0 17 4
A isTablePlaceholder() 0 4 1
A findIsolatedDiffTagsInOld() 0 6 1
C insertTag() 0 60 19
A checkCondition() 0 4 2
A wrapText() 0 4 1
C extractConsecutiveWords() 10 34 11
A isTag() 0 4 2
A isOpeningTag() 0 4 1
A isClosingTag() 0 4 1
D operations() 0 33 10
A matchingBlocks() 0 7 1
B findMatchingBlocks() 0 13 6
A stripTagAttributes() 0 6 1
C findMatch() 0 55 15

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 12 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 12
        $diff = new self($oldText, $newText);
43
44 12
        if (null !== $config) {
45 12
            $diff->setConfig($config);
46 12
        }
47
48 12
        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 14
    public function build()
93
    {
94 14
        $this->prepare();
95
96 14
        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 14
        if ($this->oldText == $this->newText) {
106 9
            return $this->newText;
107
        }
108
109 14
        $this->splitInputsToWords();
110 14
        $this->replaceIsolatedDiffTags();
111 14
        $this->indexNewWords();
112
113 14
        $operations = $this->operations();
114
115 14
        foreach ($operations as $item) {
116 14
            $this->performOperation($item);
117 14
        }
118
119 14
        if ($this->hasDiffCache()) {
120
            $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
121
        }
122
123 14
        return $this->content;
124
    }
125
126 14
    protected function indexNewWords()
127
    {
128 14
        $this->wordIndices = array();
129 14
        foreach ($this->newWords as $i => $word) {
130 14
            if ($this->isTag($word)) {
131 8
                $word = $this->stripTagAttributes($word);
132 8
            }
133 14
            if (isset($this->wordIndices[ $word ])) {
134 11
                $this->wordIndices[ $word ][] = $i;
135 11
            } else {
136 14
                $this->wordIndices[ $word ] = array($i);
137
            }
138 14
        }
139 14
    }
140
141 14
    protected function replaceIsolatedDiffTags()
142
    {
143 14
        $this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords);
144 14
        $this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords);
145 14
    }
146
147
    /**
148
     * @param array $words
149
     *
150
     * @return array
151
     */
152 14
    protected function createIsolatedDiffTagPlaceholders(&$words)
153
    {
154 14
        $openIsolatedDiffTags = 0;
155 14
        $isolatedDiffTagIndices = array();
156 14
        $isolatedDiffTagStart = 0;
157 14
        $currentIsolatedDiffTag = null;
158 14
        foreach ($words as $index => $word) {
159 14
            $openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag);
160 14
            if ($openIsolatedDiffTag) {
161 13
                if ($this->isSelfClosingTag($word) || 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 13
                    if ($openIsolatedDiffTags === 0) {
172 13
                        $isolatedDiffTagStart = $index;
173 13
                    }
174 13
                    ++$openIsolatedDiffTags;
175 13
                    $currentIsolatedDiffTag = $openIsolatedDiffTag;
176
                }
177 14
            } elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) {
178 13
                --$openIsolatedDiffTags;
179 13 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 13
                    $isolatedDiffTagIndices[] = array('start' => $isolatedDiffTagStart, 'length' => $index - $isolatedDiffTagStart + 1, 'tagType' => $currentIsolatedDiffTag);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 174 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...
181 13
                    $currentIsolatedDiffTag = null;
182 13
                }
183 13
            }
184 14
        }
185 14
        $isolatedDiffTagScript = array();
186 14
        $offset = 0;
187 14
        foreach ($isolatedDiffTagIndices as $isolatedDiffTagIndex) {
188 13
            $start = $isolatedDiffTagIndex['start'] - $offset;
189 13
            $placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']);
190 13
            $isolatedDiffTagScript[$start] = array_splice($words, $start, $isolatedDiffTagIndex['length'], $placeholderString);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 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...
191 13
            $offset += $isolatedDiffTagIndex['length'] - 1;
192 14
        }
193
194 14
        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 14 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
        $tagsToMatch = $currentIsolatedDiffTag !== null
206 14
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
207 14
            : $this->config->getIsolatedDiffTags();
208 14
        $pattern = '#<%s(\s+[^>]*)?>#iU';
209 14
        foreach ($tagsToMatch as $key => $value) {
210 14
            if (preg_match(sprintf($pattern, $key), $item)) {
211 13
                return $key;
212
            }
213 14
        }
214
215 14
        return false;
216
    }
217
218 13
    protected function isSelfClosingTag($text)
219
    {
220 13
        return (bool) preg_match('/<[^>]+\/\s*>/', $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 13 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
        $tagsToMatch = $currentIsolatedDiffTag !== null
232 13
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
233 13
            : $this->config->getIsolatedDiffTags();
234 13
        $pattern = '#</%s(\s+[^>]*)?>#iU';
235 13
        foreach ($tagsToMatch as $key => $value) {
236 13
            if (preg_match(sprintf($pattern, $key), $item)) {
237 13
                return $key;
238
            }
239 13
        }
240
241 13
        return false;
242
    }
243
244
    /**
245
     * @param Operation $operation
246
     */
247 14
    protected function performOperation($operation)
248
    {
249 14
        switch ($operation->action) {
250 14
            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 14
            $this->processEqualOperation($operation);
252 14
            break;
253 11
            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 11
            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 8
            $this->processInsertOperation($operation, 'diffins');
258 8
            break;
259 8
            case 'replace':
260 8
            $this->processReplaceOperation($operation);
261 8
            break;
262
            default:
263
            break;
264 14
        }
265 14
    }
266
267
    /**
268
     * @param Operation $operation
269
     */
270 8
    protected function processReplaceOperation($operation)
271
    {
272 8
        $this->processDeleteOperation($operation, 'diffmod');
273 8
        $this->processInsertOperation($operation, 'diffmod');
274 8
    }
275
276
    /**
277
     * @param Operation $operation
278
     * @param string    $cssClass
279
     */
280 11 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 11
        $text = array();
283 11
        foreach ($this->newWords as $pos => $s) {
284 11
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
285 11
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
286 3
                    foreach ($this->newIsolatedDiffTags[$pos] as $word) {
287 3
                        $text[] = $word;
288 3
                    }
289 3
                } else {
290 11
                    $text[] = $s;
291
                }
292 11
            }
293 11
        }
294 11
        $this->insertTag('ins', $cssClass, $text);
295 11
    }
296
297
    /**
298
     * @param Operation $operation
299
     * @param string    $cssClass
300
     */
301 10 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 10
        $text = array();
304 10
        foreach ($this->oldWords as $pos => $s) {
305 10
            if ($pos >= $operation->startInOld && $pos < $operation->endInOld) {
306 10
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->oldIsolatedDiffTags[$pos])) {
307 6
                    foreach ($this->oldIsolatedDiffTags[$pos] as $word) {
308 6
                        $text[] = $word;
309 6
                    }
310 6
                } else {
311 10
                    $text[] = $s;
312
                }
313 10
            }
314 10
        }
315 10
        $this->insertTag('del', $cssClass, $text);
316 10
    }
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 12
    protected function diffIsolatedPlaceholder($operation, $pos, $placeholder, $stripWrappingTags = true)
327
    {
328 12
        $oldText = implode('', $this->findIsolatedDiffTagsInOld($operation, $pos));
329 12
        $newText = implode('', $this->newIsolatedDiffTags[$pos]);
330
331 12
        if ($this->isListPlaceholder($placeholder)) {
332 7
            return $this->diffList($oldText, $newText);
333 9
        } elseif ($this->config->isUseTableDiffing() && $this->isTablePlaceholder($placeholder)) {
334
            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 = '/(^<[^>]+>)|(<\/[^>]+>$)/i';
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 9
            }
364 9
            $oldText = preg_replace($pattern, '', $oldText);
365 9
            $newText = preg_replace($pattern, '', $newText);
366 9
        }
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
    protected function diffTables($oldText, $newText)
393
    {
394
        $diff = TableDiff::create($oldText, $newText, $this->config);
395
396
        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 1
            );
412
        }
413
414 1
        return $this->diffElements($oldText, $newText);
415
    }
416
417
    /**
418
     * @param Operation $operation
419
     */
420 14 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 14
        $result = array();
423 14
        foreach ($this->newWords as $pos => $s) {
424 14
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
425 14
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
426 12
                    $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s);
427 12
                } else {
428 14
                    $result[] = $s;
429
                }
430 14
            }
431 14
        }
432 14
        $this->content .= implode('', $result);
433 14
    }
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[^>]*>/i', $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 12
    protected function isListPlaceholder($text)
457
    {
458 12
        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 12
    protected function isPlaceholderType($text, $types, $strict = true)
489
    {
490 12
        if (!is_array($types)) {
491 9
            $types = array($types);
492 9
        }
493
494 12
        $criteria = array();
495 12
        foreach ($types as $type) {
496 12
            if ($this->config->isIsolatedDiffTag($type)) {
497 12
                $criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type);
498 12
            } else {
499
                $criteria[] = $type;
500
            }
501 12
        }
502
503 12
        return in_array($text, $criteria, $strict);
504
    }
505
506
    /**
507
     * @param string $text
508
     *
509
     * @return bool
510
     */
511 9
    protected function isTablePlaceholder($text)
512
    {
513 9
        return $this->isPlaceholderType($text, 'table');
514
    }
515
516
    /**
517
     * @param Operation $operation
518
     * @param int       $posInNew
519
     *
520
     * @return array
521
     */
522 12
    protected function findIsolatedDiffTagsInOld($operation, $posInNew)
523
    {
524 12
        $offset = $posInNew - $operation->startInNew;
525
526 12
        return $this->oldIsolatedDiffTags[$operation->startInOld + $offset];
527
    }
528
529
    /**
530
     * @param string $tag
531
     * @param string $cssClass
532
     * @param array  $words
533
     */
534 11
    protected function insertTag($tag, $cssClass, &$words)
535
    {
536 11
        while (true) {
537 11
            if (count($words) == 0) {
538 11
                break;
539
            }
540
541 11
            $nonTags = $this->extractConsecutiveWords($words, 'noTag');
542
543 11
            $specialCaseTagInjection = '';
544 11
            $specialCaseTagInjectionIsBefore = false;
545
546 11
            if (count($nonTags) != 0) {
547 11
                $text = $this->wrapText(implode('', $nonTags), $tag, $cssClass);
548 11
                $this->content .= $text;
549 11
            } 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 6
                }
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 11
            if (count($words) == 0 && count($specialCaseTagInjection) == 0) {
571
                break;
572
            }
573 11
            if ($specialCaseTagInjectionIsBefore) {
574
                $this->content .= $specialCaseTagInjection.implode('', $this->extractConsecutiveWords($words, 'tag'));
575
            } else {
576 11
                $workTag = $this->extractConsecutiveWords($words, 'tag');
577 11
                if (isset($workTag[ 0 ]) && $this->isOpeningTag($workTag[ 0 ]) && !$this->isClosingTag($workTag[ 0 ])) {
578 8
                    if (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 2
                    } else {
582 8
                        $workTag[ 0 ] = str_replace('>', ' class="diffmod">', $workTag[ 0 ]);
583
                    }
584 8
                }
585
586 11
                $appendContent = implode('', $workTag).$specialCaseTagInjection;
587 11
                if (isset($workTag[0]) && false !== stripos($workTag[0], '<img')) {
588
                    $appendContent = $this->wrapText($appendContent, $tag, $cssClass);
589
                }
590 11
                $this->content .= $appendContent;
591
            }
592 11
        }
593 11
    }
594
595
    /**
596
     * @param string $word
597
     * @param string $condition
598
     *
599
     * @return bool
600
     */
601 11
    protected function checkCondition($word, $condition)
602
    {
603 11
        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 12
    protected function wrapText($text, $tagName, $cssClass)
614
    {
615 12
        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 11
    protected function extractConsecutiveWords(&$words, $condition)
625
    {
626 11
        $indexOfFirstTag = null;
627 11
        $words = array_values($words);
628 11
        foreach ($words as $i => $word) {
629 11
            if (!$this->checkCondition($word, $condition)) {
630 8
                $indexOfFirstTag = $i;
631 8
                break;
632
            }
633 11
        }
634 11
        if ($indexOfFirstTag !== null) {
635 8
            $items = array();
636 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...
637 8
                if ($pos >= 0 && $pos < $indexOfFirstTag) {
638 8
                    $items[] = $s;
639 8
                }
640 8
            }
641 8
            if ($indexOfFirstTag > 0) {
642 8
                array_splice($words, 0, $indexOfFirstTag);
643 8
            }
644
645 8
            return $items;
646
        } else {
647 11
            $items = array();
648 11 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 11
                if ($pos >= 0 && $pos <= count($words)) {
650 11
                    $items[] = $s;
651 11
                }
652 11
            }
653 11
            array_splice($words, 0, count($words));
654
655 11
            return $items;
656
        }
657
    }
658
659
    /**
660
     * @param string $item
661
     *
662
     * @return bool
663
     */
664 14
    protected function isTag($item)
665
    {
666 14
        return $this->isOpeningTag($item) || $this->isClosingTag($item);
667
    }
668
669
    /**
670
     * @param string $item
671
     *
672
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be integer?

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...
673
     */
674 14
    protected function isOpeningTag($item)
675
    {
676 14
        return preg_match('#<[^>]+>\\s*#iU', $item);
677
    }
678
679
    /**
680
     * @param string $item
681
     *
682
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be integer?

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...
683
     */
684 14
    protected function isClosingTag($item)
685
    {
686 14
        return preg_match('#</[^>]+>\\s*#iU', $item);
687
    }
688
689
    /**
690
     * @return Operation[]
691
     */
692 14
    protected function operations()
693
    {
694 14
        $positionInOld = 0;
695 14
        $positionInNew = 0;
696 14
        $operations = array();
697 14
        $matches = $this->matchingBlocks();
698 14
        $matches[] = new Match(count($this->oldWords), count($this->newWords), 0);
699 14
        foreach ($matches as $i => $match) {
700 14
            $matchStartsAtCurrentPositionInOld = ($positionInOld == $match->startInOld);
701 14
            $matchStartsAtCurrentPositionInNew = ($positionInNew == $match->startInNew);
702 14
            $action = 'none';
0 ignored issues
show
Unused Code introduced by
$action is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
703
704 14
            if ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
705 8
                $action = 'replace';
706 14
            } elseif ($matchStartsAtCurrentPositionInOld == true && $matchStartsAtCurrentPositionInNew == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
707 8
                $action = 'insert';
708 14
            } elseif ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == true) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
709 4
                $action = 'delete';
710 4
            } else { // This occurs if the first few words are the same in both versions
711 14
                $action = 'none';
712
            }
713 14
            if ($action != 'none') {
714 11
                $operations[] = new Operation($action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 127 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...
715 11
            }
716 14
            if (count($match) != 0) {
717 14
                $operations[] = new Operation('equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew());
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 135 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...
718 14
            }
719 14
            $positionInOld = $match->endInOld();
720 14
            $positionInNew = $match->endInNew();
721 14
        }
722
723 14
        return $operations;
724
    }
725
726
    /**
727
     * @return Match[]
728
     */
729 14
    protected function matchingBlocks()
730
    {
731 14
        $matchingBlocks = array();
732 14
        $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks);
733
734 14
        return $matchingBlocks;
735
    }
736
737
    /**
738
     * @param int   $startInOld
739
     * @param int   $endInOld
740
     * @param int   $startInNew
741
     * @param int   $endInNew
742
     * @param array $matchingBlocks
743
     */
744 14
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
745
    {
746 14
        $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew);
747 14
        if ($match !== null) {
748 14
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
749 7
                $this->findMatchingBlocks($startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 125 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...
750 7
            }
751 14
            $matchingBlocks[] = $match;
752 14
            if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) {
753 11
                $this->findMatchingBlocks($match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

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

Loading history...
754 11
            }
755 14
        }
756 14
    }
757
758
    /**
759
     * @param string $word
760
     *
761
     * @return string
762
     */
763 8
    protected function stripTagAttributes($word)
764
    {
765 8
        $word = explode(' ', trim($word, '<>'));
766
767 8
        return '<'.$word[ 0 ].'>';
768
    }
769
770
    /**
771
     * @param int $startInOld
772
     * @param int $endInOld
773
     * @param int $startInNew
774
     * @param int $endInNew
775
     *
776
     * @return Match|null
777
     */
778 14
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
779
    {
780 14
        $bestMatchInOld = $startInOld;
781 14
        $bestMatchInNew = $startInNew;
782 14
        $bestMatchSize = 0;
783 14
        $matchLengthAt = array();
784 14
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; ++$indexInOld) {
785 14
            $newMatchLengthAt = array();
786 14
            $index = $this->oldWords[ $indexInOld ];
787 14
            if ($this->isTag($index)) {
788 6
                $index = $this->stripTagAttributes($index);
789 6
            }
790 14
            if (!isset($this->wordIndices[ $index ])) {
791 10
                $matchLengthAt = $newMatchLengthAt;
792 10
                continue;
793
            }
794 14
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
795 14
                if ($indexInNew < $startInNew) {
796 7
                    continue;
797
                }
798 14
                if ($indexInNew >= $endInNew) {
799 7
                    break;
800
                }
801 14
                $newMatchLength = (isset($matchLengthAt[ $indexInNew - 1 ]) ? $matchLengthAt[ $indexInNew - 1 ] : 0) + 1;
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 121 characters

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

Loading history...
802 14
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
803 14
                if ($newMatchLength > $bestMatchSize ||
804
                    (
805 11
                        $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...
806 11
                        $bestMatchSize > 0 &&
807 11
                        preg_match(
808 11
                            '/^\s+$/',
809 11
                            implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize))
810 11
                        )
811 11
                    )
812 14
                ) {
813 14
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
814 14
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
815 14
                    $bestMatchSize = $newMatchLength;
816 14
                }
817 14
            }
818 14
            $matchLengthAt = $newMatchLengthAt;
819 14
        }
820
821
        // Skip match if none found or match consists only of whitespace
822 14
        if ($bestMatchSize != 0 &&
823
            (
824 14
                !$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...
825 14
                !preg_match('/^\s+$/', implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize)))
826 14
            )
827 14
        ) {
828 14
            return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
829
        }
830
831 8
        return;
832
    }
833
}
834