Passed
Pull Request — master (#39)
by
unknown
04:40
created

HtmlDiff::clearContent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 0
cts 3
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace Caxy\HtmlDiff;
4
5
use Caxy\HtmlDiff\Table\TableDiff;
6
7
/**
8
 * Class HtmlDiff
9
 * @package Caxy\HtmlDiff
10
 */
11
class HtmlDiff extends AbstractDiff
12
{
13
    /**
14
     * @var array
15
     */
16
    protected $wordIndices;
17
    /**
18
     * @var array
19
     */
20
    protected $oldTables;
21
    /**
22
     * @var array
23
     */
24
    protected $newTables;
25
    /**
26
     * @var array
27
     */
28
    protected $newIsolatedDiffTags;
29
    /**
30
     * @var array
31
     */
32
    protected $oldIsolatedDiffTags;
33
34
    /**
35
     * @param string              $oldText
36
     * @param string              $newText
37
     * @param HtmlDiffConfig|null $config
38
     *
39
     * @return self
40
     */
41 8 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...
42
    {
43 8
        $diff = new self($oldText, $newText);
44
45 8
        if (null !== $config) {
46 8
            $diff->setConfig($config);
47 8
        }
48
49 8
        return $diff;
50
    }
51
52
    /**
53
     * @param $bool
54
     *
55
     * @return $this
56
     *
57
     * @deprecated since 0.1.0
58
     */
59
    public function setUseTableDiffing($bool)
60
    {
61
        $this->config->setUseTableDiffing($bool);
62
63
        return $this;
64
    }
65
66
    /**
67
     * @param  boolean  $boolean
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 boolean
81
     *
82
     * @deprecated since 0.1.0
83
     */
84
    public function getInsertSpaceInReplace()
85
    {
86
        return $this->config->isInsertSpaceInReplace();
87
    }
88
89
    public function clearContent()
90
    {
91
      $this->content = '';
92
    }
93
94
    /**
95
     * @return string
96
     */
97 11
    public function build()
98
    {
99 11
        if ($this->hasDiffCache() && $this->getDiffCache()->contains($this->oldText, $this->newText)) {
100
            $this->content = $this->getDiffCache()->fetch($this->oldText, $this->newText);
101
102
            return $this->content;
103
        }
104
105 11
        $this->splitInputsToWords();
106 11
        $this->replaceIsolatedDiffTags();
107 11
        $this->indexNewWords();
108
109 11
        $operations = $this->operations();
110 11
        foreach ($operations as $item) {
111 11
            $this->performOperation( $item );
112 11
        }
113
114 11
        if ($this->hasDiffCache()) {
115
            $this->getDiffCache()->save($this->oldText, $this->newText, $this->content);
116
        }
117
118 11
        return $this->content;
119
    }
120
121 11
    protected function indexNewWords()
122
    {
123 11
        $this->wordIndices = array();
124 11
        foreach ($this->newWords as $i => $word) {
125 11
            if ( $this->isTag( $word ) ) {
126 8
                $word = $this->stripTagAttributes( $word );
127 8
            }
128 11
            if ( isset( $this->wordIndices[ $word ] ) ) {
129 11
                $this->wordIndices[ $word ][] = $i;
130 11
            } else {
131 11
                $this->wordIndices[ $word ] = array( $i );
132
            }
133 11
        }
134 11
    }
135
136 11
    protected function replaceIsolatedDiffTags()
137
    {
138 11
        $this->oldIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->oldWords);
139 11
        $this->newIsolatedDiffTags = $this->createIsolatedDiffTagPlaceholders($this->newWords);
140 11
    }
141
142
    /**
143
     * @param array $words
144
     *
145
     * @return array
146
     */
147 11
    protected function createIsolatedDiffTagPlaceholders(&$words)
148
    {
149 11
        $openIsolatedDiffTags = 0;
150 11
        $isolatedDiffTagIndices = array();
151 11
        $isolatedDiffTagStart = 0;
152 11
        $currentIsolatedDiffTag = null;
153 11
        foreach ($words as $index => $word) {
154 11
            $openIsolatedDiffTag = $this->isOpeningIsolatedDiffTag($word, $currentIsolatedDiffTag);
155 11
            if ($openIsolatedDiffTag) {
156 10
                if ($this->isSelfClosingTag($word) || stripos($word, '<img') !== false) {
157 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...
158
                        $isolatedDiffTagIndices[] = array(
159
                            'start'   => $index,
160
                            'length'  => 1,
161
                            'tagType' => $openIsolatedDiffTag,
162
                        );
163
                        $currentIsolatedDiffTag = null;
164
                    }
165
                } else {
166 10
                    if ($openIsolatedDiffTags === 0) {
167 10
                        $isolatedDiffTagStart = $index;
168 10
                    }
169 10
                    $openIsolatedDiffTags++;
170 10
                    $currentIsolatedDiffTag = $openIsolatedDiffTag;
171
                }
172 11
            } elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) {
173 10
                $openIsolatedDiffTags--;
174 10 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...
175 10
                    $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 175 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...
176 10
                    $currentIsolatedDiffTag = null;
177 10
                }
178 10
            }
