Passed
Push — master ( d7540c...231250 )
by Josh
05:57
created

HtmlDiff::diffIsolatedPlaceholder()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.0208

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 12
nc 5
nop 4
dl 0
loc 17
ccs 11
cts 12
cp 0.9167
crap 6.0208
rs 8.8571
c 2
b 0
f 0
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 15
    public function build()
93
    {
94 15
        $this->prepare();
95
96 15
        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 15
        if ($this->oldText == $this->newText) {
106 10
            return $this->newText;
107
        }
108
109 15
        $this->splitInputsToWords();
110 15
        $this->replaceIsolatedDiffTags();
111 15
        $this->indexNewWords();
112
113 15
        $operations = $this->operations();
114
115 15
        foreach ($operations as $item) {
116 15
            $this->performOperation($item);
117
        }
118
119 15
        if ($this->hasDiffCache()) {
120
            $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
121
        }
122
123 15
        return $this->content;
124
    }
125
126 15
    protected function indexNewWords()
127
    {
128 15
        $this->wordIndices = array();
129 15
        foreach ($this->newWords as $i => $word) {
130 15
            if ($this->isTag($word)) {
131 8
                $word = $this->stripTagAttributes($word);
132
            }
133 15
            if (isset($this->wordIndices[ $word ])) {
134 11
                $this->wordIndices[ $word ][] = $i;
135
            } else {
136 15
                $this->wordIndices[ $word ] = array($i);
137
            }
138
        }
139 15
    }
140
141 15
    protected function replaceIsolatedDiffTags()
142
    {
143 15
        $this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords);
144 15
        $this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords);
145 15
    }
146
147
    /**
148
     * @param array $words
149
     *
150
     * @return array
151
     */
152 15
    protected function createIsolatedDiffTagPlaceholders(&$words)
153
    {
154 15
        $openIsolatedDiffTags = 0;
155 15
        $isolatedDiffTagIndices = array();
156 15
        $isolatedDiffTagStart = 0;
157 15
        $currentIsolatedDiffTag = null;
158 15
        foreach ($words as $index => $word) {
159 15
            $openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag);
160 15
            if ($openIsolatedDiffTag) {
161 14
                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 14
                    if ($openIsolatedDiffTags === 0) {
172 14
                        $isolatedDiffTagStart = $index;
173
                    }
174 14
                    ++$openIsolatedDiffTags;
175 14
                    $currentIsolatedDiffTag = $openIsolatedDiffTag;
176
                }
177 15
            } 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);
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 14
                    $currentIsolatedDiffTag = null;
182
                }
183
            }
184
        }
185 15
        $isolatedDiffTagScript = array();
186 15
        $offset = 0;
187 15
        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);
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 14
            $offset += $isolatedDiffTagIndex['length'] - 1;
192
        }
193
194 15
        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 15 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 15
        $tagsToMatch = $currentIsolatedDiffTag !== null
206 14
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
207 15
            : $this->config->getIsolatedDiffTags();
208 15
        $pattern = '#<%s(\s+[^>]*)?>#iU';
209 15
        foreach ($tagsToMatch as $key => $value) {
210 15
            if (preg_match(sprintf($pattern, $key), $item)) {
211 14
                return $key;
212
            }
213
        }
214
215 15
        return false;
216
    }
217
218 14
    protected function isSelfClosingTag($text)
219
    {
220 14
        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 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+[^>]*)?>#iU';
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 15
    protected function performOperation($operation)
248
    {
249 15
        switch ($operation->action) {
250 15
            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 15
            $this->processEqualOperation($operation);
252 15
            break;
253 12
            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 12
            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 9
            case 'replace':
260 9
            $this->processReplaceOperation($operation);
261 9
            break;
262
            default:
263
            break;
264
        }
265 15
    }
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 12 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 12
        $text = array();
