Passed
Push — master ( ddbfa4...e1019e )
by Josh
03:27
created

lib/Caxy/HtmlDiff/HtmlDiff.php (8 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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