Test Failed
Pull Request — master (#96)
by Sven
03:32
created

HtmlDiff::isTag()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 2
cts 2
cp 1
crap 2
1
<?php
2
3
namespace Caxy\HtmlDiff;
4
5
use Caxy\HtmlDiff\Table\TableDiff;
6
7
/**
8
 * Class HtmlDiff.
9
 */
10
class HtmlDiff extends AbstractDiff
11
{
12
    /**
13
     * @var array
14
     */
15
    protected $wordIndices;
16
    /**
17
     * @var array
18
     */
19
    protected $oldTables;
20
    /**
21
     * @var array
22
     */
23
    protected $newTables;
24
    /**
25
     * @var array
26
     */
27
    protected $newIsolatedDiffTags;
28
    /**
29
     * @var array
30
     */
31
    protected $oldIsolatedDiffTags;
32
33
    /**
34
     * @param string              $oldText
35
     * @param string              $newText
36
     * @param HtmlDiffConfig|null $config
37
     *
38
     * @return self
39
     */
40 13 View Code Duplication
    public static function create($oldText, $newText, HtmlDiffConfig $config = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
41
    {
42 13
        $diff = new self($oldText, $newText);
43
44 13
        if (null !== $config) {
45 13
            $diff->setConfig($config);
46
        }
47
48 13
        return $diff;
49
    }
50
51
    /**
52
     * @param $bool
53
     *
54
     * @return $this
55
     *
56
     * @deprecated since 0.1.0
57
     */
58
    public function setUseTableDiffing($bool)
59
    {
60
        $this->config->setUseTableDiffing($bool);
61
62
        return $this;
63
    }
64
65
    /**
66
     * @param bool $boolean
67
     *
68
     * @return HtmlDiff
69
     *
70
     * @deprecated since 0.1.0
71
     */
72
    public function setInsertSpaceInReplace($boolean)
73
    {
74
        $this->config->setInsertSpaceInReplace($boolean);
75
76
        return $this;
77
    }
78
79
    /**
80
     * @return bool
81
     *
82
     * @deprecated since 0.1.0
83
     */
84
    public function getInsertSpaceInReplace()
85
    {
86
        return $this->config->isInsertSpaceInReplace();
87
    }
88
89
    /**
90
     * @return string
91
     */
92 16
    public function build()
93
    {
94 16
        $this->prepare();
95
96 16 View Code Duplication
        if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) {
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...
97
            $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText);
98
99
            return $this->content;
100
        }
101
102
        // Pre-processing Optimizations
103
104
        // 1. Equality
105 16
        if ($this->oldText == $this->newText) {
106 10
            return $this->newText;
107
        }
108
109 16
        $this->splitInputsToWords();
110 16
        $this->replaceIsolatedDiffTags();
111 16
        $this->indexNewWords();
112
113 16
        $operations = $this->operations();
114
115 16
        foreach ($operations as $item) {
116 16
            $this->performOperation($item);
117
        }
118
119 16
        if ($this->hasDiffCache()) {
120
            $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
121
        }
122
123 16
        return $this->content;
124
    }
125
126 16
    protected function indexNewWords()
127
    {
128 16
        $this->wordIndices = array();
129 16
        foreach ($this->newWords as $i => $word) {
130 16
            if ($this->isTag($word)) {
131 8
                $word = $this->stripTagAttributes($word);
132
            }
133 16
            if (isset($this->wordIndices[ $word ])) {
134 11
                $this->wordIndices[ $word ][] = $i;
135
            } else {
136 16
                $this->wordIndices[ $word ] = array($i);
137
            }
138
        }
139 16
    }
140
141 16
    protected function replaceIsolatedDiffTags()
142
    {
143 16
        $this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords);
144 16
        $this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords);
145 16
    }
146
147
    /**
148
     * @param array $words
149
     *
150
     * @return array
151
     */
152 16
    protected function createIsolatedDiffTagPlaceholders(&$words)
