Passed
Pull Request — master (#31)
by Josh
03:20
created

HtmlDiff::isClosingIsolatedDiffTag()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 13
Code Lines 8

Duplication

Lines 11
Ratio 84.62 %

Code Coverage

Tests 8
CRAP Score 4

Importance

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