resourceloader: Add skin-based 'mediawiki.skin.variables.less' import
Add the SkinLessImportPaths attribute for skin-specific LESS import paths, which skins can use to override the mediawiki.skin.variables.less file. As a starting point, add the following 5 variables: * device widths (3x) To help phase out 'mediawiki.ui/variables'. These are commonly used by MobileFrontend. * @font-family-sans Recommended by Volker. Used by multiple skins. * @border-radius-base Recommended by Volker as example of something that we currently hardcode in MediaWiki core for Vector and OOUI/WikimediaUI in 'mediawiki.widgets.datetime' but should instead be allowed to vary by skin and OOUI theme. Remove the hardcoded value for '@border-radius-base' in various places in favour of importing from mediawiki.skin. The default is a bare default of 0 (as border-radius is off by default in the browser). The value for Vector is restored there by I47da304667811. The value for MonoBook is improved by I000f319ab31b. Bug: T112747 Change-Id: Icf86c930a3b5524254bb549624737d3b9dccb032
This commit is contained in:
parent
4d8cb4dc16
commit
0c01d8cc52
17 changed files with 163 additions and 33 deletions
|
|
@ -190,6 +190,10 @@ because of Phabricator reports.
|
||||||
existing code, but it only supports version 2 of the test file
|
existing code, but it only supports version 2 of the test file
|
||||||
specification and may be more strict when parsing invalid input,
|
specification and may be more strict when parsing invalid input,
|
||||||
including duplicate tests.
|
including duplicate tests.
|
||||||
|
* The SkinLessImportPaths attribute was added, allowing skins to add a
|
||||||
|
directory to the import path for LESS stylesheets. Skins can use this
|
||||||
|
to provide a custom version of mediawiki.skin.variables.less, setting
|
||||||
|
skin-specific values for certain LESS variables.
|
||||||
* …
|
* …
|
||||||
|
|
||||||
== Compatibility ==
|
== Compatibility ==
|
||||||
|
|
|
||||||
|
|
@ -420,6 +420,10 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "ResourceLoader sources to register"
|
"description": "ResourceLoader sources to register"
|
||||||
},
|
},
|
||||||
|
"SkinLessImportPaths": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Path to the skin-specific LESS import directory, keyed by skin name. Can be used to define skin-specific LESS variables."
|
||||||
|
},
|
||||||
"QUnitTestModule": {
|
"QUnitTestModule": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true.",
|
"description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true.",
|
||||||
|
|
|
||||||
|
|
@ -440,6 +440,10 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "ResourceLoader sources to register"
|
"description": "ResourceLoader sources to register"
|
||||||
},
|
},
|
||||||
|
"SkinLessImportPaths": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Path to the skin-specific LESS import directory, keyed by skin name. Can be used to define skin-specific LESS variables."
|
||||||
|
},
|
||||||
"QUnitTestModule": {
|
"QUnitTestModule": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true.",
|
"description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true.",
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,7 @@ class ExtensionProcessor implements Processor {
|
||||||
$this->extractHooks( $info, $path );
|
$this->extractHooks( $info, $path );
|
||||||
$this->extractExtensionMessagesFiles( $dir, $info );
|
$this->extractExtensionMessagesFiles( $dir, $info );
|
||||||
$this->extractMessagesDirs( $dir, $info );
|
$this->extractMessagesDirs( $dir, $info );
|
||||||
|
$this->extractSkinImportPaths( $dir, $info );
|
||||||
$this->extractNamespaces( $info );
|
$this->extractNamespaces( $info );
|
||||||
$this->extractResourceLoaderModules( $dir, $info );
|
$this->extractResourceLoaderModules( $dir, $info );
|
||||||
if ( isset( $info['ServiceWiringFiles'] ) ) {
|
if ( isset( $info['ServiceWiringFiles'] ) ) {
|
||||||
|
|
@ -618,6 +619,18 @@ class ExtensionProcessor implements Processor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $dir
|
||||||
|
* @param array $info
|
||||||
|
*/
|
||||||
|
protected function extractSkinImportPaths( $dir, array $info ) {
|
||||||
|
if ( isset( $info['SkinLessImportPaths'] ) ) {
|
||||||
|
foreach ( $info['SkinLessImportPaths'] as $skin => $subpath ) {
|
||||||
|
$this->attributes['SkinLessImportPaths'][$skin] = "$dir/$subpath";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $path
|
* @param string $path
|
||||||
* @param array $info
|
* @param array $info
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ class ExtensionRegistry {
|
||||||
private const LAZY_LOADED_ATTRIBUTES = [
|
private const LAZY_LOADED_ATTRIBUTES = [
|
||||||
'TrackingCategories',
|
'TrackingCategories',
|
||||||
'QUnitTestModules',
|
'QUnitTestModules',
|
||||||
|
'SkinLessImportPaths',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1906,10 +1906,11 @@ MESSAGE;
|
||||||
* @param array $vars Associative array of variables that should be used
|
* @param array $vars Associative array of variables that should be used
|
||||||
* for compilation. Since 1.32, this method no longer automatically includes
|
* for compilation. Since 1.32, this method no longer automatically includes
|
||||||
* global LESS vars from ResourceLoader::getLessVars (T191937).
|
* global LESS vars from ResourceLoader::getLessVars (T191937).
|
||||||
|
* @param array $importDirs Additional directories to look in for @import (since 1.36)
|
||||||
* @throws MWException
|
* @throws MWException
|
||||||
* @return Less_Parser
|
* @return Less_Parser
|
||||||
*/
|
*/
|
||||||
public function getLessCompiler( $vars = [] ) {
|
public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
|
||||||
global $IP;
|
global $IP;
|
||||||
// When called from the installer, it is possible that a required PHP extension
|
// When called from the installer, it is possible that a required PHP extension
|
||||||
// is missing (at least for now; see T49564). If this is the case, throw an
|
// is missing (at least for now; see T49564). If this is the case, throw an
|
||||||
|
|
@ -1918,11 +1919,12 @@ MESSAGE;
|
||||||
throw new MWException( 'MediaWiki requires the less.php parser' );
|
throw new MWException( 'MediaWiki requires the less.php parser' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$importDirs[] = "$IP/resources/src/mediawiki.less";
|
||||||
|
|
||||||
$parser = new Less_Parser;
|
$parser = new Less_Parser;
|
||||||
$parser->ModifyVars( $vars );
|
$parser->ModifyVars( $vars );
|
||||||
$parser->SetImportDirs( [
|
// SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
|
||||||
"$IP/resources/src/mediawiki.less/" => '',
|
$parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
|
||||||
] );
|
|
||||||
$parser->SetOption( 'relativeUrls', false );
|
$parser->SetOption( 'relativeUrls', false );
|
||||||
|
|
||||||
return $parser;
|
return $parser;
|
||||||
|
|
|
||||||
|
|
@ -1076,15 +1076,26 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
||||||
$cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
|
$cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$skinName = $context->getSkin();
|
||||||
|
$skinImportPaths = ExtensionRegistry::getInstance()->getAttribute( 'SkinLessImportPaths' );
|
||||||
|
$importDirs = [];
|
||||||
|
if ( isset( $skinImportPaths[ $skinName ] ) ) {
|
||||||
|
$importDirs[] = $skinImportPaths[ $skinName ];
|
||||||
|
}
|
||||||
|
|
||||||
$vars = $this->getLessVars( $context );
|
$vars = $this->getLessVars( $context );
|
||||||
// Construct a cache key from a hash of the LESS source, and a hash digest
|
// Construct a cache key from a hash of the LESS source, and a hash digest
|
||||||
// of the LESS variables used for compilation.
|
// of the LESS variables used for compilation.
|
||||||
ksort( $vars );
|
ksort( $vars );
|
||||||
|
$compilerParams = [
|
||||||
|
'vars' => $vars,
|
||||||
|
'importDirs' => $importDirs,
|
||||||
|
];
|
||||||
$key = $cache->makeGlobalKey(
|
$key = $cache->makeGlobalKey(
|
||||||
'resourceloader-less',
|
'resourceloader-less',
|
||||||
'v1',
|
'v1',
|
||||||
hash( 'md4', $style ),
|
hash( 'md4', $style ),
|
||||||
hash( 'md4', serialize( $vars ) )
|
hash( 'md4', serialize( $compilerParams ) )
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we got a cached value, we have to validate it by getting a checksum of all the
|
// If we got a cached value, we have to validate it by getting a checksum of all the
|
||||||
|
|
@ -1094,7 +1105,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
|
||||||
!$data ||
|
!$data ||
|
||||||
$data['hash'] !== FileContentsHasher::getFileContentsHash( $data['files'] )
|
$data['hash'] !== FileContentsHasher::getFileContentsHash( $data['files'] )
|
||||||
) {
|
) {
|
||||||
$compiler = $context->getResourceLoader()->getLessCompiler( $vars );
|
$compiler = $context->getResourceLoader()->getLessCompiler( $vars, $importDirs );
|
||||||
|
|
||||||
$css = $compiler->parse( $style, $stylePath )->getCss();
|
$css = $compiler->parse( $style, $stylePath )->getCss();
|
||||||
// T253055: store the implicit dependency paths in a form relative to any install
|
// T253055: store the implicit dependency paths in a form relative to any install
|
||||||
// path so that multiple version of the application can share the cache for identical
|
// path so that multiple version of the application can share the cache for identical
|
||||||
|
|
|
||||||
46
resources/src/mediawiki.less/mediawiki.skin.defaults.less
Normal file
46
resources/src/mediawiki.less/mediawiki.skin.defaults.less
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// This file is the central place where we declare
|
||||||
|
// which variables are part of the "mediawiki.skin.variables.less"
|
||||||
|
// API that core and extensions can use in their style modules.
|
||||||
|
//
|
||||||
|
// The initial values are intended merely as fallback to allow
|
||||||
|
// forward and backward compatibility to allow new variables
|
||||||
|
// to be defined without breaking existing implementations by
|
||||||
|
// skins.
|
||||||
|
//
|
||||||
|
// #### Instructions for skins
|
||||||
|
//
|
||||||
|
// In skin.json, add:
|
||||||
|
// "SkinLessImportPaths": {
|
||||||
|
// "skinname": "resources/mediawiki.less"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Create a file called resources/mediawiki.less/mediawiki.skin.variables.less, which starts with:
|
||||||
|
// @import 'mediawiki.skin.default.less';
|
||||||
|
// followed by any overrides for these variables, e.g.:
|
||||||
|
// @width-breakpoint-desktop: 1234px;
|
||||||
|
|
||||||
|
// Minimum available screen width at which a device can be considered a mobile device.
|
||||||
|
//
|
||||||
|
// Many older feature phones have screens smaller than this value.
|
||||||
|
//
|
||||||
|
// @since 1.36
|
||||||
|
@width-breakpoint-mobile: 320px;
|
||||||
|
|
||||||
|
// Minimum available screen width at which a device can be considered a tablet.
|
||||||
|
//
|
||||||
|
// The number is currently based on the device width of a Samsung Galaxy S5 mini and
|
||||||
|
// is low enough to cover iPad (768px).
|
||||||
|
//
|
||||||
|
// @since 1.36
|
||||||
|
@width-breakpoint-tablet: 720px;
|
||||||
|
|
||||||
|
// Minimum available screen width at which a device can be considered a desktop.
|
||||||
|
//
|
||||||
|
// @since 1.36
|
||||||
|
@width-breakpoint-desktop: 1000px;
|
||||||
|
|
||||||
|
// @since 1.36
|
||||||
|
@font-family-sans: sans-serif;
|
||||||
|
|
||||||
|
// @since 1.36
|
||||||
|
@border-radius-base: 0;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// This file is used as a fallback match for "mediawiki.skin.variables.less"
|
||||||
|
// in the LESS import directories for skins that do not define
|
||||||
|
// this file themselves.
|
||||||
|
|
||||||
|
@import 'mediawiki.skin.defaults.less';
|
||||||
|
|
@ -1,24 +1,4 @@
|
||||||
/**
|
@import 'mediawiki.skin.variables.less';
|
||||||
* Minimum available screen width at which a device can be considered a mobile device
|
|
||||||
* Many older feature phones have screens smaller than this value.
|
|
||||||
* Number is prone to change with new information.
|
|
||||||
* @since 1.31
|
|
||||||
*/
|
|
||||||
@width-breakpoint-mobile: 320px;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimum available screen width at which a device can be considered a tablet
|
|
||||||
* The number is currently based on the device width of a Samsung Galaxy S5 mini and is low
|
|
||||||
* enough to cover iPad (768px). Number is prone to change with new information.
|
|
||||||
* @since 1.31
|
|
||||||
*/
|
|
||||||
@width-breakpoint-tablet: 720px;
|
|
||||||
/**
|
|
||||||
* Minimum available screen width at which a device can be considered a desktop
|
|
||||||
* Number is prone to change with new information.
|
|
||||||
* @since 1.31
|
|
||||||
*/
|
|
||||||
@width-breakpoint-desktop: 1000px;
|
|
||||||
|
|
||||||
// Colors for use in mediawiki.ui
|
// Colors for use in mediawiki.ui
|
||||||
|
|
||||||
|
|
@ -97,9 +77,6 @@
|
||||||
// Equal to OOUI.
|
// Equal to OOUI.
|
||||||
@border-width-radio--checked: 6px;
|
@border-width-radio--checked: 6px;
|
||||||
|
|
||||||
// Border radius to be used to buttons and inputs
|
|
||||||
@border-radius-base: 2px;
|
|
||||||
|
|
||||||
// Box shadows
|
// Box shadows
|
||||||
@box-shadow-base: inset 0 0 0 1px transparent;
|
@box-shadow-base: inset 0 0 0 1px transparent;
|
||||||
@box-shadow-base--focus: inset 0 0 0 1px @color-primary--focus;
|
@box-shadow-base--focus: inset 0 0 0 1px @color-primary--focus;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import 'mediawiki.skin.variables.less';
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* OOUI definitions used by the existing CSS (will make it easier to put this
|
* OOUI definitions used by the existing CSS (will make it easier to put this
|
||||||
* widget in OOUI once OOUI is capable of handling it)
|
* widget in OOUI once OOUI is capable of handling it)
|
||||||
|
|
@ -70,8 +72,6 @@
|
||||||
@border-color-input--hover: @border-color-base--active;
|
@border-color-input--hover: @border-color-base--active;
|
||||||
@border-color-erroneous: @color-erroneous;
|
@border-color-erroneous: @color-erroneous;
|
||||||
|
|
||||||
@border-radius-base: 2px;
|
|
||||||
|
|
||||||
@box-shadow-base--focus: inset 0 0 0 1px @color-progressive;
|
@box-shadow-base--focus: inset 0 0 0 1px @color-progressive;
|
||||||
@box-shadow-dialog: 0 2px 2px 0 rgba( 0, 0, 0, 0.25 );
|
@box-shadow-dialog: 0 2px 2px 0 rgba( 0, 0, 0, 0.25 );
|
||||||
@box-shadow-widget: inset 0 0 0 1px transparent;
|
@box-shadow-widget: inset 0 0 0 1px transparent;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
* @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
|
* @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
|
||||||
* @license The MIT License (MIT); see LICENSE.txt
|
* @license The MIT License (MIT); see LICENSE.txt
|
||||||
*/
|
*/
|
||||||
|
@import 'mediawiki.skin.variables.less';
|
||||||
|
|
||||||
// Variables taken from OOUI's WikimediaUI theme
|
// Variables taken from OOUI's WikimediaUI theme
|
||||||
@ooui-font-size-browser: 16; // assumed browser default of `16px`
|
@ooui-font-size-browser: 16; // assumed browser default of `16px`
|
||||||
|
|
@ -17,7 +18,7 @@
|
||||||
@border-base: 1px solid #a2a9b1;
|
@border-base: 1px solid #a2a9b1;
|
||||||
@border-color-base--focus: #36c;
|
@border-color-base--focus: #36c;
|
||||||
@border-color-input--hover: #72777d;
|
@border-color-input--hover: #72777d;
|
||||||
@border-radius-base: 2px;
|
// @border-radius-base is set in mediawiki.skin.variables.less
|
||||||
|
|
||||||
@padding-input-text: @padding-vertical-base @padding-horizontal-input-text;
|
@padding-input-text: @padding-vertical-base @padding-horizontal-input-text;
|
||||||
@padding-horizontal-input-text: 8px;
|
@padding-horizontal-input-text: 8px;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
@import 'mediawiki.skin.defaults.less';
|
||||||
|
|
||||||
|
@border-radius-base: 42px;
|
||||||
3
tests/phpunit/data/less/use-variables-default.css
Normal file
3
tests/phpunit/data/less/use-variables-default.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.unit-tests {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
3
tests/phpunit/data/less/use-variables-test.css
Normal file
3
tests/phpunit/data/less/use-variables-test.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.unit-tests {
|
||||||
|
border-radius: 42px;
|
||||||
|
}
|
||||||
5
tests/phpunit/data/less/use-variables.less
Normal file
5
tests/phpunit/data/less/use-variables.less
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
@import 'mediawiki.skin.variables.less';
|
||||||
|
|
||||||
|
.unit-tests {
|
||||||
|
border-radius: @border-radius-base;
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
$this->setMwGlobals( [
|
$this->setMwGlobals( [
|
||||||
|
'wgSkinLessVariablesImportPaths' => [],
|
||||||
'wgShowExceptionDetails' => true,
|
'wgShowExceptionDetails' => true,
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
|
|
@ -284,6 +285,52 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
|
||||||
$this->assertStringEqualsFile( "$basePath/module/styles.css", $css );
|
$this->assertStringEqualsFile( "$basePath/module/styles.css", $css );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function provideMediaWikiVariablesCases() {
|
||||||
|
$basePath = __DIR__ . '/../../data/less';
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'config' => [],
|
||||||
|
'importPaths' => [],
|
||||||
|
'skin' => 'fallback',
|
||||||
|
'expected' => "$basePath/use-variables-default.css",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'config' => [
|
||||||
|
'wgValidSkinNames' => [
|
||||||
|
// Required to make ResourceLoaderContext::getSkin work
|
||||||
|
'example' => 'Example',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'importPaths' => [
|
||||||
|
'example' => "$basePath/testvariables/",
|
||||||
|
],
|
||||||
|
'skin' => 'example',
|
||||||
|
'expected' => "$basePath/use-variables-test.css",
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideMediaWikiVariablesCases
|
||||||
|
* @covers ResourceLoader::getLessCompiler
|
||||||
|
* @covers ResourceLoaderFileModule::compileLessFile
|
||||||
|
*/
|
||||||
|
public function testMediawikiVariablesDefault( array $config, array $importPaths, $skin, $expectedFile ) {
|
||||||
|
$this->setMwGlobals( $config );
|
||||||
|
$reset = ExtensionRegistry::getInstance()->setAttributeForTest( 'SkinLessImportPaths', $importPaths );
|
||||||
|
// Reset Skin::getSkinNames for ResourceLoaderContext
|
||||||
|
MediaWiki\MediaWikiServices::getInstance()->resetServiceForTesting( 'SkinFactory' );
|
||||||
|
|
||||||
|
$context = $this->getResourceLoaderContext( [ 'skin' => $skin ] );
|
||||||
|
$module = new ResourceLoaderFileModule( [
|
||||||
|
'localBasePath' => __DIR__ . '/../../data/less',
|
||||||
|
'styles' => [ 'use-variables.less' ],
|
||||||
|
] );
|
||||||
|
$module->setName( 'test.less' );
|
||||||
|
$styles = $module->getStyles( $context );
|
||||||
|
$this->assertStringEqualsFile( $expectedFile, $styles['all'] );
|
||||||
|
}
|
||||||
|
|
||||||
public static function providePackedModules() {
|
public static function providePackedModules() {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue