8use InvalidArgumentException;
14use Wikimedia\Minify\CSSMin;
119 private const FEATURE_FILES = [
121 'all' => [
'resources/src/mediawiki.skinning/accessibility.less' ],
124 'all' => [
'resources/src/mediawiki.skinning/normalize.less' ],
128 'all' => [
'resources/src/mediawiki.skinning/logo.less' ],
130 'print' => [
'resources/src/mediawiki.skinning/logo-print.less' ],
134 'resources/src/mediawiki.skinning/content.media-common.less',
137 'resources/src/mediawiki.skinning/content.media-screen.less',
140 'resources/src/mediawiki.skinning/content.media-print.less',
143 'content-media-legacy' => [
145 'resources/src/mediawiki.skinning/content.thumbnails-common.less',
148 'resources/src/mediawiki.skinning/content.thumbnails-screen.less',
151 'resources/src/mediawiki.skinning/content.thumbnails-print.less',
154 'content-media-dark' => [
155 'screen' => [
'resources/src/mediawiki.skinning/content.media-dark.less' ],
158 'screen' => [
'resources/src/mediawiki.skinning/content.links.less' ]
160 'content-links-external' => [
161 'screen' => [
'resources/src/mediawiki.skinning/content.externallinks.less' ]
164 'screen' => [
'resources/src/mediawiki.skinning/content.body.less' ],
165 'print' => [
'resources/src/mediawiki.skinning/content.body-print.less' ],
167 'content-tables' => [
168 'screen' => [
'resources/src/mediawiki.skinning/content.tables.less' ],
169 'print' => [
'resources/src/mediawiki.skinning/content.tables-print.less' ]
171 'interface-category' => [
172 'screen' => [
'resources/src/mediawiki.skinning/interface.category.less' ],
173 'print' => [
'resources/src/mediawiki.skinning/interface.category-print.less' ],
175 'interface-core' => [
176 'screen' => [
'resources/src/mediawiki.skinning/interface.less' ],
177 'print' => [
'resources/src/mediawiki.skinning/interface-print.less' ],
179 'interface-edit-section-links' => [
180 'screen' => [
'resources/src/mediawiki.skinning/interface-edit-section-links.less' ],
182 'interface-indicators' => [
183 'screen' => [
'resources/src/mediawiki.skinning/interface-indicators.less' ],
185 'interface-site-notice' => [
186 'screen' => [
'resources/src/mediawiki.skinning/interface-site-notice.less' ],
188 'interface-subtitle' => [
189 'screen' => [
'resources/src/mediawiki.skinning/interface-subtitle.less' ],
191 'interface-message-box' => [
192 'all' => [
'resources/src/mediawiki.skinning/messageBoxes.less' ],
194 'interface-user-message' => [
195 'screen' => [
'resources/src/mediawiki.skinning/interface-user-message.less' ],
198 'screen' => [
'resources/src/mediawiki.skinning/elements.less' ],
199 'print' => [
'resources/src/mediawiki.skinning/elements-print.less' ],
201 'i18n-ordered-lists' => [
202 'screen' => [
'resources/src/mediawiki.skinning/i18n-ordered-lists.less' ],
204 'i18n-all-lists-margins' => [
205 'screen' => [
'resources/src/mediawiki.skinning/i18n-all-lists-margins.less' ],
208 'screen' => [
'resources/src/mediawiki.skinning/i18n-headings.less' ],
211 'all' => [
'resources/src/mediawiki.skinning/toc/common.less' ],
212 'screen' => [
'resources/src/mediawiki.skinning/toc/screen.less' ],
213 'print' => [
'resources/src/mediawiki.skinning/toc/print.less' ],
217 private const COMPAT_ALIASES = [
219 'content-parser-output' =>
'content-body',
221 'content' =>
'content-media',
222 'content-thumbnails' =>
'content-media',
238 private const DEFAULT_FEATURES_SPECIFIED = [
239 'accessibility' =>
true,
240 'content-body' =>
true,
241 'interface-core' =>
true,
251 private const DEFAULT_FEATURES_ABSENT = [
255 private const TOC_LESS_MESSAGES = [
261 private const SECTION_LESS_MESSAGES = [
291 $features = $options[
'features'] ?? self::DEFAULT_FEATURES_ABSENT;
292 $listMode = array_keys( $features ) === range( 0, count( $features ) - 1 );
297 $features = $listMode ?
299 array_fill_keys( $features,
true ),
305 foreach ( $features as $key => $enabled ) {
306 if ( !isset( self::FEATURE_FILES[$key] ) ) {
307 throw new InvalidArgumentException(
"Feature '$key' is not recognised" );
311 $this->features = $listMode
312 ? array_keys( array_filter( $features ) )
313 : array_keys( array_filter( $features + self::DEFAULT_FEATURES_SPECIFIED ) );
318 if ( in_array(
'toc', $this->features ) ) {
319 $options[
'lessMessages'] = array_merge(
320 $options[
'lessMessages'] ?? [],
321 self::TOC_LESS_MESSAGES
324 if ( in_array(
'interface-edit-section-links', $this->features ) ) {
325 $options[
'lessMessages'] = array_merge(
326 $options[
'lessMessages'] ?? [],
327 self::SECTION_LESS_MESSAGES
332 $messages .=
'More information can be found at [[mw:Manual:ResourceLoaderSkinModule]]. ';
346 array $features,
bool $addUnspecifiedFeatures =
true, &
$messages =
''
348 if ( isset( $features[
'i18n-all-lists-margins' ] ) ) {
351 $messages .=
'[1.43] The use of the `i18n-all-lists-margins` feature with SkinModule'
352 .
' is deprecated as it is now provided by `elements`. Please remove and '
353 .
' add `elements`, drop support for RTL languages, or incorporate the '
354 .
' styles provided by this module into your skin.';
356 if ( isset( $features[
'interface-message-box' ] ) && $features[
'interface-message-box' ] ) {
361 $messages .=
'[1.43] The use of the `interface-message-box` feature with SkinModule'
362 .
' is deprecated in favor of CodexModule. Please remove this feature.';
365 foreach ( self::COMPAT_ALIASES as $from => $to ) {
366 if ( isset( $features[ $from ] ) && $to !==
null ) {
367 if ( isset( $features[ $to ] ) ) {
368 $messages .=
"SkinModule feature `$from` conflicts with `$to` and was ignored. ";
370 $features[ $to ] = $features[ $from ];
373 unset( $features[ $from ] );
377 if ( $addUnspecifiedFeatures
378 && isset( $features[
'content-links' ] )
379 && !isset( $features[
'content-links-external' ] )
382 $features[
'content-links-external' ] = $features[
'content-links' ];
387 if ( $addUnspecifiedFeatures && isset( $features[
'elements' ] ) && !isset( $features[
'content-links' ] ) ) {
388 $features[
'content-links' ] = $features[
'elements' ];
392 if ( isset( $features[
'interface' ] ) && $features[
'interface' ] ) {
393 $features[
'interface-core' ] =
true;
394 $features[
'interface-indicators' ] =
true;
395 $features[
'interface-subtitle' ] =
true;
396 $features[
'interface-user-message' ] =
true;
397 $features[
'interface-site-notice' ] =
true;
398 $features[
'interface-edit-section-links' ] =
true;
400 unset( $features[
'interface' ] );
413 [ $defaultLocalBasePath, $defaultRemoteBasePath ] =
420 $featureFilePaths = [];
422 foreach ( self::FEATURE_FILES as $feature => $featureFiles ) {
423 if ( in_array( $feature, $this->features ) ) {
424 foreach ( $featureFiles as $mediaType => $files ) {
425 foreach ( $files as $filepath ) {
426 $featureFilePaths[$mediaType][] =
new FilePath(
428 $defaultLocalBasePath,
429 $defaultRemoteBasePath
435 return $featureFilePaths;
448 private function combineFeatureAndParentStyles( $featureStyles, $parentStyles,
$request ) {
451 $combinedStyles = array_merge( $combinedFeatureStyles, $combinedParentStyles );
452 return [
'' => $combinedStyles ];
463 $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() );
464 $default = !is_array( $logo ) ? $logo : ( $logo[
'svg'] ?? $logo[
'1x'] ?? null );
468 return $featureStyles;
471 $featureStyles[
'all'][] =
'.mw-wiki-logo { background-image: ' .
472 CSSMin::buildUrlValue( $default ) .
475 if ( is_array( $logo ) ) {
476 if ( isset( $logo[
'svg'] ) ) {
477 $featureStyles[
'all'][] =
'.mw-wiki-logo { ' .
478 'background-size: 135px auto; }';
480 if ( isset( $logo[
'2x'] ) ) {
482 '(-webkit-min-device-pixel-ratio: 2), ' .
483 '(min-resolution: 2dppx), ' .
484 '(min-resolution: 192dpi)'
485 ][] =
'.mw-wiki-logo { background-image: ' .
486 CSSMin::buildUrlValue( $logo[
'2x'] ) .
';' .
487 'background-size: 135px auto; }';
491 return $featureStyles;
499 $parentStyles = parent::getStyles( $context );
500 $featureFilePaths = $this->getFeatureFilePaths();
501 $featureStyles = $this->readStyleFiles( $featureFilePaths, $context );
503 $this->normalizeStyles( $featureStyles );
504 $this->normalizeStyles( $parentStyles );
506 $isLogoFeatureEnabled = in_array(
'logo', $this->features );
507 if ( $isLogoFeatureEnabled ) {
508 $featureStyles = $this->generateAndAppendLogoStyles( $featureStyles, $context );
510 $isAccessibilityEnabled = in_array(
'accessibility', $this->features );
512 $config = $this->getConfig();
513 $limits = $config->get(
'ThumbLimits' );
518 if ( $isAccessibilityEnabled ) {
519 [ $smallSize, $defaultSize, $largeSize ] = DefaultPreferencesFactory::getNormalizedThumbSizes(
521 $config->get(
'DefaultUserOptions' )
523 $featureStyles[
'all'][] = <<<CSS
525 --image-size-small: {$smallSize}px;
526 --image-size-standard: {$defaultSize}px;
527 --image-size-large: {$largeSize}px;
532 return $this->combineFeatureAndParentStyles( $featureStyles, $parentStyles, $context->
getRequest() );
536 if ( !in_array(
'logo', $this->features ) ) {
540 $logo = $this->getLogoData( $this->getConfig(), $context->getLanguage() );
542 if ( !is_array( $logo ) ) {
544 return [ $logo => [
'as' =>
'image' ] ];
547 if ( isset( $logo[
'svg'] ) ) {
550 return [ $logo[
'svg'] => [
'as' =>
'image' ] ];
554 foreach ( $logo as $dppx => $src ) {
556 $logos[] = [
'dppx' => (float)$dppx,
'src' => $src ];
559 usort( $logos,
static fn ( $a, $b ) => $a[
'dppx'] <=> $b[
'dppx'] );
561 $logosCount = count( $logos );
567 for ( $i = 0; $i < $logosCount; $i++ ) {
572 $media_query =
'not all and (min-resolution: ' . $logos[1][
'dppx'] .
'dppx)';
573 } elseif ( $i !== $logosCount - 1 ) {
578 $upper_bound = floatval( $logos[$i + 1][
'dppx'] ) - 0.000001;
579 $media_query =
'(min-resolution: ' . $logos[$i][
'dppx'] .
580 'dppx) and (max-resolution: ' . $upper_bound .
'dppx)';
583 $media_query =
'(min-resolution: ' . $logos[$i][
'dppx'] .
'dppx)';
586 $preloadLinks[$logos[$i][
'src']] = [
588 'media' => $media_query
592 return $preloadLinks;
603 private function normalizeStyles( array &$styles ): void {
604 foreach ( $styles as $key => $val ) {
605 if ( !is_array( $val ) ) {
606 $styles[$key] = [ $val ];
617 private static function getRelativeSizedLogo( array $logoElement ) {
618 $width = $logoElement[
'width'];
619 $height = $logoElement[
'height'];
620 $widthRelative = $width / 16;
621 $heightRelative = $height / 16;
623 $logoElement[
'style'] =
'width: ' . $widthRelative .
'em; height: ' . $heightRelative .
'em;';
644 if ( $logos ===
false ) {
650 if ( $lang && isset( $logos[
'variants'][$lang] ) ) {
651 foreach ( $logos[
'variants'][$lang] as $type => $value ) {
652 $logos[$type] = $value;
657 if ( !isset( $logos[
'1x' ] ) ) {
658 $logo = $conf->
get( MainConfigNames::Logo );
660 $logos[
'1x'] = $logo;
664 if ( isset( $logos[
'wordmark'] ) ) {
666 $logos[
'wordmark'] = self::getRelativeSizedLogo( $logos[
'wordmark'] );
668 if ( isset( $logos[
'tagline'] ) ) {
669 $logos[
'tagline'] = self::getRelativeSizedLogo( $logos[
'tagline'] );
685 $logoHD = self::getAvailableLogos( $conf, $lang );
686 $logo = $logoHD[
'1x'];
688 $logo1Url = OutputPage::transformResourcePath( $conf, $logo );
694 if ( isset( $logoHD[
'svg'] ) ) {
695 $logoUrls[
'svg'] = OutputPage::transformResourcePath(
699 } elseif ( isset( $logoHD[
'2x'] ) ) {
700 $logoUrls[
'2x'] = OutputPage::transformResourcePath(
729 $lessVars = parent::getLessVars( $context );
730 $config = $this->getConfig();
731 $logos = self::getAvailableLogos( $config, $context->
getLanguage() );
732 [ $smallSize, $defaultSize, $largeSize ] = DefaultPreferencesFactory::getNormalizedThumbSizes(
733 $config->get( MainConfigNames::ThumbLimits ),
734 $config->get(
'DefaultUserOptions' )
737 $lessVars[
'image-size-standard' ] = $defaultSize;
738 if ( isset( $logos[
'wordmark'] ) ) {
739 $logo = $logos[
'wordmark'];
740 $lessVars[
'logo-enabled' ] =
true;
741 $lessVars[
'logo-wordmark-url' ] = CSSMin::buildUrlValue( $logo[
'src'] );
742 $lessVars[
'logo-wordmark-width' ] = intval( $logo[
'width'] );
743 $lessVars[
'logo-wordmark-height' ] = intval( $logo[
'height'] );
745 $lessVars[
'logo-enabled' ] =
false;
752 $summary = parent::getDefinitionSummary( $context );
753 $config = $this->getConfig();
755 'thumblimits' => $config->get(
'ThumbLimits' ),
756 'logos' => self::getAvailableLogos( $config, $context->
getLanguage() ),
if(!defined('MW_SETUP_CALLBACK'))
A class containing constants representing the names of configuration variables.
const ResourceBasePath
Name constant for the ResourceBasePath setting, for use with Config::get()
const ThumbLimits
Name constant for the ThumbLimits setting, for use with Config::get()
This is one of the Core classes and should be read at least once by any new developers.
Context object that contains information about the state of a specific ResourceLoader web request.