Passed
Push — master ( 755472...23808d )
by Adam
03:58
created

HtmlDiff::create()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 10
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 3
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 10 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 10
        $diff = new self($oldText, $newText);
43
44 10
        if (null !== $config) {
45 10
            $diff->setConfig($config);
46 10
        }
47
48 10
        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 12
    public function build()
93
    {
94 12
        if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) {
95
            $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText);
96
97
            return $this->content;
98
        }
99
100
        // Pre-processing Optimizations
101
102
        // 1. Equality
103 12
        if ($this->oldText == $this->newText) {
104 9
            return $this->newText;
105
        }
106
107 12
        $this->splitInputsToWords();
108 12
        $this->replaceIsolatedDiffTags();
109 12
        $this->indexNewWords();
110
111 12
        $operations = $this->operations();
112
113 12
        foreach ($operations as $item) {
114 12
            $this->performOperation($item);
115 12
        }
116
117 12
        if ($this->hasDiffCache()) {
118
            $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
119
        }
120
121 12
        return $this->content;
122
    }
123
124 12
    protected function indexNewWords()
125
    {
126 12
        $this->wordIndices = array();
127 12
        foreach ($this->newWords as $i => $word) {
128 12
            if ($this->isTag($word)) {
129 8
                $word = $this->stripTagAttributes($word);
130 8
            }
131 12
            if (isset($this->wordIndices[ $word ])) {
132 11
                $this->wordIndices[ $word ][] = $i;
133 11
            } else {
134 12
                $this->wordIndices[ $word ] = array($i);
135
            }
136 12
        }
137 12
    }
138
139 12
    protected function replaceIsolatedDiffTags()
140
    {
141 12
        $this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords);
142 12
        $this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords);
143 12
    }
144
145
    /**
146
     * @param array $words
147
     *
148
     * @return array
149
     */
150 12
    protected function createIsolatedDiffTagPlaceholders(&$words)
151
    {
152 12
        $openIsolatedDiffTags = 0;
153 12
        $isolatedDiffTagIndices = array();
154 12
        $isolatedDiffTagStart = 0;
155 12
        $currentIsolatedDiffTag = null;
156 12
        foreach ($words as $index => $word) {
157 12
            $openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag);
158 12
            if ($openIsolatedDiffTag) {
159 11
                if ($this->isSelfClosingTag($word) || stripos($word, '<img') !== false) {
160 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...
161
                        $isolatedDiffTagIndices[] = array(
162
                            'start' => $index,
163
                            'length' => 1,
164
                            'tagType' => $openIsolatedDiffTag,
165
                        );
166
                        $currentIsolatedDiffTag = null;
167
                    }
168
                } else {
169 11
                    if ($openIsolatedDiffTags === 0) {
170 11
                        $isolatedDiffTagStart = $index;
171 11
                    }
172 11
                    ++$openIsolatedDiffTags;
173 11
                    $currentIsolatedDiffTag = $openIsolatedDiffTag;
174
                }
175 12
            } elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) {
176 11
                --$openIsolatedDiffTags;
177 11 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...
178 11
                    $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...
179 11
                    $currentIsolatedDiffTag = null;
180 11
                }
181 11
            }
182 12
        }
183 12
        $isolatedDiffTagScript = array();
184 12
        $offset = 0;
185 12
        foreach ($isolatedDiffTagIndices as $isolatedDiffTagIndex) {
186 11
            $start = $isolatedDiffTagIndex['start'] - $offset;
187 11
            $placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']);
188 11
            $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...
189 11
            $offset += $isolatedDiffTagIndex['length'] - 1;
190 12
        }
191
192 12
        return $isolatedDiffTagScript;
193
    }
194
195
    /**
196
     * @param string      $item
197
     * @param null|string $currentIsolatedDiffTag
198
     *
199
     * @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...
200
     */
