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

lib/Caxy/HtmlDiff/HtmlDiff.php (5 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
 * @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)
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
        $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);
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->config->getIsolatedDiffTagPlaceholder($isolatedDiffTagIndex['tagType']);
159 10
            $isolatedDiffTagScript[$start] = array_splice($words, $start, $isolatedDiffTagIndex['length'], $placeholderString);
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
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)
174
    {
175
        $tagsToMatch = $currentIsolatedDiffTag !== null
176 11
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
177 11
            : $this->config->getIsolatedDiffTags();
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
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)
194
    {
195
        $tagsToMatch = $currentIsolatedDiffTag !== null
196 11
            ? array($currentIsolatedDiffTag => $this->config->getIsolatedDiffTagPlaceholder($currentIsolatedDiffTag))
197 11
            : $this->config->getIsolatedDiffTags();
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' :
214 11
            $this->processEqualOperation( $operation );
215 11
            break;
216 9
            case 'delete' :
217 5
            $this->processDeleteOperation( $operation, "diffdel" );
218 5
            break;
219 9
            case 'insert' :
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)
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 ($this->config->isIsolatedDiffTagPlaceholder($s) && 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)
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 ($this->config->isIsolatedDiffTagPlaceholder($s) && 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->config->isUseTableDiffing() && $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 = HtmlDiff::create($oldText, $newText, $this->config);
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
    protected function diffList($oldText, $newText)
341
    {
342 4
        $diff = ListDiffNew::create($oldText, $newText, $this->config);
343
344 4
        return $diff->build();
345
    }
346
347
    /**
348
     * @param string $oldText
349
     * @param string $newText
350
     *
351
     * @return string
352
     */
353
    protected function diffTables($oldText, $newText)
354
    {
355
        $diff = TableDiff::create($oldText, $newText, $this->config);
356
357
        return $diff->build();
358
    }
359
360
    /**
361
     * @param string $oldText
362
     * @param string $newText
363
     *
364
     * @return string
365
     */
366 1
    protected function diffLinks($oldText, $newText)
367
    {
368 1
        $oldHref = $this->getAttributeFromTag($oldText, 'href');
369 1
        $newHref = $this->getAttributeFromTag($newText, 'href');
370
371 1
        if ($oldHref != $newHref) {
372 1
            return sprintf(
373 1
                '%s%s',
374 1
                $this->wrapText($oldText, 'del', 'diffmod diff-href'),
375 1
                $this->wrapText($newText, 'ins', 'diffmod diff-href')
376 1
            );
377
        }
378
379 1
        return $this->diffElements($oldText, $newText);
380
    }
381
382
    /**
383
     * @param Operation $operation
384
     */
385 11 View Code Duplication
    protected function processEqualOperation($operation)
386
    {
387 11
        $result = array();
388 11
        foreach ($this->newWords as $pos => $s) {
389
390 11
            if ($pos >= $operation->startInNew && $pos < $operation->endInNew) {
391 11
                if ($this->config->isIsolatedDiffTagPlaceholder($s) && isset($this->newIsolatedDiffTags[$pos])) {
392
393 7
                    $result[] = $this->diffIsolatedPlaceholder($operation, $pos, $s);
394 7
                } else {
395 11
                    $result[] = $s;
396
                }
397 11
            }
398 11
        }
399 11
        $this->content .= implode( "", $result );
400 11
    }
401
402
    /**
403
     * @param string $text
404
     * @param string $attribute
405
     *
406
     * @return null|string
407
     */
408 1
    protected function getAttributeFromTag($text, $attribute)
409
    {
410 1
        $matches = array();
411 1
        if (preg_match(sprintf('/<a\s+[^>]*%s=([\'"])(.*)\1[^>]*>/i', $attribute), $text, $matches)) {
412 1
            return $matches[2];
413
        }
414
415
        return null;
416
    }
417
418
    /**
419
     * @param string $text
420
     *
421
     * @return bool
422
     */
423 7
    protected function isListPlaceholder($text)
424
    {
425 7
        return $this->isPlaceholderType($text, array('ol', 'dl', 'ul'));
426
    }
427
428
    /**
429
     * @param string $text
430
     *
431
     * @return bool
432
     */
433 5
    public function isLinkPlaceholder($text)
434
    {
435 5
        return $this->isPlaceholderType($text, 'a');
436
    }
437
438
    /**
439
     * @param string       $text
440
     * @param array|string $types
441
     * @param bool         $strict
442
     *
443
     * @return bool
444
     */
445 7
    protected function isPlaceholderType($text, $types, $strict = true)
446
    {
447 7
        if (!is_array($types)) {
448 5
            $types = array($types);
449 5
        }
450
451 7
        $criteria = array();
452 7
        foreach ($types as $type) {
453 7
            if ($this->config->isIsolatedDiffTag($type)) {
454 7
                $criteria[] = $this->config->getIsolatedDiffTagPlaceholder($type);
455 7
            } else {
456
                $criteria[] = $type;
457
            }
458 7
        }
459
460 7
        return in_array($text, $criteria, $strict);
461
    }
462
463
    /**
464
     * @param string $text
465
     *
466
     * @return bool
467
     */
468 5
    protected function isTablePlaceholder($text)
469
    {
470 5
        return $this->isPlaceholderType($text, 'table');
471
    }
472
473
    /**
474
     * @param Operation $operation
475
     * @param int       $posInNew
476
     *
477
     * @return array
478
     */
479 7
    protected function findIsolatedDiffTagsInOld($operation, $posInNew)
480
    {
481 7
        $offset = $posInNew - $operation->startInNew;
482
483 7
        return $this->oldIsolatedDiffTags[$operation->startInOld + $offset];
484
    }
485
486
    /**
487
     * @param string $tag
488
     * @param string $cssClass
489
     * @param array  $words
490
     */
491 9
    protected function insertTag($tag, $cssClass, &$words)
492
    {
493 9
        while (true) {
494 9
            if ( count( $words ) == 0 ) {
495 9
                break;
496
            }
497
498 9
            $nonTags = $this->extractConsecutiveWords( $words, 'noTag' );
499
500 9
            $specialCaseTagInjection = '';
501 9
            $specialCaseTagInjectionIsBefore = false;
502
503 9
            if ( count( $nonTags ) != 0 ) {
504 9
                $text = $this->wrapText( implode( "", $nonTags ), $tag, $cssClass );
505 9
                $this->content .= $text;
506 9
            } else {
507 6
                $firstOrDefault = false;
508 6
                foreach ($this->config->getSpecialCaseOpeningTags() as $x) {
509
                    if ( preg_match( $x, $words[ 0 ] ) ) {
510
                        $firstOrDefault = $x;
511
                        break;
512
                    }
513 6
                }
514 6
                if ($firstOrDefault) {
515
                    $specialCaseTagInjection = '<ins class="mod">';
516
                    if ($tag == "del") {
517
                        unset( $words[ 0 ] );
518
                    }
519 6
                } elseif ( array_search( $words[ 0 ], $this->config->getSpecialCaseClosingTags()) !== false ) {
520
                    $specialCaseTagInjection = "</ins>";
521
                    $specialCaseTagInjectionIsBefore = true;
522
                    if ($tag == "del") {
523
                        unset( $words[ 0 ] );
524
                    }
525
                }
526
            }
527 9
            if ( count( $words ) == 0 && count( $specialCaseTagInjection ) == 0 ) {
528
                break;
529
            }
530 9
            if ($specialCaseTagInjectionIsBefore) {
531
                $this->content .= $specialCaseTagInjection . implode( "", $this->extractConsecutiveWords( $words, 'tag' ) );
532
            } else {
533 9
                $workTag = $this->extractConsecutiveWords( $words, 'tag' );
534 9
                if ( isset( $workTag[ 0 ] ) && $this->isOpeningTag( $workTag[ 0 ] ) && !$this->isClosingTag( $workTag[ 0 ] ) ) {
535 8
                    if ( strpos( $workTag[ 0 ], 'class=' ) ) {
536 4
                        $workTag[ 0 ] = str_replace( 'class="', 'class="diffmod ', $workTag[ 0 ] );
537 4
                        $workTag[ 0 ] = str_replace( "class='", 'class="diffmod ', $workTag[ 0 ] );
538 4
                    } else {
539 8
                        $workTag[ 0 ] = str_replace( ">", ' class="diffmod">', $workTag[ 0 ] );
540
                    }
541 8
                }
542 9
                $this->content .= implode( "", $workTag ) . $specialCaseTagInjection;
543
            }
544 9
        }
545 9
    }
546
547
    /**
548
     * @param string $word
549
     * @param string $condition
550
     *
551
     * @return bool
552
     */
553 9
    protected function checkCondition($word, $condition)
554
    {
555 9
        return $condition == 'tag' ? $this->isTag( $word ) : !$this->isTag( $word );
556
    }
557
558
    /**
559
     * @param string $text
560
     * @param string $tagName
561
     * @param string $cssClass
562
     *
563
     * @return string
564
     */
565 10
    protected function wrapText($text, $tagName, $cssClass)
566
    {
567 10
        return sprintf( '<%1$s class="%2$s">%3$s</%1$s>', $tagName, $cssClass, $text );
568
    }
569
570
    /**
571
     * @param array  $words
572
     * @param string $condition
573
     *
574
     * @return array
575
     */
576 9
    protected function extractConsecutiveWords(&$words, $condition)
577
    {
578 9
        $indexOfFirstTag = null;
579 9
        $words = array_values($words);
580 9
        foreach ($words as $i => $word) {
581 9
            if ( !$this->checkCondition( $word, $condition ) ) {
582 8
                $indexOfFirstTag = $i;
583 8
                break;
584
            }
585 9
        }
586 9
        if ($indexOfFirstTag !== null) {
587 8
            $items = array();
588 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...
589 8
                if ($pos >= 0 && $pos < $indexOfFirstTag) {
590 8
                    $items[] = $s;
591 8
                }
592 8
            }
593 8
            if ($indexOfFirstTag > 0) {
594 8
                array_splice( $words, 0, $indexOfFirstTag );
595 8
            }
596
597 8
            return $items;
598
        } else {
599 9
            $items = array();
600 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...
601 9
                if ( $pos >= 0 && $pos <= count( $words ) ) {
602 9
                    $items[] = $s;
603 9
                }
604 9
            }
605 9
            array_splice( $words, 0, count( $words ) );
606
607 9
            return $items;
608
        }
