-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
mediawiki.js
2761 lines (2529 loc) · 85.8 KB
/
mediawiki.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Base library for MediaWiki.
*
* Exposed globally as `mediaWiki` with `mw` as shortcut.
*
* @class mw
* @alternateClassName mediaWiki
* @singleton
*/
/*jshint latedef:false */
( function ( $ ) {
'use strict';
var mw, StringSet, log,
hasOwn = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
trackCallbacks = $.Callbacks( 'memory' ),
trackHandlers = [],
trackQueue = [];
/**
* FNV132 hash function
*
* This function implements the 32-bit version of FNV-1.
* It is equivalent to hash( 'fnv132', ... ) in PHP, except
* its output is base 36 rather than hex.
* See <https://en.wikipedia.org/wiki/FNV_hash_function>
*
* @private
* @param {string} str String to hash
* @return {string} hash as an seven-character base 36 string
*/
function fnv132( str ) {
/*jshint bitwise:false */
var hash = 0x811C9DC5,
i;
for ( i = 0; i < str.length; i++ ) {
hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
hash ^= str.charCodeAt( i );
}
hash = ( hash >>> 0 ).toString( 36 );
while ( hash.length < 7 ) {
hash = '0' + hash;
}
return hash;
}
StringSet = window.Set || ( function () {
/**
* @private
* @class
*/
function StringSet() {
this.set = {};
}
StringSet.prototype.add = function ( value ) {
this.set[ value ] = true;
};
StringSet.prototype.has = function ( value ) {
return this.set.hasOwnProperty( value );
};
return StringSet;
}() );
/**
* Create an object that can be read from or written to via methods that allow
* interaction both with single and multiple properties at once.
*
* @private
* @class mw.Map
*
* @constructor
* @param {boolean} [global=false] Whether to synchronise =values to the global
* window object (for backwards-compatibility with mw.config; T72470). Values are
* copied in one direction only. Changes to globals do not reflect in the map.
*/
function Map( global ) {
this.internalValues = {};
if ( global === true ) {
// Override #set to also set the global variable
this.set = function ( selection, value ) {
var s;
if ( $.isPlainObject( selection ) ) {
for ( s in selection ) {
setGlobalMapValue( this, s, selection[ s ] );
}
return true;
}
if ( typeof selection === 'string' && arguments.length ) {
setGlobalMapValue( this, selection, value );
return true;
}
return false;
};
}
// Deprecated since MediaWiki 1.28
log.deprecate(
this,
'values',
this.internalValues,
'mw.Map#values is deprecated. Use mw.Map#get() instead.',
'Map-values'
);
}
/**
* Alias property to the global object.
*
* @private
* @static
* @param {mw.Map} map
* @param {string} key
* @param {Mixed} value
*/
function setGlobalMapValue( map, key, value ) {
map.internalValues[ key ] = value;
log.deprecate(
window,
key,
value,
// Deprecation notice for mw.config globals (T58550, T72470)
map === mw.config && 'Use mw.config instead.'
);
}
Map.prototype = {
constructor: Map,
/**
* Get the value of one or more keys.
*
* If called with no arguments, all values are returned.
*
* @param {string|Array} [selection] Key or array of keys to retrieve values for.
* @param {Mixed} [fallback=null] Value for keys that don't exist.
* @return {Mixed|Object| null} If selection was a string, returns the value,
* If selection was an array, returns an object of key/values.
* If no selection is passed, the internal container is returned. (Beware that,
* as is the default in JavaScript, the object is returned by reference.)
*/
get: function ( selection, fallback ) {
var results, i;
// If we only do this in the `return` block, it'll fail for the
// call to get() from the mutli-selection block.
fallback = arguments.length > 1 ? fallback : null;
if ( $.isArray( selection ) ) {
selection = slice.call( selection );
results = {};
for ( i = 0; i < selection.length; i++ ) {
results[ selection[ i ] ] = this.get( selection[ i ], fallback );
}
return results;
}
if ( typeof selection === 'string' ) {
if ( !hasOwn.call( this.internalValues, selection ) ) {
return fallback;
}
return this.internalValues[ selection ];
}
if ( selection === undefined ) {
return this.internalValues;
}
// Invalid selection key
return null;
},
/**
* Set one or more key/value pairs.
*
* @param {string|Object} selection Key to set value for, or object mapping keys to values
* @param {Mixed} [value] Value to set (optional, only in use when key is a string)
* @return {boolean} True on success, false on failure
*/
set: function ( selection, value ) {
var s;
if ( $.isPlainObject( selection ) ) {
for ( s in selection ) {
this.internalValues[ s ] = selection[ s ];
}
return true;
}
if ( typeof selection === 'string' && arguments.length > 1 ) {
this.internalValues[ selection ] = value;
return true;
}
return false;
},
/**
* Check if one or more keys exist.
*
* @param {Mixed} selection Key or array of keys to check
* @return {boolean} True if the key(s) exist
*/
exists: function ( selection ) {
var s;
if ( $.isArray( selection ) ) {
for ( s = 0; s < selection.length; s++ ) {
if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.internalValues, selection[ s ] ) ) {
return false;
}
}
return true;
}
return typeof selection === 'string' && hasOwn.call( this.internalValues, selection );
}
};
/**
* Object constructor for messages.
*
* Similar to the Message class in MediaWiki PHP.
*
* Format defaults to 'text'.
*
* @example
*
* var obj, str;
* mw.messages.set( {
* 'hello': 'Hello world',
* 'hello-user': 'Hello, $1!',
* 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
* } );
*
* obj = new mw.Message( mw.messages, 'hello' );
* mw.log( obj.text() );
* // Hello world
*
* obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
* mw.log( obj.text() );
* // Hello, John Doe!
*
* obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
* mw.log( obj.text() );
* // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
*
* // Using mw.message shortcut
* obj = mw.message( 'hello-user', 'John Doe' );
* mw.log( obj.text() );
* // Hello, John Doe!
*
* // Using mw.msg shortcut
* str = mw.msg( 'hello-user', 'John Doe' );
* mw.log( str );
* // Hello, John Doe!
*
* // Different formats
* obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
*
* obj.format = 'text';
* str = obj.toString();
* // Same as:
* str = obj.text();
*
* mw.log( str );
* // Hello, John "Wiki" <3 Doe!
*
* mw.log( obj.escaped() );
* // Hello, John "Wiki" <3 Doe!
*
* @class mw.Message
*
* @constructor
* @param {mw.Map} map Message store
* @param {string} key
* @param {Array} [parameters]
*/
function Message( map, key, parameters ) {
this.format = 'text';
this.map = map;
this.key = key;
this.parameters = parameters === undefined ? [] : slice.call( parameters );
return this;
}
Message.prototype = {
/**
* Get parsed contents of the message.
*
* The default parser does simple $N replacements and nothing else.
* This may be overridden to provide a more complex message parser.
* The primary override is in the mediawiki.jqueryMsg module.
*
* This function will not be called for nonexistent messages.
*
* @return {string} Parsed message
*/
parser: function () {
return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
},
/**
* Add (does not replace) parameters for `$N` placeholder values.
*
* @param {Array} parameters
* @chainable
*/
params: function ( parameters ) {
var i;
for ( i = 0; i < parameters.length; i++ ) {
this.parameters.push( parameters[ i ] );
}
return this;
},
/**
* Convert message object to its string form based on current format.
*
* @return {string} Message as a string in the current form, or `<key>` if key
* does not exist.
*/
toString: function () {
var text;
if ( !this.exists() ) {
// Use <key> as text if key does not exist
if ( this.format === 'escaped' || this.format === 'parse' ) {
// format 'escaped' and 'parse' need to have the brackets and key html escaped
return mw.html.escape( '<' + this.key + '>' );
}
return '<' + this.key + '>';
}
if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
text = this.parser();
}
if ( this.format === 'escaped' ) {
text = this.parser();
text = mw.html.escape( text );
}
return text;
},
/**
* Change format to 'parse' and convert message to string
*
* If jqueryMsg is loaded, this parses the message text from wikitext
* (where supported) to HTML
*
* Otherwise, it is equivalent to plain.
*
* @return {string} String form of parsed message
*/
parse: function () {
this.format = 'parse';
return this.toString();
},
/**
* Change format to 'plain' and convert message to string
*
* This substitutes parameters, but otherwise does not change the
* message text.
*
* @return {string} String form of plain message
*/
plain: function () {
this.format = 'plain';
return this.toString();
},
/**
* Change format to 'text' and convert message to string
*
* If jqueryMsg is loaded, {{-transformation is done where supported
* (such as {{plural:}}, {{gender:}}, {{int:}}).
*
* Otherwise, it is equivalent to plain
*
* @return {string} String form of text message
*/
text: function () {
this.format = 'text';
return this.toString();
},
/**
* Change the format to 'escaped' and convert message to string
*
* This is equivalent to using the 'text' format (see #text), then
* HTML-escaping the output.
*
* @return {string} String form of html escaped message
*/
escaped: function () {
this.format = 'escaped';
return this.toString();
},
/**
* Check if a message exists
*
* @see mw.Map#exists
* @return {boolean}
*/
exists: function () {
return this.map.exists( this.key );
}
};
log = ( function () {
// Also update the restoration of methods in mediawiki.log.js
// when adding or removing methods here.
var log = function () {},
console = window.console;
/**
* @class mw.log
* @singleton
*/
/**
* Write a message to the console's warning channel.
* Actions not supported by the browser console are silently ignored.
*
* @param {...string} msg Messages to output to console
*/
log.warn = console && console.warn && Function.prototype.bind ?
Function.prototype.bind.call( console.warn, console ) :
$.noop;
/**
* Write a message to the console's error channel.
*
* Most browsers provide a stacktrace by default if the argument
* is a caught Error object.
*
* @since 1.26
* @param {Error|...string} msg Messages to output to console
*/
log.error = console && console.error && Function.prototype.bind ?
Function.prototype.bind.call( console.error, console ) :
$.noop;
/**
* Create a property in a host object that, when accessed, will produce
* a deprecation warning in the console.
*
* @param {Object} obj Host object of deprecated property
* @param {string} key Name of property to create in `obj`
* @param {Mixed} val The value this property should return when accessed
* @param {string} [msg] Optional text to include in the deprecation message
* @param {string} [logName=key] Optional custom name for the feature.
* This is used instead of `key` in the message and `mw.deprecate` tracking.
*/
log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
obj[ key ] = val;
} : function ( obj, key, val, msg, logName ) {
logName = logName || key;
msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
var logged = new StringSet();
function uniqueTrace() {
var trace = new Error().stack;
if ( logged.has( trace ) ) {
return false;
}
logged.add( trace );
return true;
}
// Support: Safari 5.0
// Throws "not supported on DOM Objects" for Node or Element objects (incl. document)
// Safari 4.0 doesn't have this method, and it was fixed in Safari 5.1.
try {
Object.defineProperty( obj, key, {
configurable: true,
enumerable: true,
get: function () {
if ( uniqueTrace() ) {
mw.track( 'mw.deprecate', logName );
mw.log.warn( msg );
}
return val;
},
set: function ( newVal ) {
if ( uniqueTrace() ) {
mw.track( 'mw.deprecate', logName );
mw.log.warn( msg );
}
val = newVal;
}
} );
} catch ( err ) {
obj[ key ] = val;
}
};
return log;
}() );
/**
* @class mw
*/
mw = {
/**
* Get the current time, measured in milliseconds since January 1, 1970 (UTC).
*
* On browsers that implement the Navigation Timing API, this function will produce floating-point
* values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
* it will fall back to using `Date`.
*
* @return {number} Current time
*/
now: ( function () {
var perf = window.performance,
navStart = perf && perf.timing && perf.timing.navigationStart;
return navStart && typeof perf.now === 'function' ?
function () { return navStart + perf.now(); } :
function () { return +new Date(); };
}() ),
/**
* Format a string. Replace $1, $2 ... $N with positional arguments.
*
* Used by Message#parser().
*
* @since 1.25
* @param {string} formatString Format string
* @param {...Mixed} parameters Values for $N replacements
* @return {string} Formatted string
*/
format: function ( formatString ) {
var parameters = slice.call( arguments, 1 );
return formatString.replace( /\$(\d+)/g, function ( str, match ) {
var index = parseInt( match, 10 ) - 1;
return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
} );
},
/**
* Track an analytic event.
*
* This method provides a generic means for MediaWiki JavaScript code to capture state
* information for analysis. Each logged event specifies a string topic name that describes
* the kind of event that it is. Topic names consist of dot-separated path components,
* arranged from most general to most specific. Each path component should have a clear and
* well-defined purpose.
*
* Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
* events that match their subcription, including those that fired before the handler was
* bound.
*
* @param {string} topic Topic name
* @param {Object} [data] Data describing the event, encoded as an object
*/
track: function ( topic, data ) {
trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
trackCallbacks.fire( trackQueue );
},
/**
* Register a handler for subset of analytic events, specified by topic.
*
* Handlers will be called once for each tracked event, including any events that fired before the
* handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
* the exact time at which the event fired, a string 'topic' property naming the event, and a
* 'data' property which is an object of event-specific data. The event topic and event data are
* also passed to the callback as the first and second arguments, respectively.
*
* @param {string} topic Handle events whose name starts with this string prefix
* @param {Function} callback Handler to call for each matching tracked event
* @param {string} callback.topic
* @param {Object} [callback.data]
*/
trackSubscribe: function ( topic, callback ) {
var seen = 0;
function handler( trackQueue ) {
var event;
for ( ; seen < trackQueue.length; seen++ ) {
event = trackQueue[ seen ];
if ( event.topic.indexOf( topic ) === 0 ) {
callback.call( event, event.topic, event.data );
}
}
}
trackHandlers.push( [ handler, callback ] );
trackCallbacks.add( handler );
},
/**
* Stop handling events for a particular handler
*
* @param {Function} callback
*/
trackUnsubscribe: function ( callback ) {
trackHandlers = $.grep( trackHandlers, function ( fns ) {
if ( fns[ 1 ] === callback ) {
trackCallbacks.remove( fns[ 0 ] );
// Ensure the tuple is removed to avoid holding on to closures
return false;
}
return true;
} );
},
// Expose Map constructor
Map: Map,
// Expose Message constructor
Message: Message,
/**
* Map of configuration values.
*
* Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
* on mediawiki.org.
*
* If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
* global `window` object.
*
* @property {mw.Map} config
*/
// Dummy placeholder later assigned in ResourceLoaderStartUpModule
config: null,
/**
* Empty object for third-party libraries, for cases where you don't
* want to add a new global, or the global is bad and needs containment
* or wrapping.
*
* @property
*/
libs: {},
/**
* Access container for deprecated functionality that can be moved from
* from their legacy location and attached to this object (e.g. a global
* function that is deprecated and as stop-gap can be exposed through here).
*
* This was reserved for future use but never ended up being used.
*
* @deprecated since 1.22 Let deprecated identifiers keep their original name
* and use mw.log#deprecate to create an access container for tracking.
* @property
*/
legacy: {},
/**
* Store for messages.
*
* @property {mw.Map}
*/
messages: new Map(),
/**
* Store for templates associated with a module.
*
* @property {mw.Map}
*/
templates: new Map(),
/**
* Get a message object.
*
* Shortcut for `new mw.Message( mw.messages, key, parameters )`.
*
* @see mw.Message
* @param {string} key Key of message to get
* @param {...Mixed} parameters Values for $N replacements
* @return {mw.Message}
*/
message: function ( key ) {
var parameters = slice.call( arguments, 1 );
return new Message( mw.messages, key, parameters );
},
/**
* Get a message string using the (default) 'text' format.
*
* Shortcut for `mw.message( key, parameters... ).text()`.
*
* @see mw.Message
* @param {string} key Key of message to get
* @param {...Mixed} parameters Values for $N replacements
* @return {string}
*/
msg: function () {
return mw.message.apply( mw.message, arguments ).toString();
},
/**
* No-op dummy placeholder for {@link mw.log} in debug mode.
*
* @method
*/
log: log,
/**
* Client for ResourceLoader server end point.
*
* This client is in charge of maintaining the module registry and state
* machine, initiating network (batch) requests for loading modules, as
* well as dependency resolution and execution of source code.
*
* For more information, refer to
* <https://www.mediawiki.org/wiki/ResourceLoader/Features>
*
* @class mw.loader
* @singleton
*/
loader: ( function () {
/**
* Fired via mw.track on various resource loading errors.
*
* @event resourceloader_exception
* @param {Error|Mixed} e The error that was thrown. Almost always an Error
* object, but in theory module code could manually throw something else, and that
* might also end up here.
* @param {string} [module] Name of the module which caused the error. Omitted if the
* error is not module-related or the module cannot be easily identified due to
* batched handling.
* @param {string} source Source of the error. Possible values:
*
* - style: stylesheet error (only affects old IE where a special style loading method
* is used)
* - load-callback: exception thrown by user callback
* - module-execute: exception thrown by module code
* - store-eval: could not evaluate module code cached in localStorage
* - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
* - store-localstorage-json: JSON conversion error in mw.loader.store.set
* - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
*/
/**
* Fired via mw.track on resource loading error conditions.
*
* @event resourceloader_assert
* @param {string} source Source of the error. Possible values:
*
* - bug-T59567: failed to cache script due to an Opera function -> string conversion
* bug; see <https://phabricator.wikimedia.org/T59567> for details
*/
/**
* Mapping of registered modules.
*
* See #implement and #execute for exact details on support for script, style and messages.
*
* Format:
*
* {
* 'moduleName': {
* // From mw.loader.register()
* 'version': '########' (hash)
* 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
* 'group': 'somegroup', (or) null
* 'source': 'local', (or) 'anotherwiki'
* 'skip': 'return !!window.Example', (or) null
* 'module': export Object
*
* // Set from execute() or mw.loader.state()
* 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
*
* // Optionally added at run-time by mw.loader.implement()
* 'skipped': true
* 'script': closure, array of urls, or string
* 'style': { ... } (see #execute)
* 'messages': { 'key': 'value', ... }
* }
* }
*
* State machine:
*
* - `registered`:
* The module is known to the system but not yet required.
* Meta data is registered via mw.loader#register. Calls to that method are
* generated server-side by the startup module.
* - `loading`:
* The module was required through mw.loader (either directly or as dependency of
* another module). The client will fetch module contents from the server.
* The contents are then stashed in the registry via mw.loader#implement.
* - `loaded`:
* The module has been loaded from the server and stashed via mw.loader#implement.
* If the module has no more dependencies in-flight, the module will be executed
* immediately. Otherwise execution is deferred, controlled via #handlePending.
* - `executing`:
* The module is being executed.
* - `ready`:
* The module has been successfully executed.
* - `error`:
* The module (or one of its dependencies) produced an error during execution.
* - `missing`:
* The module was registered client-side and requested, but the server denied knowledge
* of the module's existence.
*
* @property
* @private
*/
var registry = {},
// Mapping of sources, keyed by source-id, values are strings.
//
// Format:
//
// {
// 'sourceId': 'http://example.org/w/load.php'
// }
//
sources = {},
// For queueModuleScript()
handlingPendingRequests = false,
pendingRequests = [],
// List of modules to be loaded
queue = [],
/**
* List of callback jobs waiting for modules to be ready.
*
* Jobs are created by #enqueue() and run by #handlePending().
*
* Typically when a job is created for a module, the job's dependencies contain
* both the required module and all its recursive dependencies.
*
* Format:
*
* {
* 'dependencies': [ module names ],
* 'ready': Function callback
* 'error': Function callback
* }
*
* @property {Object[]} jobs
* @private
*/
jobs = [],
// For getMarker()
marker = null,
// For addEmbeddedCSS()
cssBuffer = '',
cssBufferTimer = null,
cssCallbacks = $.Callbacks(),
isIE9 = document.documentMode === 9,
rAF = window.requestAnimationFrame || setTimeout;
function getMarker() {
if ( !marker ) {
// Cache
marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' );
if ( !marker ) {
mw.log( 'Create <meta name="ResourceLoaderDynamicStyles"> dynamically' );
marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' )[ 0 ];
}
}
return marker;
}
/**
* Create a new style element and add it to the DOM.
*
* @private
* @param {string} text CSS text
* @param {Node} [nextNode] The element where the style tag
* should be inserted before
* @return {HTMLElement} Reference to the created style element
*/
function newStyleTag( text, nextNode ) {
var s = document.createElement( 'style' );
s.appendChild( document.createTextNode( text ) );
if ( nextNode && nextNode.parentNode ) {
nextNode.parentNode.insertBefore( s, nextNode );
} else {
document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
}
return s;
}
/**
* Add a bit of CSS text to the current browser page.
*
* The CSS will be appended to an existing ResourceLoader-created `<style>` tag
* or create a new one based on whether the given `cssText` is safe for extension.
*
* @private
* @param {string} [cssText=cssBuffer] If called without cssText,
* the internal buffer will be inserted instead.
* @param {Function} [callback]
*/
function addEmbeddedCSS( cssText, callback ) {
var $style, styleEl;
function fireCallbacks() {
var oldCallbacks = cssCallbacks;
// Reset cssCallbacks variable so it's not polluted by any calls to
// addEmbeddedCSS() from one of the callbacks (T105973)
cssCallbacks = $.Callbacks();
oldCallbacks.fire().empty();
}
if ( callback ) {
cssCallbacks.add( callback );
}
// Yield once before creating the <style> tag. This lets multiple stylesheets
// accumulate into one buffer, allowing us to reduce how often new stylesheets
// are inserted in the browser. Appending a stylesheet and waiting for the
// browser to repaint is fairly expensive. (T47810)
if ( cssText ) {
// Don't extend the buffer if the item needs its own stylesheet.
// Keywords like `@import` are only valid at the start of a stylesheet (T37562).
if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
// Linebreak for somewhat distinguishable sections
cssBuffer += '\n' + cssText;
if ( !cssBufferTimer ) {
cssBufferTimer = rAF( function () {
// Wrap in anonymous function that takes no arguments
// Support: Firefox < 13
// Firefox 12 has non-standard behaviour of passing a number
// as first argument to a setTimeout callback.
// http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
addEmbeddedCSS();
} );
}
return;
}
// This is a scheduled flush for the buffer
} else {
cssBufferTimer = null;
cssText = cssBuffer;
cssBuffer = '';
}
// By default, always create a new <style>. Appending text to a <style> tag is
// is a performance anti-pattern as it requires CSS to be reparsed (T47810).
//
// Support: IE 6-9
// Try to re-use existing <style> tags due to the IE stylesheet limit (T33676).
if ( isIE9 ) {
$style = $( getMarker() ).prev();
// Verify that the element before the marker actually is a <style> tag created
// by mw.loader (not some other style tag, or e.g. a <meta> tag).
if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) ) {
styleEl = $style[ 0 ];
styleEl.appendChild( document.createTextNode( cssText ) );
fireCallbacks();
return;
}
// Else: No existing tag to reuse. Continue below and create the first one.
}
$style = $( newStyleTag( cssText, getMarker() ) );
if ( isIE9 ) {
$style.data( 'ResourceLoaderDynamicStyleTag', true );
}
fireCallbacks();
}
/**
* @private
* @param {Array} modules List of module names
* @return {string} Hash of concatenated version hashes.
*/
function getCombinedVersion( modules ) {
var hashes = $.map( modules, function ( module ) {
return registry[ module ].version;
} );
return fnv132( hashes.join( '' ) );
}
/**
* Determine whether all dependencies are in state 'ready', which means we may
* execute the module or job now.
*
* @private
* @param {Array} modules Names of modules to be checked
* @return {boolean} True if all modules are in state 'ready', false otherwise
*/
function allReady( modules ) {
var i;
for ( i = 0; i < modules.length; i++ ) {
if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
return false;
}
}
return true;
}