Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F5974458
util.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
40 KB
Referenced Files
None
Subscribers
None
util.js
View Options
'use strict'
;
let
config
=
require
(
'./config.json'
);
const
portletLinkOptions
=
require
(
'./portletLinkOptions.json'
);
const
infinityValues
=
require
(
'./infinityValues.json'
);
require
(
'./jquery.accessKeyLabel.js'
);
/**
* Encode the string like PHP's rawurlencode.
*
* @ignore
* @param {string} str String to be encoded.
* @return {string} Encoded string
*/
function
rawurlencode
(
str
)
{
return
encodeURIComponent
(
String
(
str
)
)
.
replace
(
/!/g
,
'%21'
)
.
replace
(
/'/g
,
'%27'
)
.
replace
(
/\(/g
,
'%28'
)
.
replace
(
/\)/g
,
'%29'
)
.
replace
(
/\*/g
,
'%2A'
)
.
replace
(
/~/g
,
'%7E'
);
}
/**
* Private helper function used by util.escapeId*()
*
* @ignore
* @param {string} str String to be encoded
* @param {string} mode Encoding mode, see documentation at
* MainConfigSchema::FragmentMode.
* @return {string} Encoded string
*/
function
escapeIdInternal
(
str
,
mode
)
{
str
=
String
(
str
);
switch
(
mode
)
{
case
'html5'
:
return
str
.
replace
(
/ /g
,
'_'
);
case
'legacy'
:
return
rawurlencode
(
str
.
replace
(
/ /g
,
'_'
)
)
.
replace
(
/%3A/g
,
':'
)
.
replace
(
/%/g
,
'.'
);
default
:
throw
new
Error
(
'Unrecognized ID escaping mode '
+
mode
);
}
}
/**
* Library providing useful common skin-agnostic utility functions. Please see
* [mediawiki.util]{@link module:mediawiki.util}.
*
* Alias for the [mediawiki.util]{@link module:mediawiki.util} module.
*
* @namespace mw.util
*/
/**
* Utility library provided by the `mediawiki.util` ResourceLoader module. Accessible inside ResourceLoader modules
* or for gadgets as part of the [mw global object]{@link mw}.
*
* @example
* // Inside MediaWiki extensions
* const util = require( 'mediawiki.util' );
* // In gadgets
* const mwUtil = mw.util;
* @exports mediawiki.util
*/
const
util
=
{
/**
* Encode the string like PHP's rawurlencode.
*
* @method
* @param {string} str String to be encoded.
* @return {string} Encoded string
*/
rawurlencode
:
rawurlencode
,
/**
* Encode a string as CSS id, for use as HTML id attribute value.
*
* Analog to `Sanitizer::escapeIdForAttribute()` in PHP.
*
* @since 1.30
* @param {string} str String to encode
* @return {string} Encoded string
*/
escapeIdForAttribute
:
function
(
str
)
{
return
escapeIdInternal
(
str
,
config
.
FragmentMode
[
0
]
);
},
/**
* Encode a string as URL fragment, for use as HTML anchor link.
*
* Analog to `Sanitizer::escapeIdForLink()` in PHP.
*
* @since 1.30
* @param {string} str String to encode
* @return {string} Encoded string
*/
escapeIdForLink
:
function
(
str
)
{
return
escapeIdInternal
(
str
,
config
.
FragmentMode
[
0
]
);
},
/**
* Get the target element from a link hash.
*
* This is the same element as you would get from
* document.querySelectorAll(':target'), but can be used on
* an arbitrary hash fragment, or after pushState/replaceState
* has been used.
*
* Link fragments can be unencoded, fully encoded or partially
* encoded, as defined in the spec.
*
* We can't just use decodeURI as that assumes the fragment
* is fully encoded, and throws an error on a string like '%A',
* so we use the percent-decode.
*
* @param {string} [hash] Hash fragment, without the leading '#'.
* Taken from location.hash if omitted.
* @return {HTMLElement|null} Element, if found
*/
getTargetFromFragment
:
function
(
hash
)
{
hash
=
hash
||
location
.
hash
.
slice
(
1
);
if
(
!
hash
)
{
// Firefox emits a console warning if you pass an empty string
// to getElementById (T272844).
return
null
;
}
// Per https://html.spec.whatwg.org/multipage/browsing-the-web.html#target-element
// we try the raw fragment first, then the percent-decoded fragment.
const
element
=
document
.
getElementById
(
hash
);
if
(
element
)
{
return
element
;
}
const
decodedHash
=
this
.
percentDecodeFragment
(
hash
);
if
(
!
decodedHash
)
{
// decodedHash can return null, calling getElementById would cast it to a string
return
null
;
}
return
document
.
getElementById
(
decodedHash
);
},
/**
* Percent-decode a string, as found in a URL hash fragment.
*
* Implements the percent-decode method as defined in
* https://url.spec.whatwg.org/#percent-decode.
*
* URLSearchParams implements https://url.spec.whatwg.org/#concept-urlencoded-parser
* which performs a '+' to ' ' substitution before running percent-decode.
*
* To get the desired behaviour we percent-encode any '+' in the fragment
* to effectively expose the percent-decode implementation.
*
* @param {string} text Text to decode
* @return {string|null} Decoded text, null if decoding failed
*/
percentDecodeFragment
:
function
(
text
)
{
const
params
=
new
URLSearchParams
(
'q='
+
text
// Query string param decoding replaces '+' with ' ' before doing the
// percent_decode, so encode '+' to prevent this.
.
replace
(
/\+/g
,
'%2B'
)
// Query strings are split on '&' and then '=' so encode these too.
.
replace
(
/&/g
,
'%26'
)
.
replace
(
/=/g
,
'%3D'
)
);
return
params
.
get
(
'q'
);
},
/**
* Return a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. If `immediate` is passed, trigger the function on the
* leading edge, instead of the trailing.
*
* Ported from Underscore.js 1.5.2, Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud
* and Investigative Reporters & Editors, distributed under the MIT license, from
* <https://github.com/jashkenas/underscore/blob/1.5.2/underscore.js#L689>.
*
* @since 1.34
* @param {Function} func Function to debounce
* @param {number} [wait=0] Wait period in milliseconds
* @param {boolean} [immediate] Trigger on leading edge
* @return {Function} Debounced function
*/
debounce
:
function
(
func
,
wait
,
immediate
)
{
// Old signature (wait, func).
if
(
typeof
func
===
'number'
)
{
const
tmpWait
=
wait
;
wait
=
func
;
func
=
tmpWait
;
}
let
timeout
;
return
function
()
{
const
context
=
this
,
args
=
arguments
,
later
=
function
()
{
timeout
=
null
;
if
(
!
immediate
)
{
func
.
apply
(
context
,
args
);
}
};
if
(
immediate
&&
!
timeout
)
{
func
.
apply
(
context
,
args
);
}
if
(
!
timeout
||
wait
)
{
clearTimeout
(
timeout
);
timeout
=
setTimeout
(
later
,
wait
);
}
};
},
/**
* Return a function, that, when invoked, will only be triggered at most once
* during a given window of time. If called again during that window, it will
* wait until the window ends and then trigger itself again.
*
* As it's not knowable to the caller whether the function will actually run
* when the wrapper is called, return values from the function are entirely
* discarded.
*
* Ported from OOUI.
*
* @param {Function} func Function to throttle
* @param {number} wait Throttle window length, in milliseconds
* @return {Function} Throttled function
*/
throttle
:
function
(
func
,
wait
)
{
let
context
,
args
,
timeout
,
previous
=
Date
.
now
()
-
wait
;
const
run
=
function
()
{
timeout
=
null
;
previous
=
Date
.
now
();
func
.
apply
(
context
,
args
);
};
return
function
()
{
// Check how long it's been since the last time the function was
// called, and whether it's more or less than the requested throttle
// period. If it's less, run the function immediately. If it's more,
// set a timeout for the remaining time -- but don't replace an
// existing timeout, since that'd indefinitely prolong the wait.
const
remaining
=
Math
.
max
(
wait
-
(
Date
.
now
()
-
previous
),
0
);
context
=
this
;
args
=
arguments
;
if
(
!
timeout
)
{
// If time is up, do setTimeout( run, 0 ) so the function
// always runs asynchronously, just like Promise#then .
timeout
=
setTimeout
(
run
,
remaining
);
}
};
},
/**
* Encode page titles in a way that matches `wfUrlencode` in PHP.
*
* This is important both for readability and consistency in the user experience,
* as well as for caching. If URLs are not formatted in the canonical way, they
* may be subject to drastically shorter cache durations and/or miss automatic
* purging after edits, thus leading to stale content being served from a
* non-canonical URL.
*
* @method
* @param {string} str String to be encoded.
* @return {string} Encoded string
*/
wikiUrlencode
:
mw
.
internalWikiUrlencode
,
/**
* Get the URL to a given local wiki page name.
*
* @param {string|null} [pageName=wgPageName] Page name
* @param {Object} [params] A mapping of query parameter names to values,
* e.g. `{ action: 'edit' }`
* @return {string} URL, relative to `wgServer`.
*/
getUrl
:
function
(
pageName
,
params
)
{
let
url
,
query
,
fragment
,
title
=
typeof
pageName
===
'string'
?
pageName
:
mw
.
config
.
get
(
'wgPageName'
);
// Find any fragment
const
fragmentIdx
=
title
.
indexOf
(
'#'
);
if
(
fragmentIdx
!==
-
1
)
{
fragment
=
title
.
slice
(
fragmentIdx
+
1
);
// Exclude the fragment from the page name
title
=
title
.
slice
(
0
,
fragmentIdx
);
}
// Produce query string
if
(
params
)
{
query
=
$
.
param
(
params
);
}
if
(
!
title
&&
fragment
)
{
// If only a fragment was given, make a fragment-only link (T288415)
url
=
''
;
}
else
if
(
query
)
{
url
=
title
?
util
.
wikiScript
()
+
'?title='
+
util
.
wikiUrlencode
(
title
)
+
'&'
+
query
:
util
.
wikiScript
()
+
'?'
+
query
;
}
else
{
// Specify a function as the replacement,
// so that "$" characters in title are not interpreted.
url
=
mw
.
config
.
get
(
'wgArticlePath'
)
.
replace
(
'$1'
,
()
=>
util
.
wikiUrlencode
(
title
)
);
}
// Append the encoded fragment
if
(
fragment
)
{
url
+=
'#'
+
util
.
escapeIdForLink
(
fragment
);
}
return
url
;
},
/**
* Get URL to a MediaWiki entry point.
*
* Similar to `wfScript()` in PHP.
*
* @since 1.18
* @param {string} [str="index"] Name of entry point (e.g. 'index' or 'api')
* @return {string} URL to the script file (e.g. `/w/api.php`)
*/
wikiScript
:
function
(
str
)
{
if
(
!
str
||
str
===
'index'
)
{
return
mw
.
config
.
get
(
'wgScript'
);
}
else
if
(
str
===
'load'
)
{
return
config
.
LoadScript
;
}
else
{
return
mw
.
config
.
get
(
'wgScriptPath'
)
+
'/'
+
str
+
'.php'
;
}
},
/**
* Append a new style block to the head and return the CSSStyleSheet object.
*
* To access the `<style>` element, reference `sheet.ownerNode`, or call
* the {@link mw.loader.addStyleTag} method directly.
*
* This function returns the CSSStyleSheet object for convenience with features
* that are managed at that level, such as toggling of styles:
* ```
* var sheet = util.addCSS( '.foobar { display: none; }' );
* $( '#myButton' ).click( function () {
* // Toggle the sheet on and off
* sheet.disabled = !sheet.disabled;
* } );
* ```
*
* See also [MDN: CSSStyleSheet](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet).
*
* @param {string} text CSS to be appended
* @return {CSSStyleSheet} The sheet object
*/
addCSS
:
function
(
text
)
{
const
s
=
mw
.
loader
.
addStyleTag
(
text
);
return
s
.
sheet
;
},
/**
* Get the value for a given URL query parameter.
*
* @example
* mw.util.getParamValue( 'foo', '/?foo=x' ); // "x"
* mw.util.getParamValue( 'foo', '/?foo=' ); // ""
* mw.util.getParamValue( 'foo', '/' ); // null
*
* @param {string} param The parameter name.
* @param {string} [url=location.href] URL to search through, defaulting to the current browsing location.
* @return {string|null} Parameter value, or null if parameter was not found.
*/
getParamValue
:
function
(
param
,
url
)
{
// Get last match, stop at hash
const
re
=
new
RegExp
(
'^[^#]*[&?]'
+
util
.
escapeRegExp
(
param
)
+
'=([^&#]*)'
),
m
=
re
.
exec
(
url
!==
undefined
?
url
:
location
.
href
);
if
(
m
)
{
// Beware that decodeURIComponent is not required to understand '+'
// by spec, as encodeURIComponent does not produce it.
try
{
return
decodeURIComponent
(
m
[
1
].
replace
(
/\+/g
,
'%20'
)
);
}
catch
(
e
)
{
// catch URIError if parameter is invalid UTF-8
// due to malformed or double-decoded values (T106244),
// e.g. "Autom%F3vil" instead of "Autom%C3%B3vil".
}
}
return
null
;
},
/**
* Get the value for an array query parameter, combined according to similar rules as PHP uses.
* Currently this does not handle associative or multi-dimensional arrays, but that may be
* improved in the future.
*
* @example
* mw.util.getArrayParam( 'foo', new URLSearchParams( '?foo[0]=a&foo[1]=b' ) ); // [ 'a', 'b' ]
* mw.util.getArrayParam( 'foo', new URLSearchParams( '?foo[]=a&foo[]=b' ) ); // [ 'a', 'b' ]
* mw.util.getArrayParam( 'foo', new URLSearchParams( '?foo=a' ) ); // null
*
* @param {string} param The parameter name.
* @param {URLSearchParams} [params] Parsed URL parameters to search through, defaulting to the current browsing location.
* @return {string[]|null} Parameter value, or null if parameter was not found.
*/
getArrayParam
:
function
(
param
,
params
)
{
const
paramRe
=
new
RegExp
(
'^'
+
util
.
escapeRegExp
(
param
)
+
'\\[(\\d*)\\]$'
);
if
(
!
params
)
{
params
=
new
URLSearchParams
(
location
.
search
);
}
const
arr
=
[];
params
.
forEach
(
(
v
,
k
)
=>
{
const
paramMatch
=
k
.
match
(
paramRe
);
if
(
paramMatch
)
{
let
i
=
paramMatch
[
1
];
if
(
i
===
''
)
{
// If no explicit index, append at the end
i
=
arr
.
length
;
}
arr
[
i
]
=
v
;
}
}
);
return
arr
.
length
?
arr
:
null
;
},
/**
* The content wrapper of the skin (`.mw-body`, for example).
*
* Populated on document ready. To use this property,
* wait for `$.ready` and be sure to have a module dependency on
* `mediawiki.util` which will ensure
* your document ready handler fires after initialization.
*
* Because of the lazy-initialised nature of this property,
* you're discouraged from using it.
*
* If you need just the wikipage content (not any of the
* extra elements output by the skin), use `$( '#mw-content-text' )`
* instead. Or listen to {@link event:'wikipage.content' wikipage.content}
* which will allow your code to re-run when the page changes (e.g. live preview
* or re-render after ajax save).
*
* @type {jQuery}
*/
$content
:
null
,
/**
* Hide a portlet.
*
* @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
*/
hidePortlet
:
function
(
portletId
)
{
const
portlet
=
document
.
getElementById
(
portletId
);
if
(
portlet
)
{
portlet
.
classList
.
add
(
'emptyPortlet'
);
}
},
/**
* Whether a portlet is visible.
*
* @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
* @return {boolean}
*/
isPortletVisible
:
function
(
portletId
)
{
const
portlet
=
document
.
getElementById
(
portletId
);
return
portlet
&&
!
portlet
.
classList
.
contains
(
'emptyPortlet'
);
},
/**
* Reveal a portlet if it is hidden.
*
* @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
*/
showPortlet
:
function
(
portletId
)
{
const
portlet
=
document
.
getElementById
(
portletId
);
if
(
portlet
)
{
portlet
.
classList
.
remove
(
'emptyPortlet'
);
}
},
/**
* Clears the entire subtitle if present in the page. Used for refreshing subtitle
* after edit with response from parse API.
*/
clearSubtitle
:
function
()
{
const
subtitle
=
document
.
getElementById
(
'mw-content-subtitle'
);
if
(
subtitle
)
{
subtitle
.
innerHTML
=
''
;
}
},
/**
* Create a message box element. Callers are responsible for ensuring suitable Codex styles
* have been added to the page e.g. mediawiki.codex.messagebox.styles.
*
* @since 1.43
* @param {string|Element} textOrElement text or node.
* @param {string} [type] defaults to notice.
* @param {boolean} [inline] whether the notice should be inline.
* @return {Element}
*/
messageBox
:
function
(
textOrElement
,
type
=
'notice'
,
inline
=
false
)
{
const
msgBoxElement
=
document
.
createElement
(
'div'
);
msgBoxElement
.
classList
.
add
(
'cdx-message'
);
if
(
[
'error'
,
'warning'
,
'success'
,
'notice'
].
indexOf
(
type
)
>
-
1
)
{
// The following CSS classes are used here:
// * cdx-message--notice
// * cdx-message--warning
// * cdx-message--error
msgBoxElement
.
classList
.
add
(
`cdx-message--
${
type
}
`
);
}
msgBoxElement
.
classList
.
add
(
inline
?
'cdx-message--inline'
:
'cdx-message--block'
);
msgBoxElement
.
setAttribute
(
'aria-live'
,
'polite'
);
const
iconElement
=
document
.
createElement
(
'span'
);
iconElement
.
classList
.
add
(
'cdx-message__icon'
);
const
contentElement
=
document
.
createElement
(
'div'
);
contentElement
.
classList
.
add
(
'cdx-message__content'
);
if
(
typeof
textOrElement
===
'string'
)
{
contentElement
.
textContent
=
textOrElement
;
}
else
{
contentElement
.
appendChild
(
textOrElement
);
}
msgBoxElement
.
appendChild
(
iconElement
);
msgBoxElement
.
appendChild
(
contentElement
);
return
msgBoxElement
;
},
/**
* Add content to the subtitle of the skin.
*
* @param {HTMLElement|string} nodeOrHTMLString
*/
addSubtitle
:
function
(
nodeOrHTMLString
)
{
const
subtitle
=
document
.
getElementById
(
'mw-content-subtitle'
);
if
(
subtitle
)
{
if
(
typeof
nodeOrHTMLString
===
'string'
)
{
subtitle
.
innerHTML
+=
nodeOrHTMLString
;
}
else
{
subtitle
.
appendChild
(
nodeOrHTMLString
);
}
}
else
{
throw
new
Error
(
'This skin does not support additions to the subtitle.'
);
}
},
/**
* Creates a detached portlet Element in the skin with no elements.
*
* @example
* // Create a portlet with 2 menu items that is styled as a dropdown in certain skins.
* const p = mw.util.addPortlet( 'p-myportlet', 'My label', '#p-cactions' );
* mw.util.addPortletLink( 'p-myportlet', '#', 'Link 1' );
* mw.util.addPortletLink( 'p-myportlet', '#', 'Link 2' );
* @param {string} id of the new portlet.
* @param {string} [label] of the new portlet.
* @param {string} [selectorHint] selector of the element the new portlet would like to
* be inserted near. Typically the portlet will be inserted after this selector, but in some
* skins, the skin may relocate the element when provided to the closest available space.
* If this argument is not passed then the caller is responsible for appending the element
* to the DOM before using addPortletLink.
* To add a portlet in an exact position do not rely on this parameter, instead using the return
* element (make sure to also assign the result to a variable), use
* ```p.parentNode.appendChild( p );```
* When provided, skins can use the parameter to infer information about how the user intended
* the menu to be rendered. For example, in vector and vector-2022 targeting '#p-cactions' will
* result in the creation of a dropdown.
* @fires Hooks~'util.addPortlet'
* @return {HTMLElement|null} will be null if it was not possible to create an portlet with
* the required information e.g. the selector given in `selectorHint` parameter could not be resolved
* to an existing element in the page.
*/
addPortlet
:
function
(
id
,
label
,
selectorHint
)
{
const
portlet
=
document
.
createElement
(
'div'
);
// These classes should be kept in sync with includes/skins/components/SkinComponentMenu.php.
// eslint-disable-next-line mediawiki/class-doc
portlet
.
classList
.
add
(
'mw-portlet'
,
'mw-portlet-'
+
id
,
'emptyPortlet'
,
// Additional class is added to allow skins to track portlets added via this mechanism.
'mw-portlet-js'
);
portlet
.
id
=
id
;
if
(
label
)
{
const
labelNode
=
document
.
createElement
(
'label'
);
labelNode
.
textContent
=
label
;
portlet
.
appendChild
(
labelNode
);
}
const
listWrapper
=
document
.
createElement
(
'div'
);
const
list
=
document
.
createElement
(
'ul'
);
listWrapper
.
appendChild
(
list
);
portlet
.
appendChild
(
listWrapper
);
if
(
selectorHint
)
{
let
referenceNode
;
try
{
referenceNode
=
document
.
querySelector
(
selectorHint
);
}
catch
(
e
)
{
// CSS selector not supported by browser.
}
if
(
referenceNode
)
{
const
parentNode
=
referenceNode
.
parentNode
;
parentNode
.
insertBefore
(
portlet
,
referenceNode
);
}
else
{
return
null
;
}
}
/**
* Fires when a portlet is successfully created.
*
* @event ~'util.addPortlet'
* @memberof Hooks
* @param {HTMLElement} portlet the portlet that was created.
* @param {string|null} selectorHint the css selector used to append to the DOM.
*
* @example
* mw.hook( 'util.addPortlet' ).add( ( p ) => {
* p.style.border = 'solid 1px black';
* } );
*/
mw
.
hook
(
'util.addPortlet'
).
fire
(
portlet
,
selectorHint
);
return
portlet
;
},
/**
* Add a link to a portlet menu on the page.
*
* The portlets that are supported include:
*
* - p-cactions (Content actions)
* - p-personal (Personal tools)
* - p-navigation (Navigation)
* - p-tb (Toolbox)
* - p-associated-pages (For namespaces and special page tabs on supported skins)
* - p-namespaces (For namespaces on legacy skins)
*
* Additional menus can be discovered through the following code:
* ```$('.mw-portlet').toArray().map((el) => el.id);```
*
* Menu availability varies by skin, wiki, and current page.
*
* The first three parameters are required, the others are optional and
* may be null. Though providing an id and tooltip is recommended.
*
* By default, the new link will be added to the end of the menu. To
* add the link before an existing item, pass the DOM node or a CSS selector
* for that item, e.g. `'#foobar'` or `document.getElementById( 'foobar' )`.
* ```
* mw.util.addPortletLink(
* 'p-tb', 'https://www.mediawiki.org/',
* 'mediawiki.org', 't-mworg', 'Go to mediawiki.org', 'm', '#t-print'
* );
*
* var node = mw.util.addPortletLink(
* 'p-tb',
* mw.util.getUrl( 'Special:Example' ),
* 'Example'
* );
* $( node ).on( 'click', function ( e ) {
* console.log( 'Example' );
* e.preventDefault();
* } );
* ```
*
* Remember that to call this inside a user script, you may have to ensure the
* `mediawiki.util` is loaded first:
* ```
* $.when( mw.loader.using( [ 'mediawiki.util' ] ), $.ready ).then( function () {
* mw.util.addPortletLink( 'p-tb', 'https://www.mediawiki.org/', 'mediawiki.org' );
* } );
* ```
*
* @param {string} portletId ID of the target portlet (e.g. 'p-cactions' or 'p-personal')
* @param {string} href Link URL
* @param {string} text Link text
* @param {string} [id] ID of the list item, should be unique and preferably have
* the appropriate prefix ('ca-', 'pt-', 'n-' or 't-')
* @param {string} [tooltip] Text to show when hovering over the link, without accesskey suffix
* @param {string} [accesskey] Access key to activate this link. One character only,
* avoid conflicts with other links. Use `$( '[accesskey=x]' )` in the console to
* see if 'x' is already used.
* @param {HTMLElement|jQuery|string} [nextnode] Element that the new item should be added before.
* Must be another item in the same list, it will be ignored otherwise.
* Can be specified as DOM reference, as jQuery object, or as CSS selector string.
* @fires Hooks~'util.addPortletLink'
* @return {HTMLElement|null} The added list item, or null if no element was added.
*/
addPortletLink
:
function
(
portletId
,
href
,
text
,
id
,
tooltip
,
accesskey
,
nextnode
)
{
if
(
!
portletId
)
{
// Avoid confusing id="undefined" lookup
return
null
;
}
const
portlet
=
document
.
getElementById
(
portletId
);
if
(
!
portlet
)
{
// Invalid portlet ID
return
null
;
}
// Setup the anchor tag and set any the properties
const
link
=
document
.
createElement
(
'a'
);
link
.
href
=
href
;
let
linkChild
=
document
.
createTextNode
(
text
);
let
i
=
portletLinkOptions
[
'text-wrapper'
].
length
;
// Wrap link using text-wrapper option if provided
// Iterate backward since the wrappers are declared from outer to inner,
// and we build it up from the inside out.
while
(
i
--
)
{
const
wrapper
=
portletLinkOptions
[
'text-wrapper'
][
i
];
const
wrapperElement
=
document
.
createElement
(
wrapper
.
tag
);
if
(
wrapper
.
attributes
)
{
$
(
wrapperElement
).
attr
(
wrapper
.
attributes
);
}
wrapperElement
.
appendChild
(
linkChild
);
linkChild
=
wrapperElement
;
}
link
.
appendChild
(
linkChild
);
if
(
tooltip
)
{
link
.
title
=
tooltip
;
}
if
(
accesskey
)
{
link
.
accessKey
=
accesskey
;
}
// Unhide portlet if it was hidden before
util
.
showPortlet
(
portletId
);
const
item
=
$
(
'<li>'
).
append
(
link
)[
0
];
// mw-list-item-js distinguishes portlet links added via javascript and the server
item
.
className
=
'mw-list-item mw-list-item-js'
;
if
(
id
)
{
item
.
id
=
id
;
}
// Select the first (most likely only) unordered list inside the portlet
let
ul
=
portlet
.
tagName
.
toLowerCase
()
===
'ul'
?
portlet
:
portlet
.
querySelector
(
'ul'
);
if
(
!
ul
)
{
// If it didn't have an unordered list yet, create one
ul
=
document
.
createElement
(
'ul'
);
const
portletDiv
=
portlet
.
querySelector
(
'div'
);
if
(
portletDiv
)
{
// Support: Legacy skins have a div (such as div.body or div.pBody).
// Append the <ul> to that.
portletDiv
.
appendChild
(
ul
);
}
else
{
// Append it to the portlet directly
portlet
.
appendChild
(
ul
);
}
}
let
next
;
if
(
nextnode
&&
(
typeof
nextnode
===
'string'
||
nextnode
.
nodeType
||
nextnode
.
jquery
)
)
{
// eslint-disable-next-line no-jquery/variable-pattern
nextnode
=
$
(
ul
).
find
(
nextnode
);
if
(
nextnode
.
length
===
1
&&
nextnode
[
0
].
parentNode
===
ul
)
{
// Insertion point: Before nextnode
nextnode
.
before
(
item
);
next
=
true
;
}
// Else: Invalid nextnode value (no match, more than one match, or not a direct child)
// Else: Invalid nextnode type
}
if
(
!
next
)
{
// Insertion point: End of list (default)
ul
.
appendChild
(
item
);
}
// Update tooltip for the access key after inserting into DOM
// to get a localized access key label (T69946).
if
(
accesskey
)
{
$
(
link
).
updateTooltipAccessKeys
();
}
/**
* Fires when a portlet link is successfully created.
*
* @event ~'util.addPortletLink'
* @memberof Hooks
* @param {HTMLElement} item the portlet link that was created.
* @param {Object} information about the item include id.
*
* @example
* mw.hook( 'util.addPortletLink' ).add( ( link ) => {
* const span = $( '<span class="icon">' );
* link.appendChild( span );
* } );
*/
mw
.
hook
(
'util.addPortletLink'
).
fire
(
item
,
{
id
:
id
}
);
return
item
;
},
/**
* Validate a string as representing a valid e-mail address.
*
* This validation is based on the HTML5 specification.
*
* @example
* mw.util.validateEmail( "me@example.org" ) === true;
*
* @param {string} email E-mail address
* @return {boolean|null} True if valid, false if invalid, null if `email` was empty.
*/
validateEmail
:
function
(
email
)
{
if
(
email
===
''
)
{
return
null
;
}
// HTML5 defines a string as valid e-mail address if it matches
// the ABNF:
// 1 * ( atext / "." ) "@" ldh-str 1*( "." ldh-str )
// With:
// - atext : defined in RFC 5322 section 3.2.3
// - ldh-str : defined in RFC 1034 section 3.5
//
// (see STD 68 / RFC 5234 https://tools.ietf.org/html/std68)
// First, define the RFC 5322 'atext' which is pretty easy:
// atext = ALPHA / DIGIT / ; Printable US-ASCII
// "!" / "#" / ; characters not including
// "$" / "%" / ; specials. Used for atoms.
// "&" / "'" /
// "*" / "+" /
// "-" / "/" /
// "=" / "?" /
// "^" / "_" /
// "`" / "{" /
// "|" / "}" /
// "~"
const
rfc5322Atext
=
'a-z0-9!#$%&\'*+\\-/=?^_`{|}~'
;
// Next define the RFC 1034 'ldh-str'
// <domain> ::= <subdomain> | " "
// <subdomain> ::= <label> | <subdomain> "." <label>
// <label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]
// <ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
// <let-dig-hyp> ::= <let-dig> | "-"
// <let-dig> ::= <letter> | <digit>
const
rfc1034LdhStr
=
'a-z0-9\\-'
;
const
html5EmailRegexp
=
new
RegExp
(
// start of string
'^'
+
// User part which is liberal :p
'['
+
rfc5322Atext
+
'\\.]+'
+
// 'at'
'@'
+
// Domain first part
'['
+
rfc1034LdhStr
+
']+'
+
// Optional second part and following are separated by a dot
'(?:\\.['
+
rfc1034LdhStr
+
']+)*'
+
// End of string
'$'
,
// RegExp is case insensitive
'i'
);
return
(
email
.
match
(
html5EmailRegexp
)
!==
null
);
},
/**
* Whether a string is a valid IPv4 address or not.
*
* Based on \Wikimedia\IPUtils::isIPv4 in PHP.
*
* @example
* // Valid
* mw.util.isIPv4Address( '80.100.20.101' );
* mw.util.isIPv4Address( '192.168.1.101' );
*
* // Invalid
* mw.util.isIPv4Address( '192.0.2.0/24' );
* mw.util.isIPv4Address( 'hello' );
*
* @param {string} address
* @param {boolean} [allowBlock=false]
* @return {boolean}
*/
isIPv4Address
:
function
(
address
,
allowBlock
)
{
if
(
typeof
address
!==
'string'
)
{
return
false
;
}
const
RE_IP_BYTE
=
'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|0?[0-9]?[0-9])'
;
const
RE_IP_ADD
=
'(?:'
+
RE_IP_BYTE
+
'\\.){3}'
+
RE_IP_BYTE
;
const
block
=
allowBlock
?
'(?:\\/(?:3[0-2]|[12]?\\d))?'
:
''
;
return
(
new
RegExp
(
'^'
+
RE_IP_ADD
+
block
+
'$'
).
test
(
address
)
);
},
/**
* Whether a string is a valid IPv6 address or not.
*
* Based on \Wikimedia\IPUtils::isIPv6 in PHP.
*
* @example
* // Valid
* mw.util.isIPv6Address( '2001:db8:a:0:0:0:0:0' );
* mw.util.isIPv6Address( '2001:db8:a::' );
*
* // Invalid
* mw.util.isIPv6Address( '2001:db8:a::/32' );
* mw.util.isIPv6Address( 'hello' );
*
* @param {string} address
* @param {boolean} [allowBlock=false]
* @return {boolean}
*/
isIPv6Address
:
function
(
address
,
allowBlock
)
{
if
(
typeof
address
!==
'string'
)
{
return
false
;
}
const
block
=
allowBlock
?
'(?:\\/(?:12[0-8]|1[01][0-9]|[1-9]?\\d))?'
:
''
;
let
RE_IPV6_ADD
=
'(?:'
+
// starts with "::" (including "::")
':(?::|(?::'
+
'[0-9A-Fa-f]{1,4}'
+
'){1,7})'
+
'|'
+
// ends with "::" (except "::")
'[0-9A-Fa-f]{1,4}'
+
'(?::'
+
'[0-9A-Fa-f]{1,4}'
+
'){0,6}::'
+
'|'
+
// contains no "::"
'[0-9A-Fa-f]{1,4}'
+
'(?::'
+
'[0-9A-Fa-f]{1,4}'
+
'){7}'
+
')'
;
if
(
new
RegExp
(
'^'
+
RE_IPV6_ADD
+
block
+
'$'
).
test
(
address
)
)
{
return
true
;
}
// contains one "::" in the middle (single '::' check below)
RE_IPV6_ADD
=
'[0-9A-Fa-f]{1,4}'
+
'(?:::?'
+
'[0-9A-Fa-f]{1,4}'
+
'){1,6}'
;
return
(
new
RegExp
(
'^'
+
RE_IPV6_ADD
+
block
+
'$'
).
test
(
address
)
&&
/::/
.
test
(
address
)
&&
!
/::.*::/
.
test
(
address
)
);
},
/**
* Check whether a string is a valid IP address.
*
* @since 1.25
* @param {string} address String to check
* @param {boolean} [allowBlock=false] If a block of IPs should be allowed
* @return {boolean}
*/
isIPAddress
:
function
(
address
,
allowBlock
)
{
return
util
.
isIPv4Address
(
address
,
allowBlock
)
||
util
.
isIPv6Address
(
address
,
allowBlock
);
},
/**
* @typedef {Object} ResizeableThumbnailUrl
* @property {string} name File name (same format as Title.getMainText()).
* @property {number} [width] Thumbnail width, in pixels. Null when the file is not
* a thumbnail.
* @property {function(number):string} [resizeUrl] A function that takes a width
* parameter and returns a thumbnail URL (URL-encoded) with that width. The width
* parameter must be smaller than the width of the original image (or equal to it; that
* only works if MediaHandler::mustRender returns true for the file). Null when the
* file in the original URL is not a thumbnail.
* On wikis with $wgGenerateThumbnailOnParse set to true, this will fall back to using
* Special:Redirect which is less efficient. Otherwise, it is a direct thumbnail URL.
*/
/**
* Parse the URL of an image uploaded to MediaWiki, or a thumbnail for such an image,
* and return the image name, thumbnail size and a template that can be used to resize
* the image.
*
* @param {string} url URL to parse (URL-encoded)
* @return {ResizeableThumbnailUrl|null} null if the URL is not a valid MediaWiki
* image/thumbnail URL.
*/
parseImageUrl
:
function
(
url
)
{
let
name
,
decodedName
,
width
,
urlTemplate
;
// thumb.php-generated thumbnails
// thumb.php?f=<name>&w[idth]=<width>[px]
if
(
/thumb\.php/
.
test
(
url
)
)
{
decodedName
=
mw
.
util
.
getParamValue
(
'f'
,
url
);
name
=
encodeURIComponent
(
decodedName
);
width
=
mw
.
util
.
getParamValue
(
'width'
,
url
)
||
mw
.
util
.
getParamValue
(
'w'
,
url
);
urlTemplate
=
url
.
replace
(
/([&?])w(?:idth)?=[^&]+/g
,
''
)
+
'&width={width}'
;
}
else
{
const
regexes
=
[
// Thumbnails
// /<hash prefix>/<name>/[<options>-]<width>-<name*>[.<ext>]
// where <name*> could be the filename, 'thumbnail.<ext>' (for long filenames)
// or the base-36 SHA1 of the filename.
/\/[\da-f]\/[\da-f]{2}\/([^\s/]+)\/(?:[^\s/]+-)?(\d+)px-(?:\1|thumbnail|[a-z\d]{31})(\.[^\s/]+)?$/
,
// Full size images
// /<hash prefix>/<name>
/\/[\da-f]\/[\da-f]{2}\/([^\s/]+)$/
,
// Thumbnails in non-hashed upload directories
// /<name>/[<options>-]<width>-<name*>[.<ext>]
/\/([^\s/]+)\/(?:[^\s/]+-)?(\d+)px-(?:\1|thumbnail|[a-z\d]{31})[^\s/]*$/
,
// Full-size images in non-hashed upload directories
// /<name>
/\/([^\s/]+)$/
];
for
(
let
i
=
0
;
i
<
regexes
.
length
;
i
++
)
{
const
match
=
url
.
match
(
regexes
[
i
]
);
if
(
match
)
{
name
=
match
[
1
];
decodedName
=
decodeURIComponent
(
name
);
width
=
match
[
2
]
||
null
;
break
;
}
}
}
if
(
name
)
{
if
(
width
!==
null
)
{
width
=
parseInt
(
width
,
10
)
||
null
;
}
if
(
config
.
GenerateThumbnailOnParse
)
{
// The wiki cannot generate thumbnails on demand. Use a special page - this means
// an extra redirect and PHP request, but it will generate the thumbnail if it does
// not exist.
urlTemplate
=
mw
.
util
.
getUrl
(
'Special:Redirect/file/'
+
decodedName
,
{
width
:
'{width}'
}
)
// getUrl urlencodes the template variable, fix that
.
replace
(
'%7Bwidth%7D'
,
'{width}'
);
}
else
if
(
width
&&
!
urlTemplate
)
{
// Javascript does not expose regexp capturing group indexes, and the width
// part could in theory also occur in the filename so hide that first.
const
strippedUrl
=
url
.
replace
(
name
,
'{name}'
)
.
replace
(
name
,
'{name}'
)
.
replace
(
width
+
'px-'
,
'{width}px-'
);
urlTemplate
=
strippedUrl
.
replace
(
/\{name\}/g
,
name
);
}
return
{
name
:
decodedName
.
replace
(
/_/g
,
' '
),
width
,
resizeUrl
:
urlTemplate
?
function
(
w
)
{
return
urlTemplate
.
replace
(
'{width}'
,
w
);
}
:
null
};
}
return
null
;
},
/**
* Escape string for safe inclusion in regular expression.
*
* The following characters are escaped:
*
* \ { } ( ) | . ? * + - ^ $ [ ]
*
* @since 1.26; moved to mw.util in 1.34
* @param {string} str String to escape
* @return {string} Escaped string
*/
escapeRegExp
:
function
(
str
)
{
// eslint-disable-next-line no-useless-escape
return
str
.
replace
(
/([\\{}()|.?*+\-^$\[\]])/g
,
'\\$1'
);
},
/**
* Convert an IP into a verbose, uppercase, normalized form.
*
* Both IPv4 and IPv6 addresses are trimmed. Additionally,
* IPv6 addresses in octet notation are expanded to 8 words;
* IPv4 addresses have leading zeros, in each octet, removed.
*
* This functionality has been adapted from \Wikimedia\IPUtils::sanitizeIP()
*
* @param {string} ip IP address in quad or octet form (CIDR or not).
* @return {string|null}
*/
sanitizeIP
:
function
(
ip
)
{
if
(
typeof
ip
!==
'string'
)
{
return
null
;
}
ip
=
ip
.
trim
();
if
(
ip
===
''
)
{
return
null
;
}
if
(
!
this
.
isIPAddress
(
ip
,
true
)
)
{
return
ip
;
}
if
(
this
.
isIPv4Address
(
ip
,
true
)
)
{
return
ip
.
replace
(
/(^|\.)0+(\d)/g
,
'$1$2'
);
}
ip
=
ip
.
toUpperCase
();
const
abbrevPos
=
ip
.
indexOf
(
'::'
);
if
(
abbrevPos
!==
-
1
)
{
const
CIDRStart
=
ip
.
indexOf
(
'/'
);
const
addressEnd
=
(
CIDRStart
!==
-
1
)
?
CIDRStart
-
1
:
ip
.
length
-
1
;
let
repeatStr
,
extra
,
pad
;
if
(
abbrevPos
===
0
)
{
repeatStr
=
'0:'
;
extra
=
ip
===
'::'
?
'0'
:
''
;
pad
=
9
;
}
else
if
(
abbrevPos
===
addressEnd
-
1
)
{
repeatStr
=
':0'
;
extra
=
''
;
pad
=
9
;
}
else
{
repeatStr
=
':0'
;
extra
=
':'
;
pad
=
8
;
}
const
count
=
pad
-
(
ip
.
split
(
':'
).
length
-
1
);
ip
=
ip
.
replace
(
'::'
,
repeatStr
.
repeat
(
count
)
+
extra
);
}
return
ip
.
replace
(
/(^|:)0+(([0-9A-Fa-f]{1,4}))/g
,
'$1$2'
);
},
/**
* Prettify an IP for display to end users.
*
* This will make it more compact and lower-case.
*
* This functionality has been adapted from \Wikimedia\IPUtils::prettifyIP()
*
* @param {string} ip IP address in quad or octet form (CIDR or not).
* @return {string|null}
*/
prettifyIP
:
function
(
ip
)
{
ip
=
this
.
sanitizeIP
(
ip
);
if
(
ip
===
null
)
{
return
null
;
}
if
(
this
.
isIPv6Address
(
ip
,
true
)
)
{
let
cidr
,
replaceZeros
;
if
(
ip
.
indexOf
(
'/'
)
!==
-
1
)
{
const
ipCidrSplit
=
ip
.
split
(
'/'
,
2
);
ip
=
ipCidrSplit
[
0
];
cidr
=
ipCidrSplit
[
1
];
}
else
{
cidr
=
''
;
}
const
matches
=
ip
.
match
(
/(?:^|:)0(?::0)+(?:$|:)/g
);
if
(
matches
)
{
replaceZeros
=
matches
[
0
];
for
(
let
i
=
1
;
i
<
matches
.
length
;
i
++
)
{
if
(
matches
[
i
].
length
>
replaceZeros
.
length
)
{
replaceZeros
=
matches
[
i
];
}
}
}
ip
=
ip
.
replace
(
replaceZeros
,
'::'
);
if
(
cidr
!==
''
)
{
ip
=
ip
.
concat
(
'/'
,
cidr
);
}
ip
=
ip
.
toLowerCase
();
}
return
ip
;
},
/**
* Checks if the given username matches $wgAutoCreateTempUser.
*
* This functionality has been adapted from MediaWiki\User\TempUser\Pattern::isMatch()
*
* @param {string|null} username
* @return {boolean}
*/
isTemporaryUser
:
function
(
username
)
{
// Just return early if temporary accounts are not known about.
if
(
!
config
.
AutoCreateTempUser
.
enabled
&&
!
config
.
AutoCreateTempUser
.
known
)
{
return
false
;
}
if
(
username
===
null
)
{
return
false
;
}
/** @type {string|string[]} */
let
matchPatterns
=
config
.
AutoCreateTempUser
.
matchPattern
;
if
(
typeof
matchPatterns
===
'string'
)
{
matchPatterns
=
[
matchPatterns
];
}
else
if
(
matchPatterns
===
null
)
{
matchPatterns
=
[
config
.
AutoCreateTempUser
.
genPattern
];
}
for
(
let
i
=
0
;
i
<
matchPatterns
.
length
;
i
++
)
{
const
autoCreateUserMatchPattern
=
matchPatterns
[
i
];
// Check each match pattern, and if any matches then return a match.
const
position
=
autoCreateUserMatchPattern
.
indexOf
(
'$1'
);
// '$1' was not found in autoCreateUserMatchPattern
if
(
position
===
-
1
)
{
return
false
;
}
const
prefix
=
autoCreateUserMatchPattern
.
slice
(
0
,
position
);
const
suffix
=
autoCreateUserMatchPattern
.
slice
(
position
+
'$1'
.
length
);
let
match
=
true
;
if
(
prefix
!==
''
)
{
match
=
(
username
.
indexOf
(
prefix
)
===
0
);
}
if
(
match
&&
suffix
!==
''
)
{
match
=
(
username
.
slice
(
-
suffix
.
length
)
===
suffix
)
&&
(
username
.
length
>=
prefix
.
length
+
suffix
.
length
);
}
if
(
match
)
{
return
true
;
}
}
// No match patterns matched the username, so the given username is not a temporary user.
return
false
;
},
/**
* Determine if an input string represents a value of infinity.
* This is used when testing for infinity in the context of expiries,
* such as watchlisting, page protection, and block expiries.
*
* @param {string|null} str
* @return {boolean}
* @stable
*/
isInfinity
:
function
(
str
)
{
return
infinityValues
.
indexOf
(
str
)
!==
-
1
;
}
};
/**
* Initialisation of mw.util.$content
*
* @ignore
*/
function
init
()
{
// The preferred standard is class "mw-body".
// You may also use class "mw-body mw-body-primary" if you use
// mw-body in multiple locations. Or class "mw-body-primary" if
// you use mw-body deeper in the DOM.
const
content
=
document
.
querySelector
(
'.mw-body-primary'
)
||
document
.
querySelector
(
'.mw-body'
)
||
// If the skin has no such class, fall back to the parser output
document
.
querySelector
(
'#mw-content-text'
)
||
// Should never happen..., except if the skin is still in development.
document
.
body
;
util
.
$content
=
$
(
content
);
}
// Backwards-compatible alias for mediawiki.RegExp module.
// @deprecated since 1.34
mw
.
RegExp
=
{};
mw
.
log
.
deprecate
(
mw
.
RegExp
,
'escape'
,
util
.
escapeRegExp
,
'Use mw.util.escapeRegExp() instead.'
,
'mw.RegExp.escape'
);
if
(
window
.
QUnit
)
{
// Not allowed outside unit tests
util
.
setOptionsForTest
=
function
(
opts
)
{
config
=
!
opts
?
require
(
'./config.json'
)
:
Object
.
assign
(
{},
config
,
opts
);
};
util
.
init
=
init
;
}
else
{
$
(
init
);
}
mw
.
util
=
util
;
module
.
exports
=
util
;
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Oct 3, 17:26 (1 d, 15 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
9f/55/f31c3bec04e7682fc491808f9bde
Default Alt Text
util.js (40 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment