Merge "Selenium: replace UserLoginPage with BlankPage where possible"
[lhc/web/wiklou.git] / resources / src / mediawiki.jqueryMsg / mediawiki.jqueryMsg.js
1 /*!
2 * Experimental advanced wikitext parser-emitter.
3 * See: https://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
4 *
5 * @author neilk@wikimedia.org
6 * @author mflaschen@wikimedia.org
7 */
8
9 /**
10 * @class mw.jqueryMsg
11 * @singleton
12 */
13
14 var oldParser,
15 slice = Array.prototype.slice,
16 parserDefaults = {
17 // Magic words and their expansions. Server-side data is added to this below.
18 magic: {
19 PAGENAME: mw.config.get( 'wgPageName' ),
20 PAGENAMEE: mw.util.wikiUrlencode( mw.config.get( 'wgPageName' ) )
21 },
22 // Whitelist for allowed HTML elements in wikitext.
23 // Self-closing tags are not currently supported.
24 // Filled in with server-side data below
25 allowedHtmlElements: [],
26 // Key tag name, value allowed attributes for that tag.
27 // See Sanitizer::setupAttributeWhitelist
28 allowedHtmlCommonAttributes: [
29 // HTML
30 'id',
31 'class',
32 'style',
33 'lang',
34 'dir',
35 'title',
36
37 // WAI-ARIA
38 'role'
39 ],
40
41 // Attributes allowed for specific elements.
42 // Key is element name in lower case
43 // Value is array of allowed attributes for that element
44 allowedHtmlAttributesByElement: {},
45 messages: mw.messages,
46 language: mw.language,
47
48 // Same meaning as in mediawiki.js.
49 //
50 // Only 'text', 'parse', and 'escaped' are supported, and the
51 // actual escaping for 'escaped' is done by other code (generally
52 // through mediawiki.js).
53 //
54 // However, note that this default only
55 // applies to direct calls to jqueryMsg. The default for mediawiki.js itself
56 // is 'text', including when it uses jqueryMsg.
57 format: 'parse'
58 };
59
60 // Add in server-side data (allowedHtmlElements and magic words)
61 $.extend( true, parserDefaults, require( './parserDefaults.json' ) );
62
63 /**
64 * Wrapper around jQuery append that converts all non-objects to TextNode so append will not
65 * convert what it detects as an htmlString to an element.
66 *
67 * If our own HtmlEmitter jQuery object is given, its children will be unwrapped and appended to
68 * new parent.
69 *
70 * Object elements of children (jQuery, HTMLElement, TextNode, etc.) will be left as is.
71 *
72 * @private
73 * @param {jQuery} $parent Parent node wrapped by jQuery
74 * @param {Object|string|Array} children What to append, with the same possible types as jQuery
75 * @return {jQuery} $parent
76 */
77 function appendWithoutParsing( $parent, children ) {
78 var i, len;
79
80 if ( !Array.isArray( children ) ) {
81 children = [ children ];
82 }
83
84 for ( i = 0, len = children.length; i < len; i++ ) {
85 if ( typeof children[ i ] !== 'object' ) {
86 children[ i ] = document.createTextNode( children[ i ] );
87 }
88 if ( children[ i ] instanceof $ && children[ i ].hasClass( 'mediaWiki_htmlEmitter' ) ) {
89 children[ i ] = children[ i ].contents();
90 }
91 }
92
93 return $parent.append( children );
94 }
95
96 /**
97 * Decodes the main HTML entities, those encoded by mw.html.escape.
98 *
99 * @private
100 * @param {string} encoded Encoded string
101 * @return {string} String with those entities decoded
102 */
103 function decodePrimaryHtmlEntities( encoded ) {
104 return encoded
105 .replace( /&#039;/g, '\'' )
106 .replace( /&quot;/g, '"' )
107 .replace( /&lt;/g, '<' )
108 .replace( /&gt;/g, '>' )
109 .replace( /&amp;/g, '&' );
110 }
111
112 /**
113 * Turn input into a string.
114 *
115 * @private
116 * @param {string|jQuery} input
117 * @return {string} Textual value of input
118 */
119 function textify( input ) {
120 if ( input instanceof $ ) {
121 input = input.text();
122 }
123 return String( input );
124 }
125
126 /**
127 * Given parser options, return a function that parses a key and replacements, returning jQuery object
128 *
129 * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
130 * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
131 * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
132 *
133 * @private
134 * @param {Object} options Parser options
135 * @return {Function}
136 * @return {Array} return.args First element is the key, replacements may be in array in 2nd element, or remaining elements.
137 * @return {jQuery} return.return
138 */
139 function getFailableParserFn( options ) {
140 return function ( args ) {
141 var fallback,
142 parser = new mw.jqueryMsg.Parser( options ),
143 key = args[ 0 ],
144 argsArray = Array.isArray( args[ 1 ] ) ? args[ 1 ] : slice.call( args, 1 );
145 try {
146 return parser.parse( key, argsArray );
147 } catch ( e ) {
148 fallback = parser.settings.messages.get( key );
149 mw.log.warn( 'mediawiki.jqueryMsg: ' + key + ': ' + e.message );
150 mw.track( 'mediawiki.jqueryMsg.error', {
151 messageKey: key,
152 errorMessage: e.message
153 } );
154 return $( '<span>' ).text( fallback );
155 }
156 };
157 }
158
159 mw.jqueryMsg = {};
160
161 /**
162 * Initialize parser defaults.
163 *
164 * ResourceLoaderJqueryMsgModule calls this to provide default values from
165 * Sanitizer.php for allowed HTML elements. To override this data for individual
166 * parsers, pass the relevant options to mw.jqueryMsg.Parser.
167 *
168 * @private
169 * @param {Object} data New data to extend parser defaults with
170 * @param {boolean} [deep=false] Whether the extend is done recursively (deep)
171 */
172 mw.jqueryMsg.setParserDefaults = function ( data, deep ) {
173 if ( deep ) {
174 $.extend( true, parserDefaults, data );
175 } else {
176 $.extend( parserDefaults, data );
177 }
178 };
179
180 /**
181 * Get current parser defaults.
182 *
183 * Primarily used for the unit test. Returns a copy.
184 *
185 * @private
186 * @return {Object}
187 */
188 mw.jqueryMsg.getParserDefaults = function () {
189 return $.extend( {}, parserDefaults );
190 };
191
192 /**
193 * Returns a function suitable for static use, to construct strings from a message key (and optional replacements).
194 *
195 * Example:
196 *
197 * var format = mediaWiki.jqueryMsg.getMessageFunction( options );
198 * $( '#example' ).text( format( 'hello-user', username ) );
199 *
200 * Tthis returns only strings, so it destroys any bindings. If you want to preserve bindings, use the
201 * jQuery plugin version instead. This was originally created to ease migration from `window.gM()`,
202 * from a time when the parser used by `mw.message` was not extendable.
203 *
204 * N.B. replacements are variadic arguments or an array in second parameter. In other words:
205 * somefunction( a, b, c, d )
206 * is equivalent to
207 * somefunction( a, [b, c, d] )
208 *
209 * @param {Object} options parser options
210 * @return {Function} Function The message formatter
211 * @return {string} return.key Message key.
212 * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
213 * @return {string} return.return Rendered HTML.
214 */
215 mw.jqueryMsg.getMessageFunction = function ( options ) {
216 var failableParserFn, format;
217
218 if ( options && options.format !== undefined ) {
219 format = options.format;
220 } else {
221 format = parserDefaults.format;
222 }
223
224 return function () {
225 var failableResult;
226 if ( !failableParserFn ) {
227 failableParserFn = getFailableParserFn( options );
228 }
229 failableResult = failableParserFn( arguments );
230 if ( format === 'text' || format === 'escaped' ) {
231 return failableResult.text();
232 } else {
233 return failableResult.html();
234 }
235 };
236 };
237
238 /**
239 * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to
240 * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.
241 * e.g.
242 *
243 * $.fn.msg = mediaWiki.jqueryMsg.getPlugin( options );
244 * var $userlink = $( '<a>' ).click( function () { alert( "hello!!" ) } );
245 * $( 'p#headline' ).msg( 'hello-user', $userlink );
246 *
247 * N.B. replacements are variadic arguments or an array in second parameter. In other words:
248 * somefunction( a, b, c, d )
249 * is equivalent to
250 * somefunction( a, [b, c, d] )
251 *
252 * We append to 'this', which in a jQuery plugin context will be the selected elements.
253 *
254 * @param {Object} options Parser options
255 * @return {Function} Function suitable for assigning to jQuery plugin, such as jQuery#msg
256 * @return {string} return.key Message key.
257 * @return {Array|Mixed} return.replacements Optional variable replacements (variadically or an array).
258 * @return {jQuery} return.return
259 */
260 mw.jqueryMsg.getPlugin = function ( options ) {
261 var failableParserFn;
262
263 return function () {
264 var $target;
265 if ( !failableParserFn ) {
266 failableParserFn = getFailableParserFn( options );
267 }
268 $target = this.empty();
269 appendWithoutParsing( $target, failableParserFn( arguments ) );
270 return $target;
271 };
272 };
273
274 /**
275 * The parser itself.
276 * Describes an object, whose primary duty is to .parse() message keys.
277 *
278 * @class
279 * @private
280 * @param {Object} options
281 */
282 mw.jqueryMsg.Parser = function ( options ) {
283 this.settings = $.extend( {}, parserDefaults, options );
284 this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
285 this.astCache = {};
286
287 this.emitter = new mw.jqueryMsg.HtmlEmitter( this.settings.language, this.settings.magic );
288 };
289 // Backwards-compatible alias
290 // @deprecated since 1.31
291 mw.jqueryMsg.parser = mw.jqueryMsg.Parser;
292
293 mw.jqueryMsg.Parser.prototype = {
294 /**
295 * Where the magic happens.
296 * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
297 * If an error is thrown, returns original key, and logs the error
298 *
299 * @param {string} key Message key.
300 * @param {Array} replacements Variable replacements for $1, $2... $n
301 * @return {jQuery}
302 */
303 parse: function ( key, replacements ) {
304 var ast = this.getAst( key, replacements );
305 return this.emitter.emit( ast, replacements );
306 },
307
308 /**
309 * Fetch the message string associated with a key, return parsed structure. Memoized.
310 * Note that we pass '⧼' + key + '⧽' back for a missing message here.
311 *
312 * @param {string} key
313 * @param {Array} replacements Variable replacements for $1, $2... $n
314 * @return {string|Array} string of '⧼key⧽' if message missing, simple string if possible, array of arrays if needs parsing
315 */
316 getAst: function ( key, replacements ) {
317 var wikiText;
318
319 if ( !Object.prototype.hasOwnProperty.call( this.astCache, key ) ) {
320 wikiText = this.settings.messages.get( key );
321 if (
322 mw.config.get( 'wgUserLanguage' ) === 'qqx' &&
323 ( !wikiText || wikiText === '(' + key + ')' )
324 ) {
325 wikiText = '(' + key + '$*)';
326 } else if ( typeof wikiText !== 'string' ) {
327 wikiText = '⧼' + key + '⧽';
328 }
329 wikiText = mw.internalDoTransformFormatForQqx( wikiText, replacements );
330 this.astCache[ key ] = this.wikiTextToAst( wikiText );
331 }
332 return this.astCache[ key ];
333 },
334
335 /**
336 * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
337 *
338 * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
339 * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
340 *
341 * @param {string} input Message string wikitext
342 * @throws Error
343 * @return {Mixed} abstract syntax tree
344 */
345 wikiTextToAst: function ( input ) {
346 var pos,
347 regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
348 doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
349 escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
350 whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
351 htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
352 openExtlink, closeExtlink, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
353 templateContents, openTemplate, closeTemplate,
354 nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
355 settings = this.settings,
356 concat = Array.prototype.concat;
357
358 // Indicates current position in input as we parse through it.
359 // Shared among all parsing functions below.
360 pos = 0;
361
362 // =========================================================
363 // parsing combinators - could be a library on its own
364 // =========================================================
365
366 /**
367 * Try parsers until one works, if none work return null
368 *
369 * @private
370 * @param {Function[]} ps
371 * @return {string|null}
372 */
373 function choice( ps ) {
374 return function () {
375 var i, result;
376 for ( i = 0; i < ps.length; i++ ) {
377 result = ps[ i ]();
378 if ( result !== null ) {
379 return result;
380 }
381 }
382 return null;
383 };
384 }
385
386 /**
387 * Try several ps in a row, all must succeed or return null.
388 * This is the only eager one.
389 *
390 * @private
391 * @param {Function[]} ps
392 * @return {string|null}
393 */
394 function sequence( ps ) {
395 var i, res,
396 originalPos = pos,
397 result = [];
398 for ( i = 0; i < ps.length; i++ ) {
399 res = ps[ i ]();
400 if ( res === null ) {
401 pos = originalPos;
402 return null;
403 }
404 result.push( res );
405 }
406 return result;
407 }
408
409 /**
410 * Run the same parser over and over until it fails.
411 * Must succeed a minimum of n times or return null.
412 *
413 * @private
414 * @param {number} n
415 * @param {Function} p
416 * @return {string|null}
417 */
418 function nOrMore( n, p ) {
419 return function () {
420 var originalPos = pos,
421 result = [],
422 parsed = p();
423 while ( parsed !== null ) {
424 result.push( parsed );
425 parsed = p();
426 }
427 if ( result.length < n ) {
428 pos = originalPos;
429 return null;
430 }
431 return result;
432 };
433 }
434
435 /**
436 * There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
437 *
438 * TODO: But using this as a combinator seems to cause problems when combined with #nOrMore().
439 * May be some scoping issue
440 *
441 * @private
442 * @param {Function} p
443 * @param {Function} fn
444 * @return {string|null}
445 */
446 function transform( p, fn ) {
447 return function () {
448 var result = p();
449 return result === null ? null : fn( result );
450 };
451 }
452
453 /**
454 * Just make parsers out of simpler JS builtin types
455 *
456 * @private
457 * @param {string} s
458 * @return {Function}
459 * @return {string} return.return
460 */
461 function makeStringParser( s ) {
462 var len = s.length;
463 return function () {
464 var result = null;
465 if ( input.substr( pos, len ) === s ) {
466 result = s;
467 pos += len;
468 }
469 return result;
470 };
471 }
472
473 /**
474 * Makes a regex parser, given a RegExp object.
475 * The regex being passed in should start with a ^ to anchor it to the start
476 * of the string.
477 *
478 * @private
479 * @param {RegExp} regex anchored regex
480 * @return {Function} function to parse input based on the regex
481 */
482 function makeRegexParser( regex ) {
483 return function () {
484 var matches = input.slice( pos ).match( regex );
485 if ( matches === null ) {
486 return null;
487 }
488 pos += matches[ 0 ].length;
489 return matches[ 0 ];
490 };
491 }
492
493 // ===================================================================
494 // General patterns above this line -- wikitext specific parsers below
495 // ===================================================================
496
497 // Parsing functions follow. All parsing functions work like this:
498 // They don't accept any arguments.
499 // Instead, they just operate non destructively on the string 'input'
500 // As they can consume parts of the string, they advance the shared variable pos,
501 // and return tokens (or whatever else they want to return).
502 // some things are defined as closures and other things as ordinary functions
503 // converting everything to a closure makes it a lot harder to debug... errors pop up
504 // but some debuggers can't tell you exactly where they come from. Also the mutually
505 // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
506 // This may be because, to save code, memoization was removed
507
508 /* eslint-disable no-useless-escape */
509 regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
510 regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
511 regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
512 regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
513 /* eslint-enable no-useless-escape */
514
515 backslash = makeStringParser( '\\' );
516 doubleQuote = makeStringParser( '"' );
517 singleQuote = makeStringParser( '\'' );
518 anyCharacter = makeRegexParser( /^./ );
519
520 openHtmlStartTag = makeStringParser( '<' );
521 optionalForwardSlash = makeRegexParser( /^\/?/ );
522 openHtmlEndTag = makeStringParser( '</' );
523 htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
524 closeHtmlTag = makeRegexParser( /^\s*>/ );
525
526 function escapedLiteral() {
527 var result = sequence( [
528 backslash,
529 anyCharacter
530 ] );
531 return result === null ? null : result[ 1 ];
532 }
533 escapedOrLiteralWithoutSpace = choice( [
534 escapedLiteral,
535 regularLiteralWithoutSpace
536 ] );
537 escapedOrLiteralWithoutBar = choice( [
538 escapedLiteral,
539 regularLiteralWithoutBar
540 ] );
541 escapedOrRegularLiteral = choice( [
542 escapedLiteral,
543 regularLiteral
544 ] );
545 // Used to define "literals" without spaces, in space-delimited situations
546 function literalWithoutSpace() {
547 var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
548 return result === null ? null : result.join( '' );
549 }
550 // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
551 // it is not a literal in the parameter
552 function literalWithoutBar() {
553 var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
554 return result === null ? null : result.join( '' );
555 }
556
557 function literal() {
558 var result = nOrMore( 1, escapedOrRegularLiteral )();
559 return result === null ? null : result.join( '' );
560 }
561
562 function curlyBraceTransformExpressionLiteral() {
563 var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
564 return result === null ? null : result.join( '' );
565 }
566
567 asciiAlphabetLiteral = makeRegexParser( /^[A-Za-z]+/ );
568 htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
569 htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
570
571 whitespace = makeRegexParser( /^\s+/ );
572 dollar = makeStringParser( '$' );
573 digits = makeRegexParser( /^\d+/ );
574
575 function replacement() {
576 var result = sequence( [
577 dollar,
578 digits
579 ] );
580 if ( result === null ) {
581 return null;
582 }
583 return [ 'REPLACE', parseInt( result[ 1 ], 10 ) - 1 ];
584 }
585 openExtlink = makeStringParser( '[' );
586 closeExtlink = makeStringParser( ']' );
587 // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
588 function extlink() {
589 var result, parsedResult, target;
590 result = null;
591 parsedResult = sequence( [
592 openExtlink,
593 nOrMore( 1, nonWhitespaceExpression ),
594 whitespace,
595 nOrMore( 1, expression ),
596 closeExtlink
597 ] );
598 if ( parsedResult !== null ) {
599 // When the entire link target is a single parameter, we can't use CONCAT, as we allow
600 // passing fancy parameters (like a whole jQuery object or a function) to use for the
601 // link. Check only if it's a single match, since we can either do CONCAT or not for
602 // singles with the same effect.
603 target = parsedResult[ 1 ].length === 1 ?
604 parsedResult[ 1 ][ 0 ] :
605 [ 'CONCAT' ].concat( parsedResult[ 1 ] );
606 result = [
607 'EXTLINK',
608 target,
609 [ 'CONCAT' ].concat( parsedResult[ 3 ] )
610 ];
611 }
612 return result;
613 }
614 openWikilink = makeStringParser( '[[' );
615 closeWikilink = makeStringParser( ']]' );
616 pipe = makeStringParser( '|' );
617
618 function template() {
619 var result = sequence( [
620 openTemplate,
621 templateContents,
622 closeTemplate
623 ] );
624 return result === null ? null : result[ 1 ];
625 }
626
627 function pipedWikilink() {
628 var result = sequence( [
629 nOrMore( 1, paramExpression ),
630 pipe,
631 nOrMore( 1, expression )
632 ] );
633 return result === null ? null : [
634 [ 'CONCAT' ].concat( result[ 0 ] ),
635 [ 'CONCAT' ].concat( result[ 2 ] )
636 ];
637 }
638
639 function unpipedWikilink() {
640 var result = sequence( [
641 nOrMore( 1, paramExpression )
642 ] );
643 return result === null ? null : [
644 [ 'CONCAT' ].concat( result[ 0 ] )
645 ];
646 }
647
648 wikilinkContents = choice( [
649 pipedWikilink,
650 unpipedWikilink
651 ] );
652
653 function wikilink() {
654 var result, parsedResult, parsedLinkContents;
655 result = null;
656
657 parsedResult = sequence( [
658 openWikilink,
659 wikilinkContents,
660 closeWikilink
661 ] );
662 if ( parsedResult !== null ) {
663 parsedLinkContents = parsedResult[ 1 ];
664 result = [ 'WIKILINK' ].concat( parsedLinkContents );
665 }
666 return result;
667 }
668
669 // TODO: Support data- if appropriate
670 function doubleQuotedHtmlAttributeValue() {
671 var parsedResult = sequence( [
672 doubleQuote,
673 htmlDoubleQuoteAttributeValue,
674 doubleQuote
675 ] );
676 return parsedResult === null ? null : parsedResult[ 1 ];
677 }
678
679 function singleQuotedHtmlAttributeValue() {
680 var parsedResult = sequence( [
681 singleQuote,
682 htmlSingleQuoteAttributeValue,
683 singleQuote
684 ] );
685 return parsedResult === null ? null : parsedResult[ 1 ];
686 }
687
688 function htmlAttribute() {
689 var parsedResult = sequence( [
690 whitespace,
691 asciiAlphabetLiteral,
692 htmlAttributeEquals,
693 choice( [
694 doubleQuotedHtmlAttributeValue,
695 singleQuotedHtmlAttributeValue
696 ] )
697 ] );
698 return parsedResult === null ? null : [ parsedResult[ 1 ], parsedResult[ 3 ] ];
699 }
700
701 /**
702 * Checks if HTML is allowed
703 *
704 * @param {string} startTagName HTML start tag name
705 * @param {string} endTagName HTML start tag name
706 * @param {Object} attributes array of consecutive key value pairs,
707 * with index 2 * n being a name and 2 * n + 1 the associated value
708 * @return {boolean} true if this is HTML is allowed, false otherwise
709 */
710 function isAllowedHtml( startTagName, endTagName, attributes ) {
711 var i, len, attributeName;
712
713 startTagName = startTagName.toLowerCase();
714 endTagName = endTagName.toLowerCase();
715 if ( startTagName !== endTagName || settings.allowedHtmlElements.indexOf( startTagName ) === -1 ) {
716 return false;
717 }
718
719 for ( i = 0, len = attributes.length; i < len; i += 2 ) {
720 attributeName = attributes[ i ];
721 if ( settings.allowedHtmlCommonAttributes.indexOf( attributeName ) === -1 &&
722 ( settings.allowedHtmlAttributesByElement[ startTagName ] || [] ).indexOf( attributeName ) === -1 ) {
723 return false;
724 }
725 }
726
727 return true;
728 }
729
730 function htmlAttributes() {
731 var parsedResult = nOrMore( 0, htmlAttribute )();
732 // Un-nest attributes array due to structure of jQueryMsg operations (see emit).
733 return concat.apply( [ 'HTMLATTRIBUTES' ], parsedResult );
734 }
735
736 // Subset of allowed HTML markup.
737 // Most elements and many attributes allowed on the server are not supported yet.
738 function html() {
739 var parsedOpenTagResult, parsedHtmlContents, parsedCloseTagResult,
740 wrappedAttributes, attributes, startTagName, endTagName, startOpenTagPos,
741 startCloseTagPos, endOpenTagPos, endCloseTagPos,
742 result = null;
743
744 // Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
745 // 1. open through closeHtmlTag
746 // 2. expression
747 // 3. openHtmlEnd through close
748 // This will allow recording the positions to reconstruct if HTML is to be treated as text.
749
750 startOpenTagPos = pos;
751 parsedOpenTagResult = sequence( [
752 openHtmlStartTag,
753 asciiAlphabetLiteral,
754 htmlAttributes,
755 optionalForwardSlash,
756 closeHtmlTag
757 ] );
758
759 if ( parsedOpenTagResult === null ) {
760 return null;
761 }
762
763 endOpenTagPos = pos;
764 startTagName = parsedOpenTagResult[ 1 ];
765
766 parsedHtmlContents = nOrMore( 0, expression )();
767
768 startCloseTagPos = pos;
769 parsedCloseTagResult = sequence( [
770 openHtmlEndTag,
771 asciiAlphabetLiteral,
772 closeHtmlTag
773 ] );
774
775 if ( parsedCloseTagResult === null ) {
776 // Closing tag failed. Return the start tag and contents.
777 return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
778 .concat( parsedHtmlContents );
779 }
780
781 endCloseTagPos = pos;
782 endTagName = parsedCloseTagResult[ 1 ];
783 wrappedAttributes = parsedOpenTagResult[ 2 ];
784 attributes = wrappedAttributes.slice( 1 );
785 if ( isAllowedHtml( startTagName, endTagName, attributes ) ) {
786 result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ]
787 .concat( parsedHtmlContents );
788 } else {
789 // HTML is not allowed, so contents will remain how
790 // it was, while HTML markup at this level will be
791 // treated as text
792 // E.g. assuming script tags are not allowed:
793 //
794 // <script>[[Foo|bar]]</script>
795 //
796 // results in '&lt;script&gt;' and '&lt;/script&gt;'
797 // (not treated as an HTML tag), surrounding a fully
798 // parsed HTML link.
799 //
800 // Concatenate everything from the tag, flattening the contents.
801 result = [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
802 .concat( parsedHtmlContents, input.slice( startCloseTagPos, endCloseTagPos ) );
803 }
804
805 return result;
806 }
807
808 // <nowiki>...</nowiki> tag. The tags are stripped and the contents are returned unparsed.
809 function nowiki() {
810 var parsedResult, plainText,
811 result = null;
812
813 parsedResult = sequence( [
814 makeStringParser( '<nowiki>' ),
815 // We use a greedy non-backtracking parser, so we must ensure here that we don't take too much
816 makeRegexParser( /^.*?(?=<\/nowiki>)/ ),
817 makeStringParser( '</nowiki>' )
818 ] );
819 if ( parsedResult !== null ) {
820 plainText = parsedResult[ 1 ];
821 result = [ 'CONCAT' ].concat( plainText );
822 }
823
824 return result;
825 }
826
827 templateName = transform(
828 // see $wgLegalTitleChars
829 // not allowing : due to the need to catch "PLURAL:$1"
830 makeRegexParser( /^[ !"$&'()*,./0-9;=?@A-Z^_`a-z~\x80-\xFF+-]+/ ),
831 function ( result ) { return result.toString(); }
832 );
833 function templateParam() {
834 var expr, result;
835 result = sequence( [
836 pipe,
837 nOrMore( 0, paramExpression )
838 ] );
839 if ( result === null ) {
840 return null;
841 }
842 expr = result[ 1 ];
843 // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
844 return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[ 0 ];
845 }
846
847 function templateWithReplacement() {
848 var result = sequence( [
849 templateName,
850 colon,
851 replacement
852 ] );
853 return result === null ? null : [ result[ 0 ], result[ 2 ] ];
854 }
855 function templateWithOutReplacement() {
856 var result = sequence( [
857 templateName,
858 colon,
859 paramExpression
860 ] );
861 return result === null ? null : [ result[ 0 ], result[ 2 ] ];
862 }
863 function templateWithOutFirstParameter() {
864 var result = sequence( [
865 templateName,
866 colon
867 ] );
868 return result === null ? null : [ result[ 0 ], '' ];
869 }
870 colon = makeStringParser( ':' );
871 templateContents = choice( [
872 function () {
873 var res = sequence( [
874 // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
875 // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
876 choice( [ templateWithReplacement, templateWithOutReplacement, templateWithOutFirstParameter ] ),
877 nOrMore( 0, templateParam )
878 ] );
879 return res === null ? null : res[ 0 ].concat( res[ 1 ] );
880 },
881 function () {
882 var res = sequence( [
883 templateName,
884 nOrMore( 0, templateParam )
885 ] );
886 if ( res === null ) {
887 return null;
888 }
889 return [ res[ 0 ] ].concat( res[ 1 ] );
890 }
891 ] );
892 openTemplate = makeStringParser( '{{' );
893 closeTemplate = makeStringParser( '}}' );
894 nonWhitespaceExpression = choice( [
895 template,
896 wikilink,
897 extlink,
898 replacement,
899 literalWithoutSpace
900 ] );
901 paramExpression = choice( [
902 template,
903 wikilink,
904 extlink,
905 replacement,
906 literalWithoutBar
907 ] );
908
909 expression = choice( [
910 template,
911 wikilink,
912 extlink,
913 replacement,
914 nowiki,
915 html,
916 literal
917 ] );
918
919 // Used when only {{-transformation is wanted, for 'text'
920 // or 'escaped' formats
921 curlyBraceTransformExpression = choice( [
922 template,
923 replacement,
924 curlyBraceTransformExpressionLiteral
925 ] );
926
927 /**
928 * Starts the parse
929 *
930 * @param {Function} rootExpression Root parse function
931 * @return {Array|null}
932 */
933 function start( rootExpression ) {
934 var result = nOrMore( 0, rootExpression )();
935 if ( result === null ) {
936 return null;
937 }
938 return [ 'CONCAT' ].concat( result );
939 }
940 // everything above this point is supposed to be stateless/static, but
941 // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
942 // finally let's do some actual work...
943
944 result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
945
946 /*
947 * For success, the p must have gotten to the end of the input
948 * and returned a non-null.
949 * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
950 */
951 if ( result === null || pos !== input.length ) {
952 throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
953 }
954 return result;
955 }
956
957 };
958
959 /**
960 * Class that primarily exists to emit HTML from parser ASTs.
961 *
962 * @private
963 * @class
964 * @param {Object} language
965 * @param {Object} magic
966 */
967 mw.jqueryMsg.HtmlEmitter = function ( language, magic ) {
968 var jmsg = this;
969 this.language = language;
970 // eslint-disable-next-line no-jquery/no-each-util
971 $.each( magic, function ( key, val ) {
972 jmsg[ key.toLowerCase() ] = function () {
973 return val;
974 };
975 } );
976
977 /**
978 * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
979 * Walk entire node structure, applying replacements and template functions when appropriate
980 *
981 * @param {Mixed} node Abstract syntax tree (top node or subnode)
982 * @param {Array} replacements for $1, $2, ... $n
983 * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
984 */
985 this.emit = function ( node, replacements ) {
986 var ret, subnodes, operation,
987 jmsg = this;
988 switch ( typeof node ) {
989 case 'string':
990 case 'number':
991 ret = node;
992 break;
993 // typeof returns object for arrays
994 case 'object':
995 // node is an array of nodes
996 // eslint-disable-next-line no-jquery/no-map-util
997 subnodes = $.map( node.slice( 1 ), function ( n ) {
998 return jmsg.emit( n, replacements );
999 } );
1000 operation = node[ 0 ].toLowerCase();
1001 if ( typeof jmsg[ operation ] === 'function' ) {
1002 ret = jmsg[ operation ]( subnodes, replacements );
1003 } else {
1004 throw new Error( 'Unknown operation "' + operation + '"' );
1005 }
1006 break;
1007 case 'undefined':
1008 // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
1009 // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
1010 // The logical thing is probably to return the empty string here when we encounter undefined.
1011 ret = '';
1012 break;
1013 default:
1014 throw new Error( 'Unexpected type in AST: ' + typeof node );
1015 }
1016 return ret;
1017 };
1018 };
1019
1020 // For everything in input that follows double-open-curly braces, there should be an equivalent parser
1021 // function. For instance {{PLURAL ... }} will be processed by 'plural'.
1022 // If you have 'magic words' then configure the parser to have them upon creation.
1023 //
1024 // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
1025 // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
1026 mw.jqueryMsg.HtmlEmitter.prototype = {
1027 /**
1028 * Parsing has been applied depth-first we can assume that all nodes here are single nodes
1029 * Must return a single node to parents -- a jQuery with synthetic span
1030 * However, unwrap any other synthetic spans in our children and pass them upwards
1031 *
1032 * @param {Mixed[]} nodes Some single nodes, some arrays of nodes
1033 * @return {jQuery}
1034 */
1035 concat: function ( nodes ) {
1036 var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
1037 // eslint-disable-next-line no-jquery/no-each-util
1038 $.each( nodes, function ( i, node ) {
1039 // Let jQuery append nodes, arrays of nodes and jQuery objects
1040 // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
1041 appendWithoutParsing( $span, node );
1042 } );
1043 return $span;
1044 },
1045
1046 /**
1047 * Return escaped replacement of correct index, or string if unavailable.
1048 * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
1049 * if the specified parameter is not found return the same string
1050 * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
1051 *
1052 * TODO: Throw error if nodes.length > 1 ?
1053 *
1054 * @param {Array} nodes List of one element, integer, n >= 0
1055 * @param {Array} replacements List of at least n strings
1056 * @return {string|jQuery} replacement
1057 */
1058 replace: function ( nodes, replacements ) {
1059 var index = parseInt( nodes[ 0 ], 10 );
1060
1061 if ( index < replacements.length ) {
1062 return replacements[ index ];
1063 } else {
1064 // index not found, fallback to displaying variable
1065 return '$' + ( index + 1 );
1066 }
1067 },
1068
1069 /**
1070 * Transform wiki-link
1071 *
1072 * TODO:
1073 * It only handles basic cases, either no pipe, or a pipe with an explicit
1074 * anchor.
1075 *
1076 * It does not attempt to handle features like the pipe trick.
1077 * However, the pipe trick should usually not be present in wikitext retrieved
1078 * from the server, since the replacement is done at save time.
1079 * It may, though, if the wikitext appears in extension-controlled content.
1080 *
1081 * @param {string[]} nodes
1082 * @return {jQuery}
1083 */
1084 wikilink: function ( nodes ) {
1085 var page, anchor, url, $el;
1086
1087 page = textify( nodes[ 0 ] );
1088 // Strip leading ':', which is used to suppress special behavior in wikitext links,
1089 // e.g. [[:Category:Foo]] or [[:File:Foo.jpg]]
1090 if ( page.charAt( 0 ) === ':' ) {
1091 page = page.slice( 1 );
1092 }
1093 url = mw.util.getUrl( page );
1094
1095 if ( nodes.length === 1 ) {
1096 // [[Some Page]] or [[Namespace:Some Page]]
1097 anchor = page;
1098 } else {
1099 // [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
1100 anchor = nodes[ 1 ];
1101 }
1102
1103 $el = $( '<a>' ).attr( {
1104 title: page,
1105 href: url
1106 } );
1107 return appendWithoutParsing( $el, anchor );
1108 },
1109
1110 /**
1111 * Converts array of HTML element key value pairs to object
1112 *
1113 * @param {Array} nodes Array of consecutive key value pairs, with index 2 * n being a
1114 * name and 2 * n + 1 the associated value
1115 * @return {Object} Object mapping attribute name to attribute value
1116 */
1117 htmlattributes: function ( nodes ) {
1118 var i, len, mapping = {};
1119 for ( i = 0, len = nodes.length; i < len; i += 2 ) {
1120 mapping[ nodes[ i ] ] = decodePrimaryHtmlEntities( nodes[ i + 1 ] );
1121 }
1122 return mapping;
1123 },
1124
1125 /**
1126 * Handles an (already-validated) HTML element.
1127 *
1128 * @param {Array} nodes Nodes to process when creating element
1129 * @return {jQuery}
1130 */
1131 htmlelement: function ( nodes ) {
1132 var tagName, attributes, contents, $element;
1133
1134 tagName = nodes.shift();
1135 attributes = nodes.shift();
1136 contents = nodes;
1137 $element = $( document.createElement( tagName ) ).attr( attributes );
1138 return appendWithoutParsing( $element, contents );
1139 },
1140
1141 /**
1142 * Transform parsed structure into external link.
1143 *
1144 * The "href" can be:
1145 * - a jQuery object, treat it as "enclosing" the link text.
1146 * - a function, treat it as the click handler.
1147 * - a string, or our HtmlEmitter jQuery object, treat it as a URI after stringifying.
1148 *
1149 * TODO: throw an error if nodes.length > 2 ?
1150 *
1151 * @param {Array} nodes List of two elements, {jQuery|Function|String} and {string}
1152 * @return {jQuery}
1153 */
1154 extlink: function ( nodes ) {
1155 var $el,
1156 arg = nodes[ 0 ],
1157 contents = nodes[ 1 ];
1158 if ( arg instanceof $ && !arg.hasClass( 'mediaWiki_htmlEmitter' ) ) {
1159 $el = arg;
1160 } else {
1161 $el = $( '<a>' );
1162 if ( typeof arg === 'function' ) {
1163 $el.attr( {
1164 role: 'button',
1165 tabindex: 0
1166 } ).on( 'click keypress', function ( e ) {
1167 if (
1168 e.type === 'click' ||
1169 e.type === 'keypress' && e.which === 13
1170 ) {
1171 arg.call( this, e );
1172 }
1173 } );
1174 } else {
1175 $el.attr( 'href', textify( arg ) );
1176 }
1177 }
1178 return appendWithoutParsing( $el.empty(), contents );
1179 },
1180
1181 /**
1182 * Transform parsed structure into pluralization
1183 * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
1184 * So convert it back with the current language's convertNumber.
1185 *
1186 * @param {Array} nodes List of nodes, [ {string|number}, {string}, {string} ... ]
1187 * @return {string|jQuery} selected pluralized form according to current language
1188 */
1189 plural: function ( nodes ) {
1190 var forms, firstChild, firstChildText, explicitPluralFormNumber, formIndex, form, count,
1191 explicitPluralForms = {};
1192
1193 count = parseFloat( this.language.convertNumber( textify( nodes[ 0 ] ), true ) );
1194 forms = nodes.slice( 1 );
1195 for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
1196 form = forms[ formIndex ];
1197
1198 if ( form instanceof $ && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
1199 // This is a nested node, may be an explicit plural form like 5=[$2 linktext]
1200 firstChild = form.contents().get( 0 );
1201 if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
1202 firstChildText = firstChild.textContent;
1203 if ( /^\d+=/.test( firstChildText ) ) {
1204 explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[ 0 ], 10 );
1205 // Use the digit part as key and rest of first text node and
1206 // rest of child nodes as value.
1207 firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
1208 explicitPluralForms[ explicitPluralFormNumber ] = form;
1209 forms[ formIndex ] = undefined;
1210 }
1211 }
1212 } else if ( /^\d+=/.test( form ) ) {
1213 // Simple explicit plural forms like 12=a dozen
1214 explicitPluralFormNumber = parseInt( form.split( /=/ )[ 0 ], 10 );
1215 explicitPluralForms[ explicitPluralFormNumber ] = form.slice( form.indexOf( '=' ) + 1 );
1216 forms[ formIndex ] = undefined;
1217 }
1218 }
1219
1220 // Remove explicit plural forms from the forms. They were set undefined in the above loop.
1221 // eslint-disable-next-line no-jquery/no-map-util
1222 forms = $.map( forms, function ( form ) {
1223 return form;
1224 } );
1225
1226 return this.language.convertPlural( count, forms, explicitPluralForms );
1227 },
1228
1229 /**
1230 * Transform parsed structure according to gender.
1231 *
1232 * Usage: {{gender:[ mw.user object | '' | 'male' | 'female' | 'unknown' ] | masculine form | feminine form | neutral form}}.
1233 *
1234 * The first node must be one of:
1235 * - the mw.user object (or a compatible one)
1236 * - an empty string - indicating the current user, same effect as passing the mw.user object
1237 * - a gender string ('male', 'female' or 'unknown')
1238 *
1239 * @param {Array} nodes List of nodes, [ {string|mw.user}, {string}, {string}, {string} ]
1240 * @return {string|jQuery} Selected gender form according to current language
1241 */
1242 gender: function ( nodes ) {
1243 var gender,
1244 maybeUser = nodes[ 0 ],
1245 forms = nodes.slice( 1 );
1246
1247 if ( maybeUser === '' ) {
1248 maybeUser = mw.user;
1249 }
1250
1251 // If we are passed a mw.user-like object, check their gender.
1252 // Otherwise, assume the gender string itself was passed .
1253 if ( maybeUser && maybeUser.options instanceof mw.Map ) {
1254 gender = maybeUser.options.get( 'gender' );
1255 } else {
1256 gender = textify( maybeUser );
1257 }
1258
1259 return this.language.gender( gender, forms );
1260 },
1261
1262 /**
1263 * Transform parsed structure into grammar conversion.
1264 * Invoked by putting `{{grammar:form|word}}` in a message
1265 *
1266 * @param {Array} nodes List of nodes [{Grammar case eg: genitive}, {string word}]
1267 * @return {string|jQuery} selected grammatical form according to current language
1268 */
1269 grammar: function ( nodes ) {
1270 var form = nodes[ 0 ],
1271 word = nodes[ 1 ];
1272 // These could be jQuery objects (passed as message parameters),
1273 // in which case we can't transform them (like rawParams() in PHP).
1274 if ( typeof form === 'string' && typeof word === 'string' ) {
1275 return this.language.convertGrammar( word, form );
1276 }
1277 return word;
1278 },
1279
1280 /**
1281 * Tranform parsed structure into a int: (interface language) message include
1282 * Invoked by putting `{{int:othermessage}}` into a message
1283 *
1284 * TODO Syntax in the included message is not parsed, this seems like a bug?
1285 *
1286 * @param {Array} nodes List of nodes
1287 * @return {string} Other message
1288 */
1289 int: function ( nodes ) {
1290 var msg = textify( nodes[ 0 ] );
1291 return mw.jqueryMsg.getMessageFunction()( msg.charAt( 0 ).toLowerCase() + msg.slice( 1 ) );
1292 },
1293
1294 /**
1295 * Get localized namespace name from canonical name or namespace number.
1296 * Invoked by putting `{{ns:foo}}` into a message
1297 *
1298 * @param {Array} nodes List of nodes
1299 * @return {string} Localized namespace name
1300 */
1301 ns: function ( nodes ) {
1302 var ns = textify( nodes[ 0 ] ).trim();
1303 if ( !/^\d+$/.test( ns ) ) {
1304 ns = mw.config.get( 'wgNamespaceIds' )[ ns.replace( / /g, '_' ).toLowerCase() ];
1305 }
1306 ns = mw.config.get( 'wgFormattedNamespaces' )[ ns ];
1307 return ns || '';
1308 },
1309
1310 /**
1311 * Takes an unformatted number (arab, no group separators and . as decimal separator)
1312 * and outputs it in the localized digit script and formatted with decimal
1313 * separator, according to the current language.
1314 *
1315 * @param {Array} nodes List of nodes
1316 * @return {number|string|jQuery} Formatted number
1317 */
1318 formatnum: function ( nodes ) {
1319 var isInteger = !!nodes[ 1 ] && nodes[ 1 ] === 'R',
1320 number = nodes[ 0 ];
1321
1322 // These could be jQuery objects (passed as message parameters),
1323 // in which case we can't transform them (like rawParams() in PHP).
1324 if ( typeof number === 'string' || typeof number === 'number' ) {
1325 return this.language.convertNumber( number, isInteger );
1326 }
1327 return number;
1328 },
1329
1330 /**
1331 * Lowercase text
1332 *
1333 * @param {Array} nodes List of nodes
1334 * @return {string} The given text, all in lowercase
1335 */
1336 lc: function ( nodes ) {
1337 return textify( nodes[ 0 ] ).toLowerCase();
1338 },
1339
1340 /**
1341 * Uppercase text
1342 *
1343 * @param {Array} nodes List of nodes
1344 * @return {string} The given text, all in uppercase
1345 */
1346 uc: function ( nodes ) {
1347 return textify( nodes[ 0 ] ).toUpperCase();
1348 },
1349
1350 /**
1351 * Lowercase first letter of input, leaving the rest unchanged
1352 *
1353 * @param {Array} nodes List of nodes
1354 * @return {string} The given text, with the first character in lowercase
1355 */
1356 lcfirst: function ( nodes ) {
1357 var text = textify( nodes[ 0 ] );
1358 return text.charAt( 0 ).toLowerCase() + text.slice( 1 );
1359 },
1360
1361 /**
1362 * Uppercase first letter of input, leaving the rest unchanged
1363 *
1364 * @param {Array} nodes List of nodes
1365 * @return {string} The given text, with the first character in uppercase
1366 */
1367 ucfirst: function ( nodes ) {
1368 var text = textify( nodes[ 0 ] );
1369 return text.charAt( 0 ).toUpperCase() + text.slice( 1 );
1370 }
1371 };
1372
1373 /**
1374 * @method
1375 * @member jQuery
1376 * @see mw.jqueryMsg#getPlugin
1377 */
1378 $.fn.msg = mw.jqueryMsg.getPlugin();
1379
1380 // Replace the default message parser with jqueryMsg
1381 oldParser = mw.Message.prototype.parser;
1382 mw.Message.prototype.parser = function () {
1383 // Fall back to mw.msg's simple parser where possible
1384 if (
1385 // Plain text output always uses the simple parser
1386 this.format === 'plain' ||
1387 (
1388 // jqueryMsg parser is needed for messages containing wikitext
1389 !/\{\{|[<>[&]/.test( this.map.get( this.key ) ) &&
1390 // jqueryMsg parser is needed when jQuery objects or DOM nodes are passed in as parameters
1391 !this.parameters.some( function ( param ) {
1392 return param instanceof $ || ( param && param.nodeType !== undefined );
1393 } )
1394 )
1395 ) {
1396 return oldParser.apply( this );
1397 }
1398
1399 if ( !Object.prototype.hasOwnProperty.call( this.map, this.format ) ) {
1400 this.map[ this.format ] = mw.jqueryMsg.getMessageFunction( {
1401 messages: this.map,
1402 // For format 'escaped', escaping part is handled by mediawiki.js
1403 format: this.format
1404 } );
1405 }
1406 return this.map[ this.format ]( this.key, this.parameters );
1407 };
1408
1409 /**
1410 * Parse the message to DOM nodes, rather than HTML string like #parse.
1411 *
1412 * This method is only available when jqueryMsg is loaded.
1413 *
1414 * @since 1.27
1415 * @method parseDom
1416 * @member mw.Message
1417 * @return {jQuery}
1418 */
1419 mw.Message.prototype.parseDom = ( function () {
1420 var $wrapper = $( '<div>' );
1421 return function () {
1422 return $wrapper.msg( this.key, this.parameters ).contents().detach();
1423 };
1424 }() );