Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.86% |
79 / 86 |
|
40.00% |
2 / 5 |
CRAP | |
0.00% |
0 / 1 |
DiffParser | |
91.86% |
79 / 86 |
|
40.00% |
2 / 5 |
21.24 | |
0.00% |
0 / 1 |
getChangeSet | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
getChangeSetFromEmptyLeft | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
1 | |||
usingInternalDiff | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parse | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
3.00 | |||
parseLine | |
88.37% |
38 / 43 |
|
0.00% |
0 / 1 |
14.31 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications; |
4 | |
5 | /** |
6 | * MediaWiki Extension: Echo |
7 | * |
8 | * Permission is hereby granted, free of charge, to any person obtaining a copy |
9 | * of this software and associated documentation files (the "Software"), to deal |
10 | * in the Software without restriction, including without limitation the rights |
11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
12 | * copies of the Software, and to permit persons to whom the Software is |
13 | * furnished to do so, subject to the following conditions: |
14 | * |
15 | * The above copyright notice and this permission notice shall be included in |
16 | * all copies or substantial portions of the Software. |
17 | * |
18 | * This program is distributed WITHOUT ANY WARRANTY. |
19 | */ |
20 | |
21 | /** |
22 | * @file |
23 | * @ingroup Extensions |
24 | * @author Erik Bernhardson |
25 | */ |
26 | |
27 | use UnexpectedValueException; |
28 | use Wikimedia\Diff\Diff; |
29 | use Wikimedia\Diff\UnifiedDiffFormatter; |
30 | |
31 | /** |
32 | * Calculates the individual sets of differences between two pieces of text |
33 | * as individual groupings of add, subtract, and change actions. Internally |
34 | * uses 0-indexing for positions. All results from the class are 1 indexed |
35 | * to stay consistent with the original diff output and the previous diff |
36 | * parsing code. |
37 | */ |
38 | class DiffParser { |
39 | |
40 | /** |
41 | * @var int The number of characters the diff prefixes a line with |
42 | */ |
43 | protected $prefixLength = 1; |
44 | |
45 | /** |
46 | * @var string[] The text of the left side of the diff operation |
47 | */ |
48 | protected $left; |
49 | |
50 | /** |
51 | * @var int The current position within the left text |
52 | */ |
53 | protected $leftPos; |
54 | |
55 | /** |
56 | * @var string[] The text of the right side of the diff operation |
57 | */ |
58 | protected $right; |
59 | |
60 | /** |
61 | * @var int The current position within the right text |
62 | */ |
63 | protected $rightPos; |
64 | |
65 | /** |
66 | * @var array[] Set of add, subtract, or change operations within the diff |
67 | */ |
68 | protected $changeSet; |
69 | |
70 | /** |
71 | * Get the set of add, subtract, and change operations required to transform leftText into rightText |
72 | * |
73 | * @param string $leftText The left, or old, revision of the text |
74 | * @param string $rightText The right, or new, revision of the text |
75 | * @return array[] Array of arrays containing changes to individual groups of lines within the text |
76 | * Each change consists of: |
77 | * An 'action', one of: |
78 | * - add |
79 | * - subtract |
80 | * - change |
81 | * 'content' that was added or removed, or in the case |
82 | * of a change, 'old_content' and 'new_content' |
83 | * 'left_pos' and 'right_pos' (in 1-indexed lines) of the change. |
84 | */ |
85 | public function getChangeSet( $leftText, $rightText ) { |
86 | $left = trim( $leftText ); |
87 | $right = trim( $rightText ); |
88 | |
89 | if ( $left === '' ) { |
90 | // fixes T155998 |
91 | return $this->getChangeSetFromEmptyLeft( $right ); |
92 | } |
93 | |
94 | $diffs = new Diff( explode( "\n", $left ), explode( "\n", $right ) ); |
95 | $format = new UnifiedDiffFormatter(); |
96 | $diff = $format->format( $diffs ); |
97 | |
98 | return $this->parse( $diff, $left, $right ); |
99 | } |
100 | |
101 | /** |
102 | * If we add content to an empty page the changeSet can be composed straightaway |
103 | * |
104 | * @param string $right |
105 | * @return array[] See {@see getChangeSet} |
106 | */ |
107 | private function getChangeSetFromEmptyLeft( $right ) { |
108 | $rightLines = explode( "\n", $right ); |
109 | |
110 | return [ |
111 | '_info' => [ |
112 | 'lhs-length' => 1, |
113 | 'rhs-length' => count( $rightLines ), |
114 | 'lhs' => [ '' ], |
115 | 'rhs' => $rightLines |
116 | ], |
117 | [ |
118 | 'right-pos' => 1, |
119 | 'left-pos' => 1, |
120 | 'action' => 'add', |
121 | 'content' => $right, |
122 | ] |
123 | ]; |
124 | } |
125 | |
126 | /** |
127 | * Duplicates the check from the global wfDiff function to determine |
128 | * if we are using internal or external diff utilities |
129 | * |
130 | * @deprecated since 1.29, the internal diff parser is always used |
131 | * @return bool |
132 | */ |
133 | protected static function usingInternalDiff() { |
134 | return true; |
135 | } |
136 | |
137 | /** |
138 | * Parse the unified diff output into an array of changes to individual groups of the text |
139 | * |
140 | * @param string $diff The unified diff output |
141 | * @param string $left The left side of the diff used for sanity checks |
142 | * @param string $right The right side of the diff used for sanity checks |
143 | * |
144 | * @return array[] |
145 | */ |
146 | protected function parse( $diff, $left, $right ) { |
147 | $this->left = explode( "\n", $left ); |
148 | $this->right = explode( "\n", $right ); |
149 | $diff = explode( "\n", $diff ); |
150 | |
151 | $this->leftPos = 0; |
152 | $this->rightPos = 0; |
153 | $this->changeSet = [ |
154 | '_info' => [ |
155 | 'lhs-length' => count( $this->left ), |
156 | 'rhs-length' => count( $this->right ), |
157 | 'lhs' => $this->left, |
158 | 'rhs' => $this->right, |
159 | ], |
160 | ]; |
161 | |
162 | $change = null; |
163 | foreach ( $diff as $line ) { |
164 | $change = $this->parseLine( $line, $change ); |
165 | } |
166 | if ( $change === null ) { |
167 | return $this->changeSet; |
168 | } |
169 | |
170 | return array_merge( $this->changeSet, $change->getChangeSet() ); |
171 | } |
172 | |
173 | /** |
174 | * Parse the next line of the unified diff output |
175 | * |
176 | * @param string $line The next line of the unified diff |
177 | * @param DiffGroup|null $change Changes the immediately previous lines |
178 | * |
179 | * @return DiffGroup|null Changes to this line and any changed lines immediately previous |
180 | */ |
181 | protected function parseLine( $line, DiffGroup $change = null ) { |
182 | if ( $line ) { |
183 | $op = $line[0]; |
184 | if ( strlen( $line ) > $this->prefixLength ) { |
185 | $line = substr( $line, $this->prefixLength ); |
186 | } else { |
187 | $line = ''; |
188 | } |
189 | } else { |
190 | $op = ' '; |
191 | } |
192 | |
193 | switch ( $op ) { |
194 | case '@': |
195 | // metadata |
196 | if ( $change !== null ) { |
197 | $this->changeSet = array_merge( $this->changeSet, $change->getChangeSet() ); |
198 | $change = null; |
199 | } |
200 | // @@ -start,numLines +start,numLines @@ |
201 | [ , $left, $right ] = explode( ' ', $line, 3 ); |
202 | [ $this->leftPos ] = explode( ',', substr( $left, 1 ), 2 ); |
203 | [ $this->rightPos ] = explode( ',', substr( $right, 1 ), 2 ); |
204 | $this->leftPos = (int)$this->leftPos; |
205 | $this->rightPos = (int)$this->rightPos; |
206 | |
207 | // -1 because diff is 1 indexed, and we are 0 indexed |
208 | $this->leftPos--; |
209 | $this->rightPos--; |
210 | break; |
211 | |
212 | case ' ': |
213 | // No changes |
214 | if ( $change !== null ) { |
215 | $this->changeSet = array_merge( $this->changeSet, $change->getChangeSet() ); |
216 | $change = null; |
217 | } |
218 | $this->leftPos++; |
219 | $this->rightPos++; |
220 | break; |
221 | |
222 | case '-': |
223 | // subtract |
224 | if ( $this->left[$this->leftPos] !== $line ) { |
225 | throw new UnexpectedValueException( 'Positional error: left' ); |
226 | } |
227 | if ( $change === null ) { |
228 | // @phan-suppress-next-line PhanTypeMismatchArgument |
229 | $change = new DiffGroup( $this->leftPos, $this->rightPos ); |
230 | } |
231 | $change->subtract( $line ); |
232 | $this->leftPos++; |
233 | break; |
234 | |
235 | case '+': |
236 | // add |
237 | if ( $this->right[$this->rightPos] !== $line ) { |
238 | throw new UnexpectedValueException( 'Positional error: right' ); |
239 | } |
240 | if ( $change === null ) { |
241 | // @phan-suppress-next-line PhanTypeMismatchArgument |
242 | $change = new DiffGroup( $this->leftPos, $this->rightPos ); |
243 | } |
244 | $change->add( $line ); |
245 | $this->rightPos++; |
246 | break; |
247 | |
248 | default: |
249 | throw new UnexpectedValueException( 'Unknown Diff Operation: ' . $op ); |
250 | } |
251 | |
252 | return $change; |
253 | } |
254 | } |