Passed
Push — master ( 65b179...c12483 )
by Josh
01:08
created

HtmlDiff::diffElementsByAttribute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

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