609
    }
610
611
    /**
612
     * @param string $item
613
     *
614
     * @return bool
615
     */
616 11
    protected function isTag($item)
617
    {
618 11
        return $this->isOpeningTag( $item ) || $this->isClosingTag( $item );
619
    }
620
621
    /**
622
     * @param string $item
623
     *
624
     * @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...
625
     */
626 11
    protected function isOpeningTag($item)
627
    {
628 11
        return preg_match( "#<[^>]+>\\s*#iU", $item );
629
    }
630
631
    /**
632
     * @param string $item
633
     *
634
     * @return bool
635
     */
636 11
    protected function isClosingTag($item)
637
    {
638 11
        return preg_match( "#</[^>]+>\\s*#iU", $item );
639
    }
640
641
    /**
642
     * @return Operation[]
643
     */
644 11
    protected function operations()
645
    {
646 11
        $positionInOld = 0;
647 11
        $positionInNew = 0;
648 11
        $operations = array();
649 11
        $matches = $this->matchingBlocks();
650 11
        $matches[] = new Match( count( $this->oldWords ), count( $this->newWords ), 0 );
651 11
        foreach ($matches as $i => $match) {
652 11
            $matchStartsAtCurrentPositionInOld = ( $positionInOld == $match->startInOld );
653 11
            $matchStartsAtCurrentPositionInNew = ( $positionInNew == $match->startInNew );
654 11
            $action = 'none';
655
656 11
            if ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == false) {
657 7
                $action = 'replace';
658 11
            } elseif ($matchStartsAtCurrentPositionInOld == true && $matchStartsAtCurrentPositionInNew == false) {
659 8
                $action = 'insert';
660 11
            } elseif ($matchStartsAtCurrentPositionInOld == false && $matchStartsAtCurrentPositionInNew == true) {
661 5
                $action = 'delete';
662 5
            } else { // This occurs if the first few words are the same in both versions
663 11
                $action = 'none';
664
            }
665 11
            if ($action != 'none') {
666 9
                $operations[] = new Operation( $action, $positionInOld, $match->startInOld, $positionInNew, $match->startInNew );
667 9
            }
668 11
            if ( count( $match ) != 0 ) {
669 11
                $operations[] = new Operation( 'equal', $match->startInOld, $match->endInOld(), $match->startInNew, $match->endInNew() );
670 11
            }
671 11
            $positionInOld = $match->endInOld();
672 11
            $positionInNew = $match->endInNew();
673 11
        }