283 12
        foreach ($this->newWords as $pos => $s) {
284 12
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
285 12
                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 12
                    $text[] = $s;
291
                }
292
            }
293
        }
294 12
        $this->insertTag('ins', $cssClass, $text);
295 12
    }
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 = '/(^<[^>]+>)|(<\/[^>]+>$)/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
            }
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 15 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 15
        $result = array();
423 15
        foreach ($this->newWords as $pos => $s) {
424 15
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
425 15
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
426 13
                    $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s);
427
                } else {
428 15
                    $result[] = $s;
429
                }
430
            }
431
        }
432 15
        $this->content .= implode('', $result);
433 15
    }
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 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 12
    protected function insertTag($tag, $cssClass, &$words)
535
    {
536 12
        while (true) {
537 12
            if (count($words) == 0) {
538 8
                break;
539
            }
540
541 12
            $nonTags = $this->extractConsecutiveWords($words, 'noTag');
542
543 12
            $specialCaseTagInjection = '';
544 12
            $specialCaseTagInjectionIsBefore = false;
545
546 12
            if (count($nonTags) != 0) {
547 12
                $text = $this->wrapText(implode('', $nonTags), $tag, $cssClass);
548 12
                $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 12
            if (count($words) == 0 && strlen($specialCaseTagInjection) == 0) {
571 11
                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 (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 !== stripos($workTag[0], '<img')) {
588
                    $appendContent = $this->wrapText($appendContent, $tag, $cssClass);
589
                }
590 8
                $this->content .= $appendContent;
591
            }
592
        }
593 12
    }
594
595
    /**
596
     * @param string $word
597
     * @param string $condition
598
     *
599
     * @return bool
600
     */
601 12
    protected function checkCondition($word, $condition)
602
    {
603 12
        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 13
    protected function wrapText($text, $tagName, $cssClass)
614
    {
615 13
        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 12
    protected function extractConsecutiveWords(&$words, $condition)
625
    {
626 12
        $indexOfFirstTag = null;
627 12
        $words = array_values($words);
628 12
        foreach ($words as $i => $word) {
629 12
            if (!$this->checkCondition($word, $condition)) {
630 8
                $indexOfFirstTag = $i;
631 8
                break;
632
            }
633
        }
634 12
        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
                }
640
            }
641 8
            if ($indexOfFirstTag > 0) {
642 8
                array_splice($words, 0, $indexOfFirstTag);
643
            }
644
645 8
            return $items;
646
        } else {
647 12
            $items = array();
648 12 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 12
                if ($pos >= 0 && $pos <= count($words)) {
650 12
                    $items[] = $s;
651
                }
652
            }
653 12
            array_splice($words, 0, count($words));
654
655 12
            return $items;
656
        }
657
    }
658
659
    /**
660
     * @param string $item
661
     *
662
     * @return bool
663
     */
664 15
    protected function isTag($item)
665
    {
666 15
        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 15
    protected function isOpeningTag($item)
675
    {
676 15
        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 15
    protected function isClosingTag($item)
685
    {
686 15
        return preg_match('#</[^>]+>\\s*#iU', $item);
687
    }
688
689
    /**
690
     * @return Operation[]
691
     */
692 15
    protected function operations()
693
    {
694 15
        $positionInOld = 0;
695 15
        $positionInNew = 0;
696 15
        $operations = array();
697
698 15
        $matches   = $this->matchingBlocks();
699 15
        $matches[] = new Match(count($this->oldWords), count($this->newWords), 0);
700
701 15
        foreach ($matches as $i => $match) {
702 15
            $matchStartsAtCurrentPositionInOld = ($positionInOld === $match->startInOld);
703 15
            $matchStartsAtCurrentPositionInNew = ($positionInNew === $match->startInNew);
704
705 15
            if ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === false) {
706 9
                $action = 'replace';
707 15
            } elseif ($matchStartsAtCurrentPositionInOld === true && $matchStartsAtCurrentPositionInNew === false) {
708 8
                $action = 'insert';
709 15
            } 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 15
                $action = 'none';
713
            }
714
715 15
            if ($action !== 'none') {
716 12
                $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...
717
            }
718
719 15
            if (count($match) !== 0) {
720 15
                $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...
721
            }
722
723 15
            $positionInOld = $match->endInOld();
724 15
            $positionInNew = $match->endInNew();
725
        }
726
727 15
        return $operations;
728
    }
