diff --git a/extension.json b/extension.json index 42ef614..6e33579 100644 --- a/extension.json +++ b/extension.json @@ -18,7 +18,10 @@ } }, "config": { - + "TemplateStylesExtenderEnablePrefersColorScheme": { + "description": "Enable the prefers-color-scheme media query. WARNING this can break things if TemplateStyles was updated upstream", + "value": true + } }, "MessagesDirs": { "TemplateStylesExtender": [ diff --git a/includes/Hooks/PropertySanitizerHook.php b/includes/Hooks/PropertySanitizerHook.php index 8e51579..fc5178e 100644 --- a/includes/Hooks/PropertySanitizerHook.php +++ b/includes/Hooks/PropertySanitizerHook.php @@ -25,7 +25,11 @@ use TemplateStylesMatcherFactory; use Wikimedia\CSS\Sanitizer\StylePropertySanitizer; class PropertySanitizerHook { - public static function onSanitize( StylePropertySanitizer $propertySanitizer, TemplateStylesMatcherFactory $matcherFactory ): void { + /** + * @param StylePropertySanitizer $propertySanitizer + * @param TemplateStylesMatcherFactory $matcherFactory + */ + public static function onSanitize( $propertySanitizer, $matcherFactory ): void { // Currently unused } } diff --git a/includes/Hooks/StylesheetSanitizerHook.php b/includes/Hooks/StylesheetSanitizerHook.php index 90c71e6..7f793ee 100644 --- a/includes/Hooks/StylesheetSanitizerHook.php +++ b/includes/Hooks/StylesheetSanitizerHook.php @@ -21,9 +21,11 @@ declare( strict_types=1 ); namespace MediaWiki\Extension\TemplateStylesExtender\Hooks; +use MediaWiki\Extension\TemplateStylesExtender\MatcherFactoryExtender; use MediaWiki\Extension\TemplateStylesExtender\StylePropertySanitizerExtender; use MediaWiki\Extension\TemplateStylesExtender\TemplateStylesExtender; use TemplateStylesMatcherFactory; +use Wikimedia\CSS\Sanitizer\MediaAtRuleSanitizer; use Wikimedia\CSS\Sanitizer\StylePropertySanitizer; use Wikimedia\CSS\Sanitizer\StylesheetSanitizer; @@ -36,7 +38,20 @@ class StylesheetSanitizerHook { * @param StylePropertySanitizer $propertySanitizer * @param TemplateStylesMatcherFactory $matcherFactory */ - public static function onSanitize( StylesheetSanitizer $sanitizer, StylePropertySanitizer $propertySanitizer, TemplateStylesMatcherFactory $matcherFactory ): void { + public static function onSanitize( $sanitizer, $propertySanitizer, $matcherFactory ): void { + if ( TemplateStylesExtender::getConfigValue( + 'TemplateStylesExtenderEnablePrefersColorScheme', + false ) === true ) { + $factory = new MatcherFactoryExtender(); + + $newRules = $sanitizer->getRuleSanitizers(); + $newRules['@media'] = new MediaAtRuleSanitizer( $factory->cssMediaQueryList() ); + $newRules['@media']->setRuleSanitizers( $newRules ); + + $sanitizer->setRuleSanitizers( $newRules ); + + } + $extended = new TemplateStylesExtender(); $extender = new StylePropertySanitizerExtender( $matcherFactory ); diff --git a/includes/MatcherFactoryExtender.php b/includes/MatcherFactoryExtender.php new file mode 100644 index 0000000..660d911 --- /dev/null +++ b/includes/MatcherFactoryExtender.php @@ -0,0 +1,164 @@ +cache[$key] ) ) { + if ( $strict ) { + $generalEnclosed = new NothingMatcher(); + + $mediaType = new KeywordMatcher( [ + 'all', 'print', 'screen', 'speech', + // deprecated + 'tty', 'tv', 'projection', 'handheld', 'braille', 'embossed', 'aural' + ] ); + + $rangeFeatures = [ + 'width', 'height', 'aspect-ratio', 'resolution', 'color', 'color-index', 'monochrome', + // deprecated + 'device-width', 'device-height', 'device-aspect-ratio' + ]; + $discreteFeatures = [ + 'orientation', 'scan', 'grid', 'update', 'overflow-block', 'overflow-inline', 'color-gamut', + 'pointer', 'hover', 'any-pointer', 'any-hover', 'scripting', 'prefers-color-scheme' + ]; + $mfName = new KeywordMatcher( array_merge( + $rangeFeatures, + array_map( function ( $f ) { + return "min-$f"; + }, $rangeFeatures ), + array_map( function ( $f ) { + return "max-$f"; + }, $rangeFeatures ), + $discreteFeatures + ) ); + } else { + $anythingPlus = new AnythingMatcher( [ 'quantifier' => '+' ] ); + $generalEnclosed = new Alternative( [ + new FunctionMatcher( null, $anythingPlus ), + new BlockMatcher( Token::T_LEFT_PAREN, + new Juxtaposition( [ $this->ident(), $anythingPlus ] ) + ), + ] ); + $mediaType = $this->ident(); + $mfName = $this->ident(); + } + + $posInt = $this->calc( + new TokenMatcher( Token::T_NUMBER, function ( Token $t ) { + return $t->typeFlag() === 'integer' && preg_match( '/^\+?\d+$/', $t->representation() ); + } ), + 'integer' + ); + $eq = new DelimMatcher( '=' ); + $oeq = Quantifier::optional( new Juxtaposition( [ new NoWhitespace, $eq ] ) ); + $ltgteq = Quantifier::optional( new Alternative( [ + $eq, + new Juxtaposition( [ new DelimMatcher( [ '<', '>' ] ), $oeq ] ), + ] ) ); + $lteq = new Juxtaposition( [ new DelimMatcher( '<' ), $oeq ] ); + $gteq = new Juxtaposition( [ new DelimMatcher( '>' ), $oeq ] ); + $mfValue = new Alternative( [ + $this->number(), + $this->dimension(), + $this->ident(), + new KeywordMatcher( [ 'light', 'dark' ] ), + new Juxtaposition( [ $posInt, new DelimMatcher( '/' ), $posInt ] ), + ] ); + + $mediaInParens = new NothingMatcher(); // temporary + $mediaNot = new Juxtaposition( [ new KeywordMatcher( 'not' ), &$mediaInParens ] ); + $mediaAnd = new Juxtaposition( [ new KeywordMatcher( 'and' ), &$mediaInParens ] ); + $mediaOr = new Juxtaposition( [ new KeywordMatcher( 'or' ), &$mediaInParens ] ); + $mediaCondition = new Alternative( [ + $mediaNot, + new Juxtaposition( [ + &$mediaInParens, + new Alternative( [ + Quantifier::star( $mediaAnd ), + Quantifier::star( $mediaOr ), + ] ) + ] ), + ] ); + $mediaConditionWithoutOr = new Alternative( [ + $mediaNot, + new Juxtaposition( [ &$mediaInParens, Quantifier::star( $mediaAnd ) ] ), + ] ); + $mediaFeature = new BlockMatcher( Token::T_LEFT_PAREN, new Alternative( [ + new Juxtaposition( [ $mfName, new TokenMatcher( Token::T_COLON ), $mfValue ] ), // + $mfName, // + new Juxtaposition( [ $mfName, $ltgteq, $mfValue ] ), // , 1st alternative + new Juxtaposition( [ $mfValue, $ltgteq, $mfName ] ), // , 2nd alternative + new Juxtaposition( [ $mfValue, $lteq, $mfName, $lteq, $mfValue ] ), // , 3rd alt + new Juxtaposition( [ $mfValue, $gteq, $mfName, $gteq, $mfValue ] ), // , 4th alt + ] ) ); + $mediaInParens = new Alternative( [ + new BlockMatcher( Token::T_LEFT_PAREN, $mediaCondition ), + $mediaFeature, + $generalEnclosed, + ] ); + + $this->cache[$key] = new Alternative( [ + $mediaCondition, + new Juxtaposition( [ + Quantifier::optional( new KeywordMatcher( [ 'not', 'only' ] ) ), + $mediaType, + Quantifier::optional( new Juxtaposition( [ + new KeywordMatcher( 'and' ), + $mediaConditionWithoutOr, + ] ) ) + ] ) + ] ); + } + + self::$extendedCssMediaQuery = true; + + return $this->cache[$key]; + } + +} diff --git a/includes/TemplateStylesExtender.php b/includes/TemplateStylesExtender.php index 69489c2..00dc72f 100644 --- a/includes/TemplateStylesExtender.php +++ b/includes/TemplateStylesExtender.php @@ -21,9 +21,12 @@ declare( strict_types=1 ); namespace MediaWiki\Extension\TemplateStylesExtender; +use ConfigException; use InvalidArgumentException; use MediaWiki\Extension\TemplateStylesExtender\Matcher\VarNameMatcher; +use MediaWiki\MediaWikiServices; use Wikimedia\CSS\Grammar\Alternative; +use Wikimedia\CSS\Grammar\AnythingMatcher; use Wikimedia\CSS\Grammar\FunctionMatcher; use Wikimedia\CSS\Grammar\Juxtaposition; use Wikimedia\CSS\Grammar\KeywordMatcher; @@ -41,17 +44,30 @@ class TemplateStylesExtender { */ public function addVarSelector( StylePropertySanitizer $propertySanitizer ): void { $propertySanitizer->setCssWideKeywordsMatcher( - new FunctionMatcher( - 'var', - new Juxtaposition( [ - new WhitespaceMatcher( [ 'significant' => false ] ), - new VarNameMatcher(), - new WhitespaceMatcher( [ 'significant' => false ] ), - Quantifier::optional( - new KeywordMatcher(['!important']) - ) - ] ) - ) + new Juxtaposition( [ + Quantifier::optional( + new AnythingMatcher() + ), + new WhitespaceMatcher( [ 'significant' => false ] ), + Quantifier::plus( + new FunctionMatcher( + 'var', + new Juxtaposition( [ + new WhitespaceMatcher( [ 'significant' => false ] ), + new VarNameMatcher(), + new WhitespaceMatcher( [ 'significant' => false ] ), + ] ) + ), + ), + new WhitespaceMatcher( [ 'significant' => false ] ), + Quantifier::optional( + new AnythingMatcher() + ), + Quantifier::optional( + new KeywordMatcher( [ '!important' ] ) + ) + ] ), + ); } @@ -121,7 +137,7 @@ class TemplateStylesExtender { * @param StylePropertySanitizer $propertySanitizer * @param MatcherFactory $factory */ - public function addScrollMarginProperties( StylePropertySanitizer $propertySanitizer, MatcherFactory $factory ): void { + public function addScrollMarginProperties( $propertySanitizer, $factory ): void { $suffixes = [ 'margin-block-end', 'margin-block-start', @@ -159,4 +175,29 @@ class TemplateStylesExtender { } } } + + /** + * Loads a config value for a given key from the main config + * Returns null on if an ConfigException was thrown + * + * @param string $key The config key + * @param null $default + * @return mixed|null + */ + public static function getConfigValue( string $key, $default = null ) { + try { + $value = MediaWikiServices::getInstance()->getMainConfig()->get( $key ); + } catch ( ConfigException $e ) { + wfLogWarning( + sprintf( + 'Could not get config for "$wg%s". %s', $key, + $e->getMessage() + ) + ); + + return $default; + } + + return $value; + } }