179 11
        }
180 11
        $isolatedDiffTagScript = array();
181 11
        $offset = 0;
182 11
        foreach ($isolatedDiffTagIndices as $isolatedDiffTagIndex) {
183 10
            $start = $isolatedDiffTagIndex['start'] - $offset;
184 10
            $placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']);
185 10
            $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...
186 10
            $offset += $isolatedDiffTagIndex['length'] - 1;
187 11
        }
188
189 11
        return $isolatedDiffTagScript;
190
191
    }
192
193
    /**
194
     * @param string      $item
195
     * @param null|string $currentIsolatedDiffTag
196
     *
197
     * @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...
198
     */
199 11 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...
200
    {
201
        $tagsToMatch = $currentIsolatedDiffTag !== null
202 11
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
203 11
            : $this->config->getIsolatedDiffTags();
204 11
        $pattern = '#<%s(\s+[^>]*)?>#iU';
205 11
        foreach ($tagsToMatch as $key => $value) {
206 11
            if (preg_match(sprintf($pattern, $key), $item)) {
207 10
                return $key;
208
            }
209 11
        }
210
211 11
        return false;
212
    }
213
214 10
    protected function isSelfClosingTag($text)
215
    {
216 10
        return (bool) preg_match('/<[^>]+\/\s*>/', $text);
217
    }
218
219
    /**
220
     * @param string      $item
221
     * @param null|string $currentIsolatedDiffTag
222
     *
223
     * @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...
224
     */
225 10 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...
226
    {
227
        $tagsToMatch = $currentIsolatedDiffTag !== null
228 10
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
229 10
            : $this->config->getIsolatedDiffTags();
230 10
        $pattern = '#</%s(\s+[^>]*)?>#iU';
231 10
        foreach ($tagsToMatch as $key => $value) {
232 10
            if (preg_match(sprintf($pattern, $key), $item)) {
233 10
                return $key;
234
            }
235 10
        }
236
237 10
        return false;
238
    }
239
240
    /**
241
     * @param Operation $operation
242
     */
243 11
    protected function performOperation($operation)
244
    {
245 11
        switch ($operation->action) {
246 11
            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...
247 11
            $this->processEqualOperation( $operation );
248 11
            break;
249 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...
250 5
            $this->processDeleteOperation( $operation, "diffdel" );
251 5
            break;
252 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...
253 9
            $this->processInsertOperation( $operation, "diffins");
254 9
            break;
255 7
            case 'replace':
256 7
            $this->processReplaceOperation( $operation );
257 7
            break;
258
            default:
259
            break;
260 11
        }
261 11
    }
