resourceloader: qualify $ variable in script() call to handle the case of jQuery
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.js
1 /**
2 * Base library for MediaWiki.
3 *
4 * Exposed globally as `mediaWiki` with `mw` as shortcut.
5 *
6 * @class mw
7 * @alternateClassName mediaWiki
8 * @singleton
9 */
10
11 ( function ( $ ) {
12 'use strict';
13
14 var mw, StringSet, log,
15 hasOwn = Object.prototype.hasOwnProperty,
16 slice = Array.prototype.slice,
17 trackQueue = [];
18
19 /**
20 * FNV132 hash function
21 *
22 * This function implements the 32-bit version of FNV-1.
23 * It is equivalent to hash( 'fnv132', ... ) in PHP, except
24 * its output is base 36 rather than hex.
25 * See <https://en.wikipedia.org/wiki/FNV_hash_function>
26 *
27 * @private
28 * @param {string} str String to hash
29 * @return {string} hash as an seven-character base 36 string
30 */
31 function fnv132( str ) {
32 /* eslint-disable no-bitwise */
33 var hash = 0x811C9DC5,
34 i;
35
36 for ( i = 0; i < str.length; i++ ) {
37 hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
38 hash ^= str.charCodeAt( i );
39 }
40
41 hash = ( hash >>> 0 ).toString( 36 );
42 while ( hash.length < 7 ) {
43 hash = '0' + hash;
44 }
45
46 return hash;
47 /* eslint-enable no-bitwise */
48 }
49
50 function defineFallbacks() {
51 // <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set>
52 StringSet = window.Set || ( function () {
53 /**
54 * @private
55 * @class
56 */
57 function StringSet() {
58 this.set = {};
59 }
60 StringSet.prototype.add = function ( value ) {
61 this.set[ value ] = true;
62 };
63 StringSet.prototype.has = function ( value ) {
64 return hasOwn.call( this.set, value );
65 };
66 return StringSet;
67 }() );
68 }
69
70 /**
71 * Alias property to the global object.
72 *
73 * @private
74 * @static
75 * @member mw.Map
76 * @param {mw.Map} map
77 * @param {string} key
78 * @param {Mixed} value
79 */
80 function setGlobalMapValue( map, key, value ) {
81 map.values[ key ] = value;
82 log.deprecate(
83 window,
84 key,
85 value,
86 // Deprecation notice for mw.config globals (T58550, T72470)
87 map === mw.config && 'Use mw.config instead.'
88 );
89 }
90
91 /**
92 * Log a message to window.console, if possible.
93 *
94 * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
95 * also in production mode). Gets console references in each invocation instead of caching the
96 * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
97 *
98 * @private
99 * @param {string} topic Stream name passed by mw.track
100 * @param {Object} data Data passed by mw.track
101 * @param {Error} [data.exception]
102 * @param {string} data.source Error source
103 * @param {string} [data.module] Name of module which caused the error
104 */
105 function logError( topic, data ) {
106 /* eslint-disable no-console */
107 var msg,
108 e = data.exception,
109 source = data.source,
110 module = data.module,
111 console = window.console;
112
113 if ( console && console.log ) {
114 msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
115 if ( module ) {
116 msg += ' in module ' + module;
117 }
118 msg += ( e ? ':' : '.' );
119 console.log( msg );
120
121 // If we have an exception object, log it to the warning channel to trigger
122 // proper stacktraces in browsers that support it.
123 if ( e && console.warn ) {
124 console.warn( e );
125 }
126 }
127 /* eslint-enable no-console */
128 }
129
130 /**
131 * Create an object that can be read from or written to via methods that allow
132 * interaction both with single and multiple properties at once.
133 *
134 * @private
135 * @class mw.Map
136 *
137 * @constructor
138 * @param {boolean} [global=false] Whether to synchronise =values to the global
139 * window object (for backwards-compatibility with mw.config; T72470). Values are
140 * copied in one direction only. Changes to globals do not reflect in the map.
141 */
142 function Map( global ) {
143 this.values = {};
144 if ( global === true ) {
145 // Override #set to also set the global variable
146 this.set = function ( selection, value ) {
147 var s;
148 if ( arguments.length > 1 ) {
149 if ( typeof selection !== 'string' ) {
150 return false;
151 }
152 setGlobalMapValue( this, selection, value );
153 return true;
154 }
155 if ( typeof selection === 'object' ) {
156 for ( s in selection ) {
157 setGlobalMapValue( this, s, selection[ s ] );
158 }
159 return true;
160 }
161 return false;
162 };
163 }
164 }
165
166 Map.prototype = {
167 constructor: Map,
168
169 /**
170 * Get the value of one or more keys.
171 *
172 * If called with no arguments, all values are returned.
173 *
174 * @param {string|Array} [selection] Key or array of keys to retrieve values for.
175 * @param {Mixed} [fallback=null] Value for keys that don't exist.
176 * @return {Mixed|Object|null} If selection was a string, returns the value,
177 * If selection was an array, returns an object of key/values.
178 * If no selection is passed, a new object with all key/values is returned.
179 */
180 get: function ( selection, fallback ) {
181 var results, i;
182 fallback = arguments.length > 1 ? fallback : null;
183
184 if ( Array.isArray( selection ) ) {
185 results = {};
186 for ( i = 0; i < selection.length; i++ ) {
187 if ( typeof selection[ i ] === 'string' ) {
188 results[ selection[ i ] ] = hasOwn.call( this.values, selection[ i ] ) ?
189 this.values[ selection[ i ] ] :
190 fallback;
191 }
192 }
193 return results;
194 }
195
196 if ( typeof selection === 'string' ) {
197 return hasOwn.call( this.values, selection ) ?
198 this.values[ selection ] :
199 fallback;
200 }
201
202 if ( selection === undefined ) {
203 results = {};
204 for ( i in this.values ) {
205 results[ i ] = this.values[ i ];
206 }
207 return results;
208 }
209
210 // Invalid selection key
211 return fallback;
212 },
213
214 /**
215 * Set one or more key/value pairs.
216 *
217 * @param {string|Object} selection Key to set value for, or object mapping keys to values
218 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
219 * @return {boolean} True on success, false on failure
220 */
221 set: function ( selection, value ) {
222 var s;
223 // Use `arguments.length` because `undefined` is also a valid value.
224 if ( arguments.length > 1 ) {
225 if ( typeof selection !== 'string' ) {
226 return false;
227 }
228 this.values[ selection ] = value;
229 return true;
230 }
231 if ( typeof selection === 'object' ) {
232 for ( s in selection ) {
233 this.values[ s ] = selection[ s ];
234 }
235 return true;
236 }
237 return false;
238 },
239
240 /**
241 * Check if one or more keys exist.
242 *
243 * @param {Mixed} selection Key or array of keys to check
244 * @return {boolean} True if the key(s) exist
245 */
246 exists: function ( selection ) {
247 var i;
248 if ( Array.isArray( selection ) ) {
249 for ( i = 0; i < selection.length; i++ ) {
250 if ( typeof selection[ i ] !== 'string' || !hasOwn.call( this.values, selection[ i ] ) ) {
251 return false;
252 }
253 }
254 return true;
255 }
256 return typeof selection === 'string' && hasOwn.call( this.values, selection );
257 }
258 };
259
260 defineFallbacks();
261
262 /* eslint-disable no-console */
263 log = ( function () {
264 /**
265 * Write a verbose message to the browser's console in debug mode.
266 *
267 * This method is mainly intended for verbose logging. It is a no-op in production mode.
268 * In ResourceLoader debug mode, it will use the browser's console if available, with
269 * fallback to creating a console interface in the DOM and logging messages there.
270 *
271 * See {@link mw.log} for other logging methods.
272 *
273 * @member mw
274 * @param {...string} msg Messages to output to console.
275 */
276 var log = function () {},
277 console = window.console;
278
279 // Note: Keep list of methods in sync with restoration in mediawiki.log.js
280 // when adding or removing mw.log methods below!
281
282 /**
283 * Collection of methods to help log messages to the console.
284 *
285 * @class mw.log
286 * @singleton
287 */
288
289 /**
290 * Write a message to the browser console's warning channel.
291 *
292 * This method is a no-op in browsers that don't implement the Console API.
293 *
294 * @param {...string} msg Messages to output to console
295 */
296 log.warn = console && console.warn && Function.prototype.bind ?
297 Function.prototype.bind.call( console.warn, console ) :
298 $.noop;
299
300 /**
301 * Write a message to the browser console's error channel.
302 *
303 * Most browsers also print a stacktrace when calling this method if the
304 * argument is an Error object.
305 *
306 * This method is a no-op in browsers that don't implement the Console API.
307 *
308 * @since 1.26
309 * @param {Error|...string} msg Messages to output to console
310 */
311 log.error = console && console.error && Function.prototype.bind ?
312 Function.prototype.bind.call( console.error, console ) :
313 $.noop;
314
315 /**
316 * Create a property on a host object that, when accessed, will produce
317 * a deprecation warning in the console.
318 *
319 * @param {Object} obj Host object of deprecated property
320 * @param {string} key Name of property to create in `obj`
321 * @param {Mixed} val The value this property should return when accessed
322 * @param {string} [msg] Optional text to include in the deprecation message
323 * @param {string} [logName=key] Optional custom name for the feature.
324 * This is used instead of `key` in the message and `mw.deprecate` tracking.
325 */
326 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
327 obj[ key ] = val;
328 } : function ( obj, key, val, msg, logName ) {
329 var logged = new StringSet();
330 logName = logName || key;
331 msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
332 function uniqueTrace() {
333 var trace = new Error().stack;
334 if ( logged.has( trace ) ) {
335 return false;
336 }
337 logged.add( trace );
338 return true;
339 }
340 // Support: Safari 5.0
341 // Throws "not supported on DOM Objects" for Node or Element objects (incl. document)
342 // Safari 4.0 doesn't have this method, and it was fixed in Safari 5.1.
343 try {
344 Object.defineProperty( obj, key, {
345 configurable: true,
346 enumerable: true,
347 get: function () {
348 if ( uniqueTrace() ) {
349 mw.track( 'mw.deprecate', logName );
350 mw.log.warn( msg );
351 }
352 return val;
353 },
354 set: function ( newVal ) {
355 if ( uniqueTrace() ) {
356 mw.track( 'mw.deprecate', logName );
357 mw.log.warn( msg );
358 }
359 val = newVal;
360 }
361 } );
362 } catch ( err ) {
363 obj[ key ] = val;
364 }
365 };
366
367 return log;
368 }() );
369 /* eslint-enable no-console */
370
371 /**
372 * @class mw
373 */
374 mw = {
375 redefineFallbacksForTest: function () {
376 if ( !window.QUnit ) {
377 throw new Error( 'Reset not allowed outside unit tests' );
378 }
379 defineFallbacks();
380 },
381
382 /**
383 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
384 *
385 * On browsers that implement the Navigation Timing API, this function will produce floating-point
386 * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
387 * it will fall back to using `Date`.
388 *
389 * @return {number} Current time
390 */
391 now: ( function () {
392 var perf = window.performance,
393 navStart = perf && perf.timing && perf.timing.navigationStart;
394 return navStart && typeof perf.now === 'function' ?
395 function () { return navStart + perf.now(); } :
396 function () { return Date.now(); };
397 }() ),
398
399 /**
400 * List of all analytic events emitted so far.
401 *
402 * @private
403 * @property {Array}
404 */
405 trackQueue: trackQueue,
406
407 track: function ( topic, data ) {
408 trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
409 // The base module extends this method to fire events here
410 },
411
412 /**
413 * Track an early error event via mw.track and send it to the window console.
414 *
415 * @private
416 * @param {string} topic Topic name
417 * @param {Object} data Data describing the event, encoded as an object; see mw#logError
418 */
419 trackError: function ( topic, data ) {
420 mw.track( topic, data );
421 logError( topic, data );
422 },
423
424 // Expose Map constructor
425 Map: Map,
426
427 /**
428 * Map of configuration values.
429 *
430 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
431 * on mediawiki.org.
432 *
433 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
434 * global `window` object.
435 *
436 * @property {mw.Map} config
437 */
438 // Dummy placeholder later assigned in ResourceLoaderStartUpModule
439 config: null,
440
441 /**
442 * Empty object for third-party libraries, for cases where you don't
443 * want to add a new global, or the global is bad and needs containment
444 * or wrapping.
445 *
446 * @property
447 */
448 libs: {},
449
450 /**
451 * Access container for deprecated functionality that can be moved from
452 * from their legacy location and attached to this object (e.g. a global
453 * function that is deprecated and as stop-gap can be exposed through here).
454 *
455 * This was reserved for future use but never ended up being used.
456 *
457 * @deprecated since 1.22 Let deprecated identifiers keep their original name
458 * and use mw.log#deprecate to create an access container for tracking.
459 * @property
460 */
461 legacy: {},
462
463 /**
464 * Store for messages.
465 *
466 * @property {mw.Map}
467 */
468 messages: new Map(),
469
470 /**
471 * Store for templates associated with a module.
472 *
473 * @property {mw.Map}
474 */
475 templates: new Map(),
476
477 // Expose mw.log
478 log: log,
479
480 /**
481 * Client for ResourceLoader server end point.
482 *
483 * This client is in charge of maintaining the module registry and state
484 * machine, initiating network (batch) requests for loading modules, as
485 * well as dependency resolution and execution of source code.
486 *
487 * For more information, refer to
488 * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
489 *
490 * @class mw.loader
491 * @singleton
492 */
493 loader: ( function () {
494
495 /**
496 * Fired via mw.track on various resource loading errors.
497 *
498 * @event resourceloader_exception
499 * @param {Error|Mixed} e The error that was thrown. Almost always an Error
500 * object, but in theory module code could manually throw something else, and that
501 * might also end up here.
502 * @param {string} [module] Name of the module which caused the error. Omitted if the
503 * error is not module-related or the module cannot be easily identified due to
504 * batched handling.
505 * @param {string} source Source of the error. Possible values:
506 *
507 * - style: stylesheet error (only affects old IE where a special style loading method
508 * is used)
509 * - load-callback: exception thrown by user callback
510 * - module-execute: exception thrown by module code
511 * - resolve: failed to sort dependencies for a module in mw.loader.load
512 * - store-eval: could not evaluate module code cached in localStorage
513 * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
514 * - store-localstorage-json: JSON conversion error in mw.loader.store.set
515 * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
516 */
517
518 /**
519 * Fired via mw.track on resource loading error conditions.
520 *
521 * @event resourceloader_assert
522 * @param {string} source Source of the error. Possible values:
523 *
524 * - bug-T59567: failed to cache script due to an Opera function -> string conversion
525 * bug; see <https://phabricator.wikimedia.org/T59567> for details
526 */
527
528 /**
529 * Mapping of registered modules.
530 *
531 * See #implement and #execute for exact details on support for script, style and messages.
532 *
533 * Format:
534 *
535 * {
536 * 'moduleName': {
537 * // From mw.loader.register()
538 * 'version': '########' (hash)
539 * 'dependencies': ['required.foo', 'bar.also', ...]
540 * 'group': 'somegroup', (or) null
541 * 'source': 'local', (or) 'anotherwiki'
542 * 'skip': 'return !!window.Example', (or) null
543 * 'module': export Object
544 *
545 * // Set from execute() or mw.loader.state()
546 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
547 *
548 * // Optionally added at run-time by mw.loader.implement()
549 * 'skipped': true
550 * 'script': closure, array of urls, or string
551 * 'style': { ... } (see #execute)
552 * 'messages': { 'key': 'value', ... }
553 * }
554 * }
555 *
556 * State machine:
557 *
558 * - `registered`:
559 * The module is known to the system but not yet required.
560 * Meta data is registered via mw.loader#register. Calls to that method are
561 * generated server-side by the startup module.
562 * - `loading`:
563 * The module was required through mw.loader (either directly or as dependency of
564 * another module). The client will fetch module contents from the server.
565 * The contents are then stashed in the registry via mw.loader#implement.
566 * - `loaded`:
567 * The module has been loaded from the server and stashed via mw.loader#implement.
568 * If the module has no more dependencies in-flight, the module will be executed
569 * immediately. Otherwise execution is deferred, controlled via #handlePending.
570 * - `executing`:
571 * The module is being executed.
572 * - `ready`:
573 * The module has been successfully executed.
574 * - `error`:
575 * The module (or one of its dependencies) produced an error during execution.
576 * - `missing`:
577 * The module was registered client-side and requested, but the server denied knowledge
578 * of the module's existence.
579 *
580 * @property
581 * @private
582 */
583 var registry = {},
584 // Mapping of sources, keyed by source-id, values are strings.
585 //
586 // Format:
587 //
588 // {
589 // 'sourceId': 'http://example.org/w/load.php'
590 // }
591 //
592 sources = {},
593
594 // For queueModuleScript()
595 handlingPendingRequests = false,
596 pendingRequests = [],
597
598 // List of modules to be loaded
599 queue = [],
600
601 /**
602 * List of callback jobs waiting for modules to be ready.
603 *
604 * Jobs are created by #enqueue() and run by #handlePending().
605 *
606 * Typically when a job is created for a module, the job's dependencies contain
607 * both the required module and all its recursive dependencies.
608 *
609 * Format:
610 *
611 * {
612 * 'dependencies': [ module names ],
613 * 'ready': Function callback
614 * 'error': Function callback
615 * }
616 *
617 * @property {Object[]} jobs
618 * @private
619 */
620 jobs = [],
621
622 /**
623 * For #addEmbeddedCSS() and #addLink()
624 *
625 * @private
626 * @property {HTMLElement|null} marker
627 */
628 marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' ),
629
630 // For addEmbeddedCSS()
631 cssBuffer = '',
632 cssBufferTimer = null,
633 cssCallbacks = [],
634 rAF = window.requestAnimationFrame || setTimeout;
635
636 /**
637 * Create a new style element and add it to the DOM.
638 *
639 * @private
640 * @param {string} text CSS text
641 * @param {Node|null} [nextNode] The element where the style tag
642 * should be inserted before
643 * @return {HTMLElement} Reference to the created style element
644 */
645 function newStyleTag( text, nextNode ) {
646 var el = document.createElement( 'style' );
647 el.appendChild( document.createTextNode( text ) );
648 if ( nextNode && nextNode.parentNode ) {
649 nextNode.parentNode.insertBefore( el, nextNode );
650 } else {
651 document.head.appendChild( el );
652 }
653 return el;
654 }
655
656 /**
657 * Add a bit of CSS text to the current browser page.
658 *
659 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
660 * or create a new one based on whether the given `cssText` is safe for extension.
661 *
662 * @private
663 * @param {string} [cssText=cssBuffer] If called without cssText,
664 * the internal buffer will be inserted instead.
665 * @param {Function} [callback]
666 */
667 function addEmbeddedCSS( cssText, callback ) {
668 function fireCallbacks() {
669 var i,
670 oldCallbacks = cssCallbacks;
671 // Reset cssCallbacks variable so it's not polluted by any calls to
672 // addEmbeddedCSS() from one of the callbacks (T105973)
673 cssCallbacks = [];
674 for ( i = 0; i < oldCallbacks.length; i++ ) {
675 oldCallbacks[ i ]();
676 }
677 }
678
679 if ( callback ) {
680 cssCallbacks.push( callback );
681 }
682
683 // Yield once before creating the <style> tag. This lets multiple stylesheets
684 // accumulate into one buffer, allowing us to reduce how often new stylesheets
685 // are inserted in the browser. Appending a stylesheet and waiting for the
686 // browser to repaint is fairly expensive. (T47810)
687 if ( cssText ) {
688 // Don't extend the buffer if the item needs its own stylesheet.
689 // Keywords like `@import` are only valid at the start of a stylesheet (T37562).
690 if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
691 // Linebreak for somewhat distinguishable sections
692 cssBuffer += '\n' + cssText;
693 if ( !cssBufferTimer ) {
694 cssBufferTimer = rAF( function () {
695 // Wrap in anonymous function that takes no arguments
696 // Support: Firefox < 13
697 // Firefox 12 has non-standard behaviour of passing a number
698 // as first argument to a setTimeout callback.
699 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
700 addEmbeddedCSS();
701 } );
702 }
703 return;
704 }
705
706 // This is a scheduled flush for the buffer
707 } else {
708 cssBufferTimer = null;
709 cssText = cssBuffer;
710 cssBuffer = '';
711 }
712
713 newStyleTag( cssText, marker );
714
715 fireCallbacks();
716 }
717
718 /**
719 * @private
720 * @param {Array} modules List of module names
721 * @return {string} Hash of concatenated version hashes.
722 */
723 function getCombinedVersion( modules ) {
724 var hashes = modules.map( function ( module ) {
725 return registry[ module ].version;
726 } );
727 return fnv132( hashes.join( '' ) );
728 }
729
730 /**
731 * Determine whether all dependencies are in state 'ready', which means we may
732 * execute the module or job now.
733 *
734 * @private
735 * @param {Array} modules Names of modules to be checked
736 * @return {boolean} True if all modules are in state 'ready', false otherwise
737 */
738 function allReady( modules ) {
739 var i;
740 for ( i = 0; i < modules.length; i++ ) {
741 if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
742 return false;
743 }
744 }
745 return true;
746 }
747
748 /**
749 * Determine whether all dependencies are in state 'ready', which means we may
750 * execute the module or job now.
751 *
752 * @private
753 * @param {Array} modules Names of modules to be checked
754 * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
755 */
756 function anyFailed( modules ) {
757 var i, state;
758 for ( i = 0; i < modules.length; i++ ) {
759 state = mw.loader.getState( modules[ i ] );
760 if ( state === 'error' || state === 'missing' ) {
761 return true;
762 }
763 }
764 return false;
765 }
766
767 /**
768 * A module has entered state 'ready', 'error', or 'missing'. Automatically update
769 * pending jobs and modules that depend upon this module. If the given module failed,
770 * propagate the 'error' state up the dependency tree. Otherwise, go ahead and execute
771 * all jobs/modules now having their dependencies satisfied.
772 *
773 * Jobs that depend on a failed module, will have their error callback ran (if any).
774 *
775 * @private
776 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
777 */
778 function handlePending( module ) {
779 var j, job, hasErrors, m, stateChange;
780
781 if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) {
782 // If the current module failed, mark all dependent modules also as failed.
783 // Iterate until steady-state to propagate the error state upwards in the
784 // dependency tree.
785 do {
786 stateChange = false;
787 for ( m in registry ) {
788 if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) {
789 if ( anyFailed( registry[ m ].dependencies ) ) {
790 registry[ m ].state = 'error';
791 stateChange = true;
792 }
793 }
794 }
795 } while ( stateChange );
796 }
797
798 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
799 for ( j = 0; j < jobs.length; j++ ) {
800 hasErrors = anyFailed( jobs[ j ].dependencies );
801 if ( hasErrors || allReady( jobs[ j ].dependencies ) ) {
802 // All dependencies satisfied, or some have errors
803 job = jobs[ j ];
804 jobs.splice( j, 1 );
805 j -= 1;
806 try {
807 if ( hasErrors ) {
808 if ( typeof job.error === 'function' ) {
809 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
810 }
811 } else {
812 if ( typeof job.ready === 'function' ) {
813 job.ready();
814 }
815 }
816 } catch ( e ) {
817 // A user-defined callback raised an exception.
818 // Swallow it to protect our state machine!
819 mw.trackError( 'resourceloader.exception', {
820 exception: e,
821 module: module,
822 source: 'load-callback'
823 } );
824 }
825 }
826 }
827
828 if ( registry[ module ].state === 'ready' ) {
829 // The current module became 'ready'. Set it in the module store, and recursively execute all
830 // dependent modules that are loaded and now have all dependencies satisfied.
831 mw.loader.store.set( module, registry[ module ] );
832 for ( m in registry ) {
833 if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
834 // eslint-disable-next-line no-use-before-define
835 execute( m );
836 }
837 }
838 }
839 }
840
841 /**
842 * Resolve dependencies and detect circular references.
843 *
844 * @private
845 * @param {string} module Name of the top-level module whose dependencies shall be
846 * resolved and sorted.
847 * @param {Array} resolved Returns a topological sort of the given module and its
848 * dependencies, such that later modules depend on earlier modules. The array
849 * contains the module names. If the array contains already some module names,
850 * this function appends its result to the pre-existing array.
851 * @param {StringSet} [unresolved] Used to track the current dependency
852 * chain, and to report loops in the dependency graph.
853 * @throws {Error} If any unregistered module or a dependency loop is encountered
854 */
855 function sortDependencies( module, resolved, unresolved ) {
856 var i, deps, skip;
857
858 if ( !hasOwn.call( registry, module ) ) {
859 throw new Error( 'Unknown dependency: ' + module );
860 }
861
862 if ( registry[ module ].skip !== null ) {
863 // eslint-disable-next-line no-new-func
864 skip = new Function( registry[ module ].skip );
865 registry[ module ].skip = null;
866 if ( skip() ) {
867 registry[ module ].skipped = true;
868 registry[ module ].dependencies = [];
869 registry[ module ].state = 'ready';
870 handlePending( module );
871 return;
872 }
873 }
874
875 if ( resolved.indexOf( module ) !== -1 ) {
876 // Module already resolved; nothing to do
877 return;
878 }
879 // Create unresolved if not passed in
880 if ( !unresolved ) {
881 unresolved = new StringSet();
882 }
883 // Tracks down dependencies
884 deps = registry[ module ].dependencies;
885 for ( i = 0; i < deps.length; i++ ) {
886 if ( resolved.indexOf( deps[ i ] ) === -1 ) {
887 if ( unresolved.has( deps[ i ] ) ) {
888 throw new Error(
889 'Circular reference detected: ' + module + ' -> ' + deps[ i ]
890 );
891 }
892
893 unresolved.add( module );
894 sortDependencies( deps[ i ], resolved, unresolved );
895 }
896 }
897 resolved.push( module );
898 }
899
900 /**
901 * Get names of module that a module depends on, in their proper dependency order.
902 *
903 * @private
904 * @param {string[]} modules Array of string module names
905 * @return {Array} List of dependencies, including 'module'.
906 * @throws {Error} If an unregistered module or a dependency loop is encountered
907 */
908 function resolve( modules ) {
909 var i, resolved = [];
910 for ( i = 0; i < modules.length; i++ ) {
911 sortDependencies( modules[ i ], resolved );
912 }
913 return resolved;
914 }
915
916 /**
917 * Like #resolve(), except it will silently ignore modules that
918 * are missing or have missing dependencies.
919 *
920 * @private
921 * @param {string[]} modules Array of string module names
922 * @return {Array} List of dependencies.
923 */
924 function resolveStubbornly( modules ) {
925 var i, saved, resolved = [];
926 for ( i = 0; i < modules.length; i++ ) {
927 saved = resolved.slice();
928 try {
929 sortDependencies( modules[ i ], resolved );
930 } catch ( err ) {
931 // This module is unknown or has unknown dependencies.
932 // Undo any incomplete resolutions made and keep going.
933 resolved = saved;
934 mw.trackError( 'resourceloader.exception', {
935 exception: err,
936 source: 'resolve'
937 } );
938 }
939 }
940 return resolved;
941 }
942
943 /**
944 * Load and execute a script.
945 *
946 * @private
947 * @param {string} src URL to script, will be used as the src attribute in the script tag
948 * @param {Function} [callback] Callback to run after request resolution
949 */
950 function addScript( src, callback ) {
951 var script = document.createElement( 'script' );
952 script.src = src;
953 script.onload = script.onerror = function () {
954 if ( script.parentNode ) {
955 script.parentNode.removeChild( script );
956 }
957 script = null;
958 if ( callback ) {
959 callback();
960 callback = null;
961 }
962 };
963 document.head.appendChild( script );
964 }
965
966 /**
967 * Queue the loading and execution of a script for a particular module.
968 *
969 * @private
970 * @param {string} src URL of the script
971 * @param {string} moduleName Name of currently executing module
972 * @param {Function} callback Callback to run after addScript() resolution
973 */
974 function queueModuleScript( src, moduleName, callback ) {
975 pendingRequests.push( function () {
976 if ( hasOwn.call( registry, moduleName ) ) {
977 // Emulate runScript() part of execute()
978 window.require = mw.loader.require;
979 window.module = registry[ moduleName ].module;
980 }
981 addScript( src, function () {
982 // 'module.exports' should not persist after the file is executed to
983 // avoid leakage to unrelated code. 'require' should be kept, however,
984 // as asynchronous access to 'require' is allowed and expected. (T144879)
985 delete window.module;
986 callback();
987 // Start the next one (if any)
988 if ( pendingRequests[ 0 ] ) {
989 pendingRequests.shift()();
990 } else {
991 handlingPendingRequests = false;
992 }
993 } );
994 } );
995 if ( !handlingPendingRequests && pendingRequests[ 0 ] ) {
996 handlingPendingRequests = true;
997 pendingRequests.shift()();
998 }
999 }
1000
1001 /**
1002 * Utility function for execute()
1003 *
1004 * @ignore
1005 * @param {string} [media] Media attribute
1006 * @param {string} url URL
1007 */
1008 function addLink( media, url ) {
1009 var el = document.createElement( 'link' );
1010
1011 el.rel = 'stylesheet';
1012 if ( media && media !== 'all' ) {
1013 el.media = media;
1014 }
1015 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1016 // see #addEmbeddedCSS, T33676, T43331, and T49277 for details.
1017 el.href = url;
1018
1019 if ( marker && marker.parentNode ) {
1020 marker.parentNode.insertBefore( el, marker );
1021 } else {
1022 document.head.appendChild( el );
1023 }
1024 }
1025
1026 /**
1027 * @private
1028 * @param {string} code JavaScript code
1029 */
1030 function domEval( code ) {
1031 var script = document.createElement( 'script' );
1032 script.text = code;
1033 document.head.appendChild( script );
1034 script.parentNode.removeChild( script );
1035 }
1036
1037 /**
1038 * Executes a loaded module, making it ready to use
1039 *
1040 * @private
1041 * @param {string} module Module name to execute
1042 */
1043 function execute( module ) {
1044 var key, value, media, i, urls, cssHandle, checkCssHandles, runScript,
1045 cssHandlesRegistered = false;
1046
1047 if ( !hasOwn.call( registry, module ) ) {
1048 throw new Error( 'Module has not been registered yet: ' + module );
1049 }
1050 if ( registry[ module ].state !== 'loaded' ) {
1051 throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module );
1052 }
1053
1054 registry[ module ].state = 'executing';
1055
1056 runScript = function () {
1057 var script, markModuleReady, nestedAddScript;
1058
1059 script = registry[ module ].script;
1060 markModuleReady = function () {
1061 registry[ module ].state = 'ready';
1062 handlePending( module );
1063 };
1064 nestedAddScript = function ( arr, callback, i ) {
1065 // Recursively call queueModuleScript() in its own callback
1066 // for each element of arr.
1067 if ( i >= arr.length ) {
1068 // We're at the end of the array
1069 callback();
1070 return;
1071 }
1072
1073 queueModuleScript( arr[ i ], module, function () {
1074 nestedAddScript( arr, callback, i + 1 );
1075 } );
1076 };
1077
1078 try {
1079 if ( Array.isArray( script ) ) {
1080 nestedAddScript( script, markModuleReady, 0 );
1081 } else if ( typeof script === 'function' ) {
1082 // Pass jQuery twice so that the signature of the closure which wraps
1083 // the script can bind both '$' and 'jQuery'.
1084 script( window.$, window.$, mw.loader.require, registry[ module ].module );
1085 markModuleReady();
1086
1087 } else if ( typeof script === 'string' ) {
1088 // Site and user modules are legacy scripts that run in the global scope.
1089 // This is transported as a string instead of a function to avoid needing
1090 // to use string manipulation to undo the function wrapper.
1091 domEval( script );
1092 markModuleReady();
1093
1094 } else {
1095 // Module without script
1096 markModuleReady();
1097 }
1098 } catch ( e ) {
1099 // Use mw.track instead of mw.log because these errors are common in production mode
1100 // (e.g. undefined variable), and mw.log is only enabled in debug mode.
1101 registry[ module ].state = 'error';
1102 mw.trackError( 'resourceloader.exception', {
1103 exception: e, module:
1104 module, source: 'module-execute'
1105 } );
1106 handlePending( module );
1107 }
1108 };
1109
1110 // Add localizations to message system
1111 if ( registry[ module ].messages ) {
1112 mw.messages.set( registry[ module ].messages );
1113 }
1114
1115 // Initialise templates
1116 if ( registry[ module ].templates ) {
1117 mw.templates.set( module, registry[ module ].templates );
1118 }
1119
1120 // Make sure we don't run the scripts until all stylesheet insertions have completed.
1121 ( function () {
1122 var pending = 0;
1123 checkCssHandles = function () {
1124 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1125 // one of the cssHandles is fired while we're still creating more handles.
1126 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1127 if ( module === 'user' ) {
1128 // Implicit dependency on the site module. Not real dependency because
1129 // it should run after 'site' regardless of whether it succeeds or fails.
1130 mw.loader.using( [ 'site' ] ).always( runScript );
1131 } else {
1132 runScript();
1133 }
1134 runScript = undefined; // Revoke
1135 }
1136 };
1137 cssHandle = function () {
1138 var check = checkCssHandles;
1139 pending++;
1140 return function () {
1141 if ( check ) {
1142 pending--;
1143 check();
1144 check = undefined; // Revoke
1145 }
1146 };
1147 };
1148 }() );
1149
1150 // Process styles (see also mw.loader.implement)
1151 // * back-compat: { <media>: css }
1152 // * back-compat: { <media>: [url, ..] }
1153 // * { "css": [css, ..] }
1154 // * { "url": { <media>: [url, ..] } }
1155 if ( registry[ module ].style ) {
1156 for ( key in registry[ module ].style ) {
1157 value = registry[ module ].style[ key ];
1158 media = undefined;
1159
1160 if ( key !== 'url' && key !== 'css' ) {
1161 // Backwards compatibility, key is a media-type
1162 if ( typeof value === 'string' ) {
1163 // back-compat: { <media>: css }
1164 // Ignore 'media' because it isn't supported (nor was it used).
1165 // Strings are pre-wrapped in "@media". The media-type was just ""
1166 // (because it had to be set to something).
1167 // This is one of the reasons why this format is no longer used.
1168 addEmbeddedCSS( value, cssHandle() );
1169 } else {
1170 // back-compat: { <media>: [url, ..] }
1171 media = key;
1172 key = 'bc-url';
1173 }
1174 }
1175
1176 // Array of css strings in key 'css',
1177 // or back-compat array of urls from media-type
1178 if ( Array.isArray( value ) ) {
1179 for ( i = 0; i < value.length; i++ ) {
1180 if ( key === 'bc-url' ) {
1181 // back-compat: { <media>: [url, ..] }
1182 addLink( media, value[ i ] );
1183 } else if ( key === 'css' ) {
1184 // { "css": [css, ..] }
1185 addEmbeddedCSS( value[ i ], cssHandle() );
1186 }
1187 }
1188 // Not an array, but a regular object
1189 // Array of urls inside media-type key
1190 } else if ( typeof value === 'object' ) {
1191 // { "url": { <media>: [url, ..] } }
1192 for ( media in value ) {
1193 urls = value[ media ];
1194 for ( i = 0; i < urls.length; i++ ) {
1195 addLink( media, urls[ i ] );
1196 }
1197 }
1198 }
1199 }
1200 }
1201
1202 // Kick off.
1203 cssHandlesRegistered = true;
1204 checkCssHandles();
1205 }
1206
1207 /**
1208 * Add one or more modules to the module load queue.
1209 *
1210 * See also #work().
1211 *
1212 * @private
1213 * @param {string|string[]} dependencies Module name or array of string module names
1214 * @param {Function} [ready] Callback to execute when all dependencies are ready
1215 * @param {Function} [error] Callback to execute when any dependency fails
1216 */
1217 function enqueue( dependencies, ready, error ) {
1218 // Allow calling by single module name
1219 if ( typeof dependencies === 'string' ) {
1220 dependencies = [ dependencies ];
1221 }
1222
1223 // Add ready and error callbacks if they were given
1224 if ( ready !== undefined || error !== undefined ) {
1225 jobs.push( {
1226 // Narrow down the list to modules that are worth waiting for
1227 dependencies: dependencies.filter( function ( module ) {
1228 var state = mw.loader.getState( module );
1229 return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
1230 } ),
1231 ready: ready,
1232 error: error
1233 } );
1234 }
1235
1236 dependencies.forEach( function ( module ) {
1237 var state = mw.loader.getState( module );
1238 // Only queue modules that are still in the initial 'registered' state
1239 // (not ones already loading, ready or error).
1240 if ( state === 'registered' && queue.indexOf( module ) === -1 ) {
1241 // Private modules must be embedded in the page. Don't bother queuing
1242 // these as the server will deny them anyway (T101806).
1243 if ( registry[ module ].group === 'private' ) {
1244 registry[ module ].state = 'error';
1245 handlePending( module );
1246 return;
1247 }
1248 queue.push( module );
1249 }
1250 } );
1251
1252 mw.loader.work();
1253 }
1254
1255 function sortQuery( o ) {
1256 var key,
1257 sorted = {},
1258 a = [];
1259
1260 for ( key in o ) {
1261 a.push( key );
1262 }
1263 a.sort();
1264 for ( key = 0; key < a.length; key++ ) {
1265 sorted[ a[ key ] ] = o[ a[ key ] ];
1266 }
1267 return sorted;
1268 }
1269
1270 /**
1271 * Converts a module map of the form `{ foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }`
1272 * to a query string of the form `foo.bar,baz|bar.baz,quux`.
1273 *
1274 * See `ResourceLoader::makePackedModulesString()` in PHP, of which this is a port.
1275 * On the server, unpacking is done by `ResourceLoaderContext::expandModuleNames()`.
1276 *
1277 * Note: This is only half of the logic, the other half has to be in #batchRequest(),
1278 * because its implementation needs to keep track of potential string size in order
1279 * to decide when to split the requests due to url size.
1280 *
1281 * @private
1282 * @param {Object} moduleMap Module map
1283 * @return {Object}
1284 * @return {string} return.str Module query string
1285 * @return {Array} return.list List of module names in matching order
1286 */
1287 function buildModulesString( moduleMap ) {
1288 var p, prefix,
1289 str = [],
1290 list = [];
1291
1292 function restore( suffix ) {
1293 return p + suffix;
1294 }
1295
1296 for ( prefix in moduleMap ) {
1297 p = prefix === '' ? '' : prefix + '.';
1298 str.push( p + moduleMap[ prefix ].join( ',' ) );
1299 list.push.apply( list, moduleMap[ prefix ].map( restore ) );
1300 }
1301 return {
1302 str: str.join( '|' ),
1303 list: list
1304 };
1305 }
1306
1307 /**
1308 * Resolve indexed dependencies.
1309 *
1310 * ResourceLoader uses an optimization to save space which replaces module names in
1311 * dependency lists with the index of that module within the array of module
1312 * registration data if it exists. The benefit is a significant reduction in the data
1313 * size of the startup module. This function changes those dependency lists back to
1314 * arrays of strings.
1315 *
1316 * @private
1317 * @param {Array} modules Modules array
1318 */
1319 function resolveIndexedDependencies( modules ) {
1320 var i, j, deps;
1321 function resolveIndex( dep ) {
1322 return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
1323 }
1324 for ( i = 0; i < modules.length; i++ ) {
1325 deps = modules[ i ][ 2 ];
1326 if ( deps ) {
1327 for ( j = 0; j < deps.length; j++ ) {
1328 deps[ j ] = resolveIndex( deps[ j ] );
1329 }
1330 }
1331 }
1332 }
1333
1334 /**
1335 * @private
1336 * @param {Object} params Map of parameter names to values
1337 * @return {string}
1338 */
1339 function makeQueryString( params ) {
1340 return Object.keys( params ).map( function ( key ) {
1341 return encodeURIComponent( key ) + '=' + encodeURIComponent( params[ key ] );
1342 } ).join( '&' );
1343 }
1344
1345 /**
1346 * Create network requests for a batch of modules.
1347 *
1348 * This is an internal method for #work(). This must not be called directly
1349 * unless the modules are already registered, and no request is in progress,
1350 * and the module state has already been set to `loading`.
1351 *
1352 * @private
1353 * @param {string[]} batch
1354 */
1355 function batchRequest( batch ) {
1356 var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
1357 source, group, i, modules, sourceLoadScript,
1358 currReqBase, currReqBaseLength, moduleMap, currReqModules, l,
1359 lastDotIndex, prefix, suffix, bytesAdded;
1360
1361 /**
1362 * Start the currently drafted request to the server.
1363 *
1364 * @ignore
1365 */
1366 function doRequest() {
1367 // Optimisation: Inherit (Object.create), not copy ($.extend)
1368 var query = Object.create( currReqBase ),
1369 packed = buildModulesString( moduleMap );
1370 query.modules = packed.str;
1371 // The packing logic can change the effective order, even if the input was
1372 // sorted. As such, the call to getCombinedVersion() must use this
1373 // effective order, instead of currReqModules, as otherwise the combined
1374 // version will not match the hash expected by the server based on
1375 // combining versions from the module query string in-order. (T188076)
1376 query.version = getCombinedVersion( packed.list );
1377 query = sortQuery( query );
1378 addScript( sourceLoadScript + '?' + makeQueryString( query ) );
1379 }
1380
1381 if ( !batch.length ) {
1382 return;
1383 }
1384
1385 // Always order modules alphabetically to help reduce cache
1386 // misses for otherwise identical content.
1387 batch.sort();
1388
1389 // Query parameters common to all requests
1390 reqBase = {
1391 skin: mw.config.get( 'skin' ),
1392 lang: mw.config.get( 'wgUserLanguage' ),
1393 debug: mw.config.get( 'debug' )
1394 };
1395 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
1396
1397 // Split module list by source and by group.
1398 splits = {};
1399 for ( b = 0; b < batch.length; b++ ) {
1400 bSource = registry[ batch[ b ] ].source;
1401 bGroup = registry[ batch[ b ] ].group;
1402 if ( !hasOwn.call( splits, bSource ) ) {
1403 splits[ bSource ] = {};
1404 }
1405 if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
1406 splits[ bSource ][ bGroup ] = [];
1407 }
1408 bSourceGroup = splits[ bSource ][ bGroup ];
1409 bSourceGroup.push( batch[ b ] );
1410 }
1411
1412 for ( source in splits ) {
1413
1414 sourceLoadScript = sources[ source ];
1415
1416 for ( group in splits[ source ] ) {
1417
1418 // Cache access to currently selected list of
1419 // modules for this group from this source.
1420 modules = splits[ source ][ group ];
1421
1422 // Query parameters common to requests for this module group
1423 // Optimisation: Inherit (Object.create), not copy ($.extend)
1424 currReqBase = Object.create( reqBase );
1425 // User modules require a user name in the query string.
1426 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1427 currReqBase.user = mw.config.get( 'wgUserName' );
1428 }
1429
1430 // In addition to currReqBase, doRequest() will also add 'modules' and 'version'.
1431 // > '&modules='.length === 9
1432 // > '&version=1234567'.length === 16
1433 // > 9 + 16 = 25
1434 currReqBaseLength = makeQueryString( currReqBase ).length + 25;
1435
1436 // We may need to split up the request to honor the query string length limit,
1437 // so build it piece by piece.
1438 l = currReqBaseLength;
1439 moduleMap = {}; // { prefix: [ suffixes ] }
1440 currReqModules = [];
1441
1442 for ( i = 0; i < modules.length; i++ ) {
1443 // Determine how many bytes this module would add to the query string
1444 lastDotIndex = modules[ i ].lastIndexOf( '.' );
1445 // If lastDotIndex is -1, substr() returns an empty string
1446 prefix = modules[ i ].substr( 0, lastDotIndex );
1447 suffix = modules[ i ].slice( lastDotIndex + 1 );
1448 bytesAdded = hasOwn.call( moduleMap, prefix ) ?
1449 suffix.length + 3 : // '%2C'.length == 3
1450 modules[ i ].length + 3; // '%7C'.length == 3
1451
1452 // If the url would become too long, create a new one, but don't create empty requests
1453 if ( maxQueryLength > 0 && currReqModules.length && l + bytesAdded > maxQueryLength ) {
1454 // Dispatch what we've got...
1455 doRequest();
1456 // .. and start again.
1457 l = currReqBaseLength;
1458 moduleMap = {};
1459 currReqModules = [];
1460
1461 mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
1462 }
1463 if ( !hasOwn.call( moduleMap, prefix ) ) {
1464 moduleMap[ prefix ] = [];
1465 }
1466 l += bytesAdded;
1467 moduleMap[ prefix ].push( suffix );
1468 currReqModules.push( modules[ i ] );
1469 }
1470 // If there's anything left in moduleMap, request that too
1471 if ( currReqModules.length ) {
1472 doRequest();
1473 }
1474 }
1475 }
1476 }
1477
1478 /**
1479 * @private
1480 * @param {string[]} implementations Array containing pieces of JavaScript code in the
1481 * form of calls to mw.loader#implement().
1482 * @param {Function} cb Callback in case of failure
1483 * @param {Error} cb.err
1484 */
1485 function asyncEval( implementations, cb ) {
1486 if ( !implementations.length ) {
1487 return;
1488 }
1489 mw.requestIdleCallback( function () {
1490 try {
1491 domEval( implementations.join( ';' ) );
1492 } catch ( err ) {
1493 cb( err );
1494 }
1495 } );
1496 }
1497
1498 /**
1499 * Make a versioned key for a specific module.
1500 *
1501 * @private
1502 * @param {string} module Module name
1503 * @return {string|null} Module key in format '`[name]@[version]`',
1504 * or null if the module does not exist
1505 */
1506 function getModuleKey( module ) {
1507 return hasOwn.call( registry, module ) ?
1508 ( module + '@' + registry[ module ].version ) : null;
1509 }
1510
1511 /**
1512 * @private
1513 * @param {string} key Module name or '`[name]@[version]`'
1514 * @return {Object}
1515 */
1516 function splitModuleKey( key ) {
1517 var index = key.indexOf( '@' );
1518 if ( index === -1 ) {
1519 return {
1520 name: key,
1521 version: ''
1522 };
1523 }
1524 return {
1525 name: key.slice( 0, index ),
1526 version: key.slice( index + 1 )
1527 };
1528 }
1529
1530 /* Public Members */
1531 return {
1532 /**
1533 * The module registry is exposed as an aid for debugging and inspecting page
1534 * state; it is not a public interface for modifying the registry.
1535 *
1536 * @see #registry
1537 * @property
1538 * @private
1539 */
1540 moduleRegistry: registry,
1541
1542 /**
1543 * @inheritdoc #newStyleTag
1544 * @method
1545 */
1546 addStyleTag: newStyleTag,
1547
1548 /**
1549 * Start loading of all queued module dependencies.
1550 *
1551 * @protected
1552 */
1553 work: function () {
1554 var q, batch, implementations, sourceModules;
1555
1556 batch = [];
1557
1558 // Appends a list of modules from the queue to the batch
1559 for ( q = 0; q < queue.length; q++ ) {
1560 // Only load modules which are registered
1561 if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
1562 // Prevent duplicate entries
1563 if ( batch.indexOf( queue[ q ] ) === -1 ) {
1564 batch.push( queue[ q ] );
1565 // Mark registered modules as loading
1566 registry[ queue[ q ] ].state = 'loading';
1567 }
1568 }
1569 }
1570
1571 // Now that the queue has been processed into a batch, clear the queue.
1572 // This MUST happen before we initiate any eval or network request. Otherwise,
1573 // it is possible for a cached script to instantly trigger the same work queue
1574 // again; all before we've cleared it causing each request to include modules
1575 // which are already loaded.
1576 queue = [];
1577
1578 if ( !batch.length ) {
1579 return;
1580 }
1581
1582 mw.loader.store.init();
1583 if ( mw.loader.store.enabled ) {
1584 implementations = [];
1585 sourceModules = [];
1586 batch = batch.filter( function ( module ) {
1587 var implementation = mw.loader.store.get( module );
1588 if ( implementation ) {
1589 implementations.push( implementation );
1590 sourceModules.push( module );
1591 return false;
1592 }
1593 return true;
1594 } );
1595 asyncEval( implementations, function ( err ) {
1596 var failed;
1597 // Not good, the cached mw.loader.implement calls failed! This should
1598 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1599 // Depending on how corrupt the string is, it is likely that some
1600 // modules' implement() succeeded while the ones after the error will
1601 // never run and leave their modules in the 'loading' state forever.
1602 mw.loader.store.stats.failed++;
1603
1604 // Since this is an error not caused by an individual module but by
1605 // something that infected the implement call itself, don't take any
1606 // risks and clear everything in this cache.
1607 mw.loader.store.clear();
1608
1609 mw.trackError( 'resourceloader.exception', {
1610 exception: err,
1611 source: 'store-eval'
1612 } );
1613 // Re-add the failed ones that are still pending back to the batch
1614 failed = sourceModules.filter( function ( module ) {
1615 return registry[ module ].state === 'loading';
1616 } );
1617 batchRequest( failed );
1618 } );
1619 }
1620
1621 batchRequest( batch );
1622 },
1623
1624 /**
1625 * Register a source.
1626 *
1627 * The #work() method will use this information to split up requests by source.
1628 *
1629 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1630 *
1631 * @param {string|Object} id Source ID, or object mapping ids to load urls
1632 * @param {string} loadUrl Url to a load.php end point
1633 * @throws {Error} If source id is already registered
1634 */
1635 addSource: function ( id, loadUrl ) {
1636 var source;
1637 // Allow multiple additions
1638 if ( typeof id === 'object' ) {
1639 for ( source in id ) {
1640 mw.loader.addSource( source, id[ source ] );
1641 }
1642 return;
1643 }
1644
1645 if ( hasOwn.call( sources, id ) ) {
1646 throw new Error( 'source already registered: ' + id );
1647 }
1648
1649 sources[ id ] = loadUrl;
1650 },
1651
1652 /**
1653 * Register a module, letting the system know about it and its properties.
1654 *
1655 * The startup modules contain calls to this method.
1656 *
1657 * When using multiple module registration by passing an array, dependencies that
1658 * are specified as references to modules within the array will be resolved before
1659 * the modules are registered.
1660 *
1661 * @param {string|Array} module Module name or array of arrays, each containing
1662 * a list of arguments compatible with this method
1663 * @param {string|number} version Module version hash (falls backs to empty string)
1664 * Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
1665 * @param {string|Array} dependencies One string or array of strings of module
1666 * names on which this module depends.
1667 * @param {string} [group=null] Group which the module is in
1668 * @param {string} [source='local'] Name of the source
1669 * @param {string} [skip=null] Script body of the skip function
1670 */
1671 register: function ( module, version, dependencies, group, source, skip ) {
1672 var i, deps;
1673 // Allow multiple registration
1674 if ( typeof module === 'object' ) {
1675 resolveIndexedDependencies( module );
1676 for ( i = 0; i < module.length; i++ ) {
1677 // module is an array of module names
1678 if ( typeof module[ i ] === 'string' ) {
1679 mw.loader.register( module[ i ] );
1680 // module is an array of arrays
1681 } else if ( typeof module[ i ] === 'object' ) {
1682 mw.loader.register.apply( mw.loader, module[ i ] );
1683 }
1684 }
1685 return;
1686 }
1687 if ( hasOwn.call( registry, module ) ) {
1688 throw new Error( 'module already registered: ' + module );
1689 }
1690 if ( typeof dependencies === 'string' ) {
1691 // A single module name
1692 deps = [ dependencies ];
1693 } else if ( typeof dependencies === 'object' ) {
1694 // Array of module names
1695 deps = dependencies;
1696 }
1697 // List the module as registered
1698 registry[ module ] = {
1699 // Exposed to execute() for mw.loader.implement() closures.
1700 // Import happens via require().
1701 module: {
1702 exports: {}
1703 },
1704 version: version !== undefined ? String( version ) : '',
1705 dependencies: deps || [],
1706 group: typeof group === 'string' ? group : null,
1707 source: typeof source === 'string' ? source : 'local',
1708 state: 'registered',
1709 skip: typeof skip === 'string' ? skip : null
1710 };
1711 },
1712
1713 /**
1714 * Implement a module given the components that make up the module.
1715 *
1716 * When #load() or #using() requests one or more modules, the server
1717 * response contain calls to this function.
1718 *
1719 * @param {string} module Name of module and current module version. Formatted
1720 * as '`[name]@[version]`". This version should match the requested version
1721 * (from #batchRequest and #registry). This avoids race conditions (T117587).
1722 * For back-compat with MediaWiki 1.27 and earlier, the version may be omitted.
1723 * @param {Function|Array|string} [script] Function with module code, list of URLs
1724 * to load via `<script src>`, or string of module code for `$.globalEval()`.
1725 * @param {Object} [style] Should follow one of the following patterns:
1726 *
1727 * { "css": [css, ..] }
1728 * { "url": { <media>: [url, ..] } }
1729 *
1730 * And for backwards compatibility (needs to be supported forever due to caching):
1731 *
1732 * { <media>: css }
1733 * { <media>: [url, ..] }
1734 *
1735 * The reason css strings are not concatenated anymore is T33676. We now check
1736 * whether it's safe to extend the stylesheet.
1737 *
1738 * @protected
1739 * @param {Object} [messages] List of key/value pairs to be added to mw#messages.
1740 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1741 */
1742 implement: function ( module, script, style, messages, templates ) {
1743 var split = splitModuleKey( module ),
1744 name = split.name,
1745 version = split.version;
1746 // Automatically register module
1747 if ( !hasOwn.call( registry, name ) ) {
1748 mw.loader.register( name );
1749 }
1750 // Check for duplicate implementation
1751 if ( hasOwn.call( registry, name ) && registry[ name ].script !== undefined ) {
1752 throw new Error( 'module already implemented: ' + name );
1753 }
1754 if ( version ) {
1755 // Without this reset, if there is a version mismatch between the
1756 // requested and received module version, then mw.loader.store would
1757 // cache the response under the requested key. Thus poisoning the cache
1758 // indefinitely with a stale value. (T117587)
1759 registry[ name ].version = version;
1760 }
1761 // Attach components
1762 registry[ name ].script = script || null;
1763 registry[ name ].style = style || null;
1764 registry[ name ].messages = messages || null;
1765 registry[ name ].templates = templates || null;
1766 // The module may already have been marked as erroneous
1767 if ( registry[ name ].state !== 'error' && registry[ name ].state !== 'missing' ) {
1768 registry[ name ].state = 'loaded';
1769 if ( allReady( registry[ name ].dependencies ) ) {
1770 execute( name );
1771 }
1772 }
1773 },
1774
1775 /**
1776 * Execute a function as soon as one or more required modules are ready.
1777 *
1778 * Example of inline dependency on OOjs:
1779 *
1780 * mw.loader.using( 'oojs', function () {
1781 * OO.compare( [ 1 ], [ 1 ] );
1782 * } );
1783 *
1784 * Example of inline dependency obtained via `require()`:
1785 *
1786 * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
1787 * var util = require( 'mediawiki.util' );
1788 * } );
1789 *
1790 * Since MediaWiki 1.23 this also returns a promise.
1791 *
1792 * Since MediaWiki 1.28 the promise is resolved with a `require` function.
1793 *
1794 * @param {string|Array} dependencies Module name or array of modules names the
1795 * callback depends on to be ready before executing
1796 * @param {Function} [ready] Callback to execute when all dependencies are ready
1797 * @param {Function} [error] Callback to execute if one or more dependencies failed
1798 * @return {jQuery.Promise} With a `require` function
1799 */
1800 using: function ( dependencies, ready, error ) {
1801 var deferred = $.Deferred();
1802
1803 // Allow calling with a single dependency as a string
1804 if ( typeof dependencies === 'string' ) {
1805 dependencies = [ dependencies ];
1806 }
1807
1808 if ( ready ) {
1809 deferred.done( ready );
1810 }
1811 if ( error ) {
1812 deferred.fail( error );
1813 }
1814
1815 try {
1816 // Resolve entire dependency map
1817 dependencies = resolve( dependencies );
1818 } catch ( e ) {
1819 return deferred.reject( e ).promise();
1820 }
1821 if ( allReady( dependencies ) ) {
1822 // Run ready immediately
1823 deferred.resolve( mw.loader.require );
1824 } else if ( anyFailed( dependencies ) ) {
1825 // Execute error immediately if any dependencies have errors
1826 deferred.reject(
1827 new Error( 'One or more dependencies failed to load' ),
1828 dependencies
1829 );
1830 } else {
1831 // Not all dependencies are ready, add to the load queue
1832 enqueue( dependencies, function () {
1833 deferred.resolve( mw.loader.require );
1834 }, deferred.reject );
1835 }
1836
1837 return deferred.promise();
1838 },
1839
1840 /**
1841 * Load an external script or one or more modules.
1842 *
1843 * This method takes a list of unrelated modules. Use cases:
1844 *
1845 * - A web page will be composed of many different widgets. These widgets independently
1846 * queue their ResourceLoader modules (`OutputPage::addModules()`). If any of them
1847 * have problems, or are no longer known (e.g. cached HTML), the other modules
1848 * should still be loaded.
1849 * - This method is used for preloading, which must not throw. Later code that
1850 * calls #using() will handle the error.
1851 *
1852 * @param {string|Array} modules Either the name of a module, array of modules,
1853 * or a URL of an external script or style
1854 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1855 * external script or style; acceptable values are "text/css" and
1856 * "text/javascript"; if no type is provided, text/javascript is assumed.
1857 */
1858 load: function ( modules, type ) {
1859 var filtered, l;
1860
1861 // Allow calling with a url or single dependency as a string
1862 if ( typeof modules === 'string' ) {
1863 // "https://example.org/x.js", "http://example.org/x.js", "//example.org/x.js", "/x.js"
1864 if ( /^(https?:)?\/?\//.test( modules ) ) {
1865 if ( type === 'text/css' ) {
1866 l = document.createElement( 'link' );
1867 l.rel = 'stylesheet';
1868 l.href = modules;
1869 document.head.appendChild( l );
1870 return;
1871 }
1872 if ( type === 'text/javascript' || type === undefined ) {
1873 addScript( modules );
1874 return;
1875 }
1876 // Unknown type
1877 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1878 }
1879 // Called with single module
1880 modules = [ modules ];
1881 }
1882
1883 // Filter out top-level modules that are unknown or failed to load before.
1884 filtered = modules.filter( function ( module ) {
1885 var state = mw.loader.getState( module );
1886 return state !== 'error' && state !== 'missing';
1887 } );
1888 // Resolve remaining list using the known dependency tree.
1889 // This also filters out modules with unknown dependencies. (T36853)
1890 filtered = resolveStubbornly( filtered );
1891 // If all modules are ready, or if any modules have errors, nothing to be done.
1892 if ( allReady( filtered ) || anyFailed( filtered ) ) {
1893 return;
1894 }
1895 if ( filtered.length === 0 ) {
1896 return;
1897 }
1898 // Some modules are not yet ready, add to module load queue.
1899 enqueue( filtered, undefined, undefined );
1900 },
1901
1902 /**
1903 * Change the state of one or more modules.
1904 *
1905 * @param {string|Object} module Module name or object of module name/state pairs
1906 * @param {string} state State name
1907 */
1908 state: function ( module, state ) {
1909 var m;
1910
1911 if ( typeof module === 'object' ) {
1912 for ( m in module ) {
1913 mw.loader.state( m, module[ m ] );
1914 }
1915 return;
1916 }
1917 if ( !hasOwn.call( registry, module ) ) {
1918 mw.loader.register( module );
1919 }
1920 registry[ module ].state = state;
1921 if ( state === 'ready' || state === 'error' || state === 'missing' ) {
1922 // Make sure pending modules depending on this one get executed if their
1923 // dependencies are now fulfilled!
1924 handlePending( module );
1925 }
1926 },
1927
1928 /**
1929 * Get the version of a module.
1930 *
1931 * @param {string} module Name of module
1932 * @return {string|null} The version, or null if the module (or its version) is not
1933 * in the registry.
1934 */
1935 getVersion: function ( module ) {
1936 return hasOwn.call( registry, module ) ? registry[ module ].version : null;
1937 },
1938
1939 /**
1940 * Get the state of a module.
1941 *
1942 * @param {string} module Name of module
1943 * @return {string|null} The state, or null if the module (or its state) is not
1944 * in the registry.
1945 */
1946 getState: function ( module ) {
1947 return hasOwn.call( registry, module ) ? registry[ module ].state : null;
1948 },
1949
1950 /**
1951 * Get the names of all registered modules.
1952 *
1953 * @return {Array}
1954 */
1955 getModuleNames: function () {
1956 return Object.keys( registry );
1957 },
1958
1959 /**
1960 * Get the exported value of a module.
1961 *
1962 * This static method is publicly exposed for debugging purposes
1963 * only and must not be used in production code. In production code,
1964 * please use the dynamically provided `require()` function instead.
1965 *
1966 * In case of lazy-loaded modules via mw.loader#using(), the returned
1967 * Promise provides the function, see #using() for examples.
1968 *
1969 * @private
1970 * @since 1.27
1971 * @param {string} moduleName Module name
1972 * @return {Mixed} Exported value
1973 */
1974 require: function ( moduleName ) {
1975 var state = mw.loader.getState( moduleName );
1976
1977 // Only ready modules can be required
1978 if ( state !== 'ready' ) {
1979 // Module may've forgotten to declare a dependency
1980 throw new Error( 'Module "' + moduleName + '" is not loaded.' );
1981 }
1982
1983 return registry[ moduleName ].module.exports;
1984 },
1985
1986 /**
1987 * On browsers that implement the localStorage API, the module store serves as a
1988 * smart complement to the browser cache. Unlike the browser cache, the module store
1989 * can slice a concatenated response from ResourceLoader into its constituent
1990 * modules and cache each of them separately, using each module's versioning scheme
1991 * to determine when the cache should be invalidated.
1992 *
1993 * @singleton
1994 * @class mw.loader.store
1995 */
1996 store: {
1997 // Whether the store is in use on this page.
1998 enabled: null,
1999
2000 // Modules whose string representation exceeds 100 kB are
2001 // ineligible for storage. See bug T66721.
2002 MODULE_SIZE_MAX: 100 * 1000,
2003
2004 // The contents of the store, mapping '[name]@[version]' keys
2005 // to module implementations.
2006 items: {},
2007
2008 // Cache hit stats
2009 stats: { hits: 0, misses: 0, expired: 0, failed: 0 },
2010
2011 /**
2012 * Construct a JSON-serializable object representing the content of the store.
2013 *
2014 * @return {Object} Module store contents.
2015 */
2016 toJSON: function () {
2017 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
2018 },
2019
2020 /**
2021 * Get the localStorage key for the entire module store. The key references
2022 * $wgDBname to prevent clashes between wikis which share a common host.
2023 *
2024 * @return {string} localStorage item key
2025 */
2026 getStoreKey: function () {
2027 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
2028 },
2029
2030 /**
2031 * Get a key on which to vary the module cache.
2032 *
2033 * @return {string} String of concatenated vary conditions.
2034 */
2035 getVary: function () {
2036 return [
2037 mw.config.get( 'skin' ),
2038 mw.config.get( 'wgResourceLoaderStorageVersion' ),
2039 mw.config.get( 'wgUserLanguage' )
2040 ].join( ':' );
2041 },
2042
2043 /**
2044 * Initialize the store.
2045 *
2046 * Retrieves store from localStorage and (if successfully retrieved) decoding
2047 * the stored JSON value to a plain object.
2048 *
2049 * The try / catch block is used for JSON & localStorage feature detection.
2050 * See the in-line documentation for Modernizr's localStorage feature detection
2051 * code for a full account of why we need a try / catch:
2052 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2053 */
2054 init: function () {
2055 var raw, data;
2056
2057 if ( mw.loader.store.enabled !== null ) {
2058 // Init already ran
2059 return;
2060 }
2061
2062 if (
2063 // Disabled because localStorage quotas are tight and (in Firefox's case)
2064 // shared by multiple origins.
2065 // See T66721, and <https://bugzilla.mozilla.org/show_bug.cgi?id=1064466>.
2066 /Firefox|Opera/.test( navigator.userAgent ) ||
2067
2068 // Disabled by configuration.
2069 !mw.config.get( 'wgResourceLoaderStorageEnabled' )
2070 ) {
2071 // Clear any previous store to free up space. (T66721)
2072 mw.loader.store.clear();
2073 mw.loader.store.enabled = false;
2074 return;
2075 }
2076 if ( mw.config.get( 'debug' ) ) {
2077 // Disable module store in debug mode
2078 mw.loader.store.enabled = false;
2079 return;
2080 }
2081
2082 try {
2083 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2084 // If we get here, localStorage is available; mark enabled
2085 mw.loader.store.enabled = true;
2086 data = JSON.parse( raw );
2087 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2088 mw.loader.store.items = data.items;
2089 return;
2090 }
2091 } catch ( e ) {
2092 mw.trackError( 'resourceloader.exception', {
2093 exception: e,
2094 source: 'store-localstorage-init'
2095 } );
2096 }
2097
2098 if ( raw === undefined ) {
2099 // localStorage failed; disable store
2100 mw.loader.store.enabled = false;
2101 } else {
2102 mw.loader.store.update();
2103 }
2104 },
2105
2106 /**
2107 * Retrieve a module from the store and update cache hit stats.
2108 *
2109 * @param {string} module Module name
2110 * @return {string|boolean} Module implementation or false if unavailable
2111 */
2112 get: function ( module ) {
2113 var key;
2114
2115 if ( !mw.loader.store.enabled ) {
2116 return false;
2117 }
2118
2119 key = getModuleKey( module );
2120 if ( key in mw.loader.store.items ) {
2121 mw.loader.store.stats.hits++;
2122 return mw.loader.store.items[ key ];
2123 }
2124 mw.loader.store.stats.misses++;
2125 return false;
2126 },
2127
2128 /**
2129 * Stringify a module and queue it for storage.
2130 *
2131 * @param {string} module Module name
2132 * @param {Object} descriptor The module's descriptor as set in the registry
2133 * @return {boolean} Module was set
2134 */
2135 set: function ( module, descriptor ) {
2136 var args, key, src;
2137
2138 if ( !mw.loader.store.enabled ) {
2139 return false;
2140 }
2141
2142 key = getModuleKey( module );
2143
2144 if (
2145 // Already stored a copy of this exact version
2146 key in mw.loader.store.items ||
2147 // Module failed to load
2148 descriptor.state !== 'ready' ||
2149 // Unversioned, private, or site-/user-specific
2150 !descriptor.version ||
2151 descriptor.group === 'private' ||
2152 descriptor.group === 'user' ||
2153 // Partial descriptor
2154 // (e.g. skipped module, or style module with state=ready)
2155 [ descriptor.script, descriptor.style, descriptor.messages,
2156 descriptor.templates ].indexOf( undefined ) !== -1
2157 ) {
2158 // Decline to store
2159 return false;
2160 }
2161
2162 try {
2163 args = [
2164 JSON.stringify( key ),
2165 typeof descriptor.script === 'function' ?
2166 String( descriptor.script ) :
2167 JSON.stringify( descriptor.script ),
2168 JSON.stringify( descriptor.style ),
2169 JSON.stringify( descriptor.messages ),
2170 JSON.stringify( descriptor.templates )
2171 ];
2172 // Attempted workaround for a possible Opera bug (bug T59567).
2173 // This regex should never match under sane conditions.
2174 if ( /^\s*\(/.test( args[ 1 ] ) ) {
2175 args[ 1 ] = 'function' + args[ 1 ];
2176 mw.trackError( 'resourceloader.assert', { source: 'bug-T59567' } );
2177 }
2178 } catch ( e ) {
2179 mw.trackError( 'resourceloader.exception', {
2180 exception: e,
2181 source: 'store-localstorage-json'
2182 } );
2183 return false;
2184 }
2185
2186 src = 'mw.loader.implement(' + args.join( ',' ) + ');';
2187 if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
2188 return false;
2189 }
2190 mw.loader.store.items[ key ] = src;
2191 mw.loader.store.update();
2192 return true;
2193 },
2194
2195 /**
2196 * Iterate through the module store, removing any item that does not correspond
2197 * (in name and version) to an item in the module registry.
2198 *
2199 * @return {boolean} Store was pruned
2200 */
2201 prune: function () {
2202 var key, module;
2203
2204 if ( !mw.loader.store.enabled ) {
2205 return false;
2206 }
2207
2208 for ( key in mw.loader.store.items ) {
2209 module = key.slice( 0, key.indexOf( '@' ) );
2210 if ( getModuleKey( module ) !== key ) {
2211 mw.loader.store.stats.expired++;
2212 delete mw.loader.store.items[ key ];
2213 } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
2214 // This value predates the enforcement of a size limit on cached modules.
2215 delete mw.loader.store.items[ key ];
2216 }
2217 }
2218 return true;
2219 },
2220
2221 /**
2222 * Clear the entire module store right now.
2223 */
2224 clear: function () {
2225 mw.loader.store.items = {};
2226 try {
2227 localStorage.removeItem( mw.loader.store.getStoreKey() );
2228 } catch ( ignored ) {}
2229 },
2230
2231 /**
2232 * Sync in-memory store back to localStorage.
2233 *
2234 * This function debounces updates. When called with a flush already pending,
2235 * the call is coalesced into the pending update. The call to
2236 * localStorage.setItem will be naturally deferred until the page is quiescent.
2237 *
2238 * Because localStorage is shared by all pages from the same origin, if multiple
2239 * pages are loaded with different module sets, the possibility exists that
2240 * modules saved by one page will be clobbered by another. But the impact would
2241 * be minor and the problem would be corrected by subsequent page views.
2242 *
2243 * @method
2244 */
2245 update: ( function () {
2246 var hasPendingWrite = false;
2247
2248 function flushWrites() {
2249 var data, key;
2250 if ( !hasPendingWrite || !mw.loader.store.enabled ) {
2251 return;
2252 }
2253
2254 mw.loader.store.prune();
2255 key = mw.loader.store.getStoreKey();
2256 try {
2257 // Replacing the content of the module store might fail if the new
2258 // contents would exceed the browser's localStorage size limit. To
2259 // avoid clogging the browser with stale data, always remove the old
2260 // value before attempting to set the new one.
2261 localStorage.removeItem( key );
2262 data = JSON.stringify( mw.loader.store );
2263 localStorage.setItem( key, data );
2264 } catch ( e ) {
2265 mw.trackError( 'resourceloader.exception', {
2266 exception: e,
2267 source: 'store-localstorage-update'
2268 } );
2269 }
2270
2271 hasPendingWrite = false;
2272 }
2273
2274 return function () {
2275 if ( !hasPendingWrite ) {
2276 hasPendingWrite = true;
2277 mw.requestIdleCallback( flushWrites );
2278 }
2279 };
2280 }() )
2281 }
2282 };
2283 }() ),
2284
2285 /**
2286 * HTML construction helper functions
2287 *
2288 * @example
2289 *
2290 * var Html, output;
2291 *
2292 * Html = mw.html;
2293 * output = Html.element( 'div', {}, new Html.Raw(
2294 * Html.element( 'img', { src: '<' } )
2295 * ) );
2296 * mw.log( output ); // <div><img src="&lt;"/></div>
2297 *
2298 * @class mw.html
2299 * @singleton
2300 */
2301 html: ( function () {
2302 function escapeCallback( s ) {
2303 switch ( s ) {
2304 case '\'':
2305 return '&#039;';
2306 case '"':
2307 return '&quot;';
2308 case '<':
2309 return '&lt;';
2310 case '>':
2311 return '&gt;';
2312 case '&':
2313 return '&amp;';
2314 }
2315 }
2316
2317 return {
2318 /**
2319 * Escape a string for HTML.
2320 *
2321 * Converts special characters to HTML entities.
2322 *
2323 * mw.html.escape( '< > \' & "' );
2324 * // Returns &lt; &gt; &#039; &amp; &quot;
2325 *
2326 * @param {string} s The string to escape
2327 * @return {string} HTML
2328 */
2329 escape: function ( s ) {
2330 return s.replace( /['"<>&]/g, escapeCallback );
2331 },
2332
2333 /**
2334 * Create an HTML element string, with safe escaping.
2335 *
2336 * @param {string} name The tag name.
2337 * @param {Object} [attrs] An object with members mapping element names to values
2338 * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
2339 *
2340 * - string: Text to be escaped.
2341 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
2342 * - this.Raw: The raw value is directly included.
2343 * - this.Cdata: The raw value is directly included. An exception is
2344 * thrown if it contains any illegal ETAGO delimiter.
2345 * See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
2346 * @return {string} HTML
2347 */
2348 element: function ( name, attrs, contents ) {
2349 var v, attrName, s = '<' + name;
2350
2351 if ( attrs ) {
2352 for ( attrName in attrs ) {
2353 v = attrs[ attrName ];
2354 // Convert name=true, to name=name
2355 if ( v === true ) {
2356 v = attrName;
2357 // Skip name=false
2358 } else if ( v === false ) {
2359 continue;
2360 }
2361 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2362 }
2363 }
2364 if ( contents === undefined || contents === null ) {
2365 // Self close tag
2366 s += '/>';
2367 return s;
2368 }
2369 // Regular open tag
2370 s += '>';
2371 switch ( typeof contents ) {
2372 case 'string':
2373 // Escaped
2374 s += this.escape( contents );
2375 break;
2376 case 'number':
2377 case 'boolean':
2378 // Convert to string
2379 s += String( contents );
2380 break;
2381 default:
2382 if ( contents instanceof this.Raw ) {
2383 // Raw HTML inclusion
2384 s += contents.value;
2385 } else if ( contents instanceof this.Cdata ) {
2386 // CDATA
2387 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2388 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2389 }
2390 s += contents.value;
2391 } else {
2392 throw new Error( 'mw.html.element: Invalid type of contents' );
2393 }
2394 }
2395 s += '</' + name + '>';
2396 return s;
2397 },
2398
2399 /**
2400 * Wrapper object for raw HTML passed to mw.html.element().
2401 *
2402 * @class mw.html.Raw
2403 * @constructor
2404 * @param {string} value
2405 */
2406 Raw: function ( value ) {
2407 this.value = value;
2408 },
2409
2410 /**
2411 * Wrapper object for CDATA element contents passed to mw.html.element()
2412 *
2413 * @class mw.html.Cdata
2414 * @constructor
2415 * @param {string} value
2416 */
2417 Cdata: function ( value ) {
2418 this.value = value;
2419 }
2420 };
2421 }() ),
2422
2423 // Skeleton user object, extended by the 'mediawiki.user' module.
2424 /**
2425 * @class mw.user
2426 * @singleton
2427 */
2428 user: {
2429 /**
2430 * @property {mw.Map}
2431 */
2432 options: new Map(),
2433 /**
2434 * @property {mw.Map}
2435 */
2436 tokens: new Map()
2437 },
2438
2439 // OOUI widgets specific to MediaWiki
2440 widgets: {},
2441
2442 /**
2443 * Registry and firing of events.
2444 *
2445 * MediaWiki has various interface components that are extended, enhanced
2446 * or manipulated in some other way by extensions, gadgets and even
2447 * in core itself.
2448 *
2449 * This framework helps streamlining the timing of when these other
2450 * code paths fire their plugins (instead of using document-ready,
2451 * which can and should be limited to firing only once).
2452 *
2453 * Features like navigating to other wiki pages, previewing an edit
2454 * and editing itself – without a refresh – can then retrigger these
2455 * hooks accordingly to ensure everything still works as expected.
2456 *
2457 * Example usage:
2458 *
2459 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2460 * mw.hook( 'wikipage.content' ).fire( $content );
2461 *
2462 * Handlers can be added and fired for arbitrary event names at any time. The same
2463 * event can be fired multiple times. The last run of an event is memorized
2464 * (similar to `$(document).ready` and `$.Deferred().done`).
2465 * This means if an event is fired, and a handler added afterwards, the added
2466 * function will be fired right away with the last given event data.
2467 *
2468 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2469 * Thus allowing flexible use and optimal maintainability and authority control.
2470 * You can pass around the `add` and/or `fire` method to another piece of code
2471 * without it having to know the event name (or `mw.hook` for that matter).
2472 *
2473 * var h = mw.hook( 'bar.ready' );
2474 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2475 *
2476 * Note: Events are documented with an underscore instead of a dot in the event
2477 * name due to jsduck not supporting dots in that position.
2478 *
2479 * @class mw.hook
2480 */
2481 hook: ( function () {
2482 var lists = {};
2483
2484 /**
2485 * Create an instance of mw.hook.
2486 *
2487 * @method hook
2488 * @member mw
2489 * @param {string} name Name of hook.
2490 * @return {mw.hook}
2491 */
2492 return function ( name ) {
2493 var list = hasOwn.call( lists, name ) ?
2494 lists[ name ] :
2495 lists[ name ] = $.Callbacks( 'memory' );
2496
2497 return {
2498 /**
2499 * Register a hook handler
2500 *
2501 * @param {...Function} handler Function to bind.
2502 * @chainable
2503 */
2504 add: list.add,
2505
2506 /**
2507 * Unregister a hook handler
2508 *
2509 * @param {...Function} handler Function to unbind.
2510 * @chainable
2511 */
2512 remove: list.remove,
2513
2514 /**
2515 * Run a hook.
2516 *
2517 * @param {...Mixed} data
2518 * @return {mw.hook}
2519 * @chainable
2520 */
2521 fire: function () {
2522 return list.fireWith.call( this, null, slice.call( arguments ) );
2523 }
2524 };
2525 };
2526 }() )
2527 };
2528
2529 // Alias $j to jQuery for backwards compatibility
2530 // @deprecated since 1.23 Use $ or jQuery instead
2531 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2532
2533 // Attach to window and globally alias
2534 window.mw = window.mediaWiki = mw;
2535 }( jQuery ) );