Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
WordLevelDiff
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 4
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 split
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 orig
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 closing
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * Copyright © 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
4 * You may copy this code freely under the conditions of the GPL.
5 *
6 * @license GPL-2.0-or-later
7 * @file
8 * @ingroup DifferenceEngine
9 * @defgroup DifferenceEngine DifferenceEngine
10 */
11
12namespace Wikimedia\Diff;
13
14/**
15 * Performs a word-level diff on several lines
16 *
17 * @newable
18 * @ingroup DifferenceEngine
19 */
20class WordLevelDiff extends Diff {
21    /**
22     * @inheritDoc
23     */
24    protected $bailoutComplexity = 40_000_000; // Roughly 6K x 6K words changed
25
26    /**
27     * @stable to call
28     * @todo Don't do work in the constructor, use a service to create diffs instead (T257472).
29     *
30     * @param string[] $linesBefore
31     * @param string[] $linesAfter
32     */
33    public function __construct( $linesBefore, $linesAfter ) {
34        [ $wordsBefore, $wordsBeforeStripped ] = $this->split( $linesBefore );
35        [ $wordsAfter, $wordsAfterStripped ] = $this->split( $linesAfter );
36
37        try {
38            parent::__construct( $wordsBeforeStripped, $wordsAfterStripped );
39        } catch ( ComplexityException ) {
40            // Too hard to diff, just show whole paragraph(s) as changed
41            $this->edits = [ new DiffOpChange( $linesBefore, $linesAfter ) ];
42        }
43
44        $xi = $yi = 0;
45        $editCount = count( $this->edits );
46        for ( $i = 0; $i < $editCount; $i++ ) {
47            $orig = &$this->edits[$i]->orig;
48            if ( is_array( $orig ) ) {
49                $orig = array_slice( $wordsBefore, $xi, count( $orig ) );
50                $xi += count( $orig );
51            }
52
53            $closing = &$this->edits[$i]->closing;
54            if ( is_array( $closing ) ) {
55                $closing = array_slice( $wordsAfter, $yi, count( $closing ) );
56                $yi += count( $closing );
57            }
58        }
59    }
60
61    /**
62     * @param string[] $lines
63     *
64     * @return array[]
65     */
66    private function split( $lines ) {
67        $words = [];
68        $stripped = [];
69        $first = true;
70        foreach ( $lines as $line ) {
71            if ( $first ) {
72                $first = false;
73            } else {
74                $words[] = "\n";
75                $stripped[] = "\n";
76            }
77            $m = [];
78            if ( preg_match_all( '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
79                $line, $m ) ) {
80                foreach ( $m[0] as $word ) {
81                    $words[] = $word;
82                }
83                foreach ( $m[1] as $stripped_word ) {
84                    $stripped[] = $stripped_word;
85                }
86            }
87        }
88
89        return [ $words, $stripped ];
90    }
91
92    /**
93     * @return string[]
94     */
95    public function orig() {
96        $orig = new WordAccumulator;
97
98        foreach ( $this->edits as $edit ) {
99            if ( $edit->type == 'copy' ) {
100                $orig->addWords( $edit->orig );
101            } elseif ( $edit->orig ) {
102                $orig->addWords( $edit->orig, 'del' );
103            }
104        }
105        $lines = $orig->getLines();
106
107        return $lines;
108    }
109
110    /**
111     * @return string[]
112     */
113    public function closing() {
114        $closing = new WordAccumulator;
115
116        foreach ( $this->edits as $edit ) {
117            if ( $edit->type == 'copy' ) {
118                $closing->addWords( $edit->closing );
119            } elseif ( $edit->closing ) {
120                $closing->addWords( $edit->closing, 'ins' );
121            }
122        }
123        $lines = $closing->getLines();
124
125        return $lines;
126    }
127
128}
129
130/** @deprecated class alias since 1.41 */
131class_alias( WordLevelDiff::class, 'WordLevelDiff' );