153
    {
154 16
        $openIsolatedDiffTags = 0;
155 16
        $isolatedDiffTagIndices = array();
156 16
        $isolatedDiffTagStart = 0;
157 16
        $currentIsolatedDiffTag = null;
158 16
        foreach ($words as $index => $word) {
159 16
            $openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag);
160 16
            if ($openIsolatedDiffTag) {
161 14
                if ($this->isSelfClosingTag($word) || $this->stringUtil->stripos($word, '<img') !== false) {
162 View Code Duplication
                    if ($openIsolatedDiffTags === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
163
                        $isolatedDiffTagIndices[] = array(
164
                            'start' => $index,
165
                            'length' => 1,
166
                            'tagType' => $openIsolatedDiffTag,
167
                        );
168
                        $currentIsolatedDiffTag = null;
169
                    }
170
                } else {
171 14
                    if ($openIsolatedDiffTags === 0) {
172 14
                        $isolatedDiffTagStart = $index;
173
                    }
174 14
                    ++$openIsolatedDiffTags;
175 14
                    $currentIsolatedDiffTag = $openIsolatedDiffTag;
176
                }
177 16
            } elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) {
178 14
                --$openIsolatedDiffTags;
179 14 View Code Duplication
                if ($openIsolatedDiffTags == 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
180 14
                    $isolatedDiffTagIndices[] = array('start' => $isolatedDiffTagStart, 'length' => $index - $isolatedDiffTagStart + 1, 'tagType' => $currentIsolatedDiffTag);
181 14
                    $currentIsolatedDiffTag = null;
182
                }
183
            }
184
        }
185 16
        $isolatedDiffTagScript = array();
186 16
        $offset = 0;
187 16
        foreach ($isolatedDiffTagIndices as $isolatedDiffTagIndex) {
188 14
            $start = $isolatedDiffTagIndex['start'] - $offset;
189 14
            $placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']);
190 14
            $isolatedDiffTagScript[$start] = array_splice($words, $start, $isolatedDiffTagIndex['length'], $placeholderString);
191 14
            $offset += $isolatedDiffTagIndex['length'] - 1;
192
        }
193
194 16
        return $isolatedDiffTagScript;
195
    }
196
197
    /**
198
     * @param string      $item
199
     * @param null|string $currentIsolatedDiffTag
200
     *
201
     * @return false|string
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
202
     */
203 16 View Code Duplication
    protected function isOpeningIsolatedDiffTag($item, $currentIsolatedDiffTag = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
204
    {
205 16
        $tagsToMatch = $currentIsolatedDiffTag !== null
206 14
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
207 16
            : $this->config->getIsolatedDiffTags();
208 16
        $pattern = '#<%s(\s+[^>]*)?>#iUu';
209 16
        foreach ($tagsToMatch as $key => $value) {
210 16
            if (preg_match(sprintf($pattern, $key), $item)) {
211 14
                return $key;
212
            }
213
        }
214
215 16
        return false;
216
    }
217
218 14
    protected function isSelfClosingTag($text)
219
    {
220 14
        return (bool) preg_match('/<[^>]+\/\s*>/u', $text);
221
    }
222
223
    /**
224
     * @param string      $item
225
     * @param null|string $currentIsolatedDiffTag
226
     *
227
     * @return false|string
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
228
     */
229 14 View Code Duplication
    protected function isClosingIsolatedDiffTag($item, $currentIsolatedDiffTag = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
230
    {
231 14
        $tagsToMatch = $currentIsolatedDiffTag !== null
232 14
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
233 14
            : $this->config->getIsolatedDiffTags();
234 14
        $pattern = '#</%s(\s+[^>]*)?>#iUu';
235 14
        foreach ($tagsToMatch as $key => $value) {
236 14
            if (preg_match(sprintf($pattern, $key), $item)) {
237 14
                return $key;
238
            }
239
        }
240
241 14
        return false;
242
    }
243
244
    /**
245
     * @param Operation $operation
246
     */
247 16
    protected function performOperation($operation)
248
    {
249 16
        switch ($operation->action) {
250 16
            case 'equal' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

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

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

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

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

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

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

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

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

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

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

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

Loading history...
257 9
            $this->processInsertOperation($operation, 'diffins');
258 9
            break;
259 9
            case 'replace':
260 9
            $this->processReplaceOperation($operation);
261 9
            break;
262
            default:
263
            break;
264
        }
265 16
    }
266
267
    /**
268
     * @param Operation $operation
269
     */
270 9
    protected function processReplaceOperation($operation)
271
    {
272 9
        $this->processDeleteOperation($operation, 'diffmod');
273 9
        $this->processInsertOperation($operation, 'diffmod');
274 9
    }
275
276
    /**
277
     * @param Operation $operation
278
     * @param string    $cssClass
279
     */
280 13 View Code Duplication
    protected function processInsertOperation($operation, $cssClass)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
281
    {
282 13
        $text = array();
283 13
        foreach ($this->newWords as $pos => $s) {
284 13
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
285 13
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
286 3
                    foreach ($this->newIsolatedDiffTags[$pos] as $word) {
287 3
                        $text[] = $word;
288
                    }
289
                } else {
290 13
                    $text[] = $s;
291
                }
292
            }
293
        }
294 13
        $this->insertTag('ins', $cssClass, $text);
295 13
    }
