Use ResourceLoaderImageModule to serve icons for OOjs UI
[lhc/web/wiklou.git] / includes / resourceloader / ResourceLoaderImageModule.php
index 8fbe497..53e0815 100644 (file)
@@ -28,6 +28,8 @@
  */
 class ResourceLoaderImageModule extends ResourceLoaderModule {
 
+       private $definition = null;
+
        /**
         * Local base path, see __construct()
         * @var string
@@ -38,7 +40,9 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
 
        protected $images = array();
        protected $variants = array();
-       protected $prefix = array();
+       protected $prefix = null;
+       protected $selectorWithoutVariant = '.{prefix}-{name}';
+       protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
        protected $targets = array( 'desktop', 'mobile' );
 
        /**
@@ -55,74 +59,106 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         *     array(
         *         // Base path to prepend to all local paths in $options. Defaults to $IP
         *         'localBasePath' => [base path],
+        *         // Path to JSON file that contains any of the settings below
+        *         'data' => [file path string]
         *         // CSS class prefix to use in all style rules
         *         'prefix' => [CSS class prefix],
+        *         // Alternatively: Format of CSS selector to use in all style rules
+        *         'selector' => [CSS selector template, variables: {prefix} {name} {variant}],
+        *         // Alternatively: When using variants
+        *         'selectorWithoutVariant' => [CSS selector template, variables: {prefix} {name}],
+        *         'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}],
         *         // List of variants that may be used for the image files
         *         'variants' => array(
-        *             // ([image type] is a string, used in generated CSS class
-        *             //  names and to match variants to images)
-        *             [image type] => array(
         *                 [variant name] => array(
         *                     'color' => [color string, e.g. '#ffff00'],
         *                     'global' => [boolean, if true, this variant is available
         *                                  for all images of this type],
         *                 ),
-        *             )
+        *             ...
         *         ),
         *         // List of image files and their options
         *         'images' => array(
-        *             [image type] => array(
         *                 [file path string],
         *                 [file path string] => array(
         *                     'name' => [image name string, defaults to file name],
         *                     'variants' => [array of variant name strings, variants
         *                                    available for this image],
         *                 ),
-        *             )
+        *             ...
         *         ),
         *     )
         * @endcode
-        * @throws MWException
+        * @throws InvalidArgumentException
         */
        public function __construct( $options = array(), $localBasePath = null ) {
                $this->localBasePath = self::extractLocalBasePath( $options, $localBasePath );
 
-               if ( !isset( $options['prefix'] ) || !$options['prefix'] ) {
-                       throw new MWException(
-                               "Required 'prefix' option not given or empty."
-                       );
+               $this->definition = $options;
+       }
+
+       /**
+        * Parse definition and external JSON data, if referenced.
+        */
+       private function ensureStuffLoaded() {
+               if ( $this->definition === null ) {
+                       return;
+               }
+
+               $options = $this->definition;
+               $this->definition = null;
+
+               if ( isset( $options['data'] ) ) {
+                       $dataPath = $this->localBasePath . '/' . $options['data'];
+                       $data = json_decode( file_get_contents( $dataPath ), true );
+                       $options = array_merge( $data, $options );
+               }
+
+               // Accepted combinations:
+               // * prefix
+               // * selector
+               // * selectorWithoutVariant + selectorWithVariant
+               // * prefix + selector
+               // * prefix + selectorWithoutVariant + selectorWithVariant
+
+               $prefix = isset( $options['prefix'] ) && $options['prefix'];
+               $selector = isset( $options['selector'] ) && $options['selector'];
+               $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] ) && $options['selectorWithoutVariant'];
+               $selectorWithVariant = isset( $options['selectorWithVariant'] ) && $options['selectorWithVariant'];
+
+               if ( $selectorWithoutVariant && !$selectorWithVariant ) {
+                       throw new InvalidArgumentException( "Given 'selectorWithoutVariant' but no 'selectorWithVariant'." );
+               }
+               if ( $selectorWithVariant && !$selectorWithoutVariant ) {
+                       throw new InvalidArgumentException( "Given 'selectorWithVariant' but no 'selectorWithoutVariant'." );
+               }
+               if ( $selector && $selectorWithVariant ) {
+                       throw new InvalidArgumentException( "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given." );
+               }
+               if ( !$prefix && !$selector && !$selectorWithVariant ) {
+                       throw new InvalidArgumentException( "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given." );
                }
 
                foreach ( $options as $member => $option ) {
                        switch ( $member ) {
                                case 'images':
-                                       if ( !is_array( $option ) ) {
-                                               throw new MWException(
-                                                       "Invalid collated file path list error. '$option' given, array expected."
-                                               );
-                                       }
-                                       foreach ( $option as $key => $value ) {
-                                               if ( !is_string( $key ) ) {
-                                                       throw new MWException(
-                                                               "Invalid collated file path list key error. '$key' given, string expected."
-                                                       );
-                                               }
-                                               $this->{$member}[$key] = (array)$value;
-                                       }
-                                       break;
-
                                case 'variants':
                                        if ( !is_array( $option ) ) {
-                                               throw new MWException(
-                                                       "Invalid variant list error. '$option' given, array expected."
+                                               throw new InvalidArgumentException(
+                                                       "Invalid list error. '$option' given, array expected."
                                                );
                                        }
                                        $this->{$member} = $option;
                                        break;
 
                                case 'prefix':
+                               case 'selectorWithoutVariant':
+                               case 'selectorWithVariant':
                                        $this->{$member} = (string)$option;
                                        break;
+
+                               case 'selector':
+                                       $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option;
                        }
                }
        }
@@ -132,15 +168,29 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         * @return string
         */
        public function getPrefix() {
+               $this->ensureStuffLoaded();
                return $this->prefix;
        }
 
+       /**
+        * Get CSS selector templates used by this module.
+        * @return string
+        */
+       public function getSelectors() {
+               $this->ensureStuffLoaded();
+               return array(
+                       'selectorWithoutVariant' => $this->selectorWithoutVariant,
+                       'selectorWithVariant' => $this->selectorWithVariant,
+               );
+       }
+
        /**
         * Get a ResourceLoaderImage object for given image.
         * @param string $name Image name
         * @return ResourceLoaderImage|null
         */
        public function getImage( $name ) {
+               $this->ensureStuffLoaded();
                $images = $this->getImages();
                return isset( $images[$name] ) ? $images[$name] : null;
        }
@@ -151,34 +201,33 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         */
        public function getImages() {
                if ( !isset( $this->imageObjects ) ) {
+                       $this->ensureStuffLoaded();
                        $this->imageObjects = array();
 
-                       foreach ( $this->images as $type => $list ) {
-                               foreach ( $list as $name => $options ) {
-                                       $imageDesc = is_string( $options ) ? $options : $options['image'];
-
-                                       $allowedVariants = array_merge(
-                                               isset( $options['variants'] ) ? $options['variants'] : array(),
-                                               $this->getGlobalVariants( $type )
-                                       );
-                                       if ( isset( $this->variants[$type] ) ) {
-                                               $variantConfig = array_intersect_key(
-                                                       $this->variants[$type],
-                                                       array_fill_keys( $allowedVariants, true )
-                                               );
-                                       } else {
-                                               $variantConfig = array();
-                                       }
+                       foreach ( $this->images as $name => $options ) {
+                               $fileDescriptor = is_string( $options ) ? $options : $options['file'];
 
-                                       $image = new ResourceLoaderImage(
-                                               $name,
-                                               $this->getName(),
-                                               $imageDesc,
-                                               $this->localBasePath,
-                                               $variantConfig
+                               $allowedVariants = array_merge(
+                                       is_array( $options ) && isset( $options['variants'] ) ? $options['variants'] : array(),
+                                       $this->getGlobalVariants()
+                               );
+                               if ( isset( $this->variants ) ) {
+                                       $variantConfig = array_intersect_key(
+                                               $this->variants,
+                                               array_fill_keys( $allowedVariants, true )
                                        );
-                                       $this->imageObjects[ $image->getName() ] = $image;
+                               } else {
+                                       $variantConfig = array();
                                }
+
+                               $image = new ResourceLoaderImage(
+                                       $name,
+                                       $this->getName(),
+                                       $fileDescriptor,
+                                       $this->localBasePath,
+                                       $variantConfig
+                               );
+                               $this->imageObjects[ $image->getName() ] = $image;
                        }
                }
 
@@ -186,43 +235,25 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
        }
 
        /**
-        * Get list of variants in this module that are 'global' for given type of images, i.e., available
-        * for every image of given type regardless of image options.
-        * @param string $type Image type
+        * Get list of variants in this module that are 'global', i.e., available
+        * for every image regardless of image options.
         * @return string[]
         */
-       public function getGlobalVariants( $type ) {
-               if ( !isset( $this->globalVariants[$type] ) ) {
-                       $this->globalVariants[$type] = array();
+       public function getGlobalVariants() {
+               if ( !isset( $this->globalVariants ) ) {
+                       $this->ensureStuffLoaded();
+                       $this->globalVariants = array();
 
-                       if ( isset( $this->variants[$type] ) ) {
-                               foreach ( $this->variants[$type] as $name => $config ) {
+                       if ( isset( $this->variants ) ) {
+                               foreach ( $this->variants as $name => $config ) {
                                        if ( isset( $config['global'] ) && $config['global'] ) {
-                                               $this->globalVariants[$type][] = $name;
+                                               $this->globalVariants[] = $name;
                                        }
                                }
                        }
                }
 
-               return $this->globalVariants[$type];
-       }
-
-       /**
-        * Get the type of given image.
-        * @param string $imageName Image name
-        * @return string
-        */
-       public function getImageType( $imageName ) {
-               foreach ( $this->images as $type => $list ) {
-                       foreach ( $list as $key => $value ) {
-                               $file = is_int( $key ) ? $value : $key;
-                               $options = is_array( $value ) ? $value : array();
-                               $name = isset( $options['name'] ) ? $options['name'] : pathinfo( $file, PATHINFO_FILENAME );
-                               if ( $name === $imageName ) {
-                                       return $type;
-                               }
-                       }
-               }
+               return $this->globalVariants;
        }
 
        /**
@@ -230,52 +261,69 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
         * @return array
         */
        public function getStyles( ResourceLoaderContext $context ) {
+               $this->ensureStuffLoaded();
+
                // Build CSS rules
                $rules = array();
                $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
-               $prefix = $this->getPrefix();
+               $selectors = $this->getSelectors();
 
                foreach ( $this->getImages() as $name => $image ) {
-                       $type = $this->getImageType( $name );
-
                        $declarations = $this->getCssDeclarations(
                                $image->getDataUri( $context, null, 'original' ),
                                $image->getUrl( $context, $script, null, 'rasterized' )
                        );
                        $declarations = implode( "\n\t", $declarations );
-                       $rules[] = ".$prefix-$type-$name {\n\t$declarations\n}";
+                       $selector = strtr(
+                               $selectors['selectorWithoutVariant'],
+                               array(
+                                       '{prefix}' => $this->getPrefix(),
+                                       '{name}' => $name,
+                                       '{variant}' => '',
+                               )
+                       );
+                       $rules[] = "$selector {\n\t$declarations\n}";
 
-                       // TODO: Get variant configurations from $context->getSkin()
                        foreach ( $image->getVariants() as $variant ) {
                                $declarations = $this->getCssDeclarations(
                                        $image->getDataUri( $context, $variant, 'original' ),
                                        $image->getUrl( $context, $script, $variant, 'rasterized' )
                                );
                                $declarations = implode( "\n\t", $declarations );
-                               $rules[] = ".$prefix-$type-$name-$variant {\n\t$declarations\n}";
+                               $selector = strtr(
+                                       $selectors['selectorWithVariant'],
+                                       array(
+                                               '{prefix}' => $this->getPrefix(),
+                                               '{name}' => $name,
+                                               '{variant}' => $variant,
+                                       )
+                               );
+                               $rules[] = "$selector {\n\t$declarations\n}";
                        }
                }
 
                $style = implode( "\n", $rules );
-               if ( $this->getFlip( $context ) ) {
-                       $style = CSSJanus::transform( $style, true, false );
-               }
                return array( 'all' => $style );
        }
 
        /**
+        * SVG support using a transparent gradient to guarantee cross-browser
+        * compatibility (browsers able to understand gradient syntax support also SVG).
+        * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
+        *
+        * Keep synchronized with the .background-image-svg LESS mixin in
+        * /resources/src/mediawiki.less/mediawiki.mixins.less.
+        *
         * @param string $primary Primary URI
         * @param string $fallback Fallback URI
         * @return string[] CSS declarations to use given URIs as background-image
         */
        protected function getCssDeclarations( $primary, $fallback ) {
-               // SVG support using a transparent gradient to guarantee cross-browser
-               // compatibility (browsers able to understand gradient syntax support also SVG).
-               // http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
                return array(
                        "background-image: url($fallback);",
                        "background-image: -webkit-linear-gradient(transparent, transparent), url($primary);",
                        "background-image: linear-gradient(transparent, transparent), url($primary);",
+                       "background-image: -o-linear-gradient(transparent, transparent), url($fallback);",
                );
        }
 
@@ -286,6 +334,51 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
                return false;
        }
 
+       /**
+        * Get the definition summary for this module.
+        *
+        * @param ResourceLoaderContext $context
+        * @return array
+        */
+       public function getDefinitionSummary( ResourceLoaderContext $context ) {
+               $this->ensureStuffLoaded();
+               $summary = parent::getDefinitionSummary( $context );
+               foreach ( array(
+                       'localBasePath',
+                       'images',
+                       'variants',
+                       'prefix',
+                       'selectorWithoutVariant',
+                       'selectorWithVariant',
+               ) as $member ) {
+                       $summary[$member] = $this->{$member};
+               };
+               return $summary;
+       }
+
+       /**
+        * Get the last modified timestamp of this module.
+        *
+        * @param ResourceLoaderContext $context Context in which to calculate
+        *     the modified time
+        * @return int UNIX timestamp
+        */
+       public function getModifiedTime( ResourceLoaderContext $context ) {
+               $this->ensureStuffLoaded();
+               $files = array();
+               foreach ( $this->getImages() as $name => $image ) {
+                       $files[] = $image->getPath( $context );
+               }
+
+               $files = array_values( array_unique( $files ) );
+               $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) );
+
+               return max(
+                       $filesMtime,
+                       $this->getDefinitionMtime( $context )
+               );
+       }
+
        /**
         * Extract a local base path from module definition information.
         *