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

lib/Caxy/HtmlDiff/HtmlDiff.php (3 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) {
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) {
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