296
297
    /**
298
     * @param Operation $operation
299
     * @param string    $cssClass
300
     */
301 11 View Code Duplication
    protected function processDeleteOperation($operation, $cssClass)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
302
    {
303 11
        $text = array();
304 11
        foreach ($this->oldWords as $pos => $s) {
305 11
            if ($pos >= $operation->startInOld && $pos < $operation->endInOld) {
306 11
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->oldIsolatedDiffTags[$pos])) {
307 6
                    foreach ($this->oldIsolatedDiffTags[$pos] as $word) {
308 6
                        $text[] = $word;
309
                    }
310
                } else {
311 11
                    $text[] = $s;
312
                }
313
            }
314
        }
315 11
        $this->insertTag('del', $cssClass, $text);
316 11
    }
317
318
    /**
319
     * @param Operation $operation
320
     * @param int       $pos
321
     * @param string    $placeholder
322
     * @param bool      $stripWrappingTags
323
     *
324
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
325
     */
326 13
    protected function diffIsolatedPlaceholder($operation, $pos, $placeholder, $stripWrappingTags = true)
327
    {
328 13
        $oldText = implode('', $this->findIsolatedDiffTagsInOld($operation, $pos));
329 13
        $newText = implode('', $this->newIsolatedDiffTags[$pos]);
330
331 13
        if ($this->isListPlaceholder($placeholder)) {
332 7
            return $this->diffList($oldText, $newText);
333 10
        } elseif ($this->config->isUseTableDiffing() && $this->isTablePlaceholder($placeholder)) {
334 1
            return $this->diffTables($oldText, $newText);
335 9
        } elseif ($this->isLinkPlaceholder($placeholder)) {
336 1
            return $this->diffElementsByAttribute($oldText, $newText, 'href', 'a');
337 8
        } elseif ($this->isImagePlaceholder($placeholder)) {
338
            return $this->diffElementsByAttribute($oldText, $newText, 'src', 'img');
339
        }
340
341 8
        return $this->diffElements($oldText, $newText, $stripWrappingTags);
342
    }
343
344
    /**
345
     * @param string $oldText
346
     * @param string $newText
347
     * @param bool   $stripWrappingTags
348
     *
349
     * @return string
350
     */
351 9
    protected function diffElements($oldText, $newText, $stripWrappingTags = true)
352
    {
353 9
        $wrapStart = '';
354 9
        $wrapEnd = '';
355
356 9
        if ($stripWrappingTags) {
357 9
            $pattern = '/(^<[^>]+>)|(<\/[^>]+>$)/iu';
358 9
            $matches = array();
359
360 9
            if (preg_match_all($pattern, $newText, $matches)) {
361 9
                $wrapStart = isset($matches[0][0]) ? $matches[0][0] : '';
362 9
                $wrapEnd = isset($matches[0][1]) ? $matches[0][1] : '';
363
            }
364 9
            $oldText = preg_replace($pattern, '', $oldText);
365 9
            $newText = preg_replace($pattern, '', $newText);
366
        }
367
368 9
        $diff = self::create($oldText, $newText, $this->config);
369
370 9
        return $wrapStart.$diff->build().$wrapEnd;
371
    }
372
373
    /**
374
     * @param string $oldText
375
     * @param string $newText
376
     *
377
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
378
     */
379 7
    protected function diffList($oldText, $newText)
380
    {
381 7
        $diff = ListDiffLines::create($oldText, $newText, $this->config);
382
383 7
        return $diff->build();
384
    }
385
386
    /**
387
     * @param string $oldText
388
     * @param string $newText
389
     *
390
     * @return string
391
     */
392 1
    protected function diffTables($oldText, $newText)
393
    {
394 1
        $diff = TableDiff::create($oldText, $newText, $this->config);
395
396 1
        return $diff->build();
397
    }