201 12 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...
202
    {
203
        $tagsToMatch = $currentIsolatedDiffTag !== null
204 12
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
205 12
            : $this->config->getIsolatedDiffTags();
206 12
        $pattern = '#<%s(\s+[^>]*)?>#iU';
207 12
        foreach ($tagsToMatch as $key => $value) {
208 12
            if (preg_match(sprintf($pattern, $key), $item)) {
209 11
                return $key;
210
            }
211 12
        }
212
213 12
        return false;
214
    }
215
216 11
    protected function isSelfClosingTag($text)
217
    {
218 11
        return (bool) preg_match('/<[^>]+\/\s*>/', $text);
219
    }
220
221
    /**
222
     * @param string      $item
223
     * @param null|string $currentIsolatedDiffTag
224
     *
225
     * @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...
226
     */
227 11 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...
228
    {
229
        $tagsToMatch = $currentIsolatedDiffTag !== null
230 11
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
231 11
            : $this->config->getIsolatedDiffTags();
232 11
        $pattern = '#</%s(\s+[^>]*)?>#iU';
233 11
        foreach ($tagsToMatch as $key => $value) {
234 11
            if (preg_match(sprintf($pattern, $key), $item)) {
235 11
                return $key;
236
            }
237 11
        }
238
239 11
        return false;
240
    }
241
242
    /**
243
     * @param Operation $operation
244
     */
245 12
    protected function performOperation($operation)
246
    {
247 12
        switch ($operation->action) {
248 12
            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...
249 12
            $this->processEqualOperation($operation);
250 12
            break;
251 9
            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...
252 4
            $this->processDeleteOperation($operation, 'diffdel');
253 4
            break;
254 9
            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...
255 8
            $this->processInsertOperation($operation, 'diffins');
256 8
            break;
257 6
            case 'replace':
258 6
            $this->processReplaceOperation($operation);
259 6
            break;
260
            default:
261
            break;
262 12
        }
263 12
    }
264
265
    /**
266
     * @param Operation $operation
267
     */
268 6
    protected function processReplaceOperation($operation)
269
    {
270 6
        $this->processDeleteOperation($operation, 'diffmod');
271 6
        $this->processInsertOperation($operation, 'diffmod');
272 6
    }
273
274
    /**
275
     * @param Operation $operation
276
     * @param string    $cssClass
277
     */
278 9 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...
279
    {
280 9
        $text = array();
281 9
        foreach ($this->newWords as $pos => $s) {
282 9
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
283 9
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
284 3
                    foreach ($this->newIsolatedDiffTags[$pos] as $word) {
285 3
                        $text[] = $word;
286 3
                    }
287 3
                } else {
288 9
                    $text[] = $s;
289
                }
290 9
            }
291 9
        }
292 9
        $this->insertTag('ins', $cssClass, $text);
293 9
    }
294
295
    /**
296
     * @param Operation $operation
297
     * @param string    $cssClass
298
     */
299 8 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...
300
    {
301 8
        $text = array();
302 8
        foreach ($this->oldWords as $pos => $s) {
303 8
            if ($pos >= $operation->startInOld && $pos < $operation->endInOld) {
304 8
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->oldIsolatedDiffTags[$pos])) {
305 6
                    foreach ($this->oldIsolatedDiffTags[$pos] as $word) {
306 6
                        $text[] = $word;
307 6
                    }
308 6
                } else {
309 8
                    $text[] = $s;
310
                }
311 8
            }
312 8
        }
313 8
        $this->insertTag('del', $cssClass, $text);
314 8
    }
315
316
    /**
317
     * @param Operation $operation
318
     * @param int       $pos
319
     * @param string    $placeholder
320
     * @param bool      $stripWrappingTags
321
     *
322
     * @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...
323
     */
324 10
    protected function diffIsolatedPlaceholder($operation, $pos, $placeholder, $stripWrappingTags = true)
