Merge "Fix use of GenderCache in ApiPageSet::processTitlesArray"
[lhc/web/wiklou.git] / resources / src / mediawiki.base / mediawiki.base.js
1 /*!
2 * This file is currently loaded as part of the 'mediawiki' module and therefore
3 * concatenated to mediawiki.js and executed at the same time. This file exists
4 * to help prepare for splitting up the 'mediawiki' module.
5 * This effort is tracked at https://phabricator.wikimedia.org/T192623
6 *
7 * In short:
8 *
9 * - mediawiki.js will be reduced to the minimum needed to define mw.loader and
10 * mw.config, and then moved to its own private "mediawiki.loader" module that
11 * can be embedded within the StartupModule response.
12 *
13 * - mediawiki.base.js and other files in this directory will remain part of the
14 * "mediawiki" module, and will remain a default/implicit dependency for all
15 * regular modules, just like jquery and wikibits already are.
16 */
17 ( function () {
18 'use strict';
19
20 var slice = Array.prototype.slice,
21 mwLoaderTrack = mw.track,
22 trackCallbacks = $.Callbacks( 'memory' ),
23 trackHandlers = [],
24 queue;
25
26 /**
27 * Object constructor for messages.
28 *
29 * Similar to the Message class in MediaWiki PHP.
30 *
31 * Format defaults to 'text'.
32 *
33 * @example
34 *
35 * var obj, str;
36 * mw.messages.set( {
37 * 'hello': 'Hello world',
38 * 'hello-user': 'Hello, $1!',
39 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
40 * } );
41 *
42 * obj = new mw.Message( mw.messages, 'hello' );
43 * mw.log( obj.text() );
44 * // Hello world
45 *
46 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
47 * mw.log( obj.text() );
48 * // Hello, John Doe!
49 *
50 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
51 * mw.log( obj.text() );
52 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
53 *
54 * // Using mw.message shortcut
55 * obj = mw.message( 'hello-user', 'John Doe' );
56 * mw.log( obj.text() );
57 * // Hello, John Doe!
58 *
59 * // Using mw.msg shortcut
60 * str = mw.msg( 'hello-user', 'John Doe' );
61 * mw.log( str );
62 * // Hello, John Doe!
63 *
64 * // Different formats
65 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
66 *
67 * obj.format = 'text';
68 * str = obj.toString();
69 * // Same as:
70 * str = obj.text();
71 *
72 * mw.log( str );
73 * // Hello, John "Wiki" <3 Doe!
74 *
75 * mw.log( obj.escaped() );
76 * // Hello, John &quot;Wiki&quot; &lt;3 Doe!
77 *
78 * @class mw.Message
79 *
80 * @constructor
81 * @param {mw.Map} map Message store
82 * @param {string} key
83 * @param {Array} [parameters]
84 */
85 function Message( map, key, parameters ) {
86 this.format = 'text';
87 this.map = map;
88 this.key = key;
89 this.parameters = parameters === undefined ? [] : slice.call( parameters );
90 return this;
91 }
92
93 Message.prototype = {
94 /**
95 * Get parsed contents of the message.
96 *
97 * The default parser does simple $N replacements and nothing else.
98 * This may be overridden to provide a more complex message parser.
99 * The primary override is in the mediawiki.jqueryMsg module.
100 *
101 * This function will not be called for nonexistent messages.
102 *
103 * @return {string} Parsed message
104 */
105 parser: function () {
106 var text = this.map.get( this.key );
107 if (
108 mw.config.get( 'wgUserLanguage' ) === 'qqx' &&
109 ( !text || text === '(' + this.key + ')' )
110 ) {
111 text = '(' + this.key + '$*)';
112 }
113 return mw.format.apply( null, [ text ].concat( this.parameters ) );
114 },
115
116 /**
117 * Add (does not replace) parameters for `$N` placeholder values.
118 *
119 * @param {Array} parameters
120 * @return {mw.Message}
121 * @chainable
122 */
123 params: function ( parameters ) {
124 var i;
125 for ( i = 0; i < parameters.length; i++ ) {
126 this.parameters.push( parameters[ i ] );
127 }
128 return this;
129 },
130
131 /**
132 * Convert message object to its string form based on current format.
133 *
134 * @return {string} Message as a string in the current form, or `<key>` if key
135 * does not exist.
136 */
137 toString: function () {
138 var text;
139
140 if ( !this.exists() ) {
141 // Use ⧼key⧽ as text if key does not exist
142 // Err on the side of safety, ensure that the output
143 // is always html safe in the event the message key is
144 // missing, since in that case its highly likely the
145 // message key is user-controlled.
146 // '⧼' is used instead of '<' to side-step any
147 // double-escaping issues.
148 // (Keep synchronised with Message::toString() in PHP.)
149 return '⧼' + mw.html.escape( this.key ) + '⧽';
150 }
151
152 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
153 text = this.parser();
154 }
155
156 if ( this.format === 'escaped' ) {
157 text = this.parser();
158 text = mw.html.escape( text );
159 }
160
161 return text;
162 },
163
164 /**
165 * Change format to 'parse' and convert message to string
166 *
167 * If jqueryMsg is loaded, this parses the message text from wikitext
168 * (where supported) to HTML
169 *
170 * Otherwise, it is equivalent to plain.
171 *
172 * @return {string} String form of parsed message
173 */
174 parse: function () {
175 this.format = 'parse';
176 return this.toString();
177 },
178
179 /**
180 * Change format to 'plain' and convert message to string
181 *
182 * This substitutes parameters, but otherwise does not change the
183 * message text.
184 *
185 * @return {string} String form of plain message
186 */
187 plain: function () {
188 this.format = 'plain';
189 return this.toString();
190 },
191
192 /**
193 * Change format to 'text' and convert message to string
194 *
195 * If jqueryMsg is loaded, {{-transformation is done where supported
196 * (such as {{plural:}}, {{gender:}}, {{int:}}).
197 *
198 * Otherwise, it is equivalent to plain
199 *
200 * @return {string} String form of text message
201 */
202 text: function () {
203 this.format = 'text';
204 return this.toString();
205 },
206
207 /**
208 * Change the format to 'escaped' and convert message to string
209 *
210 * This is equivalent to using the 'text' format (see #text), then
211 * HTML-escaping the output.
212 *
213 * @return {string} String form of html escaped message
214 */
215 escaped: function () {
216 this.format = 'escaped';
217 return this.toString();
218 },
219
220 /**
221 * Check if a message exists
222 *
223 * @see mw.Map#exists
224 * @return {boolean}
225 */
226 exists: function () {
227 if ( mw.config.get( 'wgUserLanguage' ) === 'qqx' ) {
228 return true;
229 }
230 return this.map.exists( this.key );
231 }
232 };
233
234 /**
235 * @class mw
236 * @singleton
237 */
238
239 /**
240 * Empty object for third-party libraries, for cases where you don't
241 * want to add a new global, or the global is bad and needs containment
242 * or wrapping.
243 *
244 * @property
245 */
246 mw.libs = {};
247
248 // OOUI widgets specific to MediaWiki
249 mw.widgets = {};
250
251 /**
252 * @inheritdoc mw.inspect#runReports
253 * @method
254 */
255 mw.inspect = function () {
256 var args = arguments;
257 // Lazy-load
258 mw.loader.using( 'mediawiki.inspect', function () {
259 mw.inspect.runReports.apply( mw.inspect, args );
260 } );
261 };
262
263 /**
264 * Replace $* with a list of parameters for &uselang=qqx.
265 *
266 * @private
267 * @since 1.33
268 * @param {string} formatString Format string
269 * @param {Array} parameters Values for $N replacements
270 * @return {string} Transformed format string
271 */
272 mw.internalDoTransformFormatForQqx = function ( formatString, parameters ) {
273 var parametersString;
274 if ( formatString.indexOf( '$*' ) !== -1 ) {
275 parametersString = '';
276 if ( parameters.length ) {
277 parametersString = ': ' + parameters.map( function ( _, i ) {
278 return '$' + ( i + 1 );
279 } ).join( ', ' );
280 }
281 return formatString.replace( '$*', parametersString );
282 }
283 return formatString;
284 };
285
286 /**
287 * Format a string. Replace $1, $2 ... $N with positional arguments.
288 *
289 * Used by Message#parser().
290 *
291 * @since 1.25
292 * @param {string} formatString Format string
293 * @param {...Mixed} parameters Values for $N replacements
294 * @return {string} Formatted string
295 */
296 mw.format = function ( formatString ) {
297 var parameters = slice.call( arguments, 1 );
298 formatString = mw.internalDoTransformFormatForQqx( formatString, parameters );
299 return formatString.replace( /\$(\d+)/g, function ( str, match ) {
300 var index = parseInt( match, 10 ) - 1;
301 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
302 } );
303 };
304
305 // Expose Message constructor
306 mw.Message = Message;
307
308 /**
309 * Get a message object.
310 *
311 * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
312 *
313 * @see mw.Message
314 * @param {string} key Key of message to get
315 * @param {...Mixed} parameters Values for $N replacements
316 * @return {mw.Message}
317 */
318 mw.message = function ( key ) {
319 var parameters = slice.call( arguments, 1 );
320 return new Message( mw.messages, key, parameters );
321 };
322
323 /**
324 * Get a message string using the (default) 'text' format.
325 *
326 * Shortcut for `mw.message( key, parameters... ).text()`.
327 *
328 * @see mw.Message
329 * @param {string} key Key of message to get
330 * @param {...Mixed} parameters Values for $N replacements
331 * @return {string}
332 */
333 mw.msg = function () {
334 return mw.message.apply( mw.message, arguments ).toString();
335 };
336
337 /**
338 * Track an analytic event.
339 *
340 * This method provides a generic means for MediaWiki JavaScript code to capture state
341 * information for analysis. Each logged event specifies a string topic name that describes
342 * the kind of event that it is. Topic names consist of dot-separated path components,
343 * arranged from most general to most specific. Each path component should have a clear and
344 * well-defined purpose.
345 *
346 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
347 * events that match their subcription, including those that fired before the handler was
348 * bound.
349 *
350 * @param {string} topic Topic name
351 * @param {Object} [data] Data describing the event, encoded as an object
352 */
353 mw.track = function ( topic, data ) {
354 mwLoaderTrack( topic, data );
355 trackCallbacks.fire( mw.trackQueue );
356 };
357
358 /**
359 * Register a handler for subset of analytic events, specified by topic.
360 *
361 * Handlers will be called once for each tracked event, including any events that fired before the
362 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
363 * the exact time at which the event fired, a string 'topic' property naming the event, and a
364 * 'data' property which is an object of event-specific data. The event topic and event data are
365 * also passed to the callback as the first and second arguments, respectively.
366 *
367 * @param {string} topic Handle events whose name starts with this string prefix
368 * @param {Function} callback Handler to call for each matching tracked event
369 * @param {string} callback.topic
370 * @param {Object} [callback.data]
371 */
372 mw.trackSubscribe = function ( topic, callback ) {
373 var seen = 0;
374 function handler( trackQueue ) {
375 var event;
376 for ( ; seen < trackQueue.length; seen++ ) {
377 event = trackQueue[ seen ];
378 if ( event.topic.indexOf( topic ) === 0 ) {
379 callback.call( event, event.topic, event.data );
380 }
381 }
382 }
383
384 trackHandlers.push( [ handler, callback ] );
385
386 trackCallbacks.add( handler );
387 };
388
389 /**
390 * Stop handling events for a particular handler
391 *
392 * @param {Function} callback
393 */
394 mw.trackUnsubscribe = function ( callback ) {
395 trackHandlers = trackHandlers.filter( function ( fns ) {
396 if ( fns[ 1 ] === callback ) {
397 trackCallbacks.remove( fns[ 0 ] );
398 // Ensure the tuple is removed to avoid holding on to closures
399 return false;
400 }
401 return true;
402 } );
403 };
404
405 // Fire events from before track() triggered fire()
406 trackCallbacks.fire( mw.trackQueue );
407
408 /**
409 * Registry and firing of events.
410 *
411 * MediaWiki has various interface components that are extended, enhanced
412 * or manipulated in some other way by extensions, gadgets and even
413 * in core itself.
414 *
415 * This framework helps streamlining the timing of when these other
416 * code paths fire their plugins (instead of using document-ready,
417 * which can and should be limited to firing only once).
418 *
419 * Features like navigating to other wiki pages, previewing an edit
420 * and editing itself – without a refresh – can then retrigger these
421 * hooks accordingly to ensure everything still works as expected.
422 *
423 * Example usage:
424 *
425 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
426 * mw.hook( 'wikipage.content' ).fire( $content );
427 *
428 * Handlers can be added and fired for arbitrary event names at any time. The same
429 * event can be fired multiple times. The last run of an event is memorized
430 * (similar to `$(document).ready` and `$.Deferred().done`).
431 * This means if an event is fired, and a handler added afterwards, the added
432 * function will be fired right away with the last given event data.
433 *
434 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
435 * Thus allowing flexible use and optimal maintainability and authority control.
436 * You can pass around the `add` and/or `fire` method to another piece of code
437 * without it having to know the event name (or `mw.hook` for that matter).
438 *
439 * var h = mw.hook( 'bar.ready' );
440 * new mw.Foo( .. ).fetch( { callback: h.fire } );
441 *
442 * Note: Events are documented with an underscore instead of a dot in the event
443 * name due to jsduck not supporting dots in that position.
444 *
445 * @class mw.hook
446 */
447 mw.hook = ( function () {
448 var lists = Object.create( null );
449
450 /**
451 * Create an instance of mw.hook.
452 *
453 * @method hook
454 * @member mw
455 * @param {string} name Name of hook.
456 * @return {mw.hook}
457 */
458 return function ( name ) {
459 var list = lists[ name ] || ( lists[ name ] = $.Callbacks( 'memory' ) );
460
461 return {
462 /**
463 * Register a hook handler
464 *
465 * @param {...Function} handler Function to bind.
466 * @chainable
467 */
468 add: list.add,
469
470 /**
471 * Unregister a hook handler
472 *
473 * @param {...Function} handler Function to unbind.
474 * @chainable
475 */
476 remove: list.remove,
477
478 /**
479 * Run a hook.
480 *
481 * @param {...Mixed} data
482 * @return {mw.hook}
483 * @chainable
484 */
485 fire: function () {
486 return list.fireWith.call( this, null, slice.call( arguments ) );
487 }
488 };
489 };
490 }() );
491
492 /**
493 * HTML construction helper functions
494 *
495 * @example
496 *
497 * var Html, output;
498 *
499 * Html = mw.html;
500 * output = Html.element( 'div', {}, new Html.Raw(
501 * Html.element( 'img', { src: '<' } )
502 * ) );
503 * mw.log( output ); // <div><img src="&lt;"/></div>
504 *
505 * @class mw.html
506 * @singleton
507 */
508 mw.html = ( function () {
509 function escapeCallback( s ) {
510 switch ( s ) {
511 case '\'':
512 return '&#039;';
513 case '"':
514 return '&quot;';
515 case '<':
516 return '&lt;';
517 case '>':
518 return '&gt;';
519 case '&':
520 return '&amp;';
521 }
522 }
523
524 return {
525 /**
526 * Escape a string for HTML.
527 *
528 * Converts special characters to HTML entities.
529 *
530 * mw.html.escape( '< > \' & "' );
531 * // Returns &lt; &gt; &#039; &amp; &quot;
532 *
533 * @param {string} s The string to escape
534 * @return {string} HTML
535 */
536 escape: function ( s ) {
537 return s.replace( /['"<>&]/g, escapeCallback );
538 },
539
540 /**
541 * Create an HTML element string, with safe escaping.
542 *
543 * @param {string} name The tag name.
544 * @param {Object} [attrs] An object with members mapping element names to values
545 * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
546 *
547 * - string: Text to be escaped.
548 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
549 * - this.Raw: The raw value is directly included.
550 * - this.Cdata: The raw value is directly included. An exception is
551 * thrown if it contains any illegal ETAGO delimiter.
552 * See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
553 * @return {string} HTML
554 */
555 element: function ( name, attrs, contents ) {
556 var v, attrName, s = '<' + name;
557
558 if ( attrs ) {
559 for ( attrName in attrs ) {
560 v = attrs[ attrName ];
561 // Convert name=true, to name=name
562 if ( v === true ) {
563 v = attrName;
564 // Skip name=false
565 } else if ( v === false ) {
566 continue;
567 }
568 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
569 }
570 }
571 if ( contents === undefined || contents === null ) {
572 // Self close tag
573 s += '/>';
574 return s;
575 }
576 // Regular open tag
577 s += '>';
578 switch ( typeof contents ) {
579 case 'string':
580 // Escaped
581 s += this.escape( contents );
582 break;
583 case 'number':
584 case 'boolean':
585 // Convert to string
586 s += String( contents );
587 break;
588 default:
589 if ( contents instanceof this.Raw ) {
590 // Raw HTML inclusion
591 s += contents.value;
592 } else if ( contents instanceof this.Cdata ) {
593 // CDATA
594 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
595 throw new Error( 'Illegal end tag found in CDATA' );
596 }
597 s += contents.value;
598 } else {
599 throw new Error( 'Invalid type of contents' );
600 }
601 }
602 s += '</' + name + '>';
603 return s;
604 },
605
606 /**
607 * Wrapper object for raw HTML passed to mw.html.element().
608 *
609 * @class mw.html.Raw
610 * @constructor
611 * @param {string} value
612 */
613 Raw: function ( value ) {
614 this.value = value;
615 },
616
617 /**
618 * Wrapper object for CDATA element contents passed to mw.html.element()
619 *
620 * @class mw.html.Cdata
621 * @constructor
622 * @param {string} value
623 */
624 Cdata: function ( value ) {
625 this.value = value;
626 }
627 };
628 }() );
629
630 /**
631 * Execute a function as soon as one or more required modules are ready.
632 *
633 * Example of inline dependency on OOjs:
634 *
635 * mw.loader.using( 'oojs', function () {
636 * OO.compare( [ 1 ], [ 1 ] );
637 * } );
638 *
639 * Example of inline dependency obtained via `require()`:
640 *
641 * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
642 * var util = require( 'mediawiki.util' );
643 * } );
644 *
645 * Since MediaWiki 1.23 this also returns a promise.
646 *
647 * Since MediaWiki 1.28 the promise is resolved with a `require` function.
648 *
649 * @member mw.loader
650 * @param {string|Array} dependencies Module name or array of modules names the
651 * callback depends on to be ready before executing
652 * @param {Function} [ready] Callback to execute when all dependencies are ready
653 * @param {Function} [error] Callback to execute if one or more dependencies failed
654 * @return {jQuery.Promise} With a `require` function
655 */
656 mw.loader.using = function ( dependencies, ready, error ) {
657 var deferred = $.Deferred();
658
659 // Allow calling with a single dependency as a string
660 if ( !Array.isArray( dependencies ) ) {
661 dependencies = [ dependencies ];
662 }
663
664 if ( ready ) {
665 deferred.done( ready );
666 }
667 if ( error ) {
668 deferred.fail( error );
669 }
670
671 try {
672 // Resolve entire dependency map
673 dependencies = mw.loader.resolve( dependencies );
674 } catch ( e ) {
675 return deferred.reject( e ).promise();
676 }
677
678 mw.loader.enqueue(
679 dependencies,
680 function () { deferred.resolve( mw.loader.require ); },
681 deferred.reject
682 );
683
684 return deferred.promise();
685 };
686
687 /**
688 * Load a script by URL.
689 *
690 * Example:
691 *
692 * mw.loader.getScript(
693 * 'https://example.org/x-1.0.0.js'
694 * )
695 * .then( function () {
696 * // Script succeeded. You can use X now.
697 * }, function ( e ) {
698 * // Script failed. X is not avaiable
699 * mw.log.error( e.message ); // => "Failed to load script"
700 * } );
701 * } );
702 *
703 * @member mw.loader
704 * @param {string} url Script URL
705 * @return {jQuery.Promise} Resolved when the script is loaded
706 */
707 mw.loader.getScript = function ( url ) {
708 return $.ajax( url, { dataType: 'script', cache: true } )
709 .catch( function () {
710 throw new Error( 'Failed to load script' );
711 } );
712 };
713
714 // Skeleton user object, extended by the 'mediawiki.user' module.
715 /**
716 * @class mw.user
717 * @singleton
718 */
719 mw.user = {
720 /**
721 * @property {mw.Map}
722 */
723 options: new mw.Map(),
724 /**
725 * @property {mw.Map}
726 */
727 tokens: new mw.Map()
728 };
729
730 // Alias $j to jQuery for backwards compatibility
731 // @deprecated since 1.23 Use $ or jQuery instead
732 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
733
734 // Process callbacks for Grade A that require modules.
735 queue = window.RLQ;
736 // Replace temporary RLQ implementation from startup.js with the
737 // final implementation that also processes callbacks that can
738 // require modules. It must also support late arrivals of
739 // plain callbacks. (T208093)
740 window.RLQ = {
741 push: function ( entry ) {
742 if ( typeof entry === 'function' ) {
743 entry();
744 } else {
745 mw.loader.using( entry[ 0 ], entry[ 1 ] );
746 }
747 }
748 };
749 while ( queue[ 0 ] ) {
750 window.RLQ.push( queue.shift() );
751 }
752 }() );