398
399 1
    protected function diffElementsByAttribute($oldText, $newText, $attribute, $element)
400
    {
401 1
        $oldAttribute = $this->getAttributeFromTag($oldText, $attribute);
402 1
        $newAttribute = $this->getAttributeFromTag($newText, $attribute);
403
404 1
        if ($oldAttribute !== $newAttribute) {
405 1
            $diffClass = sprintf('diffmod diff%s diff%s', $element, $attribute);
406
407 1
            return sprintf(
408 1
                '%s%s',
409 1
                $this->wrapText($oldText, 'del', $diffClass),
410 1
                $this->wrapText($newText, 'ins', $diffClass)
411
            );
412
        }
413
414 1
        return $this->diffElements($oldText, $newText);
415
    }
416
417
    /**
418
     * @param Operation $operation
419
     */
420 16 View Code Duplication
    protected function processEqualOperation($operation)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
421
    {
422 16
        $result = array();
423 16
        foreach ($this->newWords as $pos => $s) {
424 16
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
425 16
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
426 13
                    $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s);
427
                } else {
428 12
                    $result[] = $s;
429
                }
430
            }
431
        }
432 16
        $this->content .= implode('', $result);
433 16
    }
434
435
    /**
436
     * @param string $text
437
     * @param string $attribute
438
     *
439
     * @return null|string
440
     */
441 1
    protected function getAttributeFromTag($text, $attribute)
442
    {
443 1
        $matches = array();
444 1
        if (preg_match(sprintf('/<[^>]*\b%s\s*=\s*([\'"])(.*)\1[^>]*>/iu', $attribute), $text, $matches)) {
445 1
            return htmlspecialchars_decode($matches[2]);
446
        }
447
448
        return;
449
    }
450
451
    /**
452
     * @param string $text
453
     *
454
     * @return bool
455
     */
456 13
    protected function isListPlaceholder($text)
457
    {
458 13
        return $this->isPlaceholderType($text, array('ol', 'dl', 'ul'));
459
    }
460
461
    /**
462
     * @param string $text
463
     *
464
     * @return bool
465
     */
466 9
    public function isLinkPlaceholder($text)
467
    {
468 9
        return $this->isPlaceholderType($text, 'a');
469
    }
470
471
    /**
472
     * @param string $text
473
     *
474
     * @return bool
475
     */
476 8
    public function isImagePlaceholder($text)
477
    {
478 8
        return $this->isPlaceholderType($text, 'img');
479
    }
480
481
    /**
482
     * @param string       $text
483
     * @param array|string $types
484
     * @param bool         $strict
485
     *
486
     * @return bool
487
     */
488 13
    protected function isPlaceholderType($text, $types, $strict = true)
489
    {
490 13
        if (!is_array($types)) {
491 10
            $types = array($types);
492
        }
493
494 13
        $criteria = array();
495 13
        foreach ($types as $type) {
496 13
            if ($this->config->isIsolatedDiffTag($type)) {
497 13
                $criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type);
498
            } else {
499
                $criteria[] = $type;
500
            }
501
        }
502
503 13
        return in_array($text, $criteria, $strict);
504
    }
505
506
    /**
507
     * @param string $text
508
     *
509
     * @return bool
510
     */
511 10
    protected function isTablePlaceholder($text)
512
    {
513 10
        return $this->isPlaceholderType($text, 'table');
514
    }
515
516
    /**
517
     * @param Operation $operation
518
     * @param int       $posInNew
519
     *
520
     * @return array
521
     */
522 13
    protected function findIsolatedDiffTagsInOld($operation, $posInNew)
523
    {
524 13
        $offset = $posInNew - $operation->startInNew;
525
526 13
        return $this->oldIsolatedDiffTags[$operation->startInOld + $offset];
527
    }
528
529
    /**
530
     * @param string $tag
531
     * @param string $cssClass
532
     * @param array  $words
533
     */
534 13
    protected function insertTag($tag, $cssClass, &$words)
535
    {
536 13
        while (true) {
537 13
            if (count($words) == 0) {
538 8
                break;
539
            }
540
541 13
            $nonTags = $this->extractConsecutiveWords($words, 'noTag');
542
543 13
            $specialCaseTagInjection = '';
544 13
            $specialCaseTagInjectionIsBefore = false;
545
546 13
            if (count($nonTags) !== 0) {
547 13
                $text = $this->wrapText(implode('', $nonTags), $tag, $cssClass);
548 13
                $this->content .= $text;
549
            } else {
550 6
                $firstOrDefault = false;
551 6
                foreach ($this->config->getSpecialCaseOpeningTags() as $x) {
552
                    if (preg_match($x, $words[ 0 ])) {
553
                        $firstOrDefault = $x;
554
                        break;
555
                    }
556
                }
557 6
                if ($firstOrDefault) {
558
                    $specialCaseTagInjection = '<ins class="mod">';
559
                    if ($tag == 'del') {
560
                        unset($words[ 0 ]);
561
                    }
562 6
                } elseif (array_search($words[ 0 ], $this->config->getSpecialCaseClosingTags()) !== false) {
563
                    $specialCaseTagInjection = '</ins>';
564
                    $specialCaseTagInjectionIsBefore = true;
565
                    if ($tag == 'del') {
566
                        unset($words[ 0 ]);
567
                    }
568
                }
569
            }
570 13
            if (count($words) == 0 && $this->stringUtil->strlen($specialCaseTagInjection) == 0) {
571 12
                break;
572
            }
573 8
            if ($specialCaseTagInjectionIsBefore) {
574
                $this->content .= $specialCaseTagInjection.implode('', $this->extractConsecutiveWords($words, 'tag'));
575
            } else {
576 8
                $workTag = $this->extractConsecutiveWords($words, 'tag');
577 8
578 8
                if (
579 2
                    isset($workTag[0]) &&
580 2
                    $this->isOpeningTag($workTag[0]) === true &&
581
                    $this->isClosingTag($workTag[0]) === false
582 8
                ) {
583
                    if ($this->stringUtil->strpos($workTag[0], 'class=') !== false) {
584
                        $workTag[0] = str_replace('class="', 'class="diffmod ', $workTag[0]);
585
                        $workTag[0] = str_replace("class='", 'class="diffmod ', $workTag[0]);
586 8
                    } elseif ($this->stringUtil->strpos($workTag[0], '/>') !== false) {
587 8
                        $workTag[0] = str_replace('/>', ' class="diffmod" />', $workTag[0]);
588
                    } else {
589
                        $workTag[0] = str_replace('>', ' class="diffmod">', $workTag[0]);
590 8
                    }
591
592
                    var_dump($workTag[ 0 ]);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($workTag[0]); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
593 13
                }
594
595
                $appendContent = implode('', $workTag).$specialCaseTagInjection;
596
                if (isset($workTag[0]) && false !== $this->stringUtil->stripos($workTag[0], '<img')) {
597
                    $appendContent = $this->wrapText($appendContent, $tag, $cssClass);
598
                }
599
                $this->content .= $appendContent;
600
            }
601 13
        }
602
    }
603 13
604
    /**
605
     * @param string $word
606
     * @param string $condition
607
     *
608
     * @return bool
609
     */
610
    protected function checkCondition($word, $condition)
611
    {
612
        return $condition == 'tag' ? $this->isTag($word) : !$this->isTag($word);
613 14
    }
614
615 14
    /**
616
     * @param string $text
617
     * @param string $tagName
618
     * @param string $cssClass
619
     *
620
     * @return string
621
     */
622
    protected function wrapText($text, $tagName, $cssClass)
623
    {
624 13
        return sprintf('<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text);
625
    }
626 13
627 13
    /**
628 13
     * @param array  $words
629 13
     * @param string $condition
630 8
     *
631 8
     * @return array
632
     */
633
    protected function extractConsecutiveWords(&$words, $condition)
634 13
    {
635 8
        $indexOfFirstTag = null;
636 8
        $words = array_values($words);
637 8
        foreach ($words as $i => $word) {
638 8
            if (!$this->checkCondition($word, $condition)) {
639
                $indexOfFirstTag = $i;
640
                break;
641 8
            }
642 8
        }
643
        if ($indexOfFirstTag !== null) {
644
            $items = array();
645 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...
646
                if ($pos >= 0 && $pos < $indexOfFirstTag) {
647 13
                    $items[] = $s;
648 13
                }
649 13
            }
650 13
            if ($indexOfFirstTag > 0) {
651
                array_splice($words, 0, $indexOfFirstTag);
652
            }
653 13
654
            return $items;
655 13
        } else {
656
            $items = array();
657 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...
658
                if ($pos >= 0 && $pos <= count($words)) {
659
                    $items[] = $s;
660
                }
661
            }
662
            array_splice($words, 0, count($words));
663
664 16
            return $items;
665
        }
