Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
67.29% |
72 / 107 |
|
50.00% |
7 / 14 |
CRAP | |
0.00% |
0 / 1 |
Wikidiff2TextDiffer | |
67.29% |
72 / 107 |
|
50.00% |
7 / 14 |
64.63 | |
0.00% |
0 / 1 |
isInstalled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
__construct | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
4.01 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFormatContext | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getCacheKeys | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getOptionsHash | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
doRenderBatch | |
68.00% |
17 / 25 |
|
0.00% |
0 / 1 |
8.61 | |||
doTableFormat | |
50.00% |
6 / 12 |
|
0.00% |
0 / 1 |
2.50 | |||
doInlineFormat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFormats | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTablePrefixes | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
2 | |||
localize | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
addLocalizedTitleTooltips | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
getPreferredFormatBatch | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Diff\TextDiffer; |
4 | |
5 | use MediaWiki\Html\Html; |
6 | use TextSlotDiffRenderer; |
7 | |
8 | /** |
9 | * @since 1.41 |
10 | */ |
11 | class Wikidiff2TextDiffer extends BaseTextDiffer { |
12 | /** @var string */ |
13 | private $version; |
14 | /** @var bool */ |
15 | private $haveMoveSupport; |
16 | /** @var bool */ |
17 | private $haveMultiFormatSupport; |
18 | /** @var bool */ |
19 | private $haveCutoffParameter; |
20 | /** @var bool */ |
21 | private $useMultiFormat; |
22 | /** @var array */ |
23 | private $defaultOptions; |
24 | /** @var array[] */ |
25 | private $formatOptions; |
26 | /** @var string */ |
27 | private $optionsHash; |
28 | |
29 | private const OPT_NAMES = [ |
30 | 'numContextLines', |
31 | 'changeThreshold', |
32 | 'movedLineThreshold', |
33 | 'maxMovedLines', |
34 | 'maxWordLevelDiffComplexity', |
35 | 'maxSplitSize', |
36 | 'initialSplitThreshold', |
37 | 'finalSplitThreshold', |
38 | ]; |
39 | |
40 | /** |
41 | * Fake wikidiff2 extension version for PHPUnit testing |
42 | * @var string|null |
43 | */ |
44 | public static $fakeVersionForTesting = null; |
45 | |
46 | /** |
47 | * Determine whether the extension is installed (or mocked for testing) |
48 | * |
49 | * @return bool |
50 | */ |
51 | public static function isInstalled() { |
52 | return self::$fakeVersionForTesting !== null |
53 | || function_exists( 'wikidiff2_do_diff' ); |
54 | } |
55 | |
56 | /** |
57 | * @param array $options |
58 | */ |
59 | public function __construct( $options ) { |
60 | $this->version = self::$fakeVersionForTesting ?? phpversion( 'wikidiff2' ); |
61 | $this->haveMoveSupport = version_compare( $this->version, '1.5.0', '>=' ); |
62 | $this->haveMultiFormatSupport = version_compare( $this->version, '1.14.0', '>=' ); |
63 | $this->haveCutoffParameter = $this->haveMoveSupport |
64 | && version_compare( $this->version, '1.8.0', '<' ); |
65 | |
66 | $this->useMultiFormat = $this->haveMultiFormatSupport && !empty( $options['useMultiFormat'] ); |
67 | $validOpts = array_fill_keys( self::OPT_NAMES, true ); |
68 | $this->defaultOptions = array_intersect_key( $options, $validOpts ); |
69 | $this->formatOptions = []; |
70 | foreach ( $options['formatOptions'] ?? [] as $format => $formatOptions ) { |
71 | $this->formatOptions[$format] = array_intersect_key( $formatOptions, $validOpts ); |
72 | } |
73 | } |
74 | |
75 | public function getName(): string { |
76 | return 'wikidiff2'; |
77 | } |
78 | |
79 | public function getFormatContext( string $format ) { |
80 | return $format === 'inline' ? self::CONTEXT_PLAIN : self::CONTEXT_ROW; |
81 | } |
82 | |
83 | public function getCacheKeys( array $formats ): array { |
84 | return [ |
85 | '20-wikidiff2-version' => $this->version, |
86 | '21-wikidiff2-options' => $this->getOptionsHash(), |
87 | ]; |
88 | } |
89 | |
90 | /** |
91 | * Get a hash of the cache-varying constructor options |
92 | * |
93 | * @return string |
94 | */ |
95 | private function getOptionsHash() { |
96 | if ( $this->optionsHash === null ) { |
97 | $json = json_encode( |
98 | [ |
99 | $this->useMultiFormat, |
100 | $this->defaultOptions, |
101 | $this->formatOptions, |
102 | ], |
103 | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE |
104 | ); |
105 | $this->optionsHash = substr( md5( $json ), 0, 8 ); |
106 | } |
107 | return $this->optionsHash; |
108 | } |
109 | |
110 | public function doRenderBatch( string $oldText, string $newText, array $formats ): array { |
111 | if ( $this->useMultiFormat ) { |
112 | if ( !$this->formatOptions ) { |
113 | /** @var array $result */ |
114 | $result = wikidiff2_multi_format_diff( |
115 | $oldText, |
116 | $newText, |
117 | [ 'formats' => $formats ] + $this->defaultOptions |
118 | ); |
119 | } else { |
120 | $result = []; |
121 | foreach ( $formats as $format ) { |
122 | $result[$format] = wikidiff2_multi_format_diff( |
123 | $oldText, |
124 | $newText, |
125 | [ 'formats' => $formats ] |
126 | + ( $this->formatOptions[$format] ?? [] ) |
127 | + $this->defaultOptions |
128 | ); |
129 | } |
130 | } |
131 | } else { |
132 | $result = []; |
133 | foreach ( $formats as $format ) { |
134 | switch ( $format ) { |
135 | case 'table': |
136 | $result['table'] = $this->doTableFormat( $oldText, $newText ); |
137 | break; |
138 | |
139 | case 'inline': |
140 | $result['inline'] = $this->doInlineFormat( $oldText, $newText ); |
141 | break; |
142 | } |
143 | } |
144 | } |
145 | return $result; |
146 | } |
147 | |
148 | /** |
149 | * Do a table format diff |
150 | * |
151 | * @param string $old |
152 | * @param string $new |
153 | * @return string |
154 | */ |
155 | private function doTableFormat( $old, $new ) { |
156 | if ( $this->haveCutoffParameter ) { |
157 | return wikidiff2_do_diff( |
158 | $old, |
159 | $new, |
160 | 2, |
161 | 0 |
162 | ); |
163 | } else { |
164 | // Don't pass the 4th parameter introduced in version 1.5.0 and removed in version 1.8.0 |
165 | return wikidiff2_do_diff( |
166 | $old, |
167 | $new, |
168 | 2 |
169 | ); |
170 | } |
171 | } |
172 | |
173 | /** |
174 | * Do an inline format diff |
175 | * |
176 | * @param string $oldText |
177 | * @param string $newText |
178 | * @return string |
179 | */ |
180 | private function doInlineFormat( $oldText, $newText ) { |
181 | return wikidiff2_inline_diff( $oldText, $newText, 2 ); |
182 | } |
183 | |
184 | public function getFormats(): array { |
185 | return [ 'table', 'inline' ]; |
186 | } |
187 | |
188 | public function getTablePrefixes( string $format ): array { |
189 | $localizer = $this->getLocalizer(); |
190 | $ins = Html::element( 'span', |
191 | [ 'class' => 'mw-diff-inline-legend-ins' ], |
192 | $localizer->msg( 'diff-inline-tooltip-ins' )->plain() |
193 | ); |
194 | $del = Html::element( 'span', |
195 | [ 'class' => 'mw-diff-inline-legend-del' ], |
196 | $localizer->msg( 'diff-inline-tooltip-del' )->plain() |
197 | ); |
198 | $hideDiffClass = $format === 'inline' ? '' : 'oo-ui-element-hidden'; |
199 | $legend = Html::rawElement( 'div', |
200 | [ 'class' => 'mw-diff-inline-legend ' . $hideDiffClass ], "$del $ins" |
201 | ); |
202 | return [ TextSlotDiffRenderer::INLINE_LEGEND_KEY => $legend ]; |
203 | } |
204 | |
205 | public function localize( string $format, string $diff, array $options = [] ): string { |
206 | $diff = $this->localizeLineNumbers( $diff, |
207 | $options['reducedLineNumbers'] ?? false |
208 | ); |
209 | if ( $this->haveMoveSupport ) { |
210 | $diff = $this->addLocalizedTitleTooltips( $format, $diff ); |
211 | } |
212 | return $diff; |
213 | } |
214 | |
215 | /** |
216 | * Add title attributes for tooltips on various diff elements |
217 | * |
218 | * @param string $format |
219 | * @param string $text |
220 | * @return string |
221 | */ |
222 | private function addLocalizedTitleTooltips( $format, $text ) { |
223 | // Moved paragraph indicators. |
224 | $localizer = $this->getLocalizer(); |
225 | $replacements = [ |
226 | 'class="mw-diff-movedpara-right"' => |
227 | 'class="mw-diff-movedpara-right" title="' . |
228 | $localizer->msg( 'diff-paragraph-moved-toold' )->escaped() . '"', |
229 | 'class="mw-diff-movedpara-left"' => |
230 | 'class="mw-diff-movedpara-left" title="' . |
231 | $localizer->msg( 'diff-paragraph-moved-tonew' )->escaped() . '"', |
232 | ]; |
233 | // For inline diffs, add tooltips to `<ins>` and `<del>`. |
234 | if ( $format == 'inline' ) { |
235 | $replacements['<ins>'] = Html::openElement( 'ins', |
236 | [ 'title' => $localizer->msg( 'diff-inline-tooltip-ins' )->plain() ] ); |
237 | $replacements['<del>'] = Html::openElement( 'del', |
238 | [ 'title' => $localizer->msg( 'diff-inline-tooltip-del' )->plain() ] ); |
239 | } |
240 | return strtr( $text, $replacements ); |
241 | } |
242 | |
243 | public function getPreferredFormatBatch( string $format ): array { |
244 | if ( $this->formatOptions ) { |
245 | return [ $format ]; |
246 | } else { |
247 | return [ 'table', 'inline' ]; |
248 | } |
249 | } |
250 | } |