262
263
    /**
264
     * @param Operation $operation
265
     */
266 7
    protected function processReplaceOperation($operation)
267
    {
268 7
        $this->processDeleteOperation( $operation, "diffmod" );
269 7
        $this->processInsertOperation( $operation, "diffmod" );
270 7
    }
271
272
    /**
273
     * @param Operation $operation
274
     * @param string    $cssClass
275
     */
276 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...
277
    {
278 9
        $text = array();
279 9
        foreach ($this->newWords as $pos => $s) {
280 9
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
281 9
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
282 4
                    foreach ($this->newIsolatedDiffTags[$pos] as $word) {
283 4
                        $text[] = $word;
284 4
                    }
285 4
                } else {
286 9
                    $text[] = $s;
287
                }
288 9
            }
289 9
        }
290 9
        $this->insertTag( "ins", $cssClass, $text );
291 9
    }
292
293
    /**
294
     * @param Operation $operation
295
     * @param string    $cssClass
296
     */
297 9 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...
298
    {
299 9
        $text = array();
300 9
        foreach ($this->oldWords as $pos => $s) {
301 9
            if ($pos >= $operation->startInOld && $pos < $operation->endInOld) {
302 9
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->oldIsolatedDiffTags[$pos])) {
303 7
                    foreach ($this->oldIsolatedDiffTags[$pos] as $word) {
304 7
                        $text[] = $word;
305 7
                    }
306 7
                } else {
307 8
                    $text[] = $s;
308
                }
309 9
            }
310 9
        }
311 9
        $this->insertTag( "del", $cssClass, $text );
312 9
    }
313
314
    /**
315
     * @param Operation $operation
316
     * @param int       $pos
317
     * @param string    $placeholder
318
     * @param bool      $stripWrappingTags
319
     *
320
     * @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...
321
     */
322 8
    protected function diffIsolatedPlaceholder($operation, $pos, $placeholder, $stripWrappingTags = true)
323
    {
324 8
        $oldText = implode("", $this->findIsolatedDiffTagsInOld($operation, $pos));
325 8
        $newText = implode("", $this->newIsolatedDiffTags[$pos]);
326
327 8
        if ($this->isListPlaceholder($placeholder)) {
328 7
            return $this->diffList($oldText, $newText);
329 5
        } elseif ($this->config->isUseTableDiffing() && $this->isTablePlaceholder($placeholder)) {
330
            return $this->diffTables($oldText, $newText);
331 5
        } elseif ($this->isLinkPlaceholder($placeholder)) {
332 1
            return $this->diffElementsByAttribute($oldText, $newText, 'href', 'a');
333 4
        } elseif ($this->isImagePlaceholder($placeholder)) {
334
            return $this->diffElementsByAttribute($oldText, $newText, 'src', 'img');
335
        }
336
337 4
        return $this->diffElements($oldText, $newText, $stripWrappingTags);
338
    }
339
340
    /**
341
     * @param string $oldText
342
     * @param string $newText
343
     * @param bool   $stripWrappingTags
344
     *
345
     * @return string
346
     */
347 5
    protected function diffElements($oldText, $newText, $stripWrappingTags = true)
348
    {
349 5
        $wrapStart = '';
350 5
        $wrapEnd = '';
351
352 5
        if ($stripWrappingTags) {
353 5
            $pattern = '/(^<[^>]+>)|(<\/[^>]+>$)/i';
354 5
            $matches = array();
355
356 5
            if (preg_match_all($pattern, $newText, $matches)) {
357 5
                $wrapStart = isset($matches[0][0]) ? $matches[0][0] : '';
358 5
                $wrapEnd = isset($matches[0][1]) ? $matches[0][1] : '';
359 5
            }
360 5
            $oldText = preg_replace($pattern, '', $oldText);
361 5
            $newText = preg_replace($pattern, '', $newText);
362 5
        }
363
364 5
        $diff = HtmlDiff::create($oldText, $newText, $this->config);
365
366 5
        return $wrapStart . $diff->build() . $wrapEnd;
367
    }
