3 let config
= require( './config.json' );
4 const portletLinkOptions
= require( './portletLinkOptions.json' );
5 const infinityValues
= require( './infinityValues.json' );
7 require( './jquery.accessKeyLabel.js' );
10 * Encode the string like PHP's rawurlencode.
13 * @param {string} str String to be encoded.
14 * @return {string} Encoded string
16 function rawurlencode( str
) {
17 return encodeURIComponent( String( str
) )
18 .replace( /!/g
, '%21' )
19 .replace( /'/g, '%27' )
20 .replace( /\(/g, '%28' )
21 .replace( /\)/g, '%29' )
22 .replace( /\*/g, '%2A
' )
23 .replace( /~/g, '%7E
' );
27 * Private helper function used by util.escapeId*()
30 * @param {string} str String to be encoded
31 * @param {string} mode Encoding mode, see documentation at
32 * MainConfigSchema::FragmentMode.
33 * @return {string} Encoded string
35 function escapeIdInternal( str, mode ) {
40 return str.replace( / /g, '_
' );
42 return rawurlencode( str.replace( / /g, '_
' ) )
43 .replace( /%3A/g, ':' )
44 .replace( /%/g, '.' );
46 throw new Error( 'Unrecognized ID escaping mode
' + mode );
51 * Library providing useful common skin-agnostic utility functions. Please see
52 * [mediawiki.util]{@link module:mediawiki.util}.
54 * Alias for the [mediawiki.util]{@link module:mediawiki.util} module.
60 * Utility library provided by the `mediawiki.util` ResourceLoader module. Accessible inside ResourceLoader modules
61 * or for gadgets as part of the [mw global object]{@link mw}.
64 * // Inside MediaWiki extensions
65 * const util = require( 'mediawiki
.util
' );
67 * const mwUtil = mw.util;
68 * @exports mediawiki.util
73 * Encode the string like PHP's rawurlencode
.
76 * @param
{string
} str String to be encoded
.
77 * @return {string
} Encoded string
79 rawurlencode
: rawurlencode
,
82 * Encode a string as CSS id, for use as HTML id attribute value.
84 * Analog to `Sanitizer::escapeIdForAttribute()` in PHP.
87 * @param {string} str String to encode
88 * @return {string} Encoded string
90 escapeIdForAttribute: function ( str
) {
91 return escapeIdInternal( str
, config
.FragmentMode
[ 0 ] );
95 * Encode a string as URL fragment, for use as HTML anchor link.
97 * Analog to `Sanitizer::escapeIdForLink()` in PHP.
100 * @param {string} str String to encode
101 * @return {string} Encoded string
103 escapeIdForLink: function ( str
) {
104 return escapeIdInternal( str
, config
.FragmentMode
[ 0 ] );
108 * Get the target element from a link hash.
110 * This is the same element as you would get from
111 * document.querySelectorAll(':target'), but can be used on
112 * an arbitrary hash fragment, or after pushState/replaceState
115 * Link fragments can be unencoded, fully encoded or partially
116 * encoded, as defined in the spec.
118 * We can't just use decodeURI as that assumes the fragment
119 * is fully encoded, and throws an error on a string like '%A',
120 * so we use the percent-decode.
122 * @param {string} [hash] Hash fragment, without the leading '#'.
123 * Taken from location.hash if omitted.
124 * @return {HTMLElement|null} Element, if found
126 getTargetFromFragment: function ( hash
) {
127 hash
= hash
|| location
.hash
.slice( 1 );
129 // Firefox emits a console warning if you pass an empty string
130 // to getElementById (T272844).
133 // Per https://html.spec.whatwg.org/multipage/browsing-the-web.html#target-element
134 // we try the raw fragment first, then the percent-decoded fragment.
135 const element
= document
.getElementById( hash
);
139 const decodedHash
= this.percentDecodeFragment( hash
);
140 if ( !decodedHash
) {
141 // decodedHash can return null, calling getElementById would cast it to a string
144 return document
.getElementById( decodedHash
);
148 * Percent-decode a string, as found in a URL hash fragment.
150 * Implements the percent-decode method as defined in
151 * https://url.spec.whatwg.org/#percent-decode.
153 * URLSearchParams implements https://url.spec.whatwg.org/#concept-urlencoded-parser
154 * which performs a '+' to ' ' substitution before running percent-decode.
156 * To get the desired behaviour we percent-encode any '+' in the fragment
157 * to effectively expose the percent-decode implementation.
159 * @param {string} text Text to decode
160 * @return {string|null} Decoded text, null if decoding failed
162 percentDecodeFragment: function ( text
) {
163 const params
= new URLSearchParams(
166 // Query string param decoding replaces '+' with ' ' before doing the
167 // percent_decode, so encode '+' to prevent this.
168 .replace( /\+/g, '%2B' )
169 // Query strings are split on '&' and then '=' so encode these too.
170 .replace( /&/g
, '%26' )
171 .replace( /=/g
, '%3D' )
173 return params
.get( 'q' );
177 * Return a function, that, as long as it continues to be invoked, will not
178 * be triggered. The function will be called after it stops being called for
179 * N milliseconds. If `immediate` is passed, trigger the function on the
180 * leading edge, instead of the trailing.
182 * Ported from Underscore.js 1.5.2, Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud
183 * and Investigative Reporters & Editors, distributed under the MIT license, from
184 * <https://github.com/jashkenas/underscore/blob/1.5.2/underscore.js#L689>.
187 * @param {Function} func Function to debounce
188 * @param {number} [wait=0] Wait period in milliseconds
189 * @param {boolean} [immediate] Trigger on leading edge
190 * @return {Function} Debounced function
192 debounce: function ( func
, wait
, immediate
) {
193 // Old signature (wait, func).
194 if ( typeof func
=== 'number' ) {
195 const tmpWait
= wait
;
201 const context
= this,
203 later = function () {
206 func
.apply( context
, args
);
209 if ( immediate
&& !timeout
) {
210 func
.apply( context
, args
);
212 if ( !timeout
|| wait
) {
213 clearTimeout( timeout
);
214 timeout
= setTimeout( later
, wait
);
220 * Return a function, that, when invoked, will only be triggered at most once
221 * during a given window of time. If called again during that window, it will
222 * wait until the window ends and then trigger itself again.
224 * As it's not knowable to the caller whether the function will actually run
225 * when the wrapper is called, return values from the function are entirely
230 * @param {Function} func Function to throttle
231 * @param {number} wait Throttle window length, in milliseconds
232 * @return {Function} Throttled function
234 throttle: function ( func
, wait
) {
235 let context
, args
, timeout
,
236 previous
= Date
.now() - wait
;
237 const run = function () {
239 previous
= Date
.now();
240 func
.apply( context
, args
);
243 // Check how long it's been since the last time the function was
244 // called, and whether it's more or less than the requested throttle
245 // period. If it's less, run the function immediately. If it's more,
246 // set a timeout for the remaining time -- but don't replace an
247 // existing timeout, since that'd indefinitely prolong the wait.
248 const remaining
= Math
.max( wait
- ( Date
.now() - previous
), 0 );
252 // If time is up, do setTimeout( run, 0 ) so the function
253 // always runs asynchronously, just like Promise#then .
254 timeout
= setTimeout( run
, remaining
);
260 * Encode page titles in a way that matches `wfUrlencode` in PHP.
262 * This is important both for readability and consistency in the user experience,
263 * as well as for caching. If URLs are not formatted in the canonical way, they
264 * may be subject to drastically shorter cache durations and/or miss automatic
265 * purging after edits, thus leading to stale content being served from a
269 * @param {string} str String to be encoded.
270 * @return {string} Encoded string
272 wikiUrlencode
: mw
.internalWikiUrlencode
,
275 * Get the URL to a given local wiki page name.
277 * @param {string|null} [pageName=wgPageName] Page name
278 * @param {Object} [params] A mapping of query parameter names to values,
279 * e.g. `{ action: 'edit' }`
280 * @return {string} URL, relative to `wgServer`.
282 getUrl: function ( pageName
, params
) {
283 let url
, query
, fragment
,
284 title
= typeof pageName
=== 'string' ? pageName
: mw
.config
.get( 'wgPageName' );
287 const fragmentIdx
= title
.indexOf( '#' );
288 if ( fragmentIdx
!== -1 ) {
289 fragment
= title
.slice( fragmentIdx
+ 1 );
290 // Exclude the fragment from the page name
291 title
= title
.slice( 0, fragmentIdx
);
294 // Produce query string
296 query
= $.param( params
);
299 if ( !title
&& fragment
) {
300 // If only a fragment was given, make a fragment-only link (T288415)
302 } else if ( query
) {
304 util
.wikiScript() + '?title=' + util
.wikiUrlencode( title
) + '&' + query
:
305 util
.wikiScript() + '?' + query
;
307 // Specify a function as the replacement,
308 // so that "$" characters in title are not interpreted.
309 url
= mw
.config
.get( 'wgArticlePath' )
310 .replace( '$1', () => util
.wikiUrlencode( title
) );
313 // Append the encoded fragment
315 url
+= '#' + util
.escapeIdForLink( fragment
);
322 * Get URL to a MediaWiki entry point.
324 * Similar to `wfScript()` in PHP.
327 * @param {string} [str="index"] Name of entry point (e.g. 'index' or 'api')
328 * @return {string} URL to the script file (e.g. `/w/api.php`)
330 wikiScript: function ( str
) {
331 if ( !str
|| str
=== 'index' ) {
332 return mw
.config
.get( 'wgScript' );
333 } else if ( str
=== 'load' ) {
334 return config
.LoadScript
;
336 return mw
.config
.get( 'wgScriptPath' ) + '/' + str
+ '.php';
341 * Append a new style block to the head and return the CSSStyleSheet object.
343 * To access the `<style>` element, reference `sheet.ownerNode`, or call
344 * the {@link mw.loader.addStyleTag} method directly.
346 * This function returns the CSSStyleSheet object for convenience with features
347 * that are managed at that level, such as toggling of styles:
349 * var sheet = util.addCSS( '.foobar { display: none; }' );
350 * $( '#myButton' ).click( function () {
351 * // Toggle the sheet on and off
352 * sheet.disabled = !sheet.disabled;
356 * See also [MDN: CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet).
358 * @param {string} text CSS to be appended
359 * @return {CSSStyleSheet} The sheet object
361 addCSS: function ( text
) {
362 const s
= mw
.loader
.addStyleTag( text
);
367 * Get the value for a given URL query parameter.
370 * mw.util.getParamValue( 'foo', '/?foo=x' ); // "x"
371 * mw.util.getParamValue( 'foo', '/?foo=' ); // ""
372 * mw.util.getParamValue( 'foo', '/' ); // null
374 * @param {string} param The parameter name.
375 * @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
376 * @return {string|null} Parameter value, or null if parameter was not found.
378 getParamValue: function ( param
, url
) {
379 // Get last match, stop at hash
381 const re
= new RegExp( '^[^#]*[&?]' + util
.escapeRegExp( param
) + '=([^&#]*)' ),
382 m
= re
.exec( url
!== undefined ? url
: location
.href
);
385 // Beware that decodeURIComponent is not required to understand '+'
386 // by spec, as encodeURIComponent does not produce it.
388 return decodeURIComponent( m
[ 1 ].replace( /\+/g, '%20' ) );
390 // catch URIError if parameter is invalid UTF-8
391 // due to malformed or double-decoded values (T106244),
392 // e.g. "Autom%F3vil" instead of "Autom%C3%B3vil".
399 * Get the value for an array query parameter, combined according to similar rules as PHP uses.
400 * Currently this does not handle associative or multi-dimensional arrays, but that may be
401 * improved in the future.
404 * mw.util.getArrayParam( 'foo', new URLSearchParams( '?foo[0]=a&foo[1]=b' ) ); // [ 'a', 'b' ]
405 * mw.util.getArrayParam( 'foo', new URLSearchParams( '?foo[]=a&foo[]=b' ) ); // [ 'a', 'b' ]
406 * mw.util.getArrayParam( 'foo', new URLSearchParams( '?foo=a' ) ); // null
408 * @param {string} param The parameter name.
409 * @param {URLSearchParams} [params] Parsed URL parameters to search through, defaulting to the current browsing location.
410 * @return {string[]|null} Parameter value, or null if parameter was not found.
412 getArrayParam: function ( param
, params
) {
414 const paramRe
= new RegExp( '^' + util
.escapeRegExp( param
) + '\\[(\\d*)\\]$' );
417 params
= new URLSearchParams( location
.search
);
421 params
.forEach( ( v
, k
) => {
422 const paramMatch
= k
.match( paramRe
);
424 let i
= paramMatch
[ 1 ];
426 // If no explicit index, append at the end
433 return arr
.length
? arr
: null;
437 * The content wrapper of the skin (`.mw-body`, for example).
439 * Populated on document ready. To use this property,
440 * wait for `$.ready` and be sure to have a module dependency on
441 * `mediawiki.util` which will ensure
442 * your document ready handler fires after initialization.
444 * Because of the lazy-initialised nature of this property,
445 * you're discouraged from using it.
447 * If you need just the wikipage content (not any of the
448 * extra elements output by the skin), use `$( '#mw-content-text' )`
449 * instead. Or listen to {@link event:'wikipage.content' wikipage.content}
450 * which will allow your code to re-run when the page changes (e.g. live preview
451 * or re-render after ajax save).
460 * @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
462 hidePortlet: function ( portletId
) {
463 const portlet
= document
.getElementById( portletId
);
465 portlet
.classList
.add( 'emptyPortlet' );
470 * Whether a portlet is visible.
472 * @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
475 isPortletVisible: function ( portletId
) {
476 const portlet
= document
.getElementById( portletId
);
477 return portlet
&& !portlet
.classList
.contains( 'emptyPortlet' );
481 * Reveal a portlet if it is hidden.
483 * @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
485 showPortlet: function ( portletId
) {
486 const portlet
= document
.getElementById( portletId
);
488 portlet
.classList
.remove( 'emptyPortlet' );
493 * Clears the entire subtitle if present in the page. Used for refreshing subtitle
494 * after edit with response from parse API.
496 clearSubtitle: function () {
497 const subtitle
= document
.getElementById( 'mw-content-subtitle' );
499 subtitle
.innerHTML
= '';
504 * Create a message box element. Callers are responsible for ensuring suitable Codex styles
505 * have been added to the page e.g. mediawiki.codex.messagebox.styles.
508 * @param {string|Node} textOrElement text or node.
509 * @param {string} [type] defaults to notice.
510 * @param {boolean} [inline] whether the notice should be inline.
513 messageBox: function ( textOrElement
, type
= 'notice', inline
= false ) {
514 const msgBoxElement
= document
.createElement( 'div' );
515 msgBoxElement
.classList
.add( 'cdx-message' );
517 if ( [ 'error', 'warning', 'success', 'notice' ].indexOf( type
) > -1 ) {
518 // The following CSS classes are used here:
519 // * cdx-message--notice
520 // * cdx-message--warning
521 // * cdx-message--error
522 // * cdx-message--success
523 msgBoxElement
.classList
.add( `cdx-message--${ type }` );
525 msgBoxElement
.classList
.add( inline
? 'cdx-message--inline' : 'cdx-message--block' );
527 if ( type
=== 'error' ) {
528 msgBoxElement
.setAttribute( 'role', 'alert' );
530 msgBoxElement
.setAttribute( 'aria-live', 'polite' );
533 const iconElement
= document
.createElement( 'span' );
534 iconElement
.classList
.add( 'cdx-message__icon' );
535 const contentElement
= document
.createElement( 'div' );
536 contentElement
.classList
.add( 'cdx-message__content' );
537 if ( typeof textOrElement
=== 'string' ) {
538 contentElement
.textContent
= textOrElement
;
540 contentElement
.appendChild( textOrElement
);
542 msgBoxElement
.appendChild( iconElement
);
543 msgBoxElement
.appendChild( contentElement
);
544 return msgBoxElement
;
548 * Add content to the subtitle of the skin.
550 * @param {HTMLElement|string} nodeOrHTMLString
552 addSubtitle: function ( nodeOrHTMLString
) {
553 const subtitle
= document
.getElementById( 'mw-content-subtitle' );
555 if ( typeof nodeOrHTMLString
=== 'string' ) {
556 subtitle
.innerHTML
+= nodeOrHTMLString
;
558 subtitle
.appendChild( nodeOrHTMLString
);
561 throw new Error( 'This skin does not support additions to the subtitle.' );
566 * Creates a detached portlet Element in the skin with no elements.
569 * // Create a portlet with 2 menu items that is styled as a dropdown in certain skins.
570 * const p = mw.util.addPortlet( 'p-myportlet', 'My label', '#p-cactions' );
571 * mw.util.addPortletLink( 'p-myportlet', '#', 'Link 1' );
572 * mw.util.addPortletLink( 'p-myportlet', '#', 'Link 2' );
573 * @param {string} id of the new portlet.
574 * @param {string} [label] of the new portlet.
575 * @param {string} [selectorHint] selector of the element the new portlet would like to
576 * be inserted near. Typically the portlet will be inserted after this selector, but in some
577 * skins, the skin may relocate the element when provided to the closest available space.
578 * If this argument is not passed then the caller is responsible for appending the element
579 * to the DOM before using addPortletLink.
580 * To add a portlet in an exact position do not rely on this parameter, instead using the return
581 * element (make sure to also assign the result to a variable), use
582 * ```p.parentNode.appendChild( p );```
583 * When provided, skins can use the parameter to infer information about how the user intended
584 * the menu to be rendered. For example, in vector and vector-2022 targeting '#p-cactions' will
585 * result in the creation of a dropdown.
586 * @fires Hooks~'util.addPortlet'
587 * @return {HTMLElement|null} will be null if it was not possible to create an portlet with
588 * the required information e.g. the selector given in `selectorHint` parameter could not be resolved
589 * to an existing element in the page.
591 addPortlet: function ( id
, label
, selectorHint
) {
592 const portlet
= document
.createElement( 'div' );
593 // These classes should be kept in sync with includes/skins/components/SkinComponentMenu.php.
594 // eslint-disable-next-line mediawiki/class-doc
595 portlet
.classList
.add( 'mw-portlet', 'mw-portlet-' + id
, 'emptyPortlet',
596 // Additional class is added to allow skins to track portlets added via this mechanism.
601 const labelNode
= document
.createElement( 'label' );
602 labelNode
.textContent
= label
;
603 portlet
.appendChild( labelNode
);
605 const listWrapper
= document
.createElement( 'div' );
606 const list
= document
.createElement( 'ul' );
607 listWrapper
.appendChild( list
);
608 portlet
.appendChild( listWrapper
);
609 if ( selectorHint
) {
612 referenceNode
= document
.querySelector( selectorHint
);
614 // CSS selector not supported by browser.
616 if ( referenceNode
) {
617 const parentNode
= referenceNode
.parentNode
;
618 parentNode
.insertBefore( portlet
, referenceNode
);
624 * Fires when a portlet is successfully created.
626 * @event ~'util.addPortlet'
628 * @param {HTMLElement} portlet the portlet that was created.
629 * @param {string|null} selectorHint the css selector used to append to the DOM.
632 * mw.hook( 'util.addPortlet' ).add( ( p ) => {
633 * p.style.border = 'solid 1px black';
636 mw
.hook( 'util.addPortlet' ).fire( portlet
, selectorHint
);
640 * Add a link to a portlet menu on the page.
642 * The portlets that are supported include:
644 * - p-cactions (Content actions)
645 * - p-personal (Personal tools)
646 * - p-navigation (Navigation)
648 * - p-associated-pages (For namespaces and special page tabs on supported skins)
649 * - p-namespaces (For namespaces on legacy skins)
651 * Additional menus can be discovered through the following code:
652 * ```$('.mw-portlet').toArray().map((el) => el.id);```
654 * Menu availability varies by skin, wiki, and current page.
656 * The first three parameters are required, the others are optional and
657 * may be null. Though providing an id and tooltip is recommended.
659 * By default, the new link will be added to the end of the menu. To
660 * add the link before an existing item, pass the DOM node or a CSS selector
661 * for that item, e.g. `'#foobar'` or `document.getElementById( 'foobar' )`.
663 * mw.util.addPortletLink(
664 * 'p-tb', 'https://www.mediawiki.org/',
665 * 'mediawiki.org', 't-mworg', 'Go to mediawiki.org', 'm', '#t-print'
668 * var node = mw.util.addPortletLink(
670 * mw.util.getUrl( 'Special:Example' ),
673 * $( node ).on( 'click', function ( e ) {
674 * console.log( 'Example' );
675 * e.preventDefault();
679 * Remember that to call this inside a user script, you may have to ensure the
680 * `mediawiki.util` is loaded first:
682 * $.when( mw.loader.using( [ 'mediawiki.util' ] ), $.ready ).then( function () {
683 * mw.util.addPortletLink( 'p-tb', 'https://www.mediawiki.org/', 'mediawiki.org' );
687 * @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
688 * @param {string} href Link URL
689 * @param {string} text Link text
690 * @param {string} [id] ID of the list item, should be unique and preferably have
691 * the appropriate prefix ('ca-', 'pt-', 'n-' or 't-')
692 * @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix
693 * @param {string} [accesskey] Access key to activate this link. One character only,
694 * avoid conflicts with other links. Use `$( '[accesskey=x]' )` in the console to
695 * see if 'x' is already used.
696 * @param {HTMLElement|jQuery|string} [nextnode] Element that the new item should be added before.
697 * Must be another item in the same list, it will be ignored otherwise.
698 * Can be specified as DOM reference, as jQuery object, or as CSS selector string.
699 * @fires Hooks~'util.addPortletLink'
700 * @return {HTMLElement|null} The added list item, or null if no element was added.
702 addPortletLink: function ( portletId
, href
, text
, id
, tooltip
, accesskey
, nextnode
) {
704 // Avoid confusing id="undefined" lookup
708 const portlet
= document
.getElementById( portletId
);
710 // Invalid portlet ID
714 // Setup the anchor tag and set any the properties
715 const link
= document
.createElement( 'a' );
718 let linkChild
= document
.createTextNode( text
);
719 let i
= portletLinkOptions
[ 'text-wrapper' ].length
;
720 // Wrap link using text-wrapper option if provided
721 // Iterate backward since the wrappers are declared from outer to inner,
722 // and we build it up from the inside out.
724 const wrapper
= portletLinkOptions
[ 'text-wrapper' ][ i
];
725 const wrapperElement
= document
.createElement( wrapper
.tag
);
726 if ( wrapper
.attributes
) {
727 $( wrapperElement
).attr( wrapper
.attributes
);
729 wrapperElement
.appendChild( linkChild
);
730 linkChild
= wrapperElement
;
732 link
.appendChild( linkChild
);
735 link
.title
= tooltip
;
738 link
.accessKey
= accesskey
;
741 // Unhide portlet if it was hidden before
742 util
.showPortlet( portletId
);
744 const item
= $( '<li>' ).append( link
)[ 0 ];
745 // mw-list-item-js distinguishes portlet links added via javascript and the server
746 item
.className
= 'mw-list-item mw-list-item-js';
751 // Select the first (most likely only) unordered list inside the portlet
752 let ul
= portlet
.tagName
.toLowerCase() === 'ul' ? portlet
: portlet
.querySelector( 'ul' );
754 // If it didn't have an unordered list yet, create one
755 ul
= document
.createElement( 'ul' );
756 const portletDiv
= portlet
.querySelector( 'div' );
758 // Support: Legacy skins have a div (such as div.body or div.pBody).
759 // Append the <ul> to that.
760 portletDiv
.appendChild( ul
);
762 // Append it to the portlet directly
763 portlet
.appendChild( ul
);
768 if ( nextnode
&& ( typeof nextnode
=== 'string' || nextnode
.nodeType
|| nextnode
.jquery
) ) {
769 // eslint-disable-next-line no-jquery/variable-pattern
770 nextnode
= $( ul
).find( nextnode
);
771 if ( nextnode
.length
=== 1 && nextnode
[ 0 ].parentNode
=== ul
) {
772 // Insertion point: Before nextnode
773 nextnode
.before( item
);
776 // Else: Invalid nextnode value (no match, more than one match, or not a direct child)
777 // Else: Invalid nextnode type
781 // Insertion point: End of list (default)
782 ul
.appendChild( item
);
785 // Update tooltip for the access key after inserting into DOM
786 // to get a localized access key label (T69946).
788 $( link
).updateTooltipAccessKeys();
792 * Fires when a portlet link is successfully created.
794 * @event ~'util.addPortletLink'
796 * @param {HTMLElement} item the portlet link that was created.
797 * @param {Object} information about the item include id.
800 * mw.hook( 'util.addPortletLink' ).add( ( link ) => {
801 * const span = $( '<span class="icon">' );
802 * link.appendChild( span );
805 mw
.hook( 'util.addPortletLink' ).fire( item
, {
812 * Validate a string as representing a valid e-mail address.
814 * This validation is based on the HTML5 specification.
817 * mw.util.validateEmail( "me@example.org" ) === true;
819 * @param {string} email E-mail address
820 * @return {boolean|null} True if valid, false if invalid, null if `email` was empty.
822 validateEmail: function ( email
) {
823 if ( email
=== '' ) {
827 // HTML5 defines a string as valid e-mail address if it matches
829 // 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
831 // - atext : defined in RFC 5322 section 3.2.3
832 // - ldh-str : defined in RFC 1034 section 3.5
834 // (see STD 68 / RFC 5234 https://tools.ietf.org/html/std68)
835 // First, define the RFC 5322 'atext' which is pretty easy:
836 // atext = ALPHA / DIGIT / ; Printable US-ASCII
837 // "!" / "#" / ; characters not including
838 // "$" / "%" / ; specials. Used for atoms.
847 const rfc5322Atext
= 'a-z0-9!#$%&\'*+\\-/=?^_`{|}~';
849 // Next define the RFC 1034 'ldh-str'
850 // <domain> ::= <subdomain> | " "
851 // <subdomain> ::= <label> | <subdomain> "." <label>
852 // <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
853 // <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
854 // <let-dig-hyp> ::= <let-dig> | "-"
855 // <let-dig> ::= <letter> | <digit>
856 const rfc1034LdhStr
= 'a-z0-9\\-';
858 const html5EmailRegexp
= new RegExp(
861 // User part which is liberal :p
862 '[' + rfc5322Atext
+ '\\.]+' +
866 '[' + rfc1034LdhStr
+ ']+' +
867 // Optional second part and following are separated by a dot
868 '(?:\\.[' + rfc1034LdhStr
+ ']+)*' +
871 // RegExp is case insensitive
874 return ( email
.match( html5EmailRegexp
) !== null );
878 * Whether a string is a valid IPv4 address or not.
880 * Based on \Wikimedia\IPUtils::isIPv4 in PHP.
884 * mw.util.isIPv4Address( '80.100.20.101' );
885 * mw.util.isIPv4Address( '192.168.1.101' );
888 * mw.util.isIPv4Address( '192.0.2.0/24' );
889 * mw.util.isIPv4Address( 'hello' );
891 * @param {string} address
892 * @param {boolean} [allowBlock=false]
895 isIPv4Address: function ( address
, allowBlock
) {
897 if ( typeof address
!== 'string' ) {
901 const RE_IP_BYTE
= '(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])';
902 const RE_IP_ADD
= '(?:' + RE_IP_BYTE
+ '\\.){3}' + RE_IP_BYTE
;
903 const block
= allowBlock
? '(?:\\/(?:3[0-2]|[12]?\\d))?' : '';
905 return ( new RegExp( '^' + RE_IP_ADD
+ block
+ '$' ).test( address
) );
909 * Whether a string is a valid IPv6 address or not.
911 * Based on \Wikimedia\IPUtils::isIPv6 in PHP.
915 * mw.util.isIPv6Address( '2001:db8:a:0:0:0:0:0' );
916 * mw.util.isIPv6Address( '2001:db8:a::' );
919 * mw.util.isIPv6Address( '2001:db8:a::/32' );
920 * mw.util.isIPv6Address( 'hello' );
922 * @param {string} address
923 * @param {boolean} [allowBlock=false]
926 isIPv6Address: function ( address
, allowBlock
) {
927 if ( typeof address
!== 'string' ) {
931 const block
= allowBlock
? '(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?' : '';
933 '(?:' + // starts with "::" (including "::")
937 '|' + // ends with "::" (except "::")
942 '|' + // contains no "::"
949 if ( new RegExp( '^' + RE_IPV6_ADD
+ block
+ '$' ).test( address
) ) {
953 // contains one "::" in the middle (single '::' check below)
962 new RegExp( '^' + RE_IPV6_ADD
+ block
+ '$' ).test( address
) &&
963 /::/.test( address
) &&
964 !/::.*::/.test( address
)
969 * Check whether a string is a valid IP address.
972 * @param {string} address String to check
973 * @param {boolean} [allowBlock=false] If a block of IPs should be allowed
976 isIPAddress: function ( address
, allowBlock
) {
977 return util
.isIPv4Address( address
, allowBlock
) ||
978 util
.isIPv6Address( address
, allowBlock
);
982 * @typedef {Object} ResizeableThumbnailUrl
983 * @property {string} name File name (same format as Title.getMainText()).
984 * @property {number} [width] Thumbnail width, in pixels. Null when the file is not
986 * @property {function(number):string} [resizeUrl] A function that takes a width
987 * parameter and returns a thumbnail URL (URL-encoded) with that width. The width
988 * parameter must be smaller than the width of the original image (or equal to it; that
989 * only works if MediaHandler::mustRender returns true for the file). Null when the
990 * file in the original URL is not a thumbnail.
991 * On wikis with $wgGenerateThumbnailOnParse set to true, this will fall back to using
992 * Special:Redirect which is less efficient. Otherwise, it is a direct thumbnail URL.
996 * Parse the URL of an image uploaded to MediaWiki, or a thumbnail for such an image,
997 * and return the image name, thumbnail size and a template that can be used to resize
1000 * @param {string} url URL to parse (URL-encoded)
1001 * @return {ResizeableThumbnailUrl|null} null if the URL is not a valid MediaWiki
1002 * image/thumbnail URL.
1004 parseImageUrl: function ( url
) {
1005 let name
, decodedName
, width
, urlTemplate
;
1007 // thumb.php-generated thumbnails
1008 // thumb.php?f=<name>&w[idth]=<width>[px]
1009 if ( /thumb\.php/.test( url
) ) {
1010 decodedName
= mw
.util
.getParamValue( 'f', url
);
1011 name
= encodeURIComponent( decodedName
);
1012 width
= mw
.util
.getParamValue( 'width', url
) || mw
.util
.getParamValue( 'w', url
);
1013 urlTemplate
= url
.replace( /([&?])w(?:idth)?=[^&]+/g, '' ) + '&width={width}';
1017 // /<hash prefix>/<name>/[<options>-]<width>-<name*>[.<ext>]
1018 // where <name*> could be the filename, 'thumbnail.<ext>' (for long filenames)
1019 // or the base-36 SHA1 of the filename.
1021 /\/[\da-f]\/[\da-f]{2}\/([^\s/]+)\/(?:[^\s/]+-)?(\d
+)px
-(?:\1|thumbnail
|[a
-z
\d
]{31})(\.[^\s
/]+)?$/,
1024 // /<hash prefix>/<name>
1025 /\/[\da-f]\/[\da-f]{2}\/([^\s/]+)$/,
1027 // Thumbnails in non-hashed upload directories
1028 // /<name>/[<options>-]<width>-<name*>[.<ext>]
1030 /\/([^\s/]+)\/(?:[^\s/]+-)?(\d
+)px
-(?:\1|thumbnail
|[a
-z
\d
]{31})[^\s
/]*$/,
1032 // Full-size images in non-hashed upload directories
1036 for ( let i
= 0; i
< regexes
.length
; i
++ ) {
1037 const match
= url
.match( regexes
[ i
] );
1040 decodedName
= decodeURIComponent( name
);
1041 width
= match
[ 2 ] || null;
1048 if ( width
!== null ) {
1049 width
= parseInt( width
, 10 ) || null;
1051 if ( config
.GenerateThumbnailOnParse
) {
1052 // The wiki cannot generate thumbnails on demand. Use a special page - this means
1053 // an extra redirect and PHP request, but it will generate the thumbnail if it does
1055 urlTemplate
= mw
.util
.getUrl( 'Special:Redirect/file/' + decodedName
, { width
: '{width}' } )
1056 // getUrl urlencodes the template variable, fix that
1057 .replace( '%7Bwidth%7D', '{width}' );
1058 } else if ( width
&& !urlTemplate
) {
1059 // Javascript does not expose regexp capturing group indexes, and the width
1060 // part could in theory also occur in the filename so hide that first.
1061 const strippedUrl
= url
.replace( name
, '{name}' )
1062 .replace( name
, '{name}' )
1063 .replace( width
+ 'px-', '{width}px-' );
1064 urlTemplate
= strippedUrl
.replace( /\{name\}/g, name
);
1067 name
: decodedName
.replace( /_
/g
, ' ' ),
1069 resizeUrl
: urlTemplate
? function ( w
) {
1070 return urlTemplate
.replace( '{width}', w
);
1078 * Escape string for safe inclusion in regular expression.
1080 * The following characters are escaped:
1082 * \ { } ( ) | . ? * + - ^ $ [ ]
1084 * @since 1.26; moved to mw.util in 1.34
1085 * @param {string} str String to escape
1086 * @return {string} Escaped string
1088 escapeRegExp: function ( str
) {
1089 // eslint-disable-next-line no-useless-escape
1090 return str
.replace( /([\\{}()|.?*+\-^$\[\]])/g, '\\$1' );
1094 * Convert an IP into a verbose, uppercase, normalized form.
1096 * Both IPv4 and IPv6 addresses are trimmed. Additionally,
1097 * IPv6 addresses in octet notation are expanded to 8 words;
1098 * IPv4 addresses have leading zeros, in each octet, removed.
1100 * This functionality has been adapted from \Wikimedia\IPUtils::sanitizeIP()
1102 * @param {string} ip IP address in quad or octet form (CIDR or not).
1103 * @return {string|null}
1105 sanitizeIP: function ( ip
) {
1106 if ( typeof ip
!== 'string' ) {
1113 if ( !this.isIPAddress( ip
, true ) ) {
1116 if ( this.isIPv4Address( ip
, true ) ) {
1117 return ip
.replace( /(^|\.)0+(\d)/g, '$1$2' );
1119 ip
= ip
.toUpperCase();
1120 const abbrevPos
= ip
.indexOf( '::' );
1121 if ( abbrevPos
!== -1 ) {
1122 const CIDRStart
= ip
.indexOf( '/' );
1123 const addressEnd
= ( CIDRStart
!== -1 ) ? CIDRStart
- 1 : ip
.length
- 1;
1124 let repeatStr
, extra
, pad
;
1125 if ( abbrevPos
=== 0 ) {
1127 extra
= ip
=== '::' ? '0' : '';
1129 } else if ( abbrevPos
=== addressEnd
- 1 ) {
1138 const count
= pad
- ( ip
.split( ':' ).length
- 1 );
1139 ip
= ip
.replace( '::', repeatStr
.repeat( count
) + extra
);
1141 return ip
.replace( /(^|:)0+(([0-9A-Fa-f]{1,4}))/g, '$1$2' );
1145 * Prettify an IP for display to end users.
1147 * This will make it more compact and lower-case.
1149 * This functionality has been adapted from \Wikimedia\IPUtils::prettifyIP()
1151 * @param {string} ip IP address in quad or octet form (CIDR or not).
1152 * @return {string|null}
1154 prettifyIP: function ( ip
) {
1155 ip
= this.sanitizeIP( ip
);
1156 if ( ip
=== null ) {
1159 if ( this.isIPv6Address( ip
, true ) ) {
1160 let cidr
, replaceZeros
;
1161 if ( ip
.indexOf( '/' ) !== -1 ) {
1162 const ipCidrSplit
= ip
.split( '/', 2 );
1163 ip
= ipCidrSplit
[ 0 ];
1164 cidr
= ipCidrSplit
[ 1 ];
1168 const matches
= ip
.match( /(?:^|:)0(?::0)+(?:$|:)/g );
1170 replaceZeros
= matches
[ 0 ];
1171 for ( let i
= 1; i
< matches
.length
; i
++ ) {
1172 if ( matches
[ i
].length
> replaceZeros
.length
) {
1173 replaceZeros
= matches
[ i
];
1177 ip
= ip
.replace( replaceZeros
, '::' );
1179 if ( cidr
!== '' ) {
1180 ip
= ip
.concat( '/', cidr
);
1182 ip
= ip
.toLowerCase();
1188 * Checks if the given username matches $wgAutoCreateTempUser.
1190 * This functionality has been adapted from MediaWiki\User\TempUser\Pattern::isMatch()
1192 * @param {string|null} username
1195 isTemporaryUser: function ( username
) {
1196 // Just return early if temporary accounts are not known about.
1197 if ( !config
.AutoCreateTempUser
.enabled
&& !config
.AutoCreateTempUser
.known
) {
1200 if ( username
=== null ) {
1203 /** @type {string|string[]} */
1204 let matchPatterns
= config
.AutoCreateTempUser
.matchPattern
;
1205 if ( typeof matchPatterns
=== 'string' ) {
1206 matchPatterns
= [ matchPatterns
];
1207 } else if ( matchPatterns
=== null ) {
1208 matchPatterns
= [ config
.AutoCreateTempUser
.genPattern
];
1210 for ( let i
= 0; i
< matchPatterns
.length
; i
++ ) {
1211 const autoCreateUserMatchPattern
= matchPatterns
[ i
];
1212 // Check each match pattern, and if any matches then return a match.
1213 const position
= autoCreateUserMatchPattern
.indexOf( '$1' );
1215 // '$1' was not found in autoCreateUserMatchPattern
1216 if ( position
=== -1 ) {
1219 const prefix
= autoCreateUserMatchPattern
.slice( 0, position
);
1220 const suffix
= autoCreateUserMatchPattern
.slice( position
+ '$1'.length
);
1223 if ( prefix
!== '' ) {
1224 match
= ( username
.indexOf( prefix
) === 0 );
1226 if ( match
&& suffix
!== '' ) {
1227 match
= ( username
.slice( -suffix
.length
) === suffix
) &&
1228 ( username
.length
>= prefix
.length
+ suffix
.length
);
1234 // No match patterns matched the username, so the given username is not a temporary user.
1239 * Determine if an input string represents a value of infinity.
1240 * This is used when testing for infinity in the context of expiries,
1241 * such as watchlisting, page protection, and block expiries.
1243 * @param {string|null} str
1247 isInfinity: function ( str
) {
1248 return infinityValues
.indexOf( str
) !== -1;
1253 * Initialisation of mw.util.$content
1258 // The preferred standard is class "mw-body".
1259 // You may also use class "mw-body mw-body-primary" if you use
1260 // mw-body in multiple locations. Or class "mw-body-primary" if
1261 // you use mw-body deeper in the DOM.
1262 const content
= document
.querySelector( '.mw-body-primary' ) ||
1263 document
.querySelector( '.mw-body' ) ||
1264 // If the skin has no such class, fall back to the parser output
1265 document
.querySelector( '#mw-content-text' ) ||
1266 // Should never happen..., except if the skin is still in development.
1269 util
.$content
= $( content
);
1272 // Backwards-compatible alias for mediawiki.RegExp module.
1273 // @deprecated since 1.34
1275 mw
.log
.deprecate( mw
.RegExp
, 'escape', util
.escapeRegExp
, 'Use mw.util.escapeRegExp() instead.', 'mw.RegExp.escape' );
1277 if ( window
.QUnit
) {
1278 // Not allowed outside unit tests
1279 util
.setOptionsForTest = function ( opts
) {
1280 config
= !opts
? require( './config.json' ) : Object
.assign( {}, config
, opts
);
1288 module
.exports
= util
;