325
    {
326 10
        $oldText = implode('', $this->findIsolatedDiffTagsInOld($operation, $pos));
327 10
        $newText = implode('', $this->newIsolatedDiffTags[$pos]);
328
329 10
        if ($this->isListPlaceholder($placeholder)) {
330 7
            return $this->diffList($oldText, $newText);
331 7
        } elseif ($this->config->isUseTableDiffing() && $this->isTablePlaceholder($placeholder)) {
332
            return $this->diffTables($oldText, $newText);
333 7
        } elseif ($this->isLinkPlaceholder($placeholder)) {
334 1
            return $this->diffElementsByAttribute($oldText, $newText, 'href', 'a');
335 6
        } elseif ($this->isImagePlaceholder($placeholder)) {
336
            return $this->diffElementsByAttribute($oldText, $newText, 'src', 'img');
337
        }
338
339 6
        return $this->diffElements($oldText, $newText, $stripWrappingTags);
340
    }
341
342
    /**
343
     * @param string $oldText
344
     * @param string $newText
345
     * @param bool   $stripWrappingTags
346
     *
347
     * @return string
348
     */
349 7
    protected function diffElements($oldText, $newText, $stripWrappingTags = true)
350
    {
351 7
        $wrapStart = '';
352 7
        $wrapEnd = '';
353
354 7
        if ($stripWrappingTags) {
355 7
            $pattern = '/(^<[^>]+>)|(<\/[^>]+>$)/i';
356 7
            $matches = array();
357
358 7
            if (preg_match_all($pattern, $newText, $matches)) {
359 7
                $wrapStart = isset($matches[0][0]) ? $matches[0][0] : '';
360 7
                $wrapEnd = isset($matches[0][1]) ? $matches[0][1] : '';
361 7
            }
362 7
            $oldText = preg_replace($pattern, '', $oldText);
363 7
            $newText = preg_replace($pattern, '', $newText);
364 7
        }
365
366 7
        $diff = self::create($oldText, $newText, $this->config);
367
368 7
        return $wrapStart.$diff->build().$wrapEnd;
369
    }
370
371
    /**
372
     * @param string $oldText
373
     * @param string $newText
374
     *
375
     * @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...
376
     */
377 7
    protected function diffList($oldText, $newText)
378
    {
379 7
        $diff = ListDiffLines::create($oldText, $newText, $this->config);
380
381 7
        return $diff->build();
382
    }
383
384
    /**
385
     * @param string $oldText
386
     * @param string $newText
387
     *
388
     * @return string
389
     */
390
    protected function diffTables($oldText, $newText)
391
    {
392
        $diff = TableDiff::create($oldText, $newText, $this->config);
393
394
        return $diff->build();
395
    }
396
397 1
    protected function diffElementsByAttribute($oldText, $newText, $attribute, $element)
398
    {
399 1
        $oldAttribute = $this->getAttributeFromTag($oldText, $attribute);
400 1
        $newAttribute = $this->getAttributeFromTag($newText, $attribute);
401
402 1
        if ($oldAttribute !== $newAttribute) {
403 1
            $diffClass = sprintf('diffmod diff%s diff%s', $element, $attribute);
404
405 1
            return sprintf(
406 1
                '%s%s',
407 1
                $this->wrapText($oldText, 'del', $diffClass),
408 1
                $this->wrapText($newText, 'ins', $diffClass)
409 1
            );
410
        }
411
412 1
        return $this->diffElements($oldText, $newText);
413
    }
414
415
    /**
416
     * @param Operation $operation
417
     */
418 12 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...
419
    {
420 12
        $result = array();
421 12
        foreach ($this->newWords as $pos => $s) {
422 12
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
423 12
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
424 10
                    $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s);
425 10
                } else {
426 12
                    $result[] = $s;
427
                }
428 12
            }
429 12
        }
430 12
        $this->content .= implode('', $result);
431 12
    }
432
433
    /**
434
     * @param string $text
435
     * @param string $attribute
436
     *
437
     * @return null|string
438
     */
439 1
    protected function getAttributeFromTag($text, $attribute)
440
    {
441 1
        $matches = array();
442 1
        if (preg_match(sprintf('/<[^>]*\b%s\s*=\s*([\'"])(.*)\1[^>]*>/i', $attribute), $text, $matches)) {
443 1
            return htmlspecialchars_decode($matches[2]);
444
        }
445
446
        return;
447
    }
