Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
68.16% |
167 / 245 |
|
28.57% |
4 / 14 |
CRAP | |
0.00% |
0 / 1 |
SkinModule | |
68.16% |
167 / 245 |
|
28.57% |
4 / 14 |
239.28 | |
0.00% |
0 / 1 |
__construct | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
7.04 | |||
applyFeaturesCompatibility | |
67.74% |
21 / 31 |
|
0.00% |
0 / 1 |
20.58 | |||
getFeatureFilePaths | |
35.29% |
18 / 51 |
|
0.00% |
0 / 1 |
30.94 | |||
combineFeatureAndParentStyles | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
generateAndAppendLogoStyles | |
96.43% |
27 / 28 |
|
0.00% |
0 / 1 |
7 | |||
getStyles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getPreloadLinks | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
9 | |||
normalizeStyles | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
getRelativeSizedLogo | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getAvailableLogos | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
9.02 | |||
getLogoData | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
6 | |||
isKnownEmpty | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLessVars | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getDefinitionSummary | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | namespace MediaWiki\ResourceLoader; |
21 | |
22 | use InvalidArgumentException; |
23 | use MediaWiki\Config\Config; |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\Output\OutputPage; |
26 | use Wikimedia\Minify\CSSMin; |
27 | |
28 | /** |
29 | * Module for skin stylesheets. |
30 | * |
31 | * @ingroup ResourceLoader |
32 | * @internal |
33 | */ |
34 | class SkinModule extends LessVarFileModule { |
35 | |
36 | /** |
37 | * Every skin should define which features it would like to reuse for core inside a |
38 | * ResourceLoader module that has set the class to SkinModule. |
39 | * For a feature to be valid it must be listed here along with the associated resources |
40 | * |
41 | * The following features are available: |
42 | * |
43 | * "accessibility": |
44 | * Adds universal accessibility rules. |
45 | * |
46 | * "logo": |
47 | * Adds CSS to style an element with class `mw-wiki-logo` using the value of wgLogos['1x']. |
48 | * This is enabled by default if no features are added. |
49 | * |
50 | * "normalize": |
51 | * Styles needed to normalize rendering across different browser rendering engines. |
52 | * All to address bugs and common browser inconsistencies for skins and extensions. |
53 | * Inspired by necolas' normalize.css. This is meant to be kept lean, |
54 | * basic styling beyond normalization should live in one of the following modules. |
55 | * |
56 | * "elements": |
57 | * The base level that only contains the most basic of common skin styles. |
58 | * Only styles for single elements are included, no styling for complex structures like the |
59 | * TOC is present. This level is for skins that want to implement the entire style of even |
60 | * content area structures like the TOC themselves. |
61 | * |
62 | * "content": |
63 | * Deprecated. Alias for "content-media". |
64 | * |
65 | * "content-thumbnails": |
66 | * Deprecated. Alias for "content-media". |
67 | * |
68 | * "content-media": |
69 | * Styles for thumbnails and floated elements. |
70 | * Will add styles for the new media structure on wikis where $wgParserEnableLegacyMediaDOM is disabled, |
71 | * or $wgUseContentMediaStyles is enabled. |
72 | * See https://www.mediawiki.org/wiki/Parsing/Media_structure |
73 | * |
74 | * "content-links": |
75 | * The skin will apply optional styling rules for links that should be styled differently |
76 | * to the rules in `elements` and `normalize`. It provides support for .mw-selflink, |
77 | * a.new (red links), a.stub (stub links) and some basic styles for external links. |
78 | * It also provides rules supporting the underline user preference. |
79 | * |
80 | * "content-links-external": |
81 | * The skin will apply optional styling rules to links to provide icons for different file types. |
82 | * |
83 | * "content-body": |
84 | * Styles for the mw-parser-output class. |
85 | * |
86 | * "content-tables": |
87 | * Styles .wikitable style tables. |
88 | * |
89 | * "interface": |
90 | * Shorthand for a set of styles that are common |
91 | * to skins like MonoBook, Vector, etc... Essentially this level is for styles that are |
92 | * common to MonoBook clones. |
93 | * This enables interface-core, interface-indicators, interface-subtitle, |
94 | * interface-user-message, interface-site-notice and interface-edit-section-links. |
95 | * |
96 | * "interface-category": |
97 | * Styles used for styling the categories in a horizontal bar at the bottom of the content. |
98 | * |
99 | * "interface-core": |
100 | * Required interface core styles. Disabling these is not recommended. |
101 | * |
102 | * "interface-edit-section-links": |
103 | * Default interface styling for edit section links. |
104 | * |
105 | * "interface-indicators": |
106 | * Default interface styling for indicators. |
107 | * |
108 | * "interface-message-box": |
109 | * Styles for message boxes. Can be used by skins that do not load Codex styles on page load. |
110 | * |
111 | * "interface-site-notice": |
112 | * Default interface styling for site notices. |
113 | * |
114 | * "interface-subtitle": |
115 | * Default interface styling for subtitle area. |
116 | * |
117 | * "interface-user-message": |
118 | * Default interface styling for html-user-message (you have new talk page messages box) |
119 | * |
120 | * "i18n-ordered-lists": |
121 | * Styles for ordered lists elements that support mixed language content. |
122 | * |
123 | * "i18n-all-lists-margins": |
124 | * Styles for margins of list elements where LTR and RTL are mixed. |
125 | * |
126 | * "i18n-headings": |
127 | * Styles for line-heights of headings across different languages. |
128 | * |
129 | * "toc" |
130 | * Styling rules for the table of contents. |
131 | * |
132 | * NOTE: The order of the keys defines the order in which the styles are output. |
133 | */ |
134 | private const FEATURE_FILES = [ |
135 | 'accessibility' => [ |
136 | 'all' => [ 'resources/src/mediawiki.skinning/accessibility.less' ], |
137 | ], |
138 | 'normalize' => [ |
139 | 'all' => [ 'resources/src/mediawiki.skinning/normalize.less' ], |
140 | ], |
141 | 'logo' => [ |
142 | // Applies the logo and ensures it downloads prior to printing. |
143 | 'all' => [ 'resources/src/mediawiki.skinning/logo.less' ], |
144 | // Reserves whitespace for the logo in a pseudo element. |
145 | 'print' => [ 'resources/src/mediawiki.skinning/logo-print.less' ], |
146 | ], |
147 | 'content-media' => [], |
148 | 'content-links' => [ |
149 | 'screen' => [ 'resources/src/mediawiki.skinning/content.links.less' ] |
150 | ], |
151 | 'content-links-external' => [ |
152 | 'screen' => [ 'resources/src/mediawiki.skinning/content.externallinks.less' ] |
153 | ], |
154 | 'content-body' => [ |
155 | 'screen' => [ 'resources/src/mediawiki.skinning/content.body.less' ], |
156 | 'print' => [ 'resources/src/mediawiki.skinning/content.body-print.less' ], |
157 | ], |
158 | 'content-tables' => [ |
159 | 'screen' => [ 'resources/src/mediawiki.skinning/content.tables.less' ], |
160 | 'print' => [ 'resources/src/mediawiki.skinning/content.tables-print.less' ] |
161 | ], |
162 | // Legacy shorthand for 6 features: interface-core, interface-edit-section-links, |
163 | // interface-indicators, interface-subtitle, interface-site-notice, interface-user-message |
164 | 'interface' => [], |
165 | 'interface-category' => [ |
166 | 'screen' => [ 'resources/src/mediawiki.skinning/interface.category.less' ], |
167 | 'print' => [ 'resources/src/mediawiki.skinning/interface.category-print.less' ], |
168 | ], |
169 | 'interface-core' => [ |
170 | 'screen' => [ 'resources/src/mediawiki.skinning/interface.less' ], |
171 | 'print' => [ 'resources/src/mediawiki.skinning/interface-print.less' ], |
172 | ], |
173 | 'interface-edit-section-links' => [ |
174 | 'screen' => [ 'resources/src/mediawiki.skinning/interface-edit-section-links.less' ], |
175 | ], |
176 | 'interface-indicators' => [ |
177 | 'screen' => [ 'resources/src/mediawiki.skinning/interface-indicators.less' ], |
178 | ], |
179 | 'interface-site-notice' => [ |
180 | 'screen' => [ 'resources/src/mediawiki.skinning/interface-site-notice.less' ], |
181 | ], |
182 | 'interface-subtitle' => [ |
183 | 'screen' => [ 'resources/src/mediawiki.skinning/interface-subtitle.less' ], |
184 | ], |
185 | 'interface-message-box' => [ |
186 | 'all' => [ 'resources/src/mediawiki.skinning/messageBoxes.less' ], |
187 | ], |
188 | 'interface-user-message' => [ |
189 | 'screen' => [ 'resources/src/mediawiki.skinning/interface-user-message.less' ], |
190 | ], |
191 | 'elements' => [ |
192 | 'screen' => [ 'resources/src/mediawiki.skinning/elements.less' ], |
193 | 'print' => [ 'resources/src/mediawiki.skinning/elements-print.less' ], |
194 | ], |
195 | // The styles of the legacy feature was removed in 1.39. This can be removed when no skins are referencing it |
196 | // (Dropping this line will trigger InvalidArgumentException: Feature 'legacy' is not recognised) |
197 | 'legacy' => [], |
198 | 'i18n-ordered-lists' => [ |
199 | 'screen' => [ 'resources/src/mediawiki.skinning/i18n-ordered-lists.less' ], |
200 | ], |
201 | 'i18n-all-lists-margins' => [ |
202 | 'screen' => [ 'resources/src/mediawiki.skinning/i18n-all-lists-margins.less' ], |
203 | ], |
204 | 'i18n-headings' => [ |
205 | 'screen' => [ 'resources/src/mediawiki.skinning/i18n-headings.less' ], |
206 | ], |
207 | 'toc' => [ |
208 | 'all' => [ 'resources/src/mediawiki.skinning/toc/common.css' ], |
209 | 'screen' => [ 'resources/src/mediawiki.skinning/toc/screen.less' ], |
210 | 'print' => [ 'resources/src/mediawiki.skinning/toc/print.css' ], |
211 | ], |
212 | ]; |
213 | |
214 | /** @var string[] */ |
215 | private $features; |
216 | |
217 | /** |
218 | * Defaults for when a 'features' parameter is specified. |
219 | * |
220 | * When these apply, they are the merged into the specified options. |
221 | * |
222 | * @var array<string,bool> |
223 | */ |
224 | private const DEFAULT_FEATURES_SPECIFIED = [ |
225 | 'accessibility' => true, |
226 | 'content-body' => true, |
227 | 'interface-core' => true, |
228 | 'toc' => true |
229 | ]; |
230 | |
231 | /** |
232 | * Default for when the 'features' parameter is absent. |
233 | * |
234 | * For backward-compatibility, when the parameter is not declared |
235 | * only 'logo' styles are loaded. |
236 | * |
237 | * @var string[] |
238 | */ |
239 | private const DEFAULT_FEATURES_ABSENT = [ |
240 | 'logo', |
241 | ]; |
242 | |
243 | private const LESS_MESSAGES = [ |
244 | // `toc` feature, used in screen.less |
245 | 'hidetoc', |
246 | 'showtoc', |
247 | ]; |
248 | |
249 | /** |
250 | * @param array $options |
251 | * - features: Map from feature keys to boolean indicating whether to load |
252 | * or not include the associated styles. |
253 | * Keys not specified get their default from self::DEFAULT_FEATURES_SPECIFIED. |
254 | * |
255 | * If this is set to a list of strings, then the defaults do not apply. |
256 | * Use this at your own risk as it means you opt-out from backwards compatibility |
257 | * provided through these defaults. For example, when features are migrated |
258 | * to the SkinModule system from other parts of MediaWiki, those new feature keys |
259 | * may be enabled by default, and opting out means you may be missing some styles |
260 | * after an upgrade until you enable them or implement them by other means. |
261 | * |
262 | * - lessMessages: Interface message keys to export as LESS variables. |
263 | * See also LessVarFileModule. |
264 | * |
265 | * @param string|null $localBasePath |
266 | * @param string|null $remoteBasePath |
267 | * @see Additonal options at $wgResourceModules |
268 | */ |
269 | public function __construct( |
270 | array $options = [], |
271 | $localBasePath = null, |
272 | $remoteBasePath = null |
273 | ) { |
274 | $features = $options['features'] ?? self::DEFAULT_FEATURES_ABSENT; |
275 | $listMode = array_keys( $features ) === range( 0, count( $features ) - 1 ); |
276 | |
277 | $messages = ''; |
278 | // NOTE: Compatibility is only applied when features are provided |
279 | // in map-form. The list-form does not currently get these. |
280 | $features = $listMode ? self::applyFeaturesCompatibility( |
281 | array_fill_keys( $features, true ), false, $messages |
282 | ) : self::applyFeaturesCompatibility( $features, true, $messages ); |
283 | |
284 | foreach ( $features as $key => $enabled ) { |
285 | if ( !isset( self::FEATURE_FILES[$key] ) ) { |
286 | throw new InvalidArgumentException( "Feature '$key' is not recognised" ); |
287 | } |
288 | } |
289 | |
290 | $this->features = $listMode |
291 | ? array_keys( array_filter( $features ) ) |
292 | : array_keys( array_filter( $features + self::DEFAULT_FEATURES_SPECIFIED ) ); |
293 | |
294 | // Only the `toc` feature makes use of interface messages. |
295 | // For skins not using the `toc` feature, make sure LocalisationCache |
296 | // remains untouched (T270027). |
297 | if ( in_array( 'toc', $this->features ) ) { |
298 | $options['lessMessages'] = array_merge( |
299 | $options['lessMessages'] ?? [], |
300 | self::LESS_MESSAGES |
301 | ); |
302 | } |
303 | |
304 | if ( $messages !== '' ) { |
305 | $messages .= 'More information can be found at [[mw:Manual:ResourceLoaderSkinModule]]. '; |
306 | $options['deprecated'] = $messages; |
307 | } |
308 | parent::__construct( $options, $localBasePath, $remoteBasePath ); |
309 | } |
310 | |
311 | /** |
312 | * @internal |
313 | * @param array $features |
314 | * @param bool $addUnspecifiedFeatures whether to add new features if missing |
315 | * @param string &$messages to report deprecations |
316 | * @return array |
317 | */ |
318 | protected static function applyFeaturesCompatibility( |
319 | array $features, bool $addUnspecifiedFeatures = true, &$messages = '' |
320 | ): array { |
321 | // The `content` feature is mapped to `content-media`. |
322 | if ( isset( $features[ 'content' ] ) ) { |
323 | $features[ 'content-media' ] = $features[ 'content' ]; |
324 | unset( $features[ 'content' ] ); |
325 | $messages .= '[1.37] The use of the `content` feature with SkinModule' |
326 | . ' is deprecated. Use `content-media` instead. '; |
327 | } |
328 | |
329 | // The `content-thumbnails` feature is mapped to `content-media`. |
330 | if ( isset( $features[ 'content-thumbnails' ] ) ) { |
331 | $features[ 'content-media' ] = $features[ 'content-thumbnails' ]; |
332 | $messages .= '[1.37] The use of the `content-thumbnails` feature with SkinModule' |
333 | . ' is deprecated. Use `content-media` instead. '; |
334 | unset( $features[ 'content-thumbnails' ] ); |
335 | } |
336 | |
337 | // If `content-links` feature is set but no preference for `content-links-external` is set |
338 | if ( $addUnspecifiedFeatures && isset( $features[ 'content-links' ] ) |
339 | && !isset( $features[ 'content-links-external' ] ) |
340 | ) { |
341 | // Assume the same true/false preference for both. |
342 | $features[ 'content-links-external' ] = $features[ 'content-links' ]; |
343 | } |
344 | |
345 | // The legacy feature no longer exists (T89981) but to avoid fatals in skins is retained. |
346 | if ( isset( $features['legacy'] ) && $features['legacy'] ) { |
347 | $messages .= '[1.37] The use of the `legacy` feature with SkinModule is deprecated' |
348 | . '(T89981) and is a NOOP since 1.39 (T304325). This should be urgently omited to retain compatibility ' |
349 | . 'with future MediaWiki versions'; |
350 | } |
351 | |
352 | // The `content-links` feature was split out from `elements`. |
353 | // Make sure skins asking for `elements` also get these by default. |
354 | if ( $addUnspecifiedFeatures && isset( $features[ 'element' ] ) && !isset( $features[ 'content-links' ] ) ) { |
355 | $features[ 'content-links' ] = $features[ 'element' ]; |
356 | } |
357 | |
358 | // `content-parser-output` was renamed to `content-body`. |
359 | // No need to go through deprecation process here since content-parser-output added and removed in 1.36. |
360 | // Remove this check when no matches for |
361 | // https://codesearch.wmcloud.org/search/?q=content-parser-output&i=nope&files=&excludeFiles=&repos= |
362 | if ( isset( $features[ 'content-parser-output' ] ) ) { |
363 | $features[ 'content-body' ] = $features[ 'content-parser-output' ]; |
364 | unset( $features[ 'content-parser-output' ] ); |
365 | } |
366 | |
367 | // The interface module is a short hand for several modules. Enable them now. |
368 | if ( isset( $features[ 'interface' ] ) && $features[ 'interface' ] ) { |
369 | unset( $features[ 'interface' ] ); |
370 | $features[ 'interface-core' ] = true; |
371 | $features[ 'interface-indicators' ] = true; |
372 | $features[ 'interface-subtitle' ] = true; |
373 | $features[ 'interface-user-message' ] = true; |
374 | $features[ 'interface-site-notice' ] = true; |
375 | $features[ 'interface-edit-section-links' ] = true; |
376 | } |
377 | return $features; |
378 | } |
379 | |
380 | /** |
381 | * Get styles defined in the module definition. |
382 | * |
383 | * @return array |
384 | */ |
385 | public function getFeatureFilePaths() { |
386 | // Bypass the current module paths so that these files are served from core, |
387 | // instead of the individual skin's module directory. |
388 | [ $defaultLocalBasePath, $defaultRemoteBasePath ] = |
389 | FileModule::extractBasePaths( |
390 | [], |
391 | null, |
392 | $this->getConfig()->get( MainConfigNames::ResourceBasePath ) |
393 | ); |
394 | |
395 | $featureFilePaths = []; |
396 | |
397 | foreach ( self::FEATURE_FILES as $feature => $featureFiles ) { |
398 | if ( in_array( $feature, $this->features ) ) { |
399 | foreach ( $featureFiles as $mediaType => $files ) { |
400 | foreach ( $files as $filepath ) { |
401 | $featureFilePaths[$mediaType][] = new FilePath( |
402 | $filepath, |
403 | $defaultLocalBasePath, |
404 | $defaultRemoteBasePath |
405 | ); |
406 | } |
407 | } |
408 | |
409 | if ( $feature === 'content-media' ) { |
410 | if ( $this->getConfig()->get( MainConfigNames::UseLegacyMediaStyles ) ) { |
411 | $featureFilePaths['all'][] = new FilePath( |
412 | 'resources/src/mediawiki.skinning/content.thumbnails-common.less', |
413 | $defaultLocalBasePath, |
414 | $defaultRemoteBasePath |
415 | ); |
416 | $featureFilePaths['screen'][] = new FilePath( |
417 | 'resources/src/mediawiki.skinning/content.thumbnails-screen.less', |
418 | $defaultLocalBasePath, |
419 | $defaultRemoteBasePath |
420 | ); |
421 | $featureFilePaths['print'][] = new FilePath( |
422 | 'resources/src/mediawiki.skinning/content.thumbnails-print.less', |
423 | $defaultLocalBasePath, |
424 | $defaultRemoteBasePath |
425 | ); |
426 | } |
427 | if ( |
428 | !$this->getConfig()->get( MainConfigNames::ParserEnableLegacyMediaDOM ) || |
429 | $this->getConfig()->get( MainConfigNames::UseContentMediaStyles ) |
430 | ) { |
431 | $featureFilePaths['all'][] = new FilePath( |
432 | 'resources/src/mediawiki.skinning/content.media-common.less', |
433 | $defaultLocalBasePath, |
434 | $defaultRemoteBasePath |
435 | ); |
436 | $featureFilePaths['screen'][] = new FilePath( |
437 | 'resources/src/mediawiki.skinning/content.media-screen.less', |
438 | $defaultLocalBasePath, |
439 | $defaultRemoteBasePath |
440 | ); |
441 | $featureFilePaths['print'][] = new FilePath( |
442 | 'resources/src/mediawiki.skinning/content.media-print.less', |
443 | $defaultLocalBasePath, |
444 | $defaultRemoteBasePath |
445 | ); |
446 | } |
447 | } |
448 | } |
449 | } |
450 | return $featureFilePaths; |
451 | } |
452 | |
453 | /** |
454 | * Combines feature styles and parent skin styles, ensuring that all |
455 | * feature styles are output *first*, followed by skin related styles. |
456 | * |
457 | * @param array $featureStyles |
458 | * @param array $parentStyles |
459 | * |
460 | * @return array |
461 | */ |
462 | private function combineFeatureAndParentStyles( $featureStyles, $parentStyles ) { |
463 | $combinedFeatureStyles = ResourceLoader::makeCombinedStyles( $featureStyles ); |
464 | $combinedParentStyles = ResourceLoader::makeCombinedStyles( $parentStyles ); |
465 | $combinedStyles = array_merge( $combinedFeatureStyles, $combinedParentStyles ); |
466 | return [ '' => $combinedStyles ]; |
467 | } |
468 | |
469 | /** |
470 | * Generates CSS for .mw-logo-logo styles and appends them |
471 | * to the skin feature styles array. |
472 | * @param array $featureStyles |
473 | * @param Context $context |
474 | * @return array |
475 | */ |
476 | public function generateAndAppendLogoStyles( $featureStyles, $context ) { |
477 | $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() ); |
478 | $default = !is_array( $logo ) ? $logo : ( $logo['svg'] ?? $logo['1x'] ?? null ); |
479 | |
480 | // Can't add logo CSS if no logo defined. |
481 | if ( !$default ) { |
482 | return $featureStyles; |
483 | } |
484 | |
485 | $featureStyles['all'][] = '.mw-wiki-logo { background-image: ' . |
486 | CSSMin::buildUrlValue( $default ) . |
487 | '; }'; |
488 | |
489 | if ( is_array( $logo ) ) { |
490 | if ( isset( $logo['svg'] ) ) { |
491 | $featureStyles['all'][] = '.mw-wiki-logo { ' . |
492 | 'background-size: 135px auto; }'; |
493 | } else { |
494 | if ( isset( $logo['1.5x'] ) ) { |
495 | $featureStyles[ |
496 | '(-webkit-min-device-pixel-ratio: 1.5), ' . |
497 | '(min-resolution: 1.5dppx), ' . |
498 | '(min-resolution: 144dpi)' |
499 | ][] = '.mw-wiki-logo { background-image: ' . |
500 | CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' . |
501 | 'background-size: 135px auto; }'; |
502 | } |
503 | if ( isset( $logo['2x'] ) ) { |
504 | $featureStyles[ |
505 | '(-webkit-min-device-pixel-ratio: 2), ' . |
506 | '(min-resolution: 2dppx), ' . |
507 | '(min-resolution: 192dpi)' |
508 | ][] = '.mw-wiki-logo { background-image: ' . |
509 | CSSMin::buildUrlValue( $logo['2x'] ) . ';' . |
510 | 'background-size: 135px auto; }'; |
511 | } |
512 | } |
513 | } |
514 | return $featureStyles; |
515 | } |
516 | |
517 | /** |
518 | * @param Context $context |
519 | * @return array |
520 | */ |
521 | public function getStyles( Context $context ) { |
522 | $parentStyles = parent::getStyles( $context ); |
523 | $featureFilePaths = $this->getFeatureFilePaths(); |
524 | $featureStyles = $this->readStyleFiles( $featureFilePaths, $context ); |
525 | |
526 | $this->normalizeStyles( $featureStyles ); |
527 | $this->normalizeStyles( $parentStyles ); |
528 | |
529 | $isLogoFeatureEnabled = in_array( 'logo', $this->features ); |
530 | if ( $isLogoFeatureEnabled ) { |
531 | $featureStyles = $this->generateAndAppendLogoStyles( $featureStyles, $context ); |
532 | } |
533 | |
534 | return $this->combineFeatureAndParentStyles( $featureStyles, $parentStyles ); |
535 | } |
536 | |
537 | /** |
538 | * @param Context $context |
539 | * @return array |
540 | */ |
541 | public function getPreloadLinks( Context $context ): array { |
542 | if ( !in_array( 'logo', $this->features ) ) { |
543 | return []; |
544 | } |
545 | |
546 | $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() ); |
547 | |
548 | if ( !is_array( $logo ) ) { |
549 | // No media queries required if we only have one variant |
550 | return [ $logo => [ 'as' => 'image' ] ]; |
551 | } |
552 | |
553 | if ( isset( $logo['svg'] ) ) { |
554 | // No media queries required if we only have a 1x and svg variant |
555 | // because all preload-capable browsers support SVGs |
556 | return [ $logo['svg'] => [ 'as' => 'image' ] ]; |
557 | } |
558 | |
559 | $logosPerDppx = []; |
560 | foreach ( $logo as $dppx => $src ) { |
561 | // Keys are in this format: "1.5x" |
562 | $dppx = substr( $dppx, 0, -1 ); |
563 | $logosPerDppx[$dppx] = $src; |
564 | } |
565 | |
566 | // Because PHP can't have floats as array keys |
567 | uksort( $logosPerDppx, static function ( $a, $b ) { |
568 | $a = floatval( $a ); |
569 | $b = floatval( $b ); |
570 | // Sort from smallest to largest (e.g. 1x, 1.5x, 2x) |
571 | return $a <=> $b; |
572 | } ); |
573 | |
574 | $logos = []; |
575 | foreach ( $logosPerDppx as $dppx => $src ) { |
576 | $logos[] = [ |
577 | 'dppx' => $dppx, |
578 | 'src' => $src |
579 | ]; |
580 | } |
581 | |
582 | $logosCount = count( $logos ); |
583 | $preloadLinks = []; |
584 | // Logic must match SkinModule: |
585 | // - 1x applies to resolution < 1.5dppx |
586 | // - 1.5x applies to resolution >= 1.5dppx && < 2dppx |
587 | // - 2x applies to resolution >= 2dppx |
588 | // Note that min-resolution and max-resolution are both inclusive. |
589 | for ( $i = 0; $i < $logosCount; $i++ ) { |
590 | if ( $i === 0 ) { |
591 | // Smallest dppx |
592 | // min-resolution is ">=" (larger than or equal to) |
593 | // "not min-resolution" is essentially "<" |
594 | $media_query = 'not all and (min-resolution: ' . $logos[1]['dppx'] . 'dppx)'; |
595 | } elseif ( $i !== $logosCount - 1 ) { |
596 | // In between |
597 | // Media query expressions can only apply "not" to the entire expression |
598 | // (e.g. can't express ">= 1.5 and not >= 2). |
599 | // Workaround: Use <= 1.9999 in place of < 2. |
600 | $upper_bound = floatval( $logos[$i + 1]['dppx'] ) - 0.000001; |
601 | $media_query = '(min-resolution: ' . $logos[$i]['dppx'] . |
602 | 'dppx) and (max-resolution: ' . $upper_bound . 'dppx)'; |
603 | } else { |
604 | // Largest dppx |
605 | $media_query = '(min-resolution: ' . $logos[$i]['dppx'] . 'dppx)'; |
606 | } |
607 | |
608 | $preloadLinks[$logos[$i]['src']] = [ |
609 | 'as' => 'image', |
610 | 'media' => $media_query |
611 | ]; |
612 | } |
613 | |
614 | return $preloadLinks; |
615 | } |
616 | |
617 | /** |
618 | * Ensure all media keys use array values. |
619 | * |
620 | * Normalises arrays returned by the FileModule::getStyles() method. |
621 | * |
622 | * @param array &$styles Associative array, keys are strings (media queries), |
623 | * values are strings or arrays |
624 | */ |
625 | private function normalizeStyles( array &$styles ): void { |
626 | foreach ( $styles as $key => $val ) { |
627 | if ( !is_array( $val ) ) { |
628 | $styles[$key] = [ $val ]; |
629 | } |
630 | } |
631 | } |
632 | |
633 | /** |
634 | * Modifies configured logo width/height to ensure they are present and scalable |
635 | * with different font-sizes. |
636 | * @param array $logoElement with width, height and src keys. |
637 | * @return array modified version of $logoElement |
638 | */ |
639 | private static function getRelativeSizedLogo( array $logoElement ) { |
640 | $width = $logoElement['width']; |
641 | $height = $logoElement['height']; |
642 | $widthRelative = $width / 16; |
643 | $heightRelative = $height / 16; |
644 | // Allow skins to scale the wordmark with browser font size (T207789) |
645 | $logoElement['style'] = 'width: ' . $widthRelative . 'em; height: ' . $heightRelative . 'em;'; |
646 | return $logoElement; |
647 | } |
648 | |
649 | /** |
650 | * Return an array of all available logos that a skin may use. |
651 | * @since 1.35 |
652 | * @param Config $conf |
653 | * @param string|null $lang Language code for logo variant, since 1.39 |
654 | * @return array with the following keys: |
655 | * - 1x(string): a square logo composing the `icon` and `wordmark` (required) |
656 | * - 2x (string): a square logo for HD displays (optional) |
657 | * - wordmark (object): a rectangle logo (wordmark) for print media and skins which desire |
658 | * horizontal logo (optional). Must declare width and height fields, defined in pixels |
659 | * which will be converted to ems based on 16px font-size. |
660 | * - tagline (object): replaces `tagline` message in certain skins. Must declare width and |
661 | * height fields defined in pixels, which are converted to ems based on 16px font-size. |
662 | * - icon (string): a square logo similar to 1x, but without the wordmark. SVG recommended. |
663 | */ |
664 | public static function getAvailableLogos( Config $conf, string $lang = null ): array { |
665 | $logos = $conf->get( MainConfigNames::Logos ); |
666 | if ( $logos === false ) { |
667 | // no logos were defined... this will either |
668 | // 1. Load from wgLogo |
669 | // 2. Trigger runtime exception if those are not defined. |
670 | $logos = []; |
671 | } |
672 | if ( $lang && isset( $logos['variants'][$lang] ) ) { |
673 | foreach ( $logos['variants'][$lang] as $type => $value ) { |
674 | $logos[$type] = $value; |
675 | } |
676 | } |
677 | |
678 | // If logos['1x'] is not defined, see if we can use wgLogo |
679 | if ( !isset( $logos[ '1x' ] ) ) { |
680 | $logo = $conf->get( MainConfigNames::Logo ); |
681 | if ( $logo ) { |
682 | $logos['1x'] = $logo; |
683 | } |
684 | } |
685 | |
686 | if ( isset( $logos['wordmark'] ) ) { |
687 | // Allow skins to scale the wordmark with browser font size (T207789) |
688 | $logos['wordmark'] = self::getRelativeSizedLogo( $logos['wordmark'] ); |
689 | } |
690 | if ( isset( $logos['tagline'] ) ) { |
691 | $logos['tagline'] = self::getRelativeSizedLogo( $logos['tagline'] ); |
692 | } |
693 | |
694 | return $logos; |
695 | } |
696 | |
697 | /** |
698 | * @since 1.31 |
699 | * @param Config $conf |
700 | * @param string|null $lang Language code for logo variant, since 1.39 |
701 | * @return string|array Single url if no variants are defined, |
702 | * or an array of logo urls keyed by dppx in form "<float>x". |
703 | * Key "1x" is always defined. Key "svg" may also be defined, |
704 | * in which case variants other than "1x" are omitted. |
705 | */ |
706 | protected function getLogoData( Config $conf, string $lang = null ) { |
707 | $logoHD = self::getAvailableLogos( $conf, $lang ); |
708 | $logo = $logoHD['1x']; |
709 | |
710 | $logo1Url = OutputPage::transformResourcePath( $conf, $logo ); |
711 | |
712 | $logoUrls = [ |
713 | '1x' => $logo1Url, |
714 | ]; |
715 | |
716 | if ( isset( $logoHD['svg'] ) ) { |
717 | $logoUrls['svg'] = OutputPage::transformResourcePath( |
718 | $conf, |
719 | $logoHD['svg'] |
720 | ); |
721 | } elseif ( isset( $logoHD['1.5x'] ) || isset( $logoHD['2x'] ) ) { |
722 | // Only 1.5x and 2x are supported |
723 | if ( isset( $logoHD['1.5x'] ) ) { |
724 | $logoUrls['1.5x'] = OutputPage::transformResourcePath( |
725 | $conf, |
726 | $logoHD['1.5x'] |
727 | ); |
728 | } |
729 | if ( isset( $logoHD['2x'] ) ) { |
730 | $logoUrls['2x'] = OutputPage::transformResourcePath( |
731 | $conf, |
732 | $logoHD['2x'] |
733 | ); |
734 | } |
735 | } else { |
736 | // Return a string rather than a one-element array, getLogoPreloadlinks depends on this |
737 | return $logo1Url; |
738 | } |
739 | |
740 | return $logoUrls; |
741 | } |
742 | |
743 | /** |
744 | * @param Context $context |
745 | * @return bool |
746 | */ |
747 | public function isKnownEmpty( Context $context ) { |
748 | // Regardless of whether the files are specified, we always |
749 | // provide mw-wiki-logo styles. |
750 | return false; |
751 | } |
752 | |
753 | /** |
754 | * Get language-specific LESS variables for this module. |
755 | * |
756 | * @param Context $context |
757 | * @return array |
758 | */ |
759 | protected function getLessVars( Context $context ) { |
760 | $lessVars = parent::getLessVars( $context ); |
761 | $logos = self::getAvailableLogos( $this->getConfig() ); |
762 | |
763 | if ( isset( $logos['wordmark'] ) ) { |
764 | $logo = $logos['wordmark']; |
765 | $lessVars[ 'logo-enabled' ] = true; |
766 | $lessVars[ 'logo-wordmark-url' ] = CSSMin::buildUrlValue( $logo['src'] ); |
767 | $lessVars[ 'logo-wordmark-width' ] = intval( $logo['width'] ); |
768 | $lessVars[ 'logo-wordmark-height' ] = intval( $logo['height'] ); |
769 | } else { |
770 | $lessVars[ 'logo-enabled' ] = false; |
771 | } |
772 | return $lessVars; |
773 | } |
774 | |
775 | public function getDefinitionSummary( Context $context ) { |
776 | $summary = parent::getDefinitionSummary( $context ); |
777 | $summary[] = [ |
778 | 'logos' => self::getAvailableLogos( $this->getConfig() ), |
779 | ]; |
780 | return $summary; |
781 | } |
782 | } |