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