448
449
    /**
450
     * @param string $text
451
     *
452
     * @return bool
453
     */
454 10
    protected function isListPlaceholder($text)
455
    {
456 10
        return $this->isPlaceholderType($text, array('ol', 'dl', 'ul'));
457
    }
458
459
    /**
460
     * @param string $text
461
     *
462
     * @return bool
463
     */
464 7
    public function isLinkPlaceholder($text)
465
    {
466 7
        return $this->isPlaceholderType($text, 'a');
467
    }
468
469
    /**
470
     * @param string $text
471
     *
472
     * @return bool
473
     */
474 6
    public function isImagePlaceholder($text)
475
    {
476 6
        return $this->isPlaceholderType($text, 'img');
477
    }
478
479
    /**
480
     * @param string       $text
481
     * @param array|string $types
482
     * @param bool         $strict
483
     *
484
     * @return bool
485
     */
486 10
    protected function isPlaceholderType($text, $types, $strict = true)
487
    {
488 10
        if (!is_array($types)) {
489 7
            $types = array($types);
490 7
        }
491
492 10
        $criteria = array();
493 10
        foreach ($types as $type) {
494 10
            if ($this->config->isIsolatedDiffTag($type)) {
495 10
                $criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type);
496 10
            } else {
497
                $criteria[] = $type;
498
            }
499 10
        }
500
501 10
        return in_array($text, $criteria, $strict);
502
    }
503
504
    /**
505
     * @param string $text
506
     *
507
     * @return bool
508
     */
509 7
    protected function isTablePlaceholder($text)
510
    {
511 7
        return $this->isPlaceholderType($text, 'table');
512
    }
513
514
    /**
515
     * @param Operation $operation
516
     * @param int       $posInNew
517
     *
518
     * @return array
519
     */
520 10
    protected function findIsolatedDiffTagsInOld($operation, $posInNew)
521
    {
522 10
        $offset = $posInNew - $operation->startInNew;
523
524 10
        return $this->oldIsolatedDiffTags[$operation->startInOld + $offset];
525
    }
526
527
    /**
528
     * @param string $tag
529
     * @param string $cssClass
530
     * @param array  $words
531
     */
532 9
    protected function insertTag($tag, $cssClass, &$words)
533
    {
534 9
        while (true) {
535 9
            if (count($words) == 0) {
536 9
                break;
537
            }
538
539 9
            $nonTags = $this->extractConsecutiveWords($words, 'noTag');
540
541 9
            $specialCaseTagInjection = '';
542 9
            $specialCaseTagInjectionIsBefore = false;
543
544 9
            if (count($nonTags) != 0) {
545 9
                $text = $this->wrapText(implode('', $nonTags), $tag, $cssClass);
546 9
                $this->content .= $text;
547 9
            } else {
548 6
                $firstOrDefault = false;
549 6
                foreach ($this->config->getSpecialCaseOpeningTags() as $x) {
550
                    if (preg_match($x, $words[ 0 ])) {
551
                        $firstOrDefault = $x;
552
                        break;
553
                    }
554 6
                }
555 6
                if ($firstOrDefault) {
556
                    $specialCaseTagInjection = '<ins class="mod">';
557
                    if ($tag == 'del') {
558
                        unset($words[ 0 ]);
559
                    }
560 6
                } elseif (array_search($words[ 0 ], $this->config->getSpecialCaseClosingTags()) !== false) {
561
                    $specialCaseTagInjection = '</ins>';
562
                    $specialCaseTagInjectionIsBefore = true;
563
                    if ($tag == 'del') {
564
                        unset($words[ 0 ]);
565
                    }
566
                }
567
            }
568 9
            if (count($words) == 0 && count($specialCaseTagInjection) == 0) {
569
                break;
570
            }
571 9
            if ($specialCaseTagInjectionIsBefore) {
572
                $this->content .= $specialCaseTagInjection.implode('', $this->extractConsecutiveWords($words, 'tag'));
573
            } else {
574 9
                $workTag = $this->extractConsecutiveWords($words, 'tag');
575 9
                if (isset($workTag[ 0 ]) && $this->isOpeningTag($workTag[ 0 ]) && !$this->isClosingTag($workTag[ 0 ])) {
576 8
                    if (strpos($workTag[ 0 ], 'class=')) {
577 2
                        $workTag[ 0 ] = str_replace('class="', 'class="diffmod ', $workTag[ 0 ]);
578 2
                        $workTag[ 0 ] = str_replace("class='", 'class="diffmod ', $workTag[ 0 ]);
579 2
                    } else {
580 8
                        $workTag[ 0 ] = str_replace('>', ' class="diffmod">', $workTag[ 0 ]);
581
                    }
582 8
                }
583
584 9
                $appendContent = implode('', $workTag).$specialCaseTagInjection;
585 9
                if (isset($workTag[0]) && false !== stripos($workTag[0], '<img')) {
586
                    $appendContent = $this->wrapText($appendContent, $tag, $cssClass);
587
                }
588 9
                $this->content .= $appendContent;
589
            }
590 9
        }
591 9
    }
