Passed
Pull Request — master (#54)
by
unknown
03:56
created

HtmlDiff::array_slice_cached()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 4
Bugs 0 Features 1
Metric Value
cc 5
eloc 16
c 4
b 0
f 1
nc 4
nop 3
dl 0
loc 29
ccs 16
cts 16
cp 1
crap 5
rs 8.439
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
698 14
        $matches   = $this->matchingBlocks();
699 14
        $matches[] = new Match(count($this->oldWords), count($this->newWords), 0);
700
701 14
        foreach ($matches as $i => $match) {
702 14
            $matchStartsAtCurrentPositionInOld = ($positionInOld === $match->startInOld);
703 14
            $matchStartsAtCurrentPositionInNew = ($positionInNew === $match->startInNew);
704
705 14
            if ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === false) {
706 8
                $action = 'replace';
707 14
            } elseif ($matchStartsAtCurrentPositionInOld === true && $matchStartsAtCurrentPositionInNew === false) {
708 8
                $action = 'insert';
709 14
            } elseif ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === true) {
710 4
                $action = 'delete';
711 4
            } else { // This occurs if the first few words are the same in both versions
712 14
                $action = 'none';
713
            }
714
715 14
            if ($action !== 'none') {
716 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...
717 11
            }
718
719 14
            if (count($match) !== 0) {
720 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...
721 14
            }
722
723 14
            $positionInOld = $match->endInOld();
724 14
            $positionInNew = $match->endInNew();
725 14
        }
726
727 14
        return $operations;
728
    }
729
730
    /**
731
     * @return Match[]
732
     */
733 14
    protected function matchingBlocks()
734
    {
735 14
        $matchingBlocks = array();
736 14
        $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks);
737
738 14
        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 14
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
749
    {
750 14
        $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew);
751
752 14
        if ($match !== null) {
753 14
            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 7
            }
756
757 14
            $matchingBlocks[] = $match;
758
759 14
            if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) {
760 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...
761 11
            }
762 14
        }
763 14
    }
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 14
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
790
    {
791 14
        $bestMatchInOld = $startInOld;
792 14
        $bestMatchInNew = $startInNew;
793 14
        $bestMatchSize = 0;
794 14
        $matchLengthAt = array();
795
796 14
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; ++$indexInOld) {
797 14
            $newMatchLengthAt = array();
798 14
            $index = $this->oldWords[ $indexInOld ];
799 14
            if ($this->isTag($index)) {
800 6
                $index = $this->stripTagAttributes($index);
801 6
            }
802 14
            if (!isset($this->wordIndices[ $index ])) {
803 10
                $matchLengthAt = $newMatchLengthAt;
804 10
                continue;
805
            }
806 14
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
807 14
                if ($indexInNew < $startInNew) {
808 7
                    continue;
809
                }
810 14
                if ($indexInNew >= $endInNew) {
811 7
                    break;
812
                }
813
814 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...
815 14
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
816
817 14
                if ($newMatchLength > $bestMatchSize ||
818
                    (
819 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...
820 11
                        $bestMatchSize > 0 &&
821 11
                        $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 11
                    )
823 14
                ) {
824 14
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
825 14
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
826 14
                    $bestMatchSize = $newMatchLength;
827 14
                }
828 14
            }
829 14
            $matchLengthAt = $newMatchLengthAt;
830 14
        }
831
832
        // Skip match if none found or match consists only of whitespace
833 14
        if ($bestMatchSize != 0 &&
834
            (
835 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...
836 14
                !$this->isOnlyWhitespace($this->array_slice_cached($this->oldWords, $bestMatchInOld, $bestMatchSize))
837 14
            )
838 14
        ) {
839 14
            return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
840
        }
841
842 8
        return null;
843
    }
844
845
    /**
846
     * @param string $str
847
     *
848
     * @return bool
849
     */
850 14
    protected function isOnlyWhitespace($str)
851
    {
852
        //  Slightly faster then using preg_match
853 14
        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 14
    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 14
        static $lastOffset = null;
874 14
        static $lastLength = null;
875 14
        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 14
        if ($this->resetCache === true) {
880 14
            $cache = null;
881
882 14
            $this->resetCache = false;
883 14
        }
884
885
        if (
886 14
            $cache !== null &&
887 14
            $lastLength === $length &&
888
            $lastOffset === $offset
889 14
        ) { // Hit
890 11
            return $cache;
891
        } // Miss
892
893 14
        $lastOffset = $offset;
894 14
        $lastLength = $length;
895
896 14
        $cache = implode('', array_slice($array, $offset, $length));
897
898 14
        return $cache;
899
    }
900
}
901