3 const slice = Array.prototype.slice;
5 // Apply site-level data
6 mw.config.set( require( './config.json' ) );
12 * @classdesc Describes a translateable text or HTML string. Similar to the Message class in MediaWiki PHP.
17 * 'hello': 'Hello world',
18 * 'hello-user': 'Hello, $1!',
19 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3',
20 * 'so-unusual': 'You will find: $1'
23 * obj = mw.message( 'hello' );
24 * mw.log( obj.text() );
27 * obj = mw.message( 'hello-user', 'John Doe' );
28 * mw.log( obj.text() );
31 * obj = mw.message( 'welcome-user', 'John Doe', 'Wikipedia', '2 hours ago' );
32 * mw.log( obj.text() );
33 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
35 * // Using mw.msg shortcut, always in "text' format.
36 * str = mw.msg( 'hello-user', 'John Doe' );
40 * // Different formats
41 * obj = mw.message( 'so-unusual', 'Time "after" <time>' );
43 * mw.log( obj.text() );
44 * // You will find: Time "after" <time>
46 * mw.log( obj.escaped() );
47 * // You will find: Time "after" <time>
50 * @description Object constructor for messages. The constructor is not publicly accessible;
51 * use {@link mw.message} instead.
52 * @param {mw.Map} map Message store
54 * @param {Array} [parameters]
56 function Message( map, key, parameters ) {
59 this.parameters = parameters || [];
62 Message.prototype = /** @lends mw.Message.prototype */ {
64 * Get parsed contents of the message.
66 * The default parser does simple $N replacements and nothing else.
67 * This may be overridden to provide a more complex message parser.
68 * The primary override is in the mediawiki.jqueryMsg module.
70 * This function will not be called for nonexistent messages.
71 * For internal use by mediawiki.jqueryMsg only
74 * @param {string} format
75 * @return {string} Parsed message
77 parser: function ( format ) {
78 let text = this.map.get( this.key );
80 // Apply qqx formatting.
82 // - Keep this synchronised with LanguageQqx/MessageCache in PHP.
83 // - Keep this synchronised with mw.jqueryMsg.Parser#getAst.
85 // Unlike LanguageQqx in PHP, this doesn't replace unconditionally.
86 // It replaces non-existent messages, and messages that were exported by
87 // load.php as "(key)" in qqx formatting. Some extensions export other data
88 // via their message blob (T222944).
90 mw.config.get( 'wgUserLanguage' ) === 'qqx' &&
91 ( !text || text === '(' + this.key + ')' )
93 text = '(' + this.key + '$*)';
95 text = mw.format( text, ...this.parameters );
96 if ( format === 'parse' ) {
97 // We don't know how to parse anything, so escape it all
98 text = mw.html.escape( text );
104 * Add (does not replace) parameters for `$N` placeholder values.
106 * @param {Array} parameters
107 * @return {mw.Message}
110 params: function ( parameters ) {
111 this.parameters.push( ...parameters );
116 * Convert message object to a string using the "text"-format .
118 * This exists for implicit string type casting only.
119 * Do not call this directly. Use mw.Message#text() instead, one of the
120 * other format methods.
123 * @param {string} [format="text"] Internal parameter. Uses "text" if called
124 * implicitly through string casting.
125 * @return {string} Message in the given format, or `⧼key⧽` if the key
128 toString: function ( format ) {
129 if ( !this.exists() ) {
130 // Make sure qqx works for non-existent messages, see parser() above.
131 if ( mw.config.get( 'wgUserLanguage' ) !== 'qqx' ) {
132 // Use ⧼key⧽ as text if key does not exist
133 // Err on the side of safety, ensure that the output
134 // is always html safe in the event the message key is
135 // missing, since in that case its highly likely the
136 // message key is user-controlled.
137 // '⧼' is used instead of '<' to side-step any
138 // double-escaping issues.
139 // (Keep synchronised with Message::toString() in PHP.)
140 return '⧼' + mw.html.escape( this.key ) + '⧽';
148 if ( format === 'plain' || format === 'text' || format === 'parse' ) {
149 return this.parser( format );
152 // Format: 'escaped' (including for any invalid format, default to safe escape)
153 return mw.html.escape( this.parser( 'escaped' ) );
157 * Parse message as wikitext and return HTML.
159 * If jqueryMsg is loaded, this transforms text and parses a subset of supported wikitext
160 * into HTML. Without jqueryMsg, it is equivalent to {@link mw.Message#escaped}.
162 * @return {string} String form of parsed message
165 return this.toString( 'parse' );
169 * Return message plainly.
171 * This substitutes parameters, but otherwise does not transform the
174 * @return {string} String form of plain message
177 return this.toString( 'plain' );
181 * Format message with text transformations applied.
183 * If jqueryMsg is loaded, `{{`-transformation is done for supported
184 * magic words such as `{{plural:}}`, `{{gender:}}`, and `{{int:}}`.
185 * Without jqueryMsg, it is equivalent to {@link mw.Message#plain}.
187 * @return {string} String form of text message
190 return this.toString( 'text' );
194 * Format message and return as escaped text in HTML.
196 * This is equivalent to the #text format, which is then HTML-escaped.
198 * @return {string} String form of html escaped message
200 escaped: function () {
201 return this.toString( 'escaped' );
205 * Check if a message exists. Equivalent to {@link mw.Map.exists}.
209 exists: function () {
210 return this.map.exists( this.key );
217 * @borrows mediawiki.inspect.runReports as inspect
221 * Empty object for third-party libraries, for cases where you don't
222 * want to add a new global, or the global is bad and needs containment
230 * OOUI widgets specific to MediaWiki.
231 * Initially empty. To expand the amount of available widgets the `mediawiki.widget` module can be loaded.
233 * @namespace mw.widgets
235 * mw.loader.using('mediawiki.widget').then(() => {
236 * OO.ui.getWindowManager().addWindows( [ new mw.widget.AbandonEditDialog() ] );
242 * Generates a ResourceLoader report using the
243 * {@link mediawiki.inspect.js.html|mediawiki.inspect module}.
247 mw.inspect = function ( ...reports ) {
249 mw.loader.using( 'mediawiki.inspect', () => {
250 mw.inspect.runReports( ...reports );
255 * Replace `$*` with a list of parameters for `uselang=qqx` support.
259 * @param {string} formatString Format string
260 * @param {Array} parameters Values for $N replacements
261 * @return {string} Transformed format string
263 mw.internalDoTransformFormatForQqx = function ( formatString, parameters ) {
264 if ( formatString.indexOf( '$*' ) !== -1 ) {
265 let replacement = '';
266 if ( parameters.length ) {
267 replacement = ': ' + parameters.map( ( _, i ) => '$' + ( i + 1 ) ).join( ', ' );
269 return formatString.replace( '$*', replacement );
275 * Encode page titles in a way that matches `wfUrlencode` in PHP.
277 * @see mw.util#wikiUrlencode
279 * @param {string} str
282 mw.internalWikiUrlencode = function ( str ) {
283 return encodeURIComponent( String( str ) )
284 .replace( /'/g, '%27' )
285 .replace( /%20/g, '_' )
286 .replace( /%3B/g, ';' )
287 .replace( /%40/g, '@' )
288 .replace( /%24/g, '$' )
289 .replace( /%2C/g, ',' )
290 .replace( /%2F/g, '/' )
291 .replace( /%3A/g, ':' );
295 * Format a string. Replace $1, $2 ... $N with positional arguments.
297 * Used by {@link mw.Message#parse}.
301 * @param {string} formatString Format string
302 * @param {...Mixed} parameters Values for $N replacements
303 * @return {string} Formatted string
305 mw.format = function ( formatString, ...parameters ) {
306 formatString = mw.internalDoTransformFormatForQqx( formatString, parameters );
307 return formatString.replace( /\$(\d+)/g, ( str, match ) => {
308 const index = parseInt( match, 10 ) - 1;
309 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
313 // Expose Message constructor
314 mw.Message = Message;
317 * Get a message object.
319 * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
322 * @see {@link mw.Message}
323 * @param {string} key Key of message to get
324 * @param {...Mixed} parameters Values for $N replacements
325 * @return {mw.Message}
327 mw.message = function ( key ) {
328 const parameters = slice.call( arguments, 1 );
329 return new Message( mw.messages, key, parameters );
333 * Get a message string using the (default) 'text' format.
335 * Shortcut for `mw.message( key, parameters... ).text()`.
338 * @see {@link mw.Message}
339 * @param {string} key Key of message to get
340 * @param {...any} parameters Values for $N replacements
343 mw.msg = function ( key, ...parameters ) {
344 // Shortcut must process text transformations by default
345 // if mediawiki.jqueryMsg is loaded. (T46459)
346 // eslint-disable-next-line mediawiki/msg-doc
347 return mw.message( key, ...parameters ).text();
351 * Convenience method for loading and accessing the
352 * {@link mw.notification.notify|mw.notification module}.
355 * @param {HTMLElement|HTMLElement[]|jQuery|mw.Message|string} message
356 * @param {Object} [options] See mw.notification#defaults for the defaults.
357 * @return {jQuery.Promise}
359 mw.notify = function ( message, options ) {
361 return mw.loader.using( 'mediawiki.notification' ).then( () => mw.notification.notify( message, options ) );
364 const trackCallbacks = $.Callbacks( 'memory' );
365 let trackHandlers = [];
368 * Track an analytic event.
370 * This method provides a generic means for MediaWiki JavaScript code to capture state
371 * information for analysis. Each logged event specifies a string topic name that describes
372 * the kind of event that it is. Topic names consist of dot-separated path components,
373 * arranged from most general to most specific. Each path component should have a clear and
374 * well-defined purpose.
376 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
377 * events that match their subscription, including buffered events that fired before the handler
381 * @param {string} topic Topic name
382 * @param {...Object|number|string} [data] Data describing the event.
384 mw.track = function ( topic, ...data ) {
385 mw.trackQueue.push( { topic, args: data } );
386 trackCallbacks.fire( mw.trackQueue );
390 * Register a handler for subset of analytic events, specified by topic.
392 * Handlers will be called once for each tracked event, including for any buffered events that
393 * fired before the handler was subscribed. The callback is passed a `topic` string, and optional
394 * `data` argument(s).
397 * // To monitor all topics for debugging
398 * mw.trackSubscribe( '', console.log );
401 * // To subscribe to any of `foo.*`, e.g. both `foo.bar` and `foo.quux`
402 * mw.trackSubscribe( 'foo.', console.log );
405 * @param {string} topic Handle events whose name starts with this string prefix
406 * @param {Function} callback Handler to call for each matching tracked event
407 * @param {string} callback.topic
408 * @param {...Object|number|string} [callback.data]
410 mw.trackSubscribe = function ( topic, callback ) {
412 function handler( trackQueue ) {
413 for ( ; seen < trackQueue.length; seen++ ) {
414 const event = trackQueue[ seen ];
415 if ( event.topic.indexOf( topic ) === 0 ) {
416 callback( event.topic, ...event.args );
421 trackHandlers.push( [ handler, callback ] );
422 trackCallbacks.add( handler );
426 * Stop handling events for a particular handler.
429 * @param {Function} callback
431 mw.trackUnsubscribe = function ( callback ) {
432 trackHandlers = trackHandlers.filter( ( fns ) => {
433 if ( fns[ 1 ] === callback ) {
434 trackCallbacks.remove( fns[ 0 ] );
435 // Ensure the tuple is removed to avoid holding on to closures
442 // Notify subscribers of any mw.trackQueue.push() calls
443 // from the startup module before mw.track() is defined.
444 trackCallbacks.fire( mw.trackQueue );
448 * @description Registry and firing of events.
450 * MediaWiki has various interface components that are extended, enhanced
451 * or manipulated in some other way by extensions, gadgets and even
454 * This framework helps streamlining the timing of when these other
455 * code paths fire their plugins (instead of using document-ready,
456 * which can and should be limited to firing only once).
458 * Features like navigating to other wiki pages, previewing an edit
459 * and editing itself – without a refresh – can then retrigger these
460 * hooks accordingly to ensure everything still works as expected.
465 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
466 * mw.hook( 'wikipage.content' ).fire( $content );
469 * Handlers can be added and fired for arbitrary event names at any time. The same
470 * event can be fired multiple times. The last run of an event is memorized
471 * (similar to `$(document).ready` and `$.Deferred().done`).
472 * This means if an event is fired, and a handler added afterwards, the added
473 * function will be fired right away with the last given event data.
475 * Like Deferreds and Promises, the {@link mw.hook} object is both detachable and chainable.
476 * Thus allowing flexible use and optimal maintainability and authority control.
477 * You can pass around the `add` and/or `fire` method to another piece of code
478 * without it having to know the event name (or {@link mw.hook} for that matter).
481 * var h = mw.hook( 'bar.ready' );
482 * new mw.Foo( .. ).fetch( { callback: h.fire } );
485 * The function signature for hooks can be considered [stable](https://www.mediawiki.org/wiki/Special:MyLanguage/Stable_interface_policy/Frontend).
486 * See available global events below.
489 const hooks = Object.create( null );
492 * Create an instance of {@link Hook}.
495 * const hook = mw.hook( 'name' );
496 * hook.add( () => alert( 'Hook was fired' ) );
499 * @param {string} name Name of hook.
502 mw.hook = function ( name ) {
503 return hooks[ name ] || ( hooks[ name ] = ( function () {
506 function rethrow( e ) {
513 * @classdesc An instance of a hook, created via [mw.hook method]{@link mw.hook}.
519 * Register a hook handler.
521 * @param {...Function} handler Function to bind.
526 for ( let i = 0; i < arguments.length; i++ ) {
527 fns.push( arguments[ i ] );
530 arguments[ i ].apply( null, memory );
539 * Unregister a hook handler.
541 * @param {...Function} handler Function to unbind.
545 remove: function () {
546 for ( let i = 0; i < arguments.length; i++ ) {
548 while ( ( j = fns.indexOf( arguments[ i ] ) ) !== -1 ) {
555 * Call hook handlers with data.
558 * @param {...any} data
563 for ( let i = 0; i < fns.length; i++ ) {
565 fns[ i ].apply( null, arguments );
570 memory = slice.call( arguments );
578 * HTML construction helper functions.
584 * output = Html.element( 'div', {}, new Html.Raw(
585 * Html.element( 'img', { src: '<' } )
587 * mw.log( output ); // <div><img src="<"/></div>
592 function escapeCallback( s ) {
608 * Escape a string for HTML.
610 * Converts special characters to HTML entities.
613 * mw.html.escape( '< > \' & "' );
614 * // Returns < > ' & "
616 * @param {string} s The string to escape
617 * @return {string} HTML
619 escape: function ( s ) {
620 return s.replace( /['"<>&]/g, escapeCallback );
624 * Create an HTML element string, with safe escaping.
626 * @param {string} name The tag name.
627 * @param {Object} [attrs] An object with members mapping element names to values
628 * @param {string|mw.html.Raw|null} [contents=null] The contents of the element.
630 * - string: Text to be escaped.
631 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
632 * - this.Raw: The raw value is directly included.
633 * @return {string} HTML
635 element: function ( name, attrs, contents ) {
639 for ( const attrName in attrs ) {
640 let v = attrs[ attrName ];
641 // Convert name=true, to name=name
645 } else if ( v === false ) {
648 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
651 if ( contents === undefined || contents === null ) {
658 if ( typeof contents === 'string' ) {
660 s += this.escape( contents );
661 } else if ( typeof contents === 'number' || typeof contents === 'boolean' ) {
663 s += String( contents );
664 } else if ( contents instanceof this.Raw ) {
665 // Raw HTML inclusion
668 throw new Error( 'Invalid content type' );
670 s += '</' + name + '>';
675 * @classdesc Wrapper object for raw HTML. Can be used with {@link mw.html.element}.
677 * @param {string} value
679 * const raw = new mw.html.Raw( 'Text' );
680 * mw.html.element( 'div', { class: 'html' }, raw );
682 Raw: function ( value ) {
688 * Schedule a function to run once the page is ready (DOM loaded).
692 * @param {Function} fn
694 window.addOnloadHook = function ( fn ) {
700 const loadedScripts = {};
703 * Import a script using an absolute URI.
707 * @param {string} url
708 * @return {HTMLElement|null} Script tag, or null if it was already imported before
710 window.importScriptURI = function ( url ) {
711 if ( loadedScripts[ url ] ) {
714 loadedScripts[ url ] = true;
715 return mw.loader.addScriptTag( url );
719 * Import a local JS content page, for use by user scripts and site-wide scripts.
721 * Note that if the same title is imported multiple times, it will only
722 * be loaded and executed once.
726 * @param {string} title
727 * @return {HTMLElement|null} Script tag, or null if it was already imported before
729 window.importScript = function ( title ) {
730 return window.importScriptURI(
731 mw.config.get( 'wgScript' ) + '?title=' + mw.internalWikiUrlencode( title ) +
732 '&action=raw&ctype=text/javascript'
737 * Import a local CSS content page, for use by user scripts and site-wide scripts.
741 * @param {string} title
742 * @return {HTMLElement} Link tag
744 window.importStylesheet = function ( title ) {
745 return mw.loader.addLinkTag(
746 mw.config.get( 'wgScript' ) + '?title=' + mw.internalWikiUrlencode( title ) +
747 '&action=raw&ctype=text/css'
752 * Import a stylesheet using an absolute URI.
756 * @param {string} url
757 * @param {string} media
758 * @return {HTMLElement} Link tag
760 window.importStylesheetURI = function ( url, media ) {
761 return mw.loader.addLinkTag( url, media );
765 * Get the names of all registered ResourceLoader modules.
767 * @memberof mw.loader
770 mw.loader.getModuleNames = function () {
771 return Object.keys( mw.loader.moduleRegistry );
775 * Execute a function after one or more modules are ready.
777 * Use this method if you need to dynamically control which modules are loaded
778 * and/or when they loaded (instead of declaring them as dependencies directly
781 * This uses the same loader as for regular module dependencies. This means
782 * ResourceLoader will not re-download or re-execute a module for the second
783 * time if something else already needed it. And the same browser HTTP cache,
784 * and localStorage are checked before considering to fetch from the network.
785 * And any on-going requests from other dependencies or using() calls are also
786 * automatically re-used.
788 * Example of inline dependency on OOjs:
790 * mw.loader.using( 'oojs', function () {
791 * OO.compare( [ 1 ], [ 1 ] );
795 * Example of inline dependency obtained via `require()`:
797 * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
798 * var util = require( 'mediawiki.util' );
802 * Since MediaWiki 1.23 this returns a promise.
804 * Since MediaWiki 1.28 the promise is resolved with a `require` function.
806 * @memberof mw.loader
807 * @param {string|Array} dependencies Module name or array of modules names the
808 * callback depends on to be ready before executing
809 * @param {Function} [ready] Callback to execute when all dependencies are ready
810 * @param {Function} [error] Callback to execute if one or more dependencies failed
811 * @return {jQuery.Promise} With a `require` function
813 mw.loader.using = function ( dependencies, ready, error ) {
814 const deferred = $.Deferred();
816 // Allow calling with a single dependency as a string
817 if ( !Array.isArray( dependencies ) ) {
818 dependencies = [ dependencies ];
822 deferred.done( ready );
825 deferred.fail( error );
829 // Resolve entire dependency map
830 dependencies = mw.loader.resolve( dependencies );
832 return deferred.reject( e ).promise();
838 deferred.resolve( mw.loader.require );
843 return deferred.promise();
847 * Load a script by URL.
850 * mw.loader.getScript(
851 * 'https://example.org/x-1.0.0.js'
853 * .then( function () {
854 * // Script succeeded. You can use X now.
855 * }, function ( e ) {
856 * // Script failed. X is not avaiable
857 * mw.log.error( e.message ); // => "Failed to load script"
861 * @memberof mw.loader
862 * @param {string} url Script URL
863 * @return {jQuery.Promise} Resolved when the script is loaded
865 mw.loader.getScript = function ( url ) {
866 return $.ajax( url, { dataType: 'script', cache: true } )
868 throw new Error( 'Failed to load script' );
872 // Skeleton user object, extended by the 'mediawiki.user' module.
879 * Map of user preferences and their values.
883 options: new mw.Map(),
885 * Map of retrieved user tokens.
892 mw.user.options.set( require( './user.json' ) );
894 // Process callbacks for modern browsers (Grade A) that require modules.
895 const queue = window.RLQ;
896 // Replace temporary RLQ implementation from startup.js with the
897 // final implementation that also processes callbacks that can
898 // require modules. It must also support late arrivals of
899 // plain callbacks. (T208093)
901 push: function ( entry ) {
902 if ( typeof entry === 'function' ) {
905 mw.loader.using( entry[ 0 ], entry[ 1 ] );
909 while ( queue[ 0 ] ) {
910 window.RLQ.push( queue.shift() );
914 * Replace document.write/writeln with basic html parsing that appends
915 * to the `<body>` to avoid blanking pages. Added JavaScript will not run.
918 * @deprecated since 1.26
920 [ 'write', 'writeln' ].forEach( ( func ) => {
921 mw.log.deprecate( document, func, function () {
922 $( document.body ).append( $.parseHTML( slice.call( arguments ).join( '' ) ) );
923 }, 'Use jQuery or mw.loader.load instead.', 'document.' + func );
926 // Load other files in the package
927 require( './errorLogger.js' );