592
593
    /**
594
     * @param string $word
595
     * @param string $condition
596
     *
597
     * @return bool
598
     */
599 9
    protected function checkCondition($word, $condition)
600
    {
601 9
        return $condition == 'tag' ? $this->isTag($word) : !$this->isTag($word);
602
    }
603
604
    /**
605
     * @param string $text
606
     * @param string $tagName
607
     * @param string $cssClass
608
     *
609
     * @return string
610
     */
611 10
    protected function wrapText($text, $tagName, $cssClass)
612
    {
613 10
        return sprintf('<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text);
614
    }
615
616
    /**
617
     * @param array  $words
618
     * @param string $condition
619
     *
620
     * @return array
621
     */
622 9
    protected function extractConsecutiveWords(&$words, $condition)
623
    {
624 9
        $indexOfFirstTag = null;
625 9
        $words = array_values($words);
626 9
        foreach ($words as $i => $word) {
627 9
            if (!$this->checkCondition($word, $condition)) {
628 8
                $indexOfFirstTag = $i;
629 8
                break;
630
            }
631 9
        }
632 9
        if ($indexOfFirstTag !== null) {
633 8
            $items = array();
634 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...
635 8
                if ($pos >= 0 && $pos < $indexOfFirstTag) {
636 8
                    $items[] = $s;
637 8
                }
638 8
            }
639 8
            if ($indexOfFirstTag > 0) {
640 8
                array_splice($words, 0, $indexOfFirstTag);
641 8
            }
642
643 8
            return $items;
644
        } else {
645 9
            $items = array();
646 9 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...
647 9
                if ($pos >= 0 && $pos <= count($words)) {
648 9
                    $items[] = $s;
649 9
                }
650 9
            }
651 9
            array_splice($words, 0, count($words));
652
653 9
            return $items;
654
        }
655
    }
656
657
    /**
658
     * @param string $item
659
     *
660
     * @return bool
661
     */
662 12
    protected function isTag($item)
663
    {
664 12
        return $this->isOpeningTag($item) || $this->isClosingTag($item);
665
    }
666
667
    /**
668
     * @param string $item
669
     *
670
     * @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...
671
     */
672 12
    protected function isOpeningTag($item)
673
    {
674 12
        return preg_match('#<[^>]+>\\s*#iU', $item);
675
    }
676
677
    /**
678
     * @param string $item
679
     *
680
     * @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...
681
     */
682 12
    protected function isClosingTag($item)
683
    {
684 12
        return preg_match('#</[^>]+>\\s*#iU', $item);
685
    }
686
687
    /**
688
     * @return Operation[]
689
     */
690 12
    protected function operations()
