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:
Timo Tijhof 2018-05-20 20:32:57 +02:00 committed by VolkerE
parent 4d8cb4dc16
commit 0c01d8cc52
17 changed files with 163 additions and 33 deletions

View file

@ -190,6 +190,10 @@ because of Phabricator reports.
existing code, but it only supports version 2 of the test file
specification and may be more strict when parsing invalid input,
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 ==

View file

@ -420,6 +420,10 @@
"type": "object",
"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": {
"type": "object",
"description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true.",

View file

@ -440,6 +440,10 @@
"type": "object",
"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": {
"type": "object",
"description": "A ResourceLoaderFileModule definition registered only when wgEnableJavaScriptTest is true.",

View file

@ -200,6 +200,7 @@ class ExtensionProcessor implements Processor {
$this->extractHooks( $info, $path );
$this->extractExtensionMessagesFiles( $dir, $info );
$this->extractMessagesDirs( $dir, $info );
$this->extractSkinImportPaths( $dir, $info );
$this->extractNamespaces( $info );
$this->extractResourceLoaderModules( $dir, $info );
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 array $info

View file

@ -59,6 +59,7 @@ class ExtensionRegistry {
private const LAZY_LOADED_ATTRIBUTES = [
'TrackingCategories',
'QUnitTestModules',
'SkinLessImportPaths',
];
/**

View file

@ -1906,10 +1906,11 @@ MESSAGE;
* @param array $vars Associative array of variables that should be used
* for compilation. Since 1.32, this method no longer automatically includes
* global LESS vars from ResourceLoader::getLessVars (T191937).
* @param array $importDirs Additional directories to look in for @import (since 1.36)
* @throws MWException
* @return Less_Parser
*/
public function getLessCompiler( $vars = [] ) {
public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
global $IP;
// 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
@ -1918,11 +1919,12 @@ MESSAGE;
throw new MWException( 'MediaWiki requires the less.php parser' );
}
$importDirs[] = "$IP/resources/src/mediawiki.less";
$parser = new Less_Parser;
$parser->ModifyVars( $vars );
$parser->SetImportDirs( [
"$IP/resources/src/mediawiki.less/" => '',
] );
// SetImportDirs expects an array like [ 'path1' => '', 'path2' => '' ]
$parser->SetImportDirs( array_fill_keys( $importDirs, '' ) );
$parser->SetOption( 'relativeUrls', false );
return $parser;

View file

@ -1076,15 +1076,26 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
$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 );
// Construct a cache key from a hash of the LESS source, and a hash digest
// of the LESS variables used for compilation.
ksort( $vars );
$compilerParams = [
'vars' => $vars,
'importDirs' => $importDirs,
];
$key = $cache->makeGlobalKey(
'resourceloader-less',
'v1',
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
@ -1094,7 +1105,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
!$data ||
$data['hash'] !== FileContentsHasher::getFileContentsHash( $data['files'] )
) {
$compiler = $context->getResourceLoader()->getLessCompiler( $vars );
$compiler = $context->getResourceLoader()->getLessCompiler( $vars, $importDirs );
$css = $compiler->parse( $style, $stylePath )->getCss();
// 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

View 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;

View file

@ -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';

View file

@ -1,24 +1,4 @@
/**
* 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;
@import 'mediawiki.skin.variables.less';
// Colors for use in mediawiki.ui
@ -97,9 +77,6 @@
// Equal to OOUI.
@border-width-radio--checked: 6px;
// Border radius to be used to buttons and inputs
@border-radius-base: 2px;
// Box shadows
@box-shadow-base: inset 0 0 0 1px transparent;
@box-shadow-base--focus: inset 0 0 0 1px @color-primary--focus;

View file

@ -1,3 +1,5 @@
@import 'mediawiki.skin.variables.less';
/*!
* OOUI definitions used by the existing CSS (will make it easier to put this
* widget in OOUI once OOUI is capable of handling it)
@ -70,8 +72,6 @@
@border-color-input--hover: @border-color-base--active;
@border-color-erroneous: @color-erroneous;
@border-radius-base: 2px;
@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-widget: inset 0 0 0 1px transparent;

View file

@ -4,6 +4,7 @@
* @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
@import 'mediawiki.skin.variables.less';
// Variables taken from OOUI's WikimediaUI theme
@ooui-font-size-browser: 16; // assumed browser default of `16px`
@ -17,7 +18,7 @@
@border-base: 1px solid #a2a9b1;
@border-color-base--focus: #36c;
@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-horizontal-input-text: 8px;

View file

@ -0,0 +1,3 @@
@import 'mediawiki.skin.defaults.less';
@border-radius-base: 42px;

View file

@ -0,0 +1,3 @@
.unit-tests {
border-radius: 0;
}

View file

@ -0,0 +1,3 @@
.unit-tests {
border-radius: 42px;
}

View file

@ -0,0 +1,5 @@
@import 'mediawiki.skin.variables.less';
.unit-tests {
border-radius: @border-radius-base;
}

View file

@ -9,6 +9,7 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
parent::setUp();
$this->setMwGlobals( [
'wgSkinLessVariablesImportPaths' => [],
'wgShowExceptionDetails' => true,
] );
}
@ -284,6 +285,52 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
$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() {
return [
[