368
369
    /**
370
     * @param string $oldText
371
     * @param string $newText
372
     *
373
     * @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...
374
     */
375 7
    protected function diffList($oldText, $newText)
376
    {
377 7
        $diff = ListDiffNew::create($oldText, $newText, $this->config);
378
379 7
        return $diff->build();
380
    }
381
382
    /**
383
     * @param string $oldText
384
     * @param string $newText
385
     *
386
     * @return string
387
     */
388
    protected function diffTables($oldText, $newText)
389
    {
390
        $diff = TableDiff::create($oldText, $newText, $this->config);
391
392
        return $diff->build();
393
    }
394
395 1
    protected function diffElementsByAttribute($oldText, $newText, $attribute, $element)
396
    {
397 1
        $oldAttribute = $this->getAttributeFromTag($oldText, $attribute);
398 1
        $newAttribute = $this->getAttributeFromTag($newText, $attribute);
399
400 1
        if ($oldAttribute !== $newAttribute) {
401 1
            $diffClass = sprintf('diffmod diff%s diff%s', $element, $attribute);
402
403 1
            return sprintf(
404 1
                '%s%s',
405 1
                $this->wrapText($oldText, 'del', $diffClass),
406 1
                $this->wrapText($newText, 'ins', $diffClass)
407 1
            );
408
        }
409
410 1
        return $this->diffElements($oldText, $newText);
411
    }
412
413
    /**
414
     * @param Operation $operation
415
     */
416 11 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...
417
    {
418 11
        $result = array();
419 11
        foreach ($this->newWords as $pos => $s) {
420
421 11
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
422 11
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
423
424 8
                    $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s);
425 8
                } else {
426 11
                    $result[] = $s;
427
                }
428 11
            }
429 11
        }
430 11
        $this->content .= implode( "", $result );
431 11
    }
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 null;
447
    }
448
449
    /**
450
     * @param string $text
451
     *
452
     * @return bool
453
     */
454 8
    protected function isListPlaceholder($text)
455
    {
456 8
        return $this->isPlaceholderType($text, array('ol', 'dl', 'ul'));
457
    }
458
459
    /**
460
     * @param string $text
461
     *
462
     * @return bool
463
     */
464 5
    public function isLinkPlaceholder($text)
465
    {
466 5
        return $this->isPlaceholderType($text, 'a');
467
    }
468
469
    /**
470
     * @param string $text
471
     *
472
     * @return bool
473
     */
474 4
    public function isImagePlaceholder($text)
475
    {
476 4
        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 8
    protected function isPlaceholderType($text, $types, $strict = true)
487
    {
488 8
        if (!is_array($types)) {
489 5
            $types = array($types);
490 5
        }
491
492 8
        $criteria = array();
493 8
        foreach ($types as $type) {
494 8
            if ($this->config->isIsolatedDiffTag($type)) {
495 8
                $criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type);
496 8
            } else {
497
                $criteria[] = $type;
498
            }
499 8
        }
500
501 8
        return in_array($text, $criteria, $strict);
502
    }
503
504
    /**
505
     * @param string $text
506
     *
507
     * @return bool
508
     */
509 5
    protected function isTablePlaceholder($text)
510
    {
511 5
        return $this->isPlaceholderType($text, 'table');
512
    }
513
514
    /**
515
     * @param Operation $operation
516
     * @param int       $posInNew
517
     *
518
     * @return array
519
     */
520 8
    protected function findIsolatedDiffTagsInOld($operation, $posInNew)
