Index: trunk/phase3/includes/ResourceLoaderContext.php |
— | — | @@ -27,6 +27,7 @@ |
28 | 28 | * of a specific loader request |
29 | 29 | */ |
30 | 30 | class ResourceLoaderContext { |
| 31 | + |
31 | 32 | /* Protected Members */ |
32 | 33 | |
33 | 34 | protected $request; |
Index: trunk/phase3/includes/ResourceLoader.php |
— | — | @@ -53,7 +53,16 @@ |
54 | 54 | } |
55 | 55 | } |
56 | 56 | |
57 | | - protected static function preloadModuleInfo( $modules, ResourceLoaderContext $context ) { |
| 57 | + /* |
| 58 | + * Loads information stored in the database about modules |
| 59 | + * |
| 60 | + * This is not inside the module code because it's so much more performant to request all of the information at once |
| 61 | + * than it is to have each module requests it's own information. |
| 62 | + * |
| 63 | + * @param $modules array list of modules to preload information for |
| 64 | + * @param $context ResourceLoaderContext context to load the information within |
| 65 | + */ |
| 66 | + protected static function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) { |
58 | 67 | $dbr = wfGetDb( DB_SLAVE ); |
59 | 68 | $skin = $context->getSkin(); |
60 | 69 | $lang = $context->getLanguage(); |
— | — | @@ -107,8 +116,7 @@ |
108 | 117 | * |
109 | 118 | * @param $filter String: name of filter to run |
110 | 119 | * @param $data String: text to filter, such as JavaScript or CSS text |
111 | | - * @param $file String: path to file being filtered, (optional: only required |
112 | | - * for CSS to resolve paths) |
| 120 | + * @param $file String: path to file being filtered, (optional: only required for CSS to resolve paths) |
113 | 121 | * @return String: filtered data |
114 | 122 | */ |
115 | 123 | protected static function filter( $filter, $data ) { |
— | — | @@ -227,54 +235,6 @@ |
228 | 236 | } |
229 | 237 | |
230 | 238 | /** |
231 | | - * Gets registration code for all modules |
232 | | - * |
233 | | - * @param $context ResourceLoaderContext object |
234 | | - * @return String: JavaScript code for registering all modules with the client loader |
235 | | - */ |
236 | | - public static function getModuleRegistrations( ResourceLoaderContext $context ) { |
237 | | - wfProfileIn( __METHOD__ ); |
238 | | - self::initialize(); |
239 | | - |
240 | | - $scripts = ''; |
241 | | - $registrations = array(); |
242 | | - |
243 | | - foreach ( self::$modules as $name => $module ) { |
244 | | - // Support module loader scripts |
245 | | - if ( ( $loader = $module->getLoaderScript() ) !== false ) { |
246 | | - $deps = FormatJson::encode( $module->getDependencies() ); |
247 | | - $group = FormatJson::encode( $module->getGroup() ); |
248 | | - $version = wfTimestamp( TS_ISO_8601, round( $module->getModifiedTime( $context ), -2 ) ); |
249 | | - $scripts .= "( function( name, version, dependencies ) { $loader } )\n" . |
250 | | - "( '$name', '$version', $deps, $group );\n"; |
251 | | - } |
252 | | - // Automatically register module |
253 | | - else { |
254 | | - // Modules without dependencies or a group pass two arguments (name, timestamp) to |
255 | | - // mediaWiki.loader.register() |
256 | | - if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) { |
257 | | - $registrations[] = array( $name, $module->getModifiedTime( $context ) ); |
258 | | - } |
259 | | - // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies) |
260 | | - // to mediaWiki.loader.register() |
261 | | - else if ( $module->getGroup() === null ) { |
262 | | - $registrations[] = array( |
263 | | - $name, $module->getModifiedTime( $context ), $module->getDependencies() ); |
264 | | - } |
265 | | - // Modules with dependencies pass four arguments (name, timestamp, dependencies, group) |
266 | | - // to mediaWiki.loader.register() |
267 | | - else { |
268 | | - $registrations[] = array( |
269 | | - $name, $module->getModifiedTime( $context ), $module->getDependencies(), $module->getGroup() ); |
270 | | - } |
271 | | - } |
272 | | - } |
273 | | - $out = $scripts . "mediaWiki.loader.register( " . FormatJson::encode( $registrations ) . " );\n"; |
274 | | - wfProfileOut( __METHOD__ ); |
275 | | - return $out; |
276 | | - } |
277 | | - |
278 | | - /** |
279 | 239 | * Get the highest modification time of all modules, based on a given |
280 | 240 | * combination of language code, skin name and debug mode flag. |
281 | 241 | * |
— | — | @@ -356,99 +316,125 @@ |
357 | 317 | return; |
358 | 318 | } |
359 | 319 | |
360 | | - // Use output buffering |
361 | | - ob_start(); |
362 | | - |
363 | 320 | // Pre-fetch blobs |
364 | 321 | $blobs = $context->shouldIncludeMessages() ? |
365 | 322 | MessageBlobStore::get( $modules, $context->getLanguage() ) : array(); |
366 | 323 | |
367 | 324 | // Generate output |
| 325 | + $out = ''; |
368 | 326 | foreach ( $modules as $name ) { |
369 | 327 | wfProfileIn( __METHOD__ . '-' . $name ); |
| 328 | + |
370 | 329 | // Scripts |
371 | 330 | $scripts = ''; |
372 | | - |
373 | 331 | if ( $context->shouldIncludeScripts() ) { |
374 | 332 | $scripts .= self::$modules[$name]->getScript( $context ) . "\n"; |
375 | 333 | } |
376 | 334 | |
377 | 335 | // Styles |
378 | 336 | $styles = array(); |
379 | | - |
380 | 337 | if ( |
381 | | - $context->shouldIncludeStyles() |
382 | | - && ( count( $styles = self::$modules[$name]->getStyles( $context ) ) ) |
| 338 | + $context->shouldIncludeStyles() && |
| 339 | + ( count( $styles = self::$modules[$name]->getStyles( $context ) ) ) |
383 | 340 | ) { |
384 | | - foreach ( $styles as $media => $style ) { |
385 | | - if ( self::$modules[$name]->getFlip( $context ) ) { |
| 341 | + // Flip CSS on a per-module basis |
| 342 | + if ( self::$modules[$name]->getFlip( $context ) ) { |
| 343 | + foreach ( $styles as $media => $style ) { |
386 | 344 | $styles[$media] = self::filter( 'flip-css', $style ); |
387 | 345 | } |
388 | | - if ( !$context->getDebug() ) { |
389 | | - $styles[$media] = self::filter( 'minify-css', $style ); |
390 | | - } |
391 | 346 | } |
392 | 347 | } |
393 | 348 | |
394 | 349 | // Messages |
395 | 350 | $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}'; |
396 | 351 | |
397 | | - // Output |
398 | | - if ( $context->getOnly() === 'styles' ) { |
399 | | - if ( $context->getDebug() ) { |
400 | | - echo "/* $name */\n"; |
401 | | - foreach ( $styles as $media => $style ) { |
402 | | - echo "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n"; |
403 | | - } |
404 | | - } else { |
405 | | - foreach ( $styles as $media => $style ) { |
406 | | - if ( strlen( $style ) ) { |
407 | | - echo "@media $media{" . $style . "}"; |
| 352 | + // Append output |
| 353 | + switch ( $context->getOnly() ) { |
| 354 | + case 'scripts': |
| 355 | + $out .= $scripts; |
| 356 | + break; |
| 357 | + case 'styles': |
| 358 | + $out .= self::makeCombinedStyles( $styles ); |
| 359 | + break; |
| 360 | + case 'messages': |
| 361 | + $out .= self::makeMessageSetScript( $messages ); |
| 362 | + break; |
| 363 | + default: |
| 364 | + // Minify CSS, unless in debug mode, before embedding in implment script |
| 365 | + if ( !$context->getDebug() ) { |
| 366 | + foreach ( $styles as $media => $style ) { |
| 367 | + $styles[$media] = self::filter( 'minify-css', $style ); |
408 | 368 | } |
409 | 369 | } |
410 | | - } |
411 | | - } else if ( $context->getOnly() === 'scripts' ) { |
412 | | - echo $scripts; |
413 | | - } else if ( $context->getOnly() === 'messages' ) { |
414 | | - echo "mediaWiki.msg.set( $messages );\n"; |
415 | | - } else { |
416 | | - if ( count( $styles ) ) { |
417 | | - $styles = FormatJson::encode( $styles ); |
418 | | - } else { |
419 | | - $styles = 'null'; |
420 | | - } |
421 | | - echo "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n$styles,\n$messages );\n"; |
| 370 | + $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages ); |
| 371 | + break; |
422 | 372 | } |
| 373 | + |
423 | 374 | wfProfileOut( __METHOD__ . '-' . $name ); |
424 | 375 | } |
425 | 376 | |
426 | | - // Update the status of script-only modules |
427 | | - if ( $context->getOnly() === 'scripts' && !in_array( 'startup', $modules ) ) { |
428 | | - $statuses = array(); |
429 | | - |
430 | | - foreach ( $modules as $name ) { |
431 | | - $statuses[$name] = 'ready'; |
432 | | - } |
433 | | - |
434 | | - $statuses = FormatJson::encode( $statuses ); |
435 | | - echo "mediaWiki.loader.state( $statuses );\n"; |
436 | | - } |
437 | | - |
438 | | - // Register missing modules |
| 377 | + // Update module states |
439 | 378 | if ( $context->shouldIncludeScripts() ) { |
440 | | - foreach ( $missing as $name ) { |
441 | | - echo "mediaWiki.loader.register( '$name', null, 'missing' );\n"; |
| 379 | + // Set the state of modules loaded as only scripts to ready |
| 380 | + if ( count( $modules ) && $context->getOnly() === 'scripts' && !in_array( 'startup', $modules ) ) { |
| 381 | + $out .= self::makeLoaderStateScript( array_fill_keys( $modules, 'ready' ) ); |
442 | 382 | } |
| 383 | + // Set the state of modules which were requested but unavailable as missing |
| 384 | + if ( count( $missing ) ) { |
| 385 | + $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) ); |
| 386 | + } |
443 | 387 | } |
444 | 388 | |
445 | | - // Output the appropriate header |
446 | | - if ( $context->getOnly() !== 'styles' ) { |
447 | | - if ( $context->getDebug() ) { |
448 | | - ob_end_flush(); |
| 389 | + // Send output |
| 390 | + if ( $context->getDebug() ) { |
| 391 | + echo $out; |
| 392 | + } else { |
| 393 | + if ( $context->getOnly() === 'styles' ) { |
| 394 | + echo self::filter( 'minify-css', $out ); |
449 | 395 | } else { |
450 | | - echo self::filter( 'minify-js', ob_get_clean() ); |
| 396 | + echo self::filter( 'minify-js', $out ); |
451 | 397 | } |
452 | 398 | } |
| 399 | + |
453 | 400 | wfProfileOut( __METHOD__ ); |
454 | 401 | } |
| 402 | + |
| 403 | + public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { |
| 404 | + if ( is_array( $scripts ) ) { |
| 405 | + $scripts = implode( $scripts, "\n" ); |
| 406 | + } |
| 407 | + if ( is_array( $styles ) ) { |
| 408 | + $styles = count( $styles ) ? FormatJson::encode( $styles ) : 'null'; |
| 409 | + } |
| 410 | + if ( is_array( $messages ) ) { |
| 411 | + $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null'; |
| 412 | + } |
| 413 | + return "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n$styles,\n$messages );\n"; |
| 414 | + } |
| 415 | + |
| 416 | + public static function makeMessageSetScript( $messages ) { |
| 417 | + if ( is_array( $messages ) ) { |
| 418 | + $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null'; |
| 419 | + } |
| 420 | + return "mediaWiki.msg.set( $messages );\n"; |
| 421 | + } |
| 422 | + |
| 423 | + public static function makeCombinedStyles( array $styles ) { |
| 424 | + $out = ''; |
| 425 | + foreach ( $styles as $media => $style ) { |
| 426 | + $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n"; |
| 427 | + } |
| 428 | + return $out; |
| 429 | + } |
| 430 | + |
| 431 | + public static function makeLoaderStateScript( $name, $state = null ) { |
| 432 | + if ( is_array( $name ) ) { |
| 433 | + $statuses = FormatJson::encode( $name ); |
| 434 | + return "mediaWiki.loader.state( $statuses );\n"; |
| 435 | + } else { |
| 436 | + $name = Xml::escapeJsString( $name ); |
| 437 | + $name = Xml::escapeJsString( $state ); |
| 438 | + return "mediaWiki.loader.state( '$name', '$state' );\n"; |
| 439 | + } |
| 440 | + } |
455 | 441 | } |
Index: trunk/phase3/includes/ResourceLoaderModule.php |
— | — | @@ -26,6 +26,7 @@ |
27 | 27 | * Abstraction for resource loader modules, with name registration and maxage functionality. |
28 | 28 | */ |
29 | 29 | abstract class ResourceLoaderModule { |
| 30 | + |
30 | 31 | /* Protected Members */ |
31 | 32 | |
32 | 33 | protected $name = null; |
— | — | @@ -1038,25 +1039,66 @@ |
1039 | 1040 | return $vars; |
1040 | 1041 | } |
1041 | 1042 | |
| 1043 | + /** |
| 1044 | + * Gets registration code for all modules |
| 1045 | + * |
| 1046 | + * @param $context ResourceLoaderContext object |
| 1047 | + * @return String: JavaScript code for registering all modules with the client loader |
| 1048 | + */ |
| 1049 | + public static function getModuleRegistrations( ResourceLoaderContext $context ) { |
| 1050 | + wfProfileIn( __METHOD__ ); |
| 1051 | + |
| 1052 | + $scripts = ''; |
| 1053 | + $registrations = array(); |
| 1054 | + foreach ( ResourceLoader::getModules() as $name => $module ) { |
| 1055 | + // Support module loader scripts |
| 1056 | + if ( ( $loader = $module->getLoaderScript() ) !== false ) { |
| 1057 | + $deps = FormatJson::encode( $module->getDependencies() ); |
| 1058 | + $group = FormatJson::encode( $module->getGroup() ); |
| 1059 | + $version = wfTimestamp( TS_ISO_8601, round( $module->getModifiedTime( $context ), -2 ) ); |
| 1060 | + $scripts .= "( function( name, version, dependencies ) { $loader } )\n" . |
| 1061 | + "( '$name', '$version', $deps, $group );\n"; |
| 1062 | + } |
| 1063 | + // Automatically register module |
| 1064 | + else { |
| 1065 | + // Modules without dependencies or a group pass two arguments (name, timestamp) to |
| 1066 | + // mediaWiki.loader.register() |
| 1067 | + if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) { |
| 1068 | + $registrations[] = array( $name, $module->getModifiedTime( $context ) ); |
| 1069 | + } |
| 1070 | + // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies) |
| 1071 | + // to mediaWiki.loader.register() |
| 1072 | + else if ( $module->getGroup() === null ) { |
| 1073 | + $registrations[] = array( |
| 1074 | + $name, $module->getModifiedTime( $context ), $module->getDependencies() ); |
| 1075 | + } |
| 1076 | + // Modules with dependencies pass four arguments (name, timestamp, dependencies, group) |
| 1077 | + // to mediaWiki.loader.register() |
| 1078 | + else { |
| 1079 | + $registrations[] = array( |
| 1080 | + $name, $module->getModifiedTime( $context ), $module->getDependencies(), $module->getGroup() ); |
| 1081 | + } |
| 1082 | + } |
| 1083 | + } |
| 1084 | + $out = $scripts . "mediaWiki.loader.register( " . FormatJson::encode( $registrations ) . " );"; |
| 1085 | + |
| 1086 | + wfProfileOut( __METHOD__ ); |
| 1087 | + return $out; |
| 1088 | + } |
| 1089 | + |
1042 | 1090 | /* Methods */ |
1043 | 1091 | |
1044 | 1092 | public function getScript( ResourceLoaderContext $context ) { |
1045 | 1093 | global $IP, $wgLoadScript; |
1046 | 1094 | |
1047 | 1095 | $scripts = file_get_contents( "$IP/resources/startup.js" ); |
1048 | | - |
1049 | 1096 | if ( $context->getOnly() === 'scripts' ) { |
1050 | 1097 | // Get all module registrations |
1051 | | - $registration = ResourceLoader::getModuleRegistrations( $context ); |
| 1098 | + $registration = self::getModuleRegistrations( $context ); |
1052 | 1099 | // Build configuration |
1053 | 1100 | $config = FormatJson::encode( $this->getConfig( $context ) ); |
1054 | 1101 | // Add a well-known start-up function |
1055 | | - $scripts .= <<<JAVASCRIPT |
1056 | | -window.startUp = function() { |
1057 | | - $registration |
1058 | | - mediaWiki.config.set( $config ); |
1059 | | -}; |
1060 | | -JAVASCRIPT; |
| 1102 | + $scripts .= "window.startUp = function() {\n\t$registration\n\tmediaWiki.config.set( $config );\n};\n"; |
1061 | 1103 | // Build load query for jquery and mediawiki modules |
1062 | 1104 | $query = array( |
1063 | 1105 | 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ), |
— | — | @@ -1074,7 +1116,7 @@ |
1075 | 1117 | // Build HTML code for loading jquery and mediawiki modules |
1076 | 1118 | $loadScript = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ); |
1077 | 1119 | // Add code to add jquery and mediawiki loading code; only if the current client is compatible |
1078 | | - $scripts .= "if ( isCompatible() ) { document.write( " . FormatJson::encode( $loadScript ) . "); }\n"; |
| 1120 | + $scripts .= "if ( isCompatible() ) {\n\tdocument.write( " . FormatJson::encode( $loadScript ) . ");\n}\n"; |
1079 | 1121 | // Delete the compatible function - it's not needed anymore |
1080 | 1122 | $scripts .= "delete window['isCompatible'];\n"; |
1081 | 1123 | } |
— | — | @@ -1090,10 +1132,10 @@ |
1091 | 1133 | return $this->modifiedTime[$hash]; |
1092 | 1134 | } |
1093 | 1135 | $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" ); |
| 1136 | + |
1094 | 1137 | // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully |
1095 | 1138 | // before making changes to this code! |
1096 | | - $this->modifiedTime[$hash] = ResourceLoader::getHighestModifiedTime( $context ); |
1097 | | - return $this->modifiedTime[$hash]; |
| 1139 | + return $this->modifiedTime[$hash] = ResourceLoader::getHighestModifiedTime( $context ); |
1098 | 1140 | } |
1099 | 1141 | |
1100 | 1142 | public function getFlip( $context ) { |
Index: trunk/phase3/resources/mediawiki/mediawiki.js |
— | — | @@ -735,9 +735,10 @@ |
736 | 736 | } |
737 | 737 | return; |
738 | 738 | } |
739 | | - if ( module in registry ) { |
740 | | - registry[module].state = state; |
| 739 | + if ( !( module in registry ) ) { |
| 740 | + that.register( module ); |
741 | 741 | } |
| 742 | + registry[module].state = state; |
742 | 743 | }; |
743 | 744 | |
744 | 745 | /** |