protected $raw = false;
protected $targets = array( 'desktop' );
+ /**
+ * Boolean: Whether getStyleURLsForDebug should return raw file paths,
+ * or return load.php urls
+ */
+ protected $hasGeneratedStyles = false;
+
/**
* Array: Cache for mtime
* @par Usage:
/**
* Gets all scripts for a given context concatenated together.
*
- * @param $context ResourceLoaderContext: Context in which to generate script
- * @return String: JavaScript code for $context
+ * @param ResourceLoaderContext $context Context in which to generate script
+ * @return string: JavaScript code for $context
*/
public function getScript( ResourceLoaderContext $context ) {
$files = $this->getScriptFiles( $context );
}
/**
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return array
*/
public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
/**
* Gets loader script.
*
- * @return String: JavaScript code to be added to startup module
+ * @return string: JavaScript code to be added to startup module
*/
public function getLoaderScript() {
if ( count( $this->loaderScripts ) == 0 ) {
/**
* Gets all styles for a given context concatenated together.
*
- * @param $context ResourceLoaderContext: Context in which to generate styles
- * @return String: CSS code for $context
+ * @param ResourceLoaderContext $context Context in which to generate styles
+ * @return string: CSS code for $context
*/
public function getStyles( ResourceLoaderContext $context ) {
$styles = $this->readStyleFiles(
}
/**
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return array
*/
public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
+ if ( $this->hasGeneratedStyles ) {
+ // Do the default behaviour of returning a url back to load.php
+ // but with only=styles.
+ return parent::getStyleURLsForDebug( $context );
+ }
+ // Our module consists entirely of real css files,
+ // in debug mode we can load those directly.
$urls = array();
foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
$urls[$mediaType] = array();
/**
* Gets list of message keys used by this module.
*
- * @return Array: List of message keys
+ * @return array: List of message keys
*/
public function getMessages() {
return $this->messages;
/**
* Gets the name of the group this module should be loaded in.
*
- * @return String: Group name
+ * @return string: Group name
*/
public function getGroup() {
return $this->group;
/**
* Gets list of names of modules this module depends on.
*
- * @return Array: List of module names
+ * @return array: List of module names
*/
public function getDependencies() {
return $this->dependencies;
* calculations on files relevant to the given language, skin and debug
* mode.
*
- * @param $context ResourceLoaderContext: Context in which to calculate
+ * @param ResourceLoaderContext $context Context in which to calculate
* the modified time
- * @return Integer: UNIX timestamp
+ * @return int: UNIX timestamp
* @see ResourceLoaderModule::getFileDependencies
*/
public function getModifiedTime( ResourceLoaderContext $context ) {
/* Protected Methods */
/**
- * @param $path string
+ * @param string $path
* @return string
*/
protected function getLocalPath( $path ) {
}
/**
- * @param $path string
+ * @param string $path
* @return string
*/
protected function getRemotePath( $path ) {
return "{$this->remoteBasePath}/$path";
}
+ /**
+ * Infer the stylesheet language from a stylesheet file path.
+ *
+ * @param string $path
+ * @return string: the stylesheet language name
+ */
+ protected function getStyleSheetLang( $path ) {
+ return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
+ }
+
/**
* Collates file paths by option (where provided).
*
* @param array $list List of file paths in any combination of index/path
* or path/options pairs
* @param string $option option name
- * @param $default Mixed: default value if the option isn't set
- * @return Array: List of file paths, collated by $option
+ * @param mixed $default default value if the option isn't set
+ * @return array: List of file paths, collated by $option
*/
protected static function collateFilePathListByOption( array $list, $option, $default ) {
$collatedFiles = array();
* @param array $list List of lists to select from
* @param string $key Key to look for in $map
* @param string $fallback Key to look for in $list if $key doesn't exist
- * @return Array: List of elements from $map which matched $key or $fallback,
+ * @return array: List of elements from $map which matched $key or $fallback,
* or an empty list in case of no match
*/
protected static function tryForKey( array $list, $key, $fallback = null ) {
/**
* Gets a list of file paths for all scripts in this module, in order of propper execution.
*
- * @param $context ResourceLoaderContext: Context
- * @return Array: List of file paths
+ * @param ResourceLoaderContext $context
+ * @return array: List of file paths
*/
protected function getScriptFiles( ResourceLoaderContext $context ) {
$files = array_merge(
/**
* Gets a list of file paths for all styles in this module, in order of propper inclusion.
*
- * @param $context ResourceLoaderContext: Context
- * @return Array: List of file paths
+ * @param ResourceLoaderContext $context
+ * @return array: List of file paths
*/
protected function getStyleFiles( ResourceLoaderContext $context ) {
return array_merge_recursive(
*
* @param array $scripts List of file paths to scripts to read, remap and concetenate
* @throws MWException
- * @return String: Concatenated and remapped JavaScript data from $scripts
+ * @return string: Concatenated and remapped JavaScript data from $scripts
*/
protected function readScriptFiles( array $scripts ) {
global $wgResourceLoaderValidateStaticJS;
* @param array $styles List of media type/list of file paths pairs, to read, remap and
* concetenate
*
- * @param $flip bool
+ * @param bool $flip
*
- * @return Array: List of concatenated and remapped CSS data from $styles,
+ * @return array: List of concatenated and remapped CSS data from $styles,
* keyed by media type
*/
protected function readStyleFiles( array $styles, $flip ) {
* This method can be used as a callback for array_map()
*
* @param string $path File path of style file to read
- * @param $flip bool
+ * @param bool $flip
*
- * @return String: CSS data in script file
+ * @return string: CSS data in script file
* @throws MWException if the file doesn't exist
*/
protected function readStyleFile( $path, $flip ) {
wfDebugLog( 'resourceloader', $msg );
throw new MWException( $msg );
}
- $style = file_get_contents( $localPath );
+
+ if ( $this->getStyleSheetLang( $path ) === 'less' ) {
+ $style = $this->compileLESSFile( $localPath );
+ $this->hasGeneratedStyles = true;
+ } else {
+ $style = file_get_contents( $localPath );
+ }
+
if ( $flip ) {
$style = CSSJanus::transform( $style, true, false );
}
/**
* Get whether CSS for this module should be flipped
- * @param $context ResourceLoaderContext
+ * @param ResourceLoaderContext $context
* @return bool
*/
public function getFlip( $context ) {
return $this->targets;
}
+ /**
+ * Generate a cache key for a LESS file.
+ * The cache key varies on the file name, the names and values of global
+ * LESS variables, and the value of $wgShowExceptionDetails. Varying on
+ * $wgShowExceptionDetails ensures the CSS comment indicating compilation
+ * failure shows the right level of detail.
+ *
+ * @param string $fileName File name of root LESS file.
+ * @return string: Cache key
+ */
+ protected static function getLESSCacheKey( $fileName ) {
+ global $wgShowExceptionDetails;
+
+ $vars = json_encode( self::getLESSVars() );
+ $hash = md5( $fileName . $vars );
+ return wfMemcKey( 'resourceloader', 'less', (string)$wgShowExceptionDetails, $hash );
+ }
+
+ /**
+ * Compile a LESS file into CSS.
+ *
+ * If invalid, returns replacement CSS source consisting of the compilation
+ * error message encoded as a comment. To save work, we cache a result object
+ * which comprises the compiled CSS and the names & mtimes of the files
+ * that were processed. lessphp compares the cached & current mtimes and
+ * recompiles as necessary.
+ *
+ * @param string $fileName File path of LESS source
+ * @return string: CSS source
+ */
+ protected function compileLESSFile( $fileName ) {
+ global $wgShowExceptionDetails;
+
+ $key = self::getLESSCacheKey( $fileName );
+ $cache = wfGetCache( CACHE_ANYTHING );
+
+ // The input to lessc. Either an associative array representing the
+ // cached results of a previous compilation, or the string file name if
+ // no cache result exists.
+ $source = $cache->get( $key );
+ if ( !is_array( $source ) || !isset( $source['root'] ) ) {
+ $source = $fileName;
+ }
+
+ $compiler = self::lessCompiler();
+ $expire = 0;
+ try {
+ $result = $compiler->cachedCompile( $source );
+ if ( !is_array( $result ) ) {
+ throw new Exception( 'LESS compiler result has type ' . gettype( $result ) . '; array expected.' );
+ }
+ } catch ( Exception $e ) {
+ // The exception might have been caused by an imported file rather
+ // than the root node. But we don't know which files were imported,
+ // because compilation failed; we thus cannot rely on file mtime to
+ // know when to reattempt compilation. Expire in 5 mins. instead.
+ $expire = 300;
+ wfDebugLog( 'resourceloader', __METHOD__ . ": $e" );
+ $result = array();
+ $result['root'] = $fileName;
+
+ if ( $wgShowExceptionDetails ) {
+ $result['compiled'] = ResourceLoader::makeComment( 'LESS error: ' . $e->getMessage() );
+ } else {
+ $result['compiled'] = ResourceLoader::makeComment( 'LESS stylesheet compilation failed. ' .
+ 'Set "$wgShowExceptionDetails = true;" to show detailed debugging information.' );
+ }
+
+ $result['files'] = array( $fileName => self::safeFilemtime( $fileName ) );
+ $result['updated'] = time();
+ }
+ // Tie cache expiry to the names and mtimes of files that were embedded
+ // as data URIs in the generated CSS source.
+ $result['files'] += $compiler->embeddedFiles;
+ $this->localFileRefs += array_keys( $result['files'] );
+ $cache->set( $key, $result, $expire );
+ return $result['compiled'];
+ }
}