691
    {
692 12
        $positionInOld = 0;
693 12
        $positionInNew = 0;
694 12
        $operations = array();
695 12
        $matches = $this->matchingBlocks();
696 12
        $matches[] = new Match(count($this->oldWords), count($this->newWords), 0);
697 12
        foreach ($matches as $i => $match) {
698 12
            $matchStartsAtCurrentPositionInOld = ($positionInOld == $match->startInOld);
699 12
            $matchStartsAtCurrentPositionInNew = ($positionInNew == $match->startInNew);
700 12
            $action = 'none';
0 ignored issues
show
Unused Code introduced by
$action is not used, you could remove the assignment.

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

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

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

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

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

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

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

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

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

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

Loading history...
707 4
                $action = 'delete';
708 4
            } else { // This occurs if the first few words are the same in both versions
709 12
                $action = 'none';
710
            }
711 12
            if ($action != 'none') {
712 9
                $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...
713 9
            }
714 12
            if (count($match) != 0) {
715 12
                $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...
716 12
            }
717 12
            $positionInOld = $match->endInOld();
718 12
            $positionInNew = $match->endInNew();
719 12
        }
720
721 12
        return $operations;
722
    }
723
724
    /**
725
     * @return Match[]
726
     */
727 12
    protected function matchingBlocks()
728
    {
729 12
        $matchingBlocks = array();
730 12
        $this->findMatchingBlocks(0, count($this->oldWords), 0, count($this->newWords), $matchingBlocks);
731
732 12
        return $matchingBlocks;
733
    }
734
735
    /**
736
     * @param int   $startInOld
737
     * @param int   $endInOld
738
     * @param int   $startInNew
739
     * @param int   $endInNew
740
     * @param array $matchingBlocks
741
     */
742 12
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
743
    {
744 12
        $match = $this->findMatch($startInOld, $endInOld, $startInNew, $endInNew);
745 12
        if ($match !== null) {
746 12
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
747 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...
748 7
            }
749 12
            $matchingBlocks[] = $match;
750 12
            if ($match->endInOld() < $endInOld && $match->endInNew() < $endInNew) {
751 9
                $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...
752 9
            }
753 12
        }
754 12
    }
755
756
    /**
757
     * @param string $word
758
     *
759
     * @return string
760
     */
761 8
    protected function stripTagAttributes($word)
762
    {
763 8
        $word = explode(' ', trim($word, '<>'));
764
765 8
        return '<'.$word[ 0 ].'>';
766
    }
767
768
    /**
769
     * @param int $startInOld
770
     * @param int $endInOld
771
     * @param int $startInNew
772
     * @param int $endInNew
773
     *
774
     * @return Match|null
775
     */
776 12
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
777
    {
778 12
        $bestMatchInOld = $startInOld;
779 12
        $bestMatchInNew = $startInNew;
780 12
        $bestMatchSize = 0;
781 12
        $matchLengthAt = array();
782 12
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; ++$indexInOld) {
783 12
            $newMatchLengthAt = array();
784 12
            $index = $this->oldWords[ $indexInOld ];
785 12
            if ($this->isTag($index)) {
786 6
                $index = $this->stripTagAttributes($index);
787 6
            }
788 12
            if (!isset($this->wordIndices[ $index ])) {
789 8
                $matchLengthAt = $newMatchLengthAt;
790 8
                continue;
791
            }
792 12
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
793 12
                if ($indexInNew < $startInNew) {
794 7
                    continue;
795
                }
796 12
                if ($indexInNew >= $endInNew) {
797 7
                    break;
798
                }
799 12
                $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...
800 12
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
801 12
                if ($newMatchLength > $bestMatchSize ||
802
                    (
803 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...
804 11
                        $bestMatchSize > 0 &&
805 11
                        preg_match(
806 11
                            '/^\s+$/',
807 11
                            implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize))
808 11
                        )
809 11
                    )
810 12
                ) {
811 12
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
812 12
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
813 12
                    $bestMatchSize = $newMatchLength;
814 12
                }
815 12
            }
816 12
            $matchLengthAt = $newMatchLengthAt;
817 12
        }
818
819
        // Skip match if none found or match consists only of whitespace
820 12
        if ($bestMatchSize != 0 &&
821
            (
822 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...
823 12
                !preg_match('/^\s+$/', implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize)))
824 12
            )
825 12
        ) {
826 12
            return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
827
        }
828
829 6
        return;
830
    }
831
}
832