MediaWiki  master
ResourceLoaderSkinModule.php
Go to the documentation of this file.
1 <?php
31  public $targets = [ 'desktop', 'mobile' ];
32 
87  private const FEATURE_FILES = [
88  'logo' => [
89  // Applies the logo and ensures it downloads prior to printing.
90  'all' => [ 'resources/src/mediawiki.skinning/logo.less' ],
91  // Reserves whitespace for the logo in a pseudo element.
92  'print' => [ 'resources/src/mediawiki.skinning/logo-print.less' ],
93  ],
94  'content' => [
95  'screen' => [ 'resources/src/mediawiki.skinning/content.css' ],
96  ],
97  'interface' => [
98  'screen' => [ 'resources/src/mediawiki.skinning/interface.less' ],
99  ],
100  'normalize' => [
101  'screen' => [ 'resources/src/mediawiki.skinning/normalize.less' ],
102  ],
103  'elements' => [
104  'screen' => [ 'resources/src/mediawiki.skinning/elements.css' ],
105  ],
106  'legacy' => [
107  'all' => [ 'resources/src/mediawiki.skinning/messageBoxes.less' ],
108  'print' => [ 'resources/src/mediawiki.skinning/commonPrint.css' ],
109  'screen' => [ 'resources/src/mediawiki.skinning/legacy.less' ],
110  ],
111  'i18n-ordered-lists' => [
112  'screen' => [ 'resources/src/mediawiki.skinning/i18n-ordered-lists.less' ],
113  ],
114  'i18n-all-lists-margins' => [
115  'screen' => [ 'resources/src/mediawiki.skinning/i18n-all-lists-margins.less' ],
116  ],
117  'i18n-headings' => [
118  'screen' => [ 'resources/src/mediawiki.skinning/i18n-headings.less' ],
119  ],
120  'toc' => [
121  'all' => [ 'resources/src/mediawiki.skinning/toc/common.css' ],
122  'screen' => [ 'resources/src/mediawiki.skinning/toc/screen.less' ],
123  'print' => [ 'resources/src/mediawiki.skinning/toc/print.css' ],
124  ],
125  ];
126 
128  private $features;
129 
131  private const DEFAULT_FEATURES = [
132  'logo' => false,
133  'content' => false,
134  'interface' => false,
135  'elements' => false,
136  'legacy' => false,
137  'i18n-ordered-lists' => false,
138  'i18n-all-lists-margins' => false,
139  'i18n-headings' => false,
140  'toc' => true,
141  ];
142 
143  private const LESS_MESSAGES = [
144  // `toc` feature, used in screen.less
145  'hidetoc',
146  'showtoc',
147  ];
148 
149  public function __construct(
150  array $options = [],
151  $localBasePath = null,
152  $remoteBasePath = null
153  ) {
154  $options['lessMessages'] = self::LESS_MESSAGES;
155  parent::__construct( $options, $localBasePath, $remoteBasePath );
156  $features = $options['features'] ??
157  // For historic reasons if nothing is declared logo and legacy features are enabled.
158  [
159  'logo' => true,
160  'legacy' => true
161  ];
162  $enabledFeatures = [];
163  $compatibilityMode = false;
164  foreach ( $features as $key => $enabled ) {
165  if ( is_bool( $enabled ) ) {
166  $enabledFeatures[$key] = $enabled;
167  } else {
168  // operating in array mode.
169  $enabledFeatures[$enabled] = true;
170  $compatibilityMode = true;
171  }
172  }
173  // If the module didn't specify an option use the default features values.
174  // This allows new features to be turned on automatically.
175  if ( !$compatibilityMode ) {
176  foreach ( self::DEFAULT_FEATURES as $key => $enabled ) {
177  if ( !isset( $enabledFeatures[$key] ) ) {
178  $enabledFeatures[$key] = $enabled;
179  }
180  }
181  }
182  $this->features = array_filter(
183  array_keys( $enabledFeatures ),
184  function ( $key ) use ( $enabledFeatures ) {
185  return $enabledFeatures[ $key ];
186  }
187  );
188  }
189 
196  public function getStyleFiles( ResourceLoaderContext $context ) {
197  $styles = parent::getStyleFiles( $context );
198 
199  list( $defaultLocalBasePath, $defaultRemoteBasePath ) =
201 
202  $featureFilePaths = [];
203 
204  foreach ( $this->features as $feature ) {
205  if ( !isset( self::FEATURE_FILES[$feature] ) ) {
206  throw new InvalidArgumentException( "Feature `$feature` is not recognised" );
207  }
208  foreach ( self::FEATURE_FILES[$feature] as $mediaType => $files ) {
209  foreach ( $files as $filepath ) {
210  $featureFilePaths[$mediaType][] = new ResourceLoaderFilePath(
211  $filepath,
212  $defaultLocalBasePath,
213  $defaultRemoteBasePath
214  );
215  }
216  }
217  }
218 
219  foreach ( $featureFilePaths as $mediaType => $paths ) {
220  $styles[$mediaType] = array_merge( $paths, $styles[$mediaType] ?? [] );
221  }
222 
223  return $styles;
224  }
225 
230  public function getStyles( ResourceLoaderContext $context ) {
231  $logo = $this->getLogoData( $this->getConfig() );
232  $styles = parent::getStyles( $context );
233  $this->normalizeStyles( $styles );
234 
235  $isLogoFeatureEnabled = in_array( 'logo', $this->features );
236  if ( $isLogoFeatureEnabled ) {
237  $default = !is_array( $logo ) ? $logo : $logo['1x'];
238  $styles['all'][] = '.mw-wiki-logo { background-image: ' .
239  CSSMin::buildUrlValue( $default ) .
240  '; }';
241 
242  if ( is_array( $logo ) ) {
243  if ( isset( $logo['svg'] ) ) {
244  $styles['all'][] = '.mw-wiki-logo { ' .
245  'background-image: -webkit-linear-gradient(transparent, transparent), ' .
246  CSSMin::buildUrlValue( $logo['svg'] ) . '; ' .
247  'background-image: linear-gradient(transparent, transparent), ' .
248  CSSMin::buildUrlValue( $logo['svg'] ) . ';' .
249  'background-size: 135px auto; }';
250  } else {
251  if ( isset( $logo['1.5x'] ) ) {
252  $styles[
253  '(-webkit-min-device-pixel-ratio: 1.5), ' .
254  '(min--moz-device-pixel-ratio: 1.5), ' .
255  '(min-resolution: 1.5dppx), ' .
256  '(min-resolution: 144dpi)'
257  ][] = '.mw-wiki-logo { background-image: ' .
258  CSSMin::buildUrlValue( $logo['1.5x'] ) . ';' .
259  'background-size: 135px auto; }';
260  }
261  if ( isset( $logo['2x'] ) ) {
262  $styles[
263  '(-webkit-min-device-pixel-ratio: 2), ' .
264  '(min--moz-device-pixel-ratio: 2), ' .
265  '(min-resolution: 2dppx), ' .
266  '(min-resolution: 192dpi)'
267  ][] = '.mw-wiki-logo { background-image: ' .
268  CSSMin::buildUrlValue( $logo['2x'] ) . ';' .
269  'background-size: 135px auto; }';
270  }
271  }
272  }
273  }
274 
275  return $styles;
276  }
277 
282  public function getPreloadLinks( ResourceLoaderContext $context ) {
283  return $this->getLogoPreloadlinks();
284  }
285 
290  private function getLogoPreloadlinks() : array {
291  if ( !in_array( 'logo', $this->features ) ) {
292  return [];
293  }
294 
295  $logo = $this->getLogoData( $this->getConfig() );
296 
297  if ( !is_array( $logo ) ) {
298  // No media queries required if we only have one variant
299  return [ $logo => [ 'as' => 'image' ] ];
300  }
301 
302  if ( isset( $logo['svg'] ) ) {
303  // No media queries required if we only have a 1x and svg variant
304  // because all preload-capable browsers support SVGs
305  return [ $logo['svg'] => [ 'as' => 'image' ] ];
306  }
307 
308  $logosPerDppx = [];
309  foreach ( $logo as $dppx => $src ) {
310  // Keys are in this format: "1.5x"
311  $dppx = substr( $dppx, 0, -1 );
312  $logosPerDppx[$dppx] = $src;
313  }
314 
315  // Because PHP can't have floats as array keys
316  uksort( $logosPerDppx, function ( $a, $b ) {
317  $a = floatval( $a );
318  $b = floatval( $b );
319  // Sort from smallest to largest (e.g. 1x, 1.5x, 2x)
320  return $a <=> $b;
321  } );
322 
323  $logos = [];
324  foreach ( $logosPerDppx as $dppx => $src ) {
325  $logos[] = [
326  'dppx' => $dppx,
327  'src' => $src
328  ];
329  }
330 
331  $logosCount = count( $logos );
332  $preloadLinks = [];
333  // Logic must match ResourceLoaderSkinModule:
334  // - 1x applies to resolution < 1.5dppx
335  // - 1.5x applies to resolution >= 1.5dppx && < 2dppx
336  // - 2x applies to resolution >= 2dppx
337  // Note that min-resolution and max-resolution are both inclusive.
338  for ( $i = 0; $i < $logosCount; $i++ ) {
339  if ( $i === 0 ) {
340  // Smallest dppx
341  // min-resolution is ">=" (larger than or equal to)
342  // "not min-resolution" is essentially "<"
343  $media_query = 'not all and (min-resolution: ' . $logos[1]['dppx'] . 'dppx)';
344  } elseif ( $i !== $logosCount - 1 ) {
345  // In between
346  // Media query expressions can only apply "not" to the entire expression
347  // (e.g. can't express ">= 1.5 and not >= 2).
348  // Workaround: Use <= 1.9999 in place of < 2.
349  $upper_bound = floatval( $logos[$i + 1]['dppx'] ) - 0.000001;
350  $media_query = '(min-resolution: ' . $logos[$i]['dppx'] .
351  'dppx) and (max-resolution: ' . $upper_bound . 'dppx)';
352  } else {
353  // Largest dppx
354  $media_query = '(min-resolution: ' . $logos[$i]['dppx'] . 'dppx)';
355  }
356 
357  $preloadLinks[$logos[$i]['src']] = [
358  'as' => 'image',
359  'media' => $media_query
360  ];
361  }
362 
363  return $preloadLinks;
364  }
365 
374  private function normalizeStyles( array &$styles ) : void {
375  foreach ( $styles as $key => $val ) {
376  if ( !is_array( $val ) ) {
377  $styles[$key] = [ $val ];
378  }
379  }
380  }
381 
392  public static function getAvailableLogos( $conf ) : array {
393  $logos = $conf->get( 'Logos' );
394  if ( $logos === false ) {
395  // no logos were defined... this will either
396  // 1. Load from wgLogo and wgLogoHD
397  // 2. Trigger runtime exception if those are not defined.
398  $logos = [];
399  }
400 
401  // If logos['1x'] is not defined, see if we can use wgLogo
402  if ( !isset( $logos[ '1x' ] ) ) {
403  $logo = $conf->get( 'Logo' );
404  if ( $logo ) {
405  $logos['1x'] = $logo;
406  }
407  }
408 
409  try {
410  $logoHD = $conf->get( 'LogoHD' );
411  // make sure not false
412  if ( $logoHD ) {
413  // wfDeprecated( __METHOD__ . ' with $wgLogoHD set instead of $wgLogos', '1.35', false, 1 );
414  $logos += $logoHD;
415  }
416  } catch ( ConfigException $e ) {
417  // no backwards compatibility changes needed.
418  }
419 
420  // check the configuration is valid
421  if ( !isset( $logos['1x'] ) ) {
422  throw new RuntimeException( "The key `1x` is required for wgLogos or wgLogo must be defined." );
423  }
424  // return the modified logos!
425  return $logos;
426  }
427 
436  protected function getLogoData( Config $conf ) {
437  $logoHD = self::getAvailableLogos( $conf );
438  $logo = $logoHD['1x'];
439 
440  $logo1Url = OutputPage::transformResourcePath( $conf, $logo );
441 
442  $logoUrls = [
443  '1x' => $logo1Url,
444  ];
445 
446  if ( isset( $logoHD['svg'] ) ) {
447  $logoUrls['svg'] = OutputPage::transformResourcePath(
448  $conf,
449  $logoHD['svg']
450  );
451  } elseif ( isset( $logoHD['1.5x'] ) || isset( $logoHD['2x'] ) ) {
452  // Only 1.5x and 2x are supported
453  if ( isset( $logoHD['1.5x'] ) ) {
454  $logoUrls['1.5x'] = OutputPage::transformResourcePath(
455  $conf,
456  $logoHD['1.5x']
457  );
458  }
459  if ( isset( $logoHD['2x'] ) ) {
460  $logoUrls['2x'] = OutputPage::transformResourcePath(
461  $conf,
462  $logoHD['2x']
463  );
464  }
465  } else {
466  // Return a string rather than a one-element array, getLogoPreloadlinks depends on this
467  return $logo1Url;
468  }
469 
470  return $logoUrls;
471  }
472 
477  public function isKnownEmpty( ResourceLoaderContext $context ) {
478  // Regardless of whether the files are specified, we always
479  // provide mw-wiki-logo styles.
480  return false;
481  }
482 
489  protected function getLessVars( ResourceLoaderContext $context ) {
490  $lessVars = parent::getLessVars( $context );
491  $logos = self::getAvailableLogos( $this->getConfig() );
492 
493  if ( isset( $logos['wordmark'] ) ) {
494  $logo = $logos['wordmark'];
495  $lessVars[ 'logo-enabled' ] = true;
496  $lessVars[ 'logo-wordmark-url' ] = CSSMin::buildUrlValue( $logo['src'] );
497  $lessVars[ 'logo-wordmark-width' ] = intval( $logo['width'] );
498  $lessVars[ 'logo-wordmark-height' ] = intval( $logo['height'] );
499  } else {
500  $lessVars[ 'logo-enabled' ] = false;
501  }
502  return $lessVars;
503  }
504 
505  public function getDefinitionSummary( ResourceLoaderContext $context ) {
506  $summary = parent::getDefinitionSummary( $context );
507  $summary[] = [
508  'logos' => self::getAvailableLogos( $this->getConfig() ),
509  ];
510  return $summary;
511  }
512 }
ResourceLoaderLessVarFileModule
Module augmented with context-specific LESS variables.
Definition: ResourceLoaderLessVarFileModule.php:30
ResourceLoaderSkinModule\getPreloadLinks
getPreloadLinks(ResourceLoaderContext $context)
Definition: ResourceLoaderSkinModule.php:282
ResourceLoaderSkinModule\LESS_MESSAGES
const LESS_MESSAGES
Definition: ResourceLoaderSkinModule.php:143
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
ResourceLoaderFileModule\$styles
array $styles
List of paths to CSS files to always include.
Definition: ResourceLoaderFileModule.php:92
ResourceLoaderSkinModule\__construct
__construct(array $options=[], $localBasePath=null, $remoteBasePath=null)
Constructs a new module from an options array.See $wgResourceModules for the available options....
Definition: ResourceLoaderSkinModule.php:149
CSSMin\buildUrlValue
static buildUrlValue( $url)
Build a CSS 'url()' value for the given URL, quoting parentheses (and other funny characters) and esc...
Definition: CSSMin.php:216
true
return true
Definition: router.php:90
ResourceLoaderSkinModule\normalizeStyles
normalizeStyles(array &$styles)
Ensure all media keys use array values.
Definition: ResourceLoaderSkinModule.php:374
ResourceLoaderFilePath
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
Definition: ResourceLoaderFilePath.php:28
ResourceLoaderSkinModule\getDefinitionSummary
getDefinitionSummary(ResourceLoaderContext $context)
Get the definition summary for this module.
Definition: ResourceLoaderSkinModule.php:505
ResourceLoaderSkinModule\$features
string[] $features
Definition: ResourceLoaderSkinModule.php:128
Config
Interface for configuration instances.
Definition: Config.php:30
ResourceLoaderSkinModule\getLessVars
getLessVars(ResourceLoaderContext $context)
Get language-specific LESS variables for this module.
Definition: ResourceLoaderSkinModule.php:489
ConfigException
Exceptions for config failures.
Definition: ConfigException.php:29
ResourceLoaderSkinModule\getLogoData
getLogoData(Config $conf)
Definition: ResourceLoaderSkinModule.php:436
ResourceLoaderSkinModule\getAvailableLogos
static getAvailableLogos( $conf)
Return an array of all available logos that a skin may use.
Definition: ResourceLoaderSkinModule.php:392
ResourceLoaderFileModule\extractBasePaths
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information.
Definition: ResourceLoaderFileModule.php:301
ResourceLoaderSkinModule\FEATURE_FILES
const FEATURE_FILES
Every skin should define which features it would like to reuse for core inside a ResourceLoader modul...
Definition: ResourceLoaderSkinModule.php:87
ResourceLoaderSkinModule\getLogoPreloadlinks
getLogoPreloadlinks()
Helper method for getPreloadLinks()
Definition: ResourceLoaderSkinModule.php:290
ResourceLoaderSkinModule\getStyles
getStyles(ResourceLoaderContext $context)
Definition: ResourceLoaderSkinModule.php:230
ResourceLoaderFileModule\$localBasePath
string $localBasePath
Local base path, see __construct()
Definition: ResourceLoaderFileModule.php:41
ResourceLoaderSkinModule\isKnownEmpty
isKnownEmpty(ResourceLoaderContext $context)
Definition: ResourceLoaderSkinModule.php:477
ResourceLoaderSkinModule\getStyleFiles
getStyleFiles(ResourceLoaderContext $context)
Get styles defined in the module definition, plus any enabled feature styles.
Definition: ResourceLoaderSkinModule.php:196
ResourceLoaderFileModule\$remoteBasePath
string $remoteBasePath
Remote base path, see __construct()
Definition: ResourceLoaderFileModule.php:44
ResourceLoaderSkinModule
Module for skin stylesheets.
Definition: ResourceLoaderSkinModule.php:27
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:218
ResourceLoaderSkinModule\$targets
$targets
All skins are assumed to be compatible with mobile.
Definition: ResourceLoaderSkinModule.php:31
OutputPage\transformResourcePath
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
Definition: OutputPage.php:3947