729
730
    /**
731
     * @return Match[]
732
     */
733 15
    protected function matchingBlocks()
734
    {
735 15
        $matchingBlocks = array();
736 15
        $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks);
737
738 15
        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 15
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
749
    {
750 15
        $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew);
751
752 15
        if ($match !== null) {
753 15
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
754 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...
755
            }
756
757 15
            $matchingBlocks[] = $match;
758
759 15
            if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) {
760 12
                $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...
761
            }
762
        }
763 15
    }
764
765
    /**
766
     * @param string $word
767
     *
768
     * @return string
769
     */
770 8
    protected function stripTagAttributes($word)
771
    {
772 8
        $space = strpos($word, ' ', 1);
773
774 8
        if ($space) {
775 5
            return '<' . 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 15
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
790
    {
791 15
        $bestMatchInOld = $startInOld;
792 15
        $bestMatchInNew = $startInNew;
793 15
        $bestMatchSize = 0;
794 15
        $matchLengthAt = array();
795
796 15
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; ++$indexInOld) {
797 15
            $newMatchLengthAt = array();
798 15
            $index = $this->oldWords[ $indexInOld ];
799 15
            if ($this->isTag($index)) {
800 6
                $index = $this->stripTagAttributes($index);
801
            }
802 15
            if (!isset($this->wordIndices[ $index ])) {
803 11
                $matchLengthAt = $newMatchLengthAt;
804 11
                continue;
805
            }
806 15
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
807 15
                if ($indexInNew < $startInNew) {
808 7
                    continue;
809
                }
810 15
                if ($indexInNew >= $endInNew) {
811 7
                    break;
812
                }
813
814 15
                $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...
815 15
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
816
817 15
                if ($newMatchLength > $bestMatchSize ||
818
                    (
819 12
                        $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...
820 12
                        $bestMatchSize > 0 &&
821 12
                        $this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize))
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 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...
822
                    )
823
                ) {
824 15
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
825 15
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
826 15
                    $bestMatchSize = $newMatchLength;
827
                }
828
            }
829 15
            $matchLengthAt = $newMatchLengthAt;
830
        }
831
832
        // Skip match if none found or match consists only of whitespace
833 15
        if ($bestMatchSize != 0 &&
834
            (
835 15
                !$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...
836 15
                !$this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize))
837
            )
838
        ) {
839 15
            return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
840
        }
841
842 9
        return null;
843
    }
844
845
    /**
846
     * @param string $str
847
     *
848
     * @return bool
849
     */
850 15
    protected function isOnlyWhitespace($str)
851
    {
852
        //  Slightly faster then using preg_match
853 15
        return $str !== '' && (strlen(trim($str)) === 0);
854
    }
855
856
    /**
857
     * Special array_slice function that caches its last request.
858
     *
859
     * The diff algorithm seems to request the same information many times in a row.
860
     * by returning the previous answer the algorithm preforms way faster.
861
     *
862
     * The result is a string instead of an array, this way we safe on the amount of
863
     * memory intensive implode() calls.
864
     *
865
     * @param array         &$array
866
     * @param integer       $offset
867
     * @param integer|null  $length
868
     *
869
     * @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...
870
     */
871 15
    protected function array_slice_cached(&$array, $offset, $length = null)
0 ignored issues
show
Coding Style introduced by
This method is not in camel caps format.

This check looks for method names that are not written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection seeker becomes databaseConnectionSeeker.

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