666 16
    }
667
668
    /**
669
     * @param string $item
670
     *
671
     * @return bool
672
     */
673
    protected function isTag($item)
674 16
    {
675
        return $this->isOpeningTag($item) || $this->isClosingTag($item);
676 16
    }
677
678
    /**
679
     * @param string $item
680
     *
681
     * @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...
682
     */
683
    protected function isOpeningTag($item)
684 16
    {
685
        return preg_match('#<[^>]+>\\s*#iUu', $item);
686 16
    }
687
688
    /**
689
     * @param string $item
690
     *
691
     * @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...
692 16
     */
693
    protected function isClosingTag($item)
694 16
    {
695 16
        return preg_match('#</[^>]+>\\s*#iUu', $item);
696 16
    }
697
698 16
    /**
699 16
     * @return Operation[]
700
     */
701 16
    protected function operations()
702 16
    {
703 16
        $positionInOld = 0;
704
        $positionInNew = 0;
705 16
        $operations = array();
706 9
707 16
        $matches   = $this->matchingBlocks();
708 9
        $matches[] = new MatchingBlock(count($this->oldWords), count($this->newWords), 0);
709 16
710 4
        foreach ($matches as $match) {
711
            $matchStartsAtCurrentPositionInOld = ($positionInOld === $match->startInOld);
712 16
            $matchStartsAtCurrentPositionInNew = ($positionInNew === $match->startInNew);
713
714
            if ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === false) {
715 16
                $action = 'replace';
716 13
            } elseif ($matchStartsAtCurrentPositionInOld === true && $matchStartsAtCurrentPositionInNew === false) {
717
                $action = 'insert';
718
            } elseif ($matchStartsAtCurrentPositionInOld === false && $matchStartsAtCurrentPositionInNew === true) {
719 16
                $action = 'delete';
720 16
            } else { // This occurs if the first few words are the same in both versions
721
                $action = 'none';
722
            }
723 16
724 16
            if ($action !== 'none') {
725
                $operations[] = new Operation($action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew);
726
            }
727 16
728
            if (count($match) !== 0) {
729
                $operations[] = new Operation('equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew());
730
            }
731
732
            $positionInOld = $match->endInOld();
733 16
            $positionInNew = $match->endInNew();
734
        }
735 16
736 16
        return $operations;
737
    }
738 16
739
    /**
740
     * @return MatchingBlock[]
741
     */
742
    protected function matchingBlocks()
743
    {
744
        $matchingBlocks = array();
745
        $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks);
746
747
        return $matchingBlocks;
748 16
    }
749
750 16
    /**
751
     * @param int   $startInOld
752 16
     * @param int   $endInOld
753 16
     * @param int   $startInNew
754 7
     * @param int   $endInNew
755
     * @param array $matchingBlocks
756
     */
757 16
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
758
    {
759 16
        $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew);
760 9
761
        if ($match !== null) {
762
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
763 16
                $this->findMatchingBlocks($startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks);
764
            }
765
766
            $matchingBlocks[] = $match;
767
768
            if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) {
769
                $this->findMatchingBlocks($match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks);
770 8
            }
771
        }
772 8
    }
773
774 8
    /**
775 5
     * @param string $word
776
     *
777
     * @return string
778 6
     */
779
    protected function stripTagAttributes($word)
780
    {
781
        $space = $this->stringUtil->strpos($word, ' ', 1);
782
783
        if ($space) {
784
            return '<' . $this->stringUtil->substr($word, 1, $space) . '>';
785
        }
786
787
        return trim($word, '<>');
788
    }
789 16
790
    /**
791 16
     * @param int $startInOld
792 16
     * @param int $endInOld
793 16
     * @param int $startInNew
794 16
     * @param int $endInNew
795 16
     *
796
     * @return MatchingBlock|null
797 16
     */
798 16
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
799 16
    {
800 16
        $groupDiffs     = $this->isGroupDiffs();
0 ignored issues
show
Deprecated Code introduced by
The method Caxy\HtmlDiff\AbstractDiff::isGroupDiffs() has been deprecated with message: since 0.1.0

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

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

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