Passed
Push — master ( 49e3d0...4782d7 )
by Josh
01:10
created

HtmlDiff::build()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.4742

Importance

Changes 9
Bugs 0 Features 4
Metric Value
c 9
b 0
f 4
dl 0
loc 23
ccs 11
cts 15
cp 0.7332
rs 8.5906
cc 5
eloc 13
nc 5
nop 0
crap 5.4742
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 7 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 7
        $diff = new self($oldText, $newText);
44
45 7
        if (null !== $config) {
46 7
            $diff->setConfig($config);
47 7
        }
48
49 7
        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
        $isolatedDiffTagIndicies = 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 11
                if ($openIsolatedDiffTags === 0) {
152 11
                    $isolatedDiffTagStart = $index;
153 11
                }
154 11
                $openIsolatedDiffTags++;
155 11
                $currentIsolatedDiffTag = $openIsolatedDiffTag;
156 11
            } elseif ($openIsolatedDiffTags > 0 && $this->isClosingIsolatedDiffTag($word, $currentIsolatedDiffTag)) {
157 10
                $openIsolatedDiffTags--;
158 10
                if ($openIsolatedDiffTags == 0) {
159 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...
160 10
                    $currentIsolatedDiffTag = null;
161 10
                }
162 10
            }
163 11
        }
164 11
        $isolatedDiffTagScript = array();
165 11
        $offset = 0;
166 11
        foreach ($isolatedDiffTagIndicies as $isolatedDiffTagIndex) {
167 10
            $start = $isolatedDiffTagIndex['start'] - $offset;
168 10
            $placeholderString = $this->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']);
169 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...
170 10
            $offset += $isolatedDiffTagIndex['length'] - 1;
171 11
        }
172
173 11
        return $isolatedDiffTagScript;
174
175
    }
176
177
    /**
178
     * @param string      $item
179
     * @param null|string $currentIsolatedDiffTag
180
     *
181
     * @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...
182
     */
183 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...
184
    {
185
        $tagsToMatch = $currentIsolatedDiffTag !== null
186 11
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
187 11
            : $this->config->getIsolatedDiffTags();
188 11
        foreach ($tagsToMatch as $key => $value) {
189 11
            if (preg_match("#<".$key."[^>]*>\\s*#iU", $item)) {
190 11
                return $key;
191
            }
192 11
        }
193
194 11
        return false;
195
    }
196
197
    /**
198
     * @param string      $item
199
     * @param null|string $currentIsolatedDiffTag
200
     *
201
     * @return false|string
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|false?

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

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