Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
69.31% |
70 / 101 |
|
42.86% |
9 / 21 |
CRAP | |
0.00% |
0 / 1 |
ManifoldTextDiffer | |
69.31% |
70 / 101 |
|
42.86% |
9 / 21 |
99.98 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFormats | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
hasFormat | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
render | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
2.26 | |||
renderBatch | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getFormatContext | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addRowWrapper | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addModules | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCacheKeys | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
localize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTablePrefixes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPreferredFormatBatch | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDiffersByFormat | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getDifferForFormat | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
2.50 | |||
setEngine | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getEngineForFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDiffers | |
87.50% |
14 / 16 |
|
0.00% |
0 / 1 |
6.07 | |||
injectDeps | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
maybeCreateDiffer | |
70.83% |
17 / 24 |
|
0.00% |
0 / 1 |
11.01 | |||
splitBatchByDiffer | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Diff\TextDiffer; |
4 | |
5 | use DomainException; |
6 | use Language; |
7 | use MediaWiki\Output\OutputPage; |
8 | use MessageLocalizer; |
9 | use UnexpectedValueException; |
10 | |
11 | /** |
12 | * A TextDiffer which acts as a container for other TextDiffers, and dispatches |
13 | * requests to them. |
14 | * |
15 | * @since 1.41 |
16 | */ |
17 | class ManifoldTextDiffer implements TextDiffer { |
18 | /** @var MessageLocalizer */ |
19 | private $localizer; |
20 | /** @var Language|null */ |
21 | private $contentLanguage; |
22 | /** @var string|null */ |
23 | private $diffEngine; |
24 | /** @var string|false|null */ |
25 | private $externalPath; |
26 | /** @var TextDiffer[]|null Differs in order of priority, from highest to lowest */ |
27 | private $differs; |
28 | /** @var TextDiffer[]|null The differ to use for each format */ |
29 | private $differsByFormat; |
30 | /** @var array */ |
31 | private $wikidiff2Options; |
32 | |
33 | /** |
34 | * @internal For use by DifferenceEngine, ContentHandler |
35 | * |
36 | * @param MessageLocalizer $localizer |
37 | * @param Language|null $contentLanguage |
38 | * @param string|null $diffEngine The DiffEngine config variable |
39 | * @param string|false|null $externalPath The ExternalDiffEngine config variable |
40 | * @param array $wikidiff2Options The Wikidiff2Options config variable |
41 | */ |
42 | public function __construct( |
43 | MessageLocalizer $localizer, |
44 | ?Language $contentLanguage, |
45 | $diffEngine, |
46 | $externalPath, |
47 | $wikidiff2Options |
48 | ) { |
49 | $this->localizer = $localizer; |
50 | $this->contentLanguage = $contentLanguage; |
51 | $this->diffEngine = $diffEngine; |
52 | $this->externalPath = $externalPath; |
53 | $this->wikidiff2Options = $wikidiff2Options; |
54 | } |
55 | |
56 | public function getName(): string { |
57 | return 'manifold'; |
58 | } |
59 | |
60 | public function getFormats(): array { |
61 | $differs = $this->getDiffersByFormat(); |
62 | return array_keys( $differs ); |
63 | } |
64 | |
65 | public function hasFormat( string $format ): bool { |
66 | $differs = $this->getDiffersByFormat(); |
67 | return isset( $differs[$format] ); |
68 | } |
69 | |
70 | public function render( string $oldText, string $newText, string $format ): string { |
71 | if ( !in_array( $format, $this->getFormats(), true ) ) { |
72 | throw new \InvalidArgumentException( |
73 | 'The requested format is not supported by this engine' ); |
74 | } |
75 | $results = $this->renderBatch( $oldText, $newText, [ $format ] ); |
76 | return reset( $results ); |
77 | } |
78 | |
79 | public function renderBatch( string $oldText, string $newText, array $formats ): array { |
80 | $result = []; |
81 | $differs = $this->splitBatchByDiffer( $formats ); |
82 | /** @var TextDiffer $differ */ |
83 | foreach ( $differs as [ $differ, $formatBatch ] ) { |
84 | $result += $differ->renderBatch( $oldText, $newText, $formatBatch ); |
85 | } |
86 | return $result; |
87 | } |
88 | |
89 | public function getFormatContext( string $format ) { |
90 | return $this->getDifferForFormat( $format )->getFormatContext( $format ); |
91 | } |
92 | |
93 | public function addRowWrapper( string $format, string $diffText ): string { |
94 | return $this->getDifferForFormat( $format )->addRowWrapper( $format, $diffText ); |
95 | } |
96 | |
97 | public function addModules( OutputPage $out, string $format ): void { |
98 | $this->getDifferForFormat( $format )->addModules( $out, $format ); |
99 | } |
100 | |
101 | public function getCacheKeys( array $formats ): array { |
102 | $keys = []; |
103 | $engines = []; |
104 | $differs = $this->splitBatchByDiffer( $formats ); |
105 | /** @var TextDiffer $differ */ |
106 | foreach ( $differs as [ $differ, $formatBatch ] ) { |
107 | $keys += $differ->getCacheKeys( $formatBatch ); |
108 | $engines[] = $differ->getName() . '=' . implode( ',', $formatBatch ); |
109 | } |
110 | $keys['10-formats-and-engines'] = implode( ';', $engines ); |
111 | return $keys; |
112 | } |
113 | |
114 | public function localize( string $format, string $diff, array $options = [] ): string { |
115 | return $this->getDifferForFormat( $format )->localize( $format, $diff, $options ); |
116 | } |
117 | |
118 | public function getTablePrefixes( string $format ): array { |
119 | return $this->getDifferForFormat( $format )->getTablePrefixes( $format ); |
120 | } |
121 | |
122 | public function getPreferredFormatBatch( string $format ): array { |
123 | return $this->getDifferForFormat( $format )->getPreferredFormatBatch( $format ); |
124 | } |
125 | |
126 | /** |
127 | * @return TextDiffer[] |
128 | */ |
129 | private function getDiffersByFormat() { |
130 | if ( $this->differsByFormat === null ) { |
131 | $differs = []; |
132 | foreach ( $this->getDiffers() as $differ ) { |
133 | foreach ( $differ->getFormats() as $format ) { |
134 | // getDiffers() is in order of priority -- don't overwrite |
135 | $differs[$format] ??= $differ; |
136 | } |
137 | } |
138 | $this->differsByFormat = $differs; |
139 | } |
140 | return $this->differsByFormat; |
141 | } |
142 | |
143 | /** |
144 | * @param string $format |
145 | * @return TextDiffer |
146 | */ |
147 | private function getDifferForFormat( $format ) { |
148 | $differs = $this->getDiffersByFormat(); |
149 | if ( !isset( $differs[$format] ) ) { |
150 | throw new \InvalidArgumentException( |
151 | "Unknown format \"$format\"" |
152 | ); |
153 | } |
154 | return $differs[$format]; |
155 | } |
156 | |
157 | /** |
158 | * Disable text differs apart from the one with the given name. |
159 | * |
160 | * @param string $name |
161 | */ |
162 | public function setEngine( string $name ) { |
163 | $this->diffEngine = $name; |
164 | $this->differs = null; |
165 | $this->differsByFormat = null; |
166 | } |
167 | |
168 | /** |
169 | * Get the text differ name which will be used for the specified format |
170 | * |
171 | * @param string $format |
172 | * @return string|null |
173 | */ |
174 | public function getEngineForFormat( string $format ) { |
175 | return $this->getDifferForFormat( $format )->getName(); |
176 | } |
177 | |
178 | /** |
179 | * Get differs in a numerically indexed array. When a format is requested, |
180 | * the first TextDiffer in this array which can handle the format will be |
181 | * used. |
182 | * |
183 | * @return TextDiffer[] |
184 | */ |
185 | private function getDiffers() { |
186 | if ( $this->differs === null ) { |
187 | $differs = []; |
188 | if ( $this->diffEngine === null ) { |
189 | $differNames = [ 'external', 'wikidiff2', 'php' ]; |
190 | } else { |
191 | $differNames = [ $this->diffEngine ]; |
192 | } |
193 | $failureReason = ''; |
194 | foreach ( $differNames as $name ) { |
195 | $differ = $this->maybeCreateDiffer( $name, $failureReason ); |
196 | if ( $differ ) { |
197 | $this->injectDeps( $differ ); |
198 | $differs[] = $differ; |
199 | } |
200 | } |
201 | if ( !$differs ) { |
202 | throw new UnexpectedValueException( |
203 | "Cannot use diff engine '{$this->diffEngine}': $failureReason" ); |
204 | } |
205 | // TODO: add a hook here, allowing extensions to add differs |
206 | $this->differs = $differs; |
207 | } |
208 | return $this->differs; |
209 | } |
210 | |
211 | /** |
212 | * Initialize an object which may be a subclass of BaseTextDiffer, passing |
213 | * down injected dependencies. |
214 | * |
215 | * @param TextDiffer $differ |
216 | */ |
217 | public function injectDeps( TextDiffer $differ ) { |
218 | if ( $differ instanceof BaseTextDiffer ) { |
219 | $differ->setLocalizer( $this->localizer ); |
220 | } |
221 | } |
222 | |
223 | /** |
224 | * Create a TextDiffer by engine name. If it can't be created due to a |
225 | * configuration or platform issue, return null and set $failureReason. |
226 | * |
227 | * @param string $engine |
228 | * @param string &$failureReason Out param which will be set to the failure reason |
229 | * @return TextDiffer|null |
230 | */ |
231 | private function maybeCreateDiffer( $engine, &$failureReason ) { |
232 | switch ( $engine ) { |
233 | case 'external': |
234 | if ( is_string( $this->externalPath ) ) { |
235 | if ( is_executable( $this->externalPath ) ) { |
236 | return new ExternalTextDiffer( |
237 | $this->externalPath |
238 | ); |
239 | } |
240 | $failureReason = 'ExternalDiffEngine config points to a non-executable'; |
241 | } elseif ( $this->externalPath ) { |
242 | $failureReason = 'ExternalDiffEngine config is set to a non-string value'; |
243 | } else { |
244 | return null; |
245 | } |
246 | wfWarn( "$failureReason, ignoring" ); |
247 | return null; |
248 | |
249 | case 'wikidiff2': |
250 | if ( Wikidiff2TextDiffer::isInstalled() ) { |
251 | return new Wikidiff2TextDiffer( |
252 | $this->wikidiff2Options |
253 | ); |
254 | } |
255 | $failureReason = 'wikidiff2 is not available'; |
256 | return null; |
257 | |
258 | case 'php': |
259 | // Always available. |
260 | return new PhpTextDiffer( |
261 | $this->contentLanguage |
262 | ); |
263 | |
264 | default: |
265 | throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine ); |
266 | } |
267 | } |
268 | |
269 | /** |
270 | * Given an array of formats, break it down by the TextDiffer object which |
271 | * will handle each format. Each element of the result array is a list in |
272 | * which the first element is the TextDiffer object, and the second element |
273 | * is the list of formats which the TextDiffer will handle. |
274 | * |
275 | * @param array $formats |
276 | * @return array|array{0:TextDiffer,1:string[]} |
277 | */ |
278 | private function splitBatchByDiffer( $formats ) { |
279 | $result = []; |
280 | foreach ( $formats as $format ) { |
281 | $differ = $this->getDifferForFormat( $format ); |
282 | $name = $differ->getName(); |
283 | if ( isset( $result[$name] ) ) { |
284 | $result[$name][1][] = $format; |
285 | } else { |
286 | $result[$name] = [ $differ, [ $format ] ]; |
287 | } |
288 | } |
289 | return array_values( $result ); |
290 | } |
291 | } |