674
675 11
        return $operations;
676
    }
677
678
    /**
679
     * @return Match[]
680
     */
681 11
    protected function matchingBlocks()
682
    {
683 11
        $matchingBlocks = array();
684 11
        $this->findMatchingBlocks( 0, count( $this->oldWords ), 0, count( $this->newWords ), $matchingBlocks );
685
686 11
        return $matchingBlocks;
687
    }
688
689
    /**
690
     * @param int   $startInOld
691
     * @param int   $endInOld
692
     * @param int   $startInNew
693
     * @param int   $endInNew
694
     * @param array $matchingBlocks
695
     */
696 11
    protected function findMatchingBlocks($startInOld, $endInOld, $startInNew, $endInNew, &$matchingBlocks)
697
    {
698 11
        $match = $this->findMatch( $startInOld, $endInOld, $startInNew, $endInNew );
699 11
        if ($match !== null) {
700 11
            if ($startInOld < $match->startInOld && $startInNew < $match->startInNew) {
701 8
                $this->findMatchingBlocks( $startInOld, $match->startInOld, $startInNew, $match->startInNew, $matchingBlocks );
702 8
            }
703 11
            $matchingBlocks[] = $match;
704 11
            if ( $match->endInOld() < $endInOld && $match->endInNew() < $endInNew ) {
705 9
                $this->findMatchingBlocks( $match->endInOld(), $endInOld, $match->endInNew(), $endInNew, $matchingBlocks );
706 9
            }
707 11
        }
708 11
    }
