Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.84% |
130 / 148 |
|
73.68% |
14 / 19 |
CRAP | |
0.00% |
0 / 1 |
ViolationMessageRenderer | |
87.84% |
130 / 148 |
|
73.68% |
14 / 19 |
40.60 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
render | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
addRole | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
msgEscaped | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renderArgument | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
renderList | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
4 | |||
renderEntityId | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
renderEntityIdList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderItemIdSnakValue | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
renderItemIdSnakValueList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderDataValue | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
renderDataValueType | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
renderInlineCode | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
renderConstraintScope | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
renderConstraintScopeList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderPropertyScope | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
renderPropertyScopeList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renderLanguage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
renderLanguageList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace WikibaseQuality\ConstraintReport\ConstraintCheck\Message; |
6 | |
7 | use DataValues\DataValue; |
8 | use InvalidArgumentException; |
9 | use LogicException; |
10 | use MediaWiki\Config\Config; |
11 | use MediaWiki\Languages\LanguageNameUtils; |
12 | use Message; |
13 | use MessageLocalizer; |
14 | use ValueFormatters\ValueFormatter; |
15 | use Wikibase\DataModel\Entity\EntityId; |
16 | use Wikibase\DataModel\Entity\ItemId; |
17 | use Wikibase\DataModel\Services\EntityId\EntityIdFormatter; |
18 | use Wikibase\Lib\TermLanguageFallbackChain; |
19 | use WikibaseQuality\ConstraintReport\ConstraintCheck\Context\Context; |
20 | use WikibaseQuality\ConstraintReport\ConstraintCheck\ItemIdSnakValue; |
21 | |
22 | /** |
23 | * Render a {@link ViolationMessage} into a localized string. |
24 | * |
25 | * Note: This class does <em>not</em> support multilingual text arguments – |
26 | * for that, use {@link MultilingualTextViolationMessageRenderer}. |
27 | * |
28 | * @license GPL-2.0-or-later |
29 | */ |
30 | class ViolationMessageRenderer { |
31 | |
32 | private EntityIdFormatter $entityIdFormatter; |
33 | private ValueFormatter $dataValueFormatter; |
34 | private LanguageNameUtils $languageNameUtils; |
35 | private string $userLanguageCode; |
36 | protected TermLanguageFallbackChain $languageFallbackChain; |
37 | protected MessageLocalizer $messageLocalizer; |
38 | private Config $config; |
39 | private int $maxListLength; |
40 | |
41 | /** |
42 | * @param EntityIdFormatter $entityIdFormatter |
43 | * @param ValueFormatter $dataValueFormatter |
44 | * @param MessageLocalizer $messageLocalizer |
45 | * @param Config $config |
46 | * @param int $maxListLength The maximum number of elements to be rendered in a list parameter. |
47 | * Longer lists are truncated to this length and then rendered with an ellipsis in the HMTL list. |
48 | */ |
49 | public function __construct( |
50 | EntityIdFormatter $entityIdFormatter, |
51 | ValueFormatter $dataValueFormatter, |
52 | LanguageNameUtils $languageNameUtils, |
53 | string $userLanguageCode, |
54 | TermLanguageFallbackChain $languageFallbackChain, |
55 | MessageLocalizer $messageLocalizer, |
56 | Config $config, |
57 | int $maxListLength = 10 |
58 | ) { |
59 | $this->entityIdFormatter = $entityIdFormatter; |
60 | $this->dataValueFormatter = $dataValueFormatter; |
61 | $this->languageNameUtils = $languageNameUtils; |
62 | $this->userLanguageCode = $userLanguageCode; |
63 | $this->languageFallbackChain = $languageFallbackChain; |
64 | $this->messageLocalizer = $messageLocalizer; |
65 | $this->config = $config; |
66 | $this->maxListLength = $maxListLength; |
67 | } |
68 | |
69 | public function render( ViolationMessage $violationMessage ): string { |
70 | $messageKey = $violationMessage->getMessageKey(); |
71 | $paramsLists = [ [] ]; |
72 | foreach ( $violationMessage->getArguments() as $argument ) { |
73 | $params = $this->renderArgument( $argument ); |
74 | $paramsLists[] = $params; |
75 | } |
76 | $allParams = call_user_func_array( 'array_merge', $paramsLists ); |
77 | return $this->messageLocalizer |
78 | ->msg( $messageKey ) |
79 | ->params( $allParams ) |
80 | ->escaped(); |
81 | } |
82 | |
83 | /** |
84 | * @param string $value HTML |
85 | * @param string|null $role one of the Role::* constants |
86 | * @return string HTML |
87 | */ |
88 | protected function addRole( string $value, ?string $role ): string { |
89 | if ( $role === null ) { |
90 | return $value; |
91 | } |
92 | |
93 | return '<span class="wbqc-role wbqc-role-' . htmlspecialchars( $role ) . '">' . |
94 | $value . |
95 | '</span>'; |
96 | } |
97 | |
98 | /** |
99 | * @param string $key message key |
100 | * @return string HTML |
101 | */ |
102 | protected function msgEscaped( string $key ): string { |
103 | return $this->messageLocalizer->msg( $key )->escaped(); |
104 | } |
105 | |
106 | /** |
107 | * @param array $argument |
108 | * @return array[] params (for Message::params) |
109 | */ |
110 | protected function renderArgument( array $argument ): array { |
111 | $methods = [ |
112 | ViolationMessage::TYPE_ENTITY_ID => 'renderEntityId', |
113 | ViolationMessage::TYPE_ENTITY_ID_LIST => 'renderEntityIdList', |
114 | ViolationMessage::TYPE_ITEM_ID_SNAK_VALUE => 'renderItemIdSnakValue', |
115 | ViolationMessage::TYPE_ITEM_ID_SNAK_VALUE_LIST => 'renderItemIdSnakValueList', |
116 | ViolationMessage::TYPE_DATA_VALUE => 'renderDataValue', |
117 | ViolationMessage::TYPE_DATA_VALUE_TYPE => 'renderDataValueType', |
118 | ViolationMessage::TYPE_INLINE_CODE => 'renderInlineCode', |
119 | ViolationMessage::TYPE_CONSTRAINT_SCOPE => 'renderConstraintScope', |
120 | ViolationMessage::TYPE_CONSTRAINT_SCOPE_LIST => 'renderConstraintScopeList', |
121 | ViolationMessage::TYPE_PROPERTY_SCOPE => 'renderPropertyScope', |
122 | ViolationMessage::TYPE_PROPERTY_SCOPE_LIST => 'renderPropertyScopeList', |
123 | ViolationMessage::TYPE_LANGUAGE => 'renderLanguage', |
124 | ViolationMessage::TYPE_LANGUAGE_LIST => 'renderLanguageList', |
125 | ]; |
126 | |
127 | $type = $argument['type']; |
128 | $value = $argument['value']; |
129 | $role = $argument['role']; |
130 | |
131 | if ( array_key_exists( $type, $methods ) ) { |
132 | $method = $methods[$type]; |
133 | $params = $this->$method( $value, $role ); |
134 | } else { |
135 | throw new InvalidArgumentException( |
136 | 'Unknown ViolationMessage argument type ' . $type . '!' |
137 | ); |
138 | } |
139 | |
140 | return $params; |
141 | } |
142 | |
143 | /** |
144 | * @param array $list |
145 | * @param string|null $role one of the Role::* constants |
146 | * @param callable $render must accept $list elements and $role as parameters |
147 | * and return a single-element array with a raw message param (i. e. [ Message::rawParam( … ) ]) |
148 | * @return array[] list of parameters as accepted by Message::params() |
149 | */ |
150 | private function renderList( array $list, ?string $role, callable $render ): array { |
151 | if ( $list === [] ) { |
152 | return [ |
153 | Message::numParam( 0 ), |
154 | Message::rawParam( '<ul></ul>' ), |
155 | ]; |
156 | } |
157 | |
158 | if ( count( $list ) > $this->maxListLength ) { |
159 | $list = array_slice( $list, 0, $this->maxListLength ); |
160 | $truncated = true; |
161 | } |
162 | |
163 | $renderedParamsLists = array_map( |
164 | $render, |
165 | $list, |
166 | array_fill( 0, count( $list ), $role ) |
167 | ); |
168 | $renderedParams = array_column( $renderedParamsLists, 0 ); |
169 | $renderedElements = array_column( $renderedParams, 'raw' ); |
170 | if ( isset( $truncated ) ) { |
171 | $renderedElements[] = $this->msgEscaped( 'ellipsis' ); |
172 | } |
173 | |
174 | return array_merge( |
175 | [ |
176 | Message::numParam( count( $list ) ), |
177 | Message::rawParam( |
178 | '<ul><li>' . |
179 | implode( '</li><li>', $renderedElements ) . |
180 | '</li></ul>' |
181 | ), |
182 | ], |
183 | $renderedParams |
184 | ); |
185 | } |
186 | |
187 | /** |
188 | * @param EntityId $entityId |
189 | * @param string|null $role one of the Role::* constants |
190 | * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ]) |
191 | */ |
192 | private function renderEntityId( EntityId $entityId, ?string $role ): array { |
193 | return [ Message::rawParam( $this->addRole( |
194 | $this->entityIdFormatter->formatEntityId( $entityId ), |
195 | $role |
196 | ) ) ]; |
197 | } |
198 | |
199 | /** |
200 | * @param EntityId[] $entityIdList |
201 | * @param string|null $role one of the Role::* constants |
202 | * @return array[] list of parameters as accepted by Message::params() |
203 | */ |
204 | private function renderEntityIdList( array $entityIdList, ?string $role ): array { |
205 | return $this->renderList( $entityIdList, $role, [ $this, 'renderEntityId' ] ); |
206 | } |
207 | |
208 | /** |
209 | * @param ItemIdSnakValue $value |
210 | * @param string|null $role one of the Role::* constants |
211 | * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ]) |
212 | */ |
213 | private function renderItemIdSnakValue( ItemIdSnakValue $value, ?string $role ): array { |
214 | switch ( true ) { |
215 | case $value->isValue(): |
216 | return $this->renderEntityId( $value->getItemId(), $role ); |
217 | case $value->isSomeValue(): |
218 | return [ Message::rawParam( $this->addRole( |
219 | '<span class="wikibase-snakview-variation-somevaluesnak">' . |
220 | $this->msgEscaped( 'wikibase-snakview-snaktypeselector-somevalue' ) . |
221 | '</span>', |
222 | $role |
223 | ) ) ]; |
224 | case $value->isNoValue(): |
225 | return [ Message::rawParam( $this->addRole( |
226 | '<span class="wikibase-snakview-variation-novaluesnak">' . |
227 | $this->msgEscaped( 'wikibase-snakview-snaktypeselector-novalue' ) . |
228 | '</span>', |
229 | $role |
230 | ) ) ]; |
231 | default: |
232 | // @codeCoverageIgnoreStart |
233 | throw new LogicException( |
234 | 'ItemIdSnakValue should guarantee that one of is{,Some,No}Value() is true' |
235 | ); |
236 | // @codeCoverageIgnoreEnd |
237 | } |
238 | } |
239 | |
240 | /** |
241 | * @param ItemIdSnakValue[] $valueList |
242 | * @param string|null $role one of the Role::* constants |
243 | * @return array[] list of parameters as accepted by Message::params() |
244 | */ |
245 | private function renderItemIdSnakValueList( array $valueList, ?string $role ): array { |
246 | return $this->renderList( $valueList, $role, [ $this, 'renderItemIdSnakValue' ] ); |
247 | } |
248 | |
249 | /** |
250 | * @param DataValue $dataValue |
251 | * @param string|null $role one of the Role::* constants |
252 | * @return array[] list of parameters as accepted by Message::params() |
253 | */ |
254 | private function renderDataValue( DataValue $dataValue, ?string $role ): array { |
255 | return [ Message::rawParam( $this->addRole( |
256 | $this->dataValueFormatter->format( $dataValue ), |
257 | $role |
258 | ) ) ]; |
259 | } |
260 | |
261 | /** |
262 | * @param string $dataValueType |
263 | * @param string|null $role one of the Role::* constants |
264 | * @return array[] list of parameters as accepted by Message::params() |
265 | */ |
266 | private function renderDataValueType( string $dataValueType, ?string $role ): array { |
267 | $messageKeys = [ |
268 | 'string' => 'datatypes-type-string', |
269 | 'monolingualtext' => 'datatypes-type-monolingualtext', |
270 | 'time' => 'datatypes-type-time', |
271 | 'quantity' => 'datatypes-type-quantity', |
272 | 'wikibase-entityid' => 'wbqc-dataValueType-wikibase-entityid', |
273 | ]; |
274 | |
275 | if ( array_key_exists( $dataValueType, $messageKeys ) ) { |
276 | return [ Message::rawParam( $this->addRole( |
277 | $this->msgEscaped( $messageKeys[$dataValueType] ), |
278 | $role |
279 | ) ) ]; |
280 | } else { |
281 | // @codeCoverageIgnoreStart |
282 | throw new LogicException( |
283 | 'Unknown data value type ' . $dataValueType |
284 | ); |
285 | // @codeCoverageIgnoreEnd |
286 | } |
287 | } |
288 | |
289 | /** |
290 | * @param string $code (not yet HTML-escaped) |
291 | * @param string|null $role one of the Role::* constants |
292 | * @return array[] list of parameters as accepted by Message::params() |
293 | */ |
294 | private function renderInlineCode( string $code, ?string $role ): array { |
295 | return [ Message::rawParam( $this->addRole( |
296 | '<code>' . htmlspecialchars( $code ) . '</code>', |
297 | $role |
298 | ) ) ]; |
299 | } |
300 | |
301 | /** |
302 | * @param string $scope one of the Context::TYPE_* constants |
303 | * @param string|null $role one of the Role::* constants |
304 | * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ]) |
305 | */ |
306 | private function renderConstraintScope( string $scope, ?string $role ): array { |
307 | switch ( $scope ) { |
308 | case Context::TYPE_STATEMENT: |
309 | $itemId = $this->config->get( |
310 | 'WBQualityConstraintsConstraintCheckedOnMainValueId' |
311 | ); |
312 | break; |
313 | case Context::TYPE_QUALIFIER: |
314 | $itemId = $this->config->get( |
315 | 'WBQualityConstraintsConstraintCheckedOnQualifiersId' |
316 | ); |
317 | break; |
318 | case Context::TYPE_REFERENCE: |
319 | $itemId = $this->config->get( |
320 | 'WBQualityConstraintsConstraintCheckedOnReferencesId' |
321 | ); |
322 | break; |
323 | default: |
324 | // callers should never let this happen, but if it does happen, |
325 | // showing “unknown value” seems reasonable |
326 | // @codeCoverageIgnoreStart |
327 | return $this->renderItemIdSnakValue( ItemIdSnakValue::someValue(), $role ); |
328 | // @codeCoverageIgnoreEnd |
329 | } |
330 | return $this->renderEntityId( new ItemId( $itemId ), $role ); |
331 | } |
332 | |
333 | /** |
334 | * @param string[] $scopeList Context::TYPE_* constants |
335 | * @param string|null $role one of the Role::* constants |
336 | * @return array[] list of parameters as accepted by Message::params() |
337 | */ |
338 | private function renderConstraintScopeList( array $scopeList, ?string $role ): array { |
339 | return $this->renderList( $scopeList, $role, [ $this, 'renderConstraintScope' ] ); |
340 | } |
341 | |
342 | /** |
343 | * @param string $scope one of the Context::TYPE_* constants |
344 | * @param string|null $role one of the Role::* constants |
345 | * @return array[] list of a single raw message param (i. e. [ Message::rawParam( … ) ]) |
346 | */ |
347 | private function renderPropertyScope( string $scope, ?string $role ): array { |
348 | switch ( $scope ) { |
349 | case Context::TYPE_STATEMENT: |
350 | $itemId = $this->config->get( 'WBQualityConstraintsAsMainValueId' ); |
351 | break; |
352 | case Context::TYPE_QUALIFIER: |
353 | $itemId = $this->config->get( 'WBQualityConstraintsAsQualifiersId' ); |
354 | break; |
355 | case Context::TYPE_REFERENCE: |
356 | $itemId = $this->config->get( 'WBQualityConstraintsAsReferencesId' ); |
357 | break; |
358 | default: |
359 | // callers should never let this happen, but if it does happen, |
360 | // showing “unknown value” seems reasonable |
361 | // @codeCoverageIgnoreStart |
362 | return $this->renderItemIdSnakValue( ItemIdSnakValue::someValue(), $role ); |
363 | // @codeCoverageIgnoreEnd |
364 | } |
365 | return $this->renderEntityId( new ItemId( $itemId ), $role ); |
366 | } |
367 | |
368 | /** |
369 | * @param string[] $scopeList Context::TYPE_* constants |
370 | * @param string|null $role one of the Role::* constants |
371 | * @return array[] list of parameters as accepted by Message::params() |
372 | */ |
373 | private function renderPropertyScopeList( array $scopeList, ?string $role ): array { |
374 | return $this->renderList( $scopeList, $role, [ $this, 'renderPropertyScope' ] ); |
375 | } |
376 | |
377 | /** |
378 | * @param string $languageCode MediaWiki language code |
379 | * @param string|null $role one of the Role::* constants |
380 | * @return array[] list of parameters as accepted by Message::params() |
381 | */ |
382 | private function renderLanguage( string $languageCode, ?string $role ): array { |
383 | return [ |
384 | // ::renderList (through ::renderLanguageList) requires 'raw' parameter |
385 | // so we effectively build Message::plaintextParam here |
386 | Message::rawParam( htmlspecialchars( |
387 | $this->languageNameUtils->getLanguageName( $languageCode, $this->userLanguageCode ) |
388 | ) ), |
389 | Message::plaintextParam( $languageCode ), |
390 | ]; |
391 | } |
392 | |
393 | /** |
394 | * @param string[] $languageCodes MediaWiki language codes |
395 | * @param string|null $role one of the Role::* constants |
396 | * @return array[] list of parameters as accepted by Message::params() |
397 | */ |
398 | private function renderLanguageList( array $languageCodes, ?string $role ): array { |
399 | return $this->renderList( $languageCodes, $role, [ $this, 'renderLanguage' ] ); |
400 | } |
401 | |
402 | } |