521
    {
522 8
        $offset = $posInNew - $operation->startInNew;
523
524 8
        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' ) );
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...
573
            } else {
574 9
                $workTag = $this->extractConsecutiveWords( $words, 'tag' );
575 9
                if ( isset( $workTag[ 0 ] ) && $this->isOpeningTag( $workTag[ 0 ] ) && !$this->isClosingTag( $workTag[ 0 ] ) ) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 128 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...
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 11
    protected function isTag($item)
663
    {
664 11
        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 11
    protected function isOpeningTag($item)
673
    {
674 11
        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 11
    protected function isClosingTag($item)
683
    {
684 11
        return preg_match( "#</[^>]+>\\s*#iU", $item );
685
    }
686
687
    /**
688
     * @return Operation[]
689
     */
690 11
    protected function operations()
691
    {
692 11
        $positionInOld = 0;
693 11
        $positionInNew = 0;
694 11
        $operations = array();
695 11
        $matches = $this->matchingBlocks();
696 11
        $matches[] = new Match( count( $this->oldWords ), count( $this->newWords ), 0 );
697 11
        foreach ($matches as $i => $match) {
698 11
            $matchStartsAtCurrentPositionInOld = ( $positionInOld == $match->startInOld );
699 11
            $matchStartsAtCurrentPositionInNew = ( $positionInNew == $match->startInNew );
700 11
            $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 11
            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 7
                $action = 'replace';
704 11
            } 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 9
                $action = 'insert';
706 11
            } 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 5
                $action = 'delete';
708 5
            } else { // This occurs if the first few words are the same in both versions
709 11
                $action = 'none';
710
            }
711 11
            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 129 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 11
            if ( count( $match ) != 0 ) {
715 11
                $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 137 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 11
            }
717 11
            $positionInOld = $match->endInOld();
718 11
            $positionInNew = $match->endInNew();
719 11
        }
720
721 11
        return $operations;
722
    }
723
724
    /**
725
     * @return Match[]
726
     */
727 11
    protected function matchingBlocks()
728
    {
729 11
        $matchingBlocks = array();
730 11
        $this->findMatchingBlocks( 0, count( $this->oldWords ), 0, count( $this->newWords ), $matchingBlocks );
731
732 11
        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 11
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
743
    {
744 11
        $match = $this->findMatch( $startInOld, $endInOld, $startInNew, $endInNew );
745 11
        if ($match !== null) {
746 11
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
747 8
                $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 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...
748 8
            }
749 11
            $matchingBlocks[] = $match;
750 11
            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 123 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 11
        }
754 11
    }
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 11
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
777
    {
778 11
        $bestMatchInOld = $startInOld;
779 11
        $bestMatchInNew = $startInNew;
780 11
        $bestMatchSize = 0;
781 11
        $matchLengthAt = array();
782 11
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; $indexInOld++) {
783 11
            $newMatchLengthAt = array();
784 11
            $index = $this->oldWords[ $indexInOld ];
785 11
            if ( $this->isTag( $index ) ) {
786 6
                $index = $this->stripTagAttributes( $index );
787 6
            }
788 11
            if ( !isset( $this->wordIndices[ $index ] ) ) {
789 9
                $matchLengthAt = $newMatchLengthAt;
790 9
                continue;
791
            }
792 11
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
793 11
                if ($indexInNew < $startInNew) {
794 9
                    continue;
795
                }
796 11
                if ($indexInNew >= $endInNew) {
797 8
                    break;
798
                }
799 11
                $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 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...
800 11
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
801 11
                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 11
                ) {
811 11
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
812 11
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
813 11
                    $bestMatchSize = $newMatchLength;
814 11
                }
815 11
            }
816 11
            $matchLengthAt = $newMatchLengthAt;
817 11
        }
818
819
        // Skip match if none found or match consists only of whitespace
820 11
        if ($bestMatchSize != 0 &&
821
            (
822 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...
823 11
                !preg_match('/^\s+$/', implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize)))
824 11
            )
825 11
        ) {
826 11
            return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
827
        }
828
829 7
        return null;
830
    }
831
}
832