709
710
    /**
711
     * @param string $word
712
     *
713
     * @return string
714
     */
715 8
    protected function stripTagAttributes($word)
716
    {
717 8
        $word = explode( ' ', trim( $word, '<>' ) );
718
719 8
        return '<' . $word[ 0 ] . '>';
720
    }
721
722
    /**
723
     * @param int $startInOld
724
     * @param int $endInOld
725
     * @param int $startInNew
726
     * @param int $endInNew
727
     *
728
     * @return Match|null
729
     */
730 11
    protected function findMatch($startInOld, $endInOld, $startInNew, $endInNew)
731
    {
732 11
        $bestMatchInOld = $startInOld;
733 11
        $bestMatchInNew = $startInNew;
734 11
        $bestMatchSize = 0;
735 11
        $matchLengthAt = array();
736 11
        for ($indexInOld = $startInOld; $indexInOld < $endInOld; $indexInOld++) {
737 11
            $newMatchLengthAt = array();
738 11
            $index = $this->oldWords[ $indexInOld ];
739 11
            if ( $this->isTag( $index ) ) {
740 6
                $index = $this->stripTagAttributes( $index );
741 6
            }
742 11
            if ( !isset( $this->wordIndices[ $index ] ) ) {
743 9
                $matchLengthAt = $newMatchLengthAt;
744 9
                continue;
745
            }
746 11
            foreach ($this->wordIndices[ $index ] as $indexInNew) {
747 11
                if ($indexInNew < $startInNew) {
748 9
                    continue;
749
                }
750 11
                if ($indexInNew >= $endInNew) {
751 8
                    break;
752
                }
753 11
                $newMatchLength = ( isset( $matchLengthAt[ $indexInNew - 1 ] ) ? $matchLengthAt[ $indexInNew - 1 ] : 0 ) + 1;
754 11
                $newMatchLengthAt[ $indexInNew ] = $newMatchLength;
755 11
                if ($newMatchLength > $bestMatchSize ||
756
                    (
757 11
                        $this->isGroupDiffs() &&
758 11
                        $bestMatchSize > 0 &&
759 11
                        preg_match(
760 11
                            '/^\s+$/',
761 11
                            implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize))
762 11
                        )
763 11
                    )
764 11
                ) {
765 11
                    $bestMatchInOld = $indexInOld - $newMatchLength + 1;
766 11
                    $bestMatchInNew = $indexInNew - $newMatchLength + 1;
767 11
                    $bestMatchSize = $newMatchLength;
768 11
                }
769 11
            }
770 11
            $matchLengthAt = $newMatchLengthAt;
771 11
        }
772
773
        // Skip match if none found or match consists only of whitespace
774 11
        if ($bestMatchSize != 0 &&
775
            (
776 11
                !$this->isGroupDiffs() ||
777 11
                !preg_match('/^\s+$/', implode('', array_slice($this->oldWords, $bestMatchInOld, $bestMatchSize)))
778 11
            )
779 11
        ) {
780 11
            return new Match($bestMatchInOld, $bestMatchInNew, $bestMatchSize);
781
        }
782
783 7
        return null;
784
    }
785
}
786