MediaWiki REL1_37
ResourceLoaderSkinModule.php
Go to the documentation of this file.
1<?php
20use Wikimedia\Minify\CSSMin;
21
32 public $targets = [ 'desktop', 'mobile' ];
33
115 private const FEATURE_FILES = [
116 'normalize' => [
117 'all' => [ 'resources/src/mediawiki.skinning/normalize.less' ],
118 ],
119 'logo' => [
120 // Applies the logo and ensures it downloads prior to printing.
121 'all' => [ 'resources/src/mediawiki.skinning/logo.less' ],
122 // Reserves whitespace for the logo in a pseudo element.
123 'print' => [ 'resources/src/mediawiki.skinning/logo-print.less' ],
124 ],
125 'content-media' => [
126 'screen' => [ 'resources/src/mediawiki.skinning/content.thumbnails.less' ],
127 'print' => [ 'resources/src/mediawiki.skinning/content.thumbnails-print.less' ],
128 ],
129 'content-links' => [
130 'screen' => [ 'resources/src/mediawiki.skinning/content.links.less' ]
131 ],
132 'content-links-external' => [
133 'screen' => [ 'resources/src/mediawiki.skinning/content.externallinks.less' ]
134 ],
135 'content-body' => [
136 'screen' => [ 'resources/src/mediawiki.skinning/content.body.less' ],
137 'print' => [ 'resources/src/mediawiki.skinning/content.body-print.less' ],
138 ],
139 'content-tables' => [
140 'screen' => [ 'resources/src/mediawiki.skinning/content.tables.less' ],
141 'print' => [ 'resources/src/mediawiki.skinning/content.tables-print.less' ]
142 ],
143 'interface' => [
144 'screen' => [ 'resources/src/mediawiki.skinning/interface.less' ],
145 'print' => [ 'resources/src/mediawiki.skinning/interface-print.less' ],
146 ],
147 'interface-category' => [
148 'screen' => [ 'resources/src/mediawiki.skinning/interface.category.less' ],
149 'print' => [ 'resources/src/mediawiki.skinning/interface.category-print.less' ],
150 ],
151 'interface-message-box' => [
152 'all' => [ 'resources/src/mediawiki.skinning/messageBoxes.less' ],
153 ],
154 'elements' => [
155 'screen' => [ 'resources/src/mediawiki.skinning/elements.less' ],
156 'print' => [ 'resources/src/mediawiki.skinning/elements-print.less' ],
157 ],
158 'legacy' => [
159 'all' => [ 'resources/src/mediawiki.skinning/messageBoxes.less' ],
160 'print' => [ 'resources/src/mediawiki.skinning/commonPrint.less' ],
161 'screen' => [ 'resources/src/mediawiki.skinning/legacy.less' ],
162 ],
163 'i18n-ordered-lists' => [
164 'screen' => [ 'resources/src/mediawiki.skinning/i18n-ordered-lists.less' ],
165 ],
166 'i18n-all-lists-margins' => [
167 'screen' => [ 'resources/src/mediawiki.skinning/i18n-all-lists-margins.less' ],
168 ],
169 'i18n-headings' => [
170 'screen' => [ 'resources/src/mediawiki.skinning/i18n-headings.less' ],
171 ],
172 'toc' => [
173 'all' => [ 'resources/src/mediawiki.skinning/toc/common.css' ],
174 'screen' => [ 'resources/src/mediawiki.skinning/toc/screen.less' ],
175 'print' => [ 'resources/src/mediawiki.skinning/toc/print.css' ],
176 ],
177 ];
178
180 private $features;
181
189 private const DEFAULT_FEATURES_SPECIFIED = [
190 'content-body' => true,
191 'toc' => true,
192 ];
193
202 private const DEFAULT_FEATURES_ABSENT = [
203 'logo',
204 'legacy',
205 ];
206
207 private const LESS_MESSAGES = [
208 // `toc` feature, used in screen.less
209 'hidetoc',
210 'showtoc',
211 ];
212
233 public function __construct(
234 array $options = [],
235 $localBasePath = null,
236 $remoteBasePath = null
237 ) {
238 $features = $options['features'] ?? self::DEFAULT_FEATURES_ABSENT;
239 $listMode = array_keys( $features ) === range( 0, count( $features ) - 1 );
240
241 $messages = '';
242 // NOTE: Compatibility is only applied when features are provided
243 // in map-form. The list-form does not currently get these.
244 $features = $listMode ? self::applyFeaturesCompatibility( array_fill_keys( $features, true ), $messages ) :
246
247 foreach ( $features as $key => $enabled ) {
248 if ( !isset( self::FEATURE_FILES[$key] ) ) {
249 throw new InvalidArgumentException( "Feature '$key' is not recognised" );
250 }
251 }
252
253 $this->features = $listMode
254 ? array_keys( array_filter( $features ) )
255 : array_keys( array_filter( $features + self::DEFAULT_FEATURES_SPECIFIED ) );
256
257 // Only the `toc` feature makes use of interface messages.
258 // For skins not using the `toc` feature, make sure LocalisationCache
259 // remains untouched (T270027).
260 if ( in_array( 'toc', $this->features ) ) {
261 $options['lessMessages'] = array_merge(
262 $options['lessMessages'] ?? [],
263 self::LESS_MESSAGES
264 );
265 }
266
267 if ( $messages !== '' ) {
268 $messages .= 'More information can be found at [[mw:Manual:ResourceLoaderSkinModule]]. ';
269 $options['deprecated'] = $messages;
270 }
271 parent::__construct( $options, $localBasePath, $remoteBasePath );
272 }
273
280 protected static function applyFeaturesCompatibility( array $features, &$messages = '' ): array {
281 // The `content` feature is mapped to `content-media`.
282 if ( isset( $features[ 'content' ] ) ) {
283 $features[ 'content-media' ] = $features[ 'content' ];
284 unset( $features[ 'content' ] );
285 $messages .= '[1.37] The use of the `content` feature with ResourceLoaderSkinModule'
286 . ' is deprecated. Use `content-media` instead. ';
287 }
288
289 // The `content-thumbnails` feature is mapped to `content-media`.
290 if ( isset( $features[ 'content-thumbnails' ] ) ) {
291 $features[ 'content-media' ] = $features[ 'content-thumbnails' ];
292 $messages .= '[1.37] The use of the `content-thumbnails` feature with ResourceLoaderSkinModule'
293 . ' is deprecated. Use `content-media` instead. ';
294 unset( $features[ 'content-thumbnails' ] );
295 }
296
297 // If `content-links` feature is set but no preference for `content-links-external` is set
298 if ( isset( $features[ 'content-links' ] ) && !isset( $features[ 'content-links-external' ] ) ) {
299 // Assume the same true/false preference for both.
300 $features[ 'content-links-external' ] = $features[ 'content-links' ];
301 }
302
303 // The legacy feature is deprecated (T89981).
304 if ( isset( $features['legacy'] ) && $features['legacy'] ) {
305 $messages .= '[1.37] The use of the `legacy` feature with ResourceLoaderSkinModule is deprecated'
306 . '(T89981). ';
307 }
308
309 // The `content-links` feature was split out from `elements`.
310 // Make sure skins asking for `elements` also get these by default.
311 if ( isset( $features[ 'element' ] ) && !isset( $features[ 'content-links' ] ) ) {
312 $features[ 'content-links' ] = $features[ 'element' ];
313 }
314
315 // `content-parser-output` was renamed to `content-body`.
316 // No need to go through deprecation process here since content-parser-output added and removed in 1.36.
317 // Remove this check when no matches for
318 // https://codesearch.wmcloud.org/search/?q=content-parser-output&i=nope&files=&excludeFiles=&repos=
319 if ( isset( $features[ 'content-parser-output' ] ) ) {
320 $features[ 'content-body' ] = $features[ 'content-parser-output' ];
321 unset( $features[ 'content-parser-output' ] );
322 }
323
324 return $features;
325 }
326
333 public function getStyleFiles( ResourceLoaderContext $context ) {
334 $styles = parent::getStyleFiles( $context );
335
336 // Bypass the current module paths so that these files are served from core,
337 // instead of the individual skin's module directory.
338 list( $defaultLocalBasePath, $defaultRemoteBasePath ) =
340 [],
341 null,
342 $this->getConfig()->get( 'ResourceBasePath' )
343 );
344
345 $featureFilePaths = [];
346
347 foreach ( self::FEATURE_FILES as $feature => $featureFiles ) {
348 if ( in_array( $feature, $this->features ) ) {
349 foreach ( $featureFiles as $mediaType => $files ) {
350 foreach ( $files as $filepath ) {
351 $featureFilePaths[$mediaType][] = new ResourceLoaderFilePath(
352 $filepath,
353 $defaultLocalBasePath,
354 $defaultRemoteBasePath
355 );
356 }
357 }
358 if ( $feature === 'content-media' && !$this->getConfig()->get( 'ParserEnableLegacyMediaDOM' ) ) {
359 $featureFilePaths['screen'][] = new ResourceLoaderFilePath(
360 'resources/src/mediawiki.skinning/content.media.less',
361 $defaultLocalBasePath,
362 $defaultRemoteBasePath
363 );
364 }
365 }
366 }
367
368 // Styles defines in options are added to the $featureFilePaths to ensure
369 // that $featureFilePaths styles precede module defined ones.
370 // This is particularly important given the `normalize` styles need to be the first
371 // outputted (see T269618).
372 foreach ( $styles as $mediaType => $paths ) {
373 $featureFilePaths[$mediaType] = array_merge( $featureFilePaths[$mediaType] ?? [], $paths );
374 }
375
376 return $featureFilePaths;
377 }
378
383 public function getStyles( ResourceLoaderContext $context ) {
384 $logo = $this->getLogoData( $this->getConfig() );
385 $styles = parent::getStyles( $context );
386 $this->normalizeStyles( $styles );
387
388 $isLogoFeatureEnabled = in_array( 'logo', $this->features );
389 if ( $isLogoFeatureEnabled ) {
390 $default = !is_array( $logo ) ? $logo : $logo['1x'];
391 $styles['all'][] = '.mw-wiki-logo { background-image: ' .
392 CSSMin::buildUrlValue( $default ) .
393 '; }';
394
395 if ( is_array( $logo ) ) {
396 if ( isset( $logo['svg'] ) ) {
397 $styles['all'][] = '.mw-wiki-logo { ' .
398 'background-image: -webkit-linear-gradient(transparent, transparent), ' .
399 CSSMin::buildUrlValue( $logo['svg'] ) . '; ' .
400 'background-image: linear-gradient(transparent, transparent), ' .
401 CSSMin::buildUrlValue( $logo['svg'] ) . ';' .
402 'background-size: 135px auto; }';
403 } else {
404 if ( isset( $logo['1.5x'] ) ) {
405 $styles[
406 '(-webkit-min-device-pixel-ratio: 1.5), ' .
407 '(min-resolution: 1.5dppx), ' .
408 '(min-resolution: 144dpi)'
409 ][] = '.mw-wiki-logo { background-image: ' .
410 CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
411 'background-size: 135px auto; }';
412 }
413 if ( isset( $logo['2x'] ) ) {
414 $styles[
415 '(-webkit-min-device-pixel-ratio: 2), ' .
416 '(min-resolution: 2dppx), ' .
417 '(min-resolution: 192dpi)'
418 ][] = '.mw-wiki-logo { background-image: ' .
419 CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
420 'background-size: 135px auto; }';
421 }
422 }
423 }
424 }
425
426 return $styles;
427 }
428
433 public function getPreloadLinks( ResourceLoaderContext $context ) {
434 return $this->getLogoPreloadlinks();
435 }
436
441 private function getLogoPreloadlinks(): array {
442 if ( !in_array( 'logo', $this->features ) ) {
443 return [];
444 }
445
446 $logo = $this->getLogoData( $this->getConfig() );
447
448 if ( !is_array( $logo ) ) {
449 // No media queries required if we only have one variant
450 return [ $logo => [ 'as' => 'image' ] ];
451 }
452
453 if ( isset( $logo['svg'] ) ) {
454 // No media queries required if we only have a 1x and svg variant
455 // because all preload-capable browsers support SVGs
456 return [ $logo['svg'] => [ 'as' => 'image' ] ];
457 }
458
459 $logosPerDppx = [];
460 foreach ( $logo as $dppx => $src ) {
461 // Keys are in this format: "1.5x"
462 $dppx = substr( $dppx, 0, -1 );
463 $logosPerDppx[$dppx] = $src;
464 }
465
466 // Because PHP can't have floats as array keys
467 uksort( $logosPerDppx, static function ( $a, $b ) {
468 $a = floatval( $a );
469 $b = floatval( $b );
470 // Sort from smallest to largest (e.g. 1x, 1.5x, 2x)
471 return $a <=> $b;
472 } );
473
474 $logos = [];
475 foreach ( $logosPerDppx as $dppx => $src ) {
476 $logos[] = [
477 'dppx' => $dppx,
478 'src' => $src
479 ];
480 }
481
482 $logosCount = count( $logos );
483 $preloadLinks = [];
484 // Logic must match ResourceLoaderSkinModule:
485 // - 1x applies to resolution < 1.5dppx
486 // - 1.5x applies to resolution >= 1.5dppx && < 2dppx
487 // - 2x applies to resolution >= 2dppx
488 // Note that min-resolution and max-resolution are both inclusive.
489 for ( $i = 0; $i < $logosCount; $i++ ) {
490 if ( $i === 0 ) {
491 // Smallest dppx
492 // min-resolution is ">=" (larger than or equal to)
493 // "not min-resolution" is essentially "<"
494 $media_query = 'not all and (min-resolution: ' . $logos[1]['dppx'] . 'dppx)';
495 } elseif ( $i !== $logosCount - 1 ) {
496 // In between
497 // Media query expressions can only apply "not" to the entire expression
498 // (e.g. can't express ">= 1.5 and not >= 2).
499 // Workaround: Use <= 1.9999 in place of < 2.
500 $upper_bound = floatval( $logos[$i + 1]['dppx'] ) - 0.000001;
501 $media_query = '(min-resolution: ' . $logos[$i]['dppx'] .
502 'dppx) and (max-resolution: ' . $upper_bound . 'dppx)';
503 } else {
504 // Largest dppx
505 $media_query = '(min-resolution: ' . $logos[$i]['dppx'] . 'dppx)';
506 }
507
508 $preloadLinks[$logos[$i]['src']] = [
509 'as' => 'image',
510 'media' => $media_query
511 ];
512 }
513
514 return $preloadLinks;
515 }
516
525 private function normalizeStyles( array &$styles ): void {
526 foreach ( $styles as $key => $val ) {
527 if ( !is_array( $val ) ) {
528 $styles[$key] = [ $val ];
529 }
530 }
531 }
532
539 private static function getRelativeSizedLogo( array $logoElement ) {
540 $width = $logoElement['width'];
541 $height = $logoElement['height'];
542 $widthRelative = $width / 16;
543 $heightRelative = $height / 16;
544 // Allow skins to scale the wordmark with browser font size (T207789)
545 $logoElement['style'] = 'width: ' . $widthRelative . 'em; height: ' . $heightRelative . 'em;';
546 return $logoElement;
547 }
548
563 public static function getAvailableLogos( $conf ): array {
564 $logos = $conf->get( 'Logos' );
565 if ( $logos === false ) {
566 // no logos were defined... this will either
567 // 1. Load from wgLogo and wgLogoHD
568 // 2. Trigger runtime exception if those are not defined.
569 $logos = [];
570 }
571
572 // If logos['1x'] is not defined, see if we can use wgLogo
573 if ( !isset( $logos[ '1x' ] ) ) {
574 $logo = $conf->get( 'Logo' );
575 if ( $logo ) {
576 $logos['1x'] = $logo;
577 }
578 }
579
580 try {
581 $logoHD = $conf->get( 'LogoHD' );
582 // make sure not false
583 if ( $logoHD ) {
584 // wfDeprecated( __METHOD__ . ' with $wgLogoHD set instead of $wgLogos', '1.35', false, 1 );
585 $logos += $logoHD;
586 }
587 } catch ( ConfigException $e ) {
588 // no backwards compatibility changes needed.
589 }
590
591 // check the configuration is valid
592 if ( !isset( $logos['1x'] ) ) {
593 throw new RuntimeException( "The key `1x` is required for wgLogos or wgLogo must be defined." );
594 }
595
596 // @todo: Note the beta cluster and other wikis may be using
597 // unsupported configuration where these values are set to false.
598 // The boolean check can be removed when this has been addressed.
599 if ( isset( $logos['wordmark'] ) && $logos['wordmark'] ) {
600 // Allow skins to scale the wordmark with browser font size (T207789)
601 $logos['wordmark'] = self::getRelativeSizedLogo( $logos['wordmark'] );
602 }
603
604 // @todo: Note the beta cluster and other wikis may be using
605 // unsupported configuration where these values are set to false.
606 // The boolean check can be removed when this has been addressed.
607 if ( isset( $logos['tagline'] ) && $logos['tagline'] ) {
608 $logos['tagline'] = self::getRelativeSizedLogo( $logos['tagline'] );
609 }
610 // return the modified logos!
611 return $logos;
612 }
613
622 protected function getLogoData( Config $conf ) {
623 $logoHD = self::getAvailableLogos( $conf );
624 $logo = $logoHD['1x'];
625
626 $logo1Url = OutputPage::transformResourcePath( $conf, $logo );
627
628 $logoUrls = [
629 '1x' => $logo1Url,
630 ];
631
632 if ( isset( $logoHD['svg'] ) ) {
633 $logoUrls['svg'] = OutputPage::transformResourcePath(
634 $conf,
635 $logoHD['svg']
636 );
637 } elseif ( isset( $logoHD['1.5x'] ) || isset( $logoHD['2x'] ) ) {
638 // Only 1.5x and 2x are supported
639 if ( isset( $logoHD['1.5x'] ) ) {
640 $logoUrls['1.5x'] = OutputPage::transformResourcePath(
641 $conf,
642 $logoHD['1.5x']
643 );
644 }
645 if ( isset( $logoHD['2x'] ) ) {
646 $logoUrls['2x'] = OutputPage::transformResourcePath(
647 $conf,
648 $logoHD['2x']
649 );
650 }
651 } else {
652 // Return a string rather than a one-element array, getLogoPreloadlinks depends on this
653 return $logo1Url;
654 }
655
656 return $logoUrls;
657 }
658
663 public function isKnownEmpty( ResourceLoaderContext $context ) {
664 // Regardless of whether the files are specified, we always
665 // provide mw-wiki-logo styles.
666 return false;
667 }
668
675 protected function getLessVars( ResourceLoaderContext $context ) {
676 $lessVars = parent::getLessVars( $context );
677 $logos = self::getAvailableLogos( $this->getConfig() );
678
679 if ( isset( $logos['wordmark'] ) ) {
680 $logo = $logos['wordmark'];
681 $lessVars[ 'logo-enabled' ] = true;
682 $lessVars[ 'logo-wordmark-url' ] = CSSMin::buildUrlValue( $logo['src'] );
683 $lessVars[ 'logo-wordmark-width' ] = intval( $logo['width'] );
684 $lessVars[ 'logo-wordmark-height' ] = intval( $logo['height'] );
685 } else {
686 $lessVars[ 'logo-enabled' ] = false;
687 }
688 return $lessVars;
689 }
690
691 public function getDefinitionSummary( ResourceLoaderContext $context ) {
692 $summary = parent::getDefinitionSummary( $context );
693 $summary[] = [
694 'logos' => self::getAvailableLogos( $this->getConfig() ),
695 ];
696 return $summary;
697 }
698}
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition Setup.php:88
Exceptions for config failures.
Context object that contains information about the state of a specific ResourceLoader web request.
string $localBasePath
Local base path, see __construct()
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information.
array $messages
List of message keys used by this module.
string $remoteBasePath
Remote base path, see __construct()
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
Module augmented with context-specific LESS variables.
Module for skin stylesheets.
const FEATURE_FILES
Every skin should define which features it would like to reuse for core inside a ResourceLoader modul...
static getRelativeSizedLogo(array $logoElement)
Modifies configured logo width/height to ensure they are present and scaleable with different font-si...
$targets
All skins are assumed to be compatible with mobile.
normalizeStyles(array &$styles)
Ensure all media keys use array values.
getDefinitionSummary(ResourceLoaderContext $context)
Get the definition summary for this module.
getStyleFiles(ResourceLoaderContext $context)
Get styles defined in the module definition, plus any enabled feature styles.
getStyles(ResourceLoaderContext $context)
getPreloadLinks(ResourceLoaderContext $context)
getLessVars(ResourceLoaderContext $context)
Get language-specific LESS variables for this module.
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
isKnownEmpty(ResourceLoaderContext $context)
getLogoPreloadlinks()
Helper method for getPreloadLinks()
__construct(array $options=[], $localBasePath=null, $remoteBasePath=null)
static applyFeaturesCompatibility(array $features, &$messages='')
Interface for configuration instances.
Definition Config.php:30
return true
Definition router.php:92