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