641661290e34fe9f9f62171af3e7d312b1ce9a5b
[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 if ( mw.config.get( 'wgUserLanguage' ) === 'qqx' ) {
321 wikiText = '(' + key + '$*)';
322 } else {
323 wikiText = this.settings.messages.get( key );
324 if ( typeof wikiText !== 'string' ) {
325 wikiText = '⧼' + key + '⧽';
326 }
327 }
328 wikiText = mw.internalDoTransformFormatForQqx( wikiText, replacements );
329 this.astCache[ key ] = this.wikiTextToAst( wikiText );
330 }
331 return this.astCache[ key ];
332 },
333
334 /**
335 * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
336 *
337 * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
338 * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
339 *
340 * @param {string} input Message string wikitext
341 * @throws Error
342 * @return {Mixed} abstract syntax tree
343 */
344 wikiTextToAst: function ( input ) {
345 var pos,
346 regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, regularLiteralWithSquareBrackets,
347 doubleQuote, singleQuote, backslash, anyCharacter, asciiAlphabetLiteral,
348 escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
349 whitespace, dollar, digits, htmlDoubleQuoteAttributeValue, htmlSingleQuoteAttributeValue,
350 htmlAttributeEquals, openHtmlStartTag, optionalForwardSlash, openHtmlEndTag, closeHtmlTag,
351 openExtlink, closeExtlink, wikilinkContents, openWikilink, closeWikilink, templateName, pipe, colon,
352 templateContents, openTemplate, closeTemplate,
353 nonWhitespaceExpression, paramExpression, expression, curlyBraceTransformExpression, result,
354 settings = this.settings,
355 concat = Array.prototype.concat;
356
357 // Indicates current position in input as we parse through it.
358 // Shared among all parsing functions below.
359 pos = 0;
360
361 // =========================================================
362 // parsing combinators - could be a library on its own
363 // =========================================================
364
365 /**
366 * Try parsers until one works, if none work return null
367 *
368 * @private
369 * @param {Function[]} ps
370 * @return {string|null}
371 */
372 function choice( ps ) {
373 return function () {
374 var i, result;
375 for ( i = 0; i < ps.length; i++ ) {
376 result = ps[ i ]();
377 if ( result !== null ) {
378 return result;
379 }
380 }
381 return null;
382 };
383 }
384
385 /**
386 * Try several ps in a row, all must succeed or return null.
387 * This is the only eager one.
388 *
389 * @private
390 * @param {Function[]} ps
391 * @return {string|null}
392 */
393 function sequence( ps ) {
394 var i, res,
395 originalPos = pos,
396 result = [];
397 for ( i = 0; i < ps.length; i++ ) {
398 res = ps[ i ]();
399 if ( res === null ) {
400 pos = originalPos;
401 return null;
402 }
403 result.push( res );
404 }
405 return result;
406 }
407
408 /**
409 * Run the same parser over and over until it fails.
410 * Must succeed a minimum of n times or return null.
411 *
412 * @private
413 * @param {number} n
414 * @param {Function} p
415 * @return {string|null}
416 */
417 function nOrMore( n, p ) {
418 return function () {
419 var originalPos = pos,
420 result = [],
421 parsed = p();
422 while ( parsed !== null ) {
423 result.push( parsed );
424 parsed = p();
425 }
426 if ( result.length < n ) {
427 pos = originalPos;
428 return null;
429 }
430 return result;
431 };
432 }
433
434 /**
435 * There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
436 *
437 * TODO: But using this as a combinator seems to cause problems when combined with #nOrMore().
438 * May be some scoping issue
439 *
440 * @private
441 * @param {Function} p
442 * @param {Function} fn
443 * @return {string|null}
444 */
445 function transform( p, fn ) {
446 return function () {
447 var result = p();
448 return result === null ? null : fn( result );
449 };
450 }
451
452 /**
453 * Just make parsers out of simpler JS builtin types
454 *
455 * @private
456 * @param {string} s
457 * @return {Function}
458 * @return {string} return.return
459 */
460 function makeStringParser( s ) {
461 var len = s.length;
462 return function () {
463 var result = null;
464 if ( input.substr( pos, len ) === s ) {
465 result = s;
466 pos += len;
467 }
468 return result;
469 };
470 }
471
472 /**
473 * Makes a regex parser, given a RegExp object.
474 * The regex being passed in should start with a ^ to anchor it to the start
475 * of the string.
476 *
477 * @private
478 * @param {RegExp} regex anchored regex
479 * @return {Function} function to parse input based on the regex
480 */
481 function makeRegexParser( regex ) {
482 return function () {
483 var matches = input.slice( pos ).match( regex );
484 if ( matches === null ) {
485 return null;
486 }
487 pos += matches[ 0 ].length;
488 return matches[ 0 ];
489 };
490 }
491
492 // ===================================================================
493 // General patterns above this line -- wikitext specific parsers below
494 // ===================================================================
495
496 // Parsing functions follow. All parsing functions work like this:
497 // They don't accept any arguments.
498 // Instead, they just operate non destructively on the string 'input'
499 // As they can consume parts of the string, they advance the shared variable pos,
500 // and return tokens (or whatever else they want to return).
501 // some things are defined as closures and other things as ordinary functions
502 // converting everything to a closure makes it a lot harder to debug... errors pop up
503 // but some debuggers can't tell you exactly where they come from. Also the mutually
504 // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
505 // This may be because, to save code, memoization was removed
506
507 /* eslint-disable no-useless-escape */
508 regularLiteral = makeRegexParser( /^[^{}\[\]$<\\]/ );
509 regularLiteralWithoutBar = makeRegexParser( /^[^{}\[\]$\\|]/ );
510 regularLiteralWithoutSpace = makeRegexParser( /^[^{}\[\]$\s]/ );
511 regularLiteralWithSquareBrackets = makeRegexParser( /^[^{}$\\]/ );
512 /* eslint-enable no-useless-escape */
513
514 backslash = makeStringParser( '\\' );
515 doubleQuote = makeStringParser( '"' );
516 singleQuote = makeStringParser( '\'' );
517 anyCharacter = makeRegexParser( /^./ );
518
519 openHtmlStartTag = makeStringParser( '<' );
520 optionalForwardSlash = makeRegexParser( /^\/?/ );
521 openHtmlEndTag = makeStringParser( '</' );
522 htmlAttributeEquals = makeRegexParser( /^\s*=\s*/ );
523 closeHtmlTag = makeRegexParser( /^\s*>/ );
524
525 function escapedLiteral() {
526 var result = sequence( [
527 backslash,
528 anyCharacter
529 ] );
530 return result === null ? null : result[ 1 ];
531 }
532 escapedOrLiteralWithoutSpace = choice( [
533 escapedLiteral,
534 regularLiteralWithoutSpace
535 ] );
536 escapedOrLiteralWithoutBar = choice( [
537 escapedLiteral,
538 regularLiteralWithoutBar
539 ] );
540 escapedOrRegularLiteral = choice( [
541 escapedLiteral,
542 regularLiteral
543 ] );
544 // Used to define "literals" without spaces, in space-delimited situations
545 function literalWithoutSpace() {
546 var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
547 return result === null ? null : result.join( '' );
548 }
549 // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default
550 // it is not a literal in the parameter
551 function literalWithoutBar() {
552 var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
553 return result === null ? null : result.join( '' );
554 }
555
556 function literal() {
557 var result = nOrMore( 1, escapedOrRegularLiteral )();
558 return result === null ? null : result.join( '' );
559 }
560
561 function curlyBraceTransformExpressionLiteral() {
562 var result = nOrMore( 1, regularLiteralWithSquareBrackets )();
563 return result === null ? null : result.join( '' );
564 }
565
566 asciiAlphabetLiteral = makeRegexParser( /^[A-Za-z]+/ );
567 htmlDoubleQuoteAttributeValue = makeRegexParser( /^[^"]*/ );
568 htmlSingleQuoteAttributeValue = makeRegexParser( /^[^']*/ );
569
570 whitespace = makeRegexParser( /^\s+/ );
571 dollar = makeStringParser( '$' );
572 digits = makeRegexParser( /^\d+/ );
573
574 function replacement() {
575 var result = sequence( [
576 dollar,
577 digits
578 ] );
579 if ( result === null ) {
580 return null;
581 }
582 return [ 'REPLACE', parseInt( result[ 1 ], 10 ) - 1 ];
583 }
584 openExtlink = makeStringParser( '[' );
585 closeExtlink = makeStringParser( ']' );
586 // this extlink MUST have inner contents, e.g. [foo] not allowed; [foo bar] [foo <i>bar</i>], etc. are allowed
587 function extlink() {
588 var result, parsedResult, target;
589 result = null;
590 parsedResult = sequence( [
591 openExtlink,
592 nOrMore( 1, nonWhitespaceExpression ),
593 whitespace,
594 nOrMore( 1, expression ),
595 closeExtlink
596 ] );
597 if ( parsedResult !== null ) {
598 // When the entire link target is a single parameter, we can't use CONCAT, as we allow
599 // passing fancy parameters (like a whole jQuery object or a function) to use for the
600 // link. Check only if it's a single match, since we can either do CONCAT or not for
601 // singles with the same effect.
602 target = parsedResult[ 1 ].length === 1 ?
603 parsedResult[ 1 ][ 0 ] :
604 [ 'CONCAT' ].concat( parsedResult[ 1 ] );
605 result = [
606 'EXTLINK',
607 target,
608 [ 'CONCAT' ].concat( parsedResult[ 3 ] )
609 ];
610 }
611 return result;
612 }
613 openWikilink = makeStringParser( '[[' );
614 closeWikilink = makeStringParser( ']]' );
615 pipe = makeStringParser( '|' );
616
617 function template() {
618 var result = sequence( [
619 openTemplate,
620 templateContents,
621 closeTemplate
622 ] );
623 return result === null ? null : result[ 1 ];
624 }
625
626 function pipedWikilink() {
627 var result = sequence( [
628 nOrMore( 1, paramExpression ),
629 pipe,
630 nOrMore( 1, expression )
631 ] );
632 return result === null ? null : [
633 [ 'CONCAT' ].concat( result[ 0 ] ),
634 [ 'CONCAT' ].concat( result[ 2 ] )
635 ];
636 }
637
638 function unpipedWikilink() {
639 var result = sequence( [
640 nOrMore( 1, paramExpression )
641 ] );
642 return result === null ? null : [
643 [ 'CONCAT' ].concat( result[ 0 ] )
644 ];
645 }
646
647 wikilinkContents = choice( [
648 pipedWikilink,
649 unpipedWikilink
650 ] );
651
652 function wikilink() {
653 var result, parsedResult, parsedLinkContents;
654 result = null;
655
656 parsedResult = sequence( [
657 openWikilink,
658 wikilinkContents,
659 closeWikilink
660 ] );
661 if ( parsedResult !== null ) {
662 parsedLinkContents = parsedResult[ 1 ];
663 result = [ 'WIKILINK' ].concat( parsedLinkContents );
664 }
665 return result;
666 }
667
668 // TODO: Support data- if appropriate
669 function doubleQuotedHtmlAttributeValue() {
670 var parsedResult = sequence( [
671 doubleQuote,
672 htmlDoubleQuoteAttributeValue,
673 doubleQuote
674 ] );
675 return parsedResult === null ? null : parsedResult[ 1 ];
676 }
677
678 function singleQuotedHtmlAttributeValue() {
679 var parsedResult = sequence( [
680 singleQuote,
681 htmlSingleQuoteAttributeValue,
682 singleQuote
683 ] );
684 return parsedResult === null ? null : parsedResult[ 1 ];
685 }
686
687 function htmlAttribute() {
688 var parsedResult = sequence( [
689 whitespace,
690 asciiAlphabetLiteral,
691 htmlAttributeEquals,
692 choice( [
693 doubleQuotedHtmlAttributeValue,
694 singleQuotedHtmlAttributeValue
695 ] )
696 ] );
697 return parsedResult === null ? null : [ parsedResult[ 1 ], parsedResult[ 3 ] ];
698 }
699
700 /**
701 * Checks if HTML is allowed
702 *
703 * @param {string} startTagName HTML start tag name
704 * @param {string} endTagName HTML start tag name
705 * @param {Object} attributes array of consecutive key value pairs,
706 * with index 2 * n being a name and 2 * n + 1 the associated value
707 * @return {boolean} true if this is HTML is allowed, false otherwise
708 */
709 function isAllowedHtml( startTagName, endTagName, attributes ) {
710 var i, len, attributeName;
711
712 startTagName = startTagName.toLowerCase();
713 endTagName = endTagName.toLowerCase();
714 if ( startTagName !== endTagName || settings.allowedHtmlElements.indexOf( startTagName ) === -1 ) {
715 return false;
716 }
717
718 for ( i = 0, len = attributes.length; i < len; i += 2 ) {
719 attributeName = attributes[ i ];
720 if ( settings.allowedHtmlCommonAttributes.indexOf( attributeName ) === -1 &&
721 ( settings.allowedHtmlAttributesByElement[ startTagName ] || [] ).indexOf( attributeName ) === -1 ) {
722 return false;
723 }
724 }
725
726 return true;
727 }
728
729 function htmlAttributes() {
730 var parsedResult = nOrMore( 0, htmlAttribute )();
731 // Un-nest attributes array due to structure of jQueryMsg operations (see emit).
732 return concat.apply( [ 'HTMLATTRIBUTES' ], parsedResult );
733 }
734
735 // Subset of allowed HTML markup.
736 // Most elements and many attributes allowed on the server are not supported yet.
737 function html() {
738 var parsedOpenTagResult, parsedHtmlContents, parsedCloseTagResult,
739 wrappedAttributes, attributes, startTagName, endTagName, startOpenTagPos,
740 startCloseTagPos, endOpenTagPos, endCloseTagPos,
741 result = null;
742
743 // Break into three sequence calls. That should allow accurate reconstruction of the original HTML, and requiring an exact tag name match.
744 // 1. open through closeHtmlTag
745 // 2. expression
746 // 3. openHtmlEnd through close
747 // This will allow recording the positions to reconstruct if HTML is to be treated as text.
748
749 startOpenTagPos = pos;
750 parsedOpenTagResult = sequence( [
751 openHtmlStartTag,
752 asciiAlphabetLiteral,
753 htmlAttributes,
754 optionalForwardSlash,
755 closeHtmlTag
756 ] );
757
758 if ( parsedOpenTagResult === null ) {
759 return null;
760 }
761
762 endOpenTagPos = pos;
763 startTagName = parsedOpenTagResult[ 1 ];
764
765 parsedHtmlContents = nOrMore( 0, expression )();
766
767 startCloseTagPos = pos;
768 parsedCloseTagResult = sequence( [
769 openHtmlEndTag,
770 asciiAlphabetLiteral,
771 closeHtmlTag
772 ] );
773
774 if ( parsedCloseTagResult === null ) {
775 // Closing tag failed. Return the start tag and contents.
776 return [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
777 .concat( parsedHtmlContents );
778 }
779
780 endCloseTagPos = pos;
781 endTagName = parsedCloseTagResult[ 1 ];
782 wrappedAttributes = parsedOpenTagResult[ 2 ];
783 attributes = wrappedAttributes.slice( 1 );
784 if ( isAllowedHtml( startTagName, endTagName, attributes ) ) {
785 result = [ 'HTMLELEMENT', startTagName, wrappedAttributes ]
786 .concat( parsedHtmlContents );
787 } else {
788 // HTML is not allowed, so contents will remain how
789 // it was, while HTML markup at this level will be
790 // treated as text
791 // E.g. assuming script tags are not allowed:
792 //
793 // <script>[[Foo|bar]]</script>
794 //
795 // results in '&lt;script&gt;' and '&lt;/script&gt;'
796 // (not treated as an HTML tag), surrounding a fully
797 // parsed HTML link.
798 //
799 // Concatenate everything from the tag, flattening the contents.
800 result = [ 'CONCAT', input.slice( startOpenTagPos, endOpenTagPos ) ]
801 .concat( parsedHtmlContents, input.slice( startCloseTagPos, endCloseTagPos ) );
802 }
803
804 return result;
805 }
806
807 // <nowiki>...</nowiki> tag. The tags are stripped and the contents are returned unparsed.
808 function nowiki() {
809 var parsedResult, plainText,
810 result = null;
811
812 parsedResult = sequence( [
813 makeStringParser( '<nowiki>' ),
814 // We use a greedy non-backtracking parser, so we must ensure here that we don't take too much
815 makeRegexParser( /^.*?(?=<\/nowiki>)/ ),
816 makeStringParser( '</nowiki>' )
817 ] );
818 if ( parsedResult !== null ) {
819 plainText = parsedResult[ 1 ];
820 result = [ 'CONCAT' ].concat( plainText );
821 }
822
823 return result;
824 }
825
826 templateName = transform(
827 // see $wgLegalTitleChars
828 // not allowing : due to the need to catch "PLURAL:$1"
829 makeRegexParser( /^[ !"$&'()*,./0-9;=?@A-Z^_`a-z~\x80-\xFF+-]+/ ),
830 function ( result ) { return result.toString(); }
831 );
832 function templateParam() {
833 var expr, result;
834 result = sequence( [
835 pipe,
836 nOrMore( 0, paramExpression )
837 ] );
838 if ( result === null ) {
839 return null;
840 }
841 expr = result[ 1 ];
842 // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
843 return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[ 0 ];
844 }
845
846 function templateWithReplacement() {
847 var result = sequence( [
848 templateName,
849 colon,
850 replacement
851 ] );
852 return result === null ? null : [ result[ 0 ], result[ 2 ] ];
853 }
854 function templateWithOutReplacement() {
855 var result = sequence( [
856 templateName,
857 colon,
858 paramExpression
859 ] );
860 return result === null ? null : [ result[ 0 ], result[ 2 ] ];
861 }
862 function templateWithOutFirstParameter() {
863 var result = sequence( [
864 templateName,
865 colon
866 ] );
867 return result === null ? null : [ result[ 0 ], '' ];
868 }
869 colon = makeStringParser( ':' );
870 templateContents = choice( [
871 function () {
872 var res = sequence( [
873 // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
874 // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
875 choice( [ templateWithReplacement, templateWithOutReplacement, templateWithOutFirstParameter ] ),
876 nOrMore( 0, templateParam )
877 ] );
878 return res === null ? null : res[ 0 ].concat( res[ 1 ] );
879 },
880 function () {
881 var res = sequence( [
882 templateName,
883 nOrMore( 0, templateParam )
884 ] );
885 if ( res === null ) {
886 return null;
887 }
888 return [ res[ 0 ] ].concat( res[ 1 ] );
889 }
890 ] );
891 openTemplate = makeStringParser( '{{' );
892 closeTemplate = makeStringParser( '}}' );
893 nonWhitespaceExpression = choice( [
894 template,
895 wikilink,
896 extlink,
897 replacement,
898 literalWithoutSpace
899 ] );
900 paramExpression = choice( [
901 template,
902 wikilink,
903 extlink,
904 replacement,
905 literalWithoutBar
906 ] );
907
908 expression = choice( [
909 template,
910 wikilink,
911 extlink,
912 replacement,
913 nowiki,
914 html,
915 literal
916 ] );
917
918 // Used when only {{-transformation is wanted, for 'text'
919 // or 'escaped' formats
920 curlyBraceTransformExpression = choice( [
921 template,
922 replacement,
923 curlyBraceTransformExpressionLiteral
924 ] );
925
926 /**
927 * Starts the parse
928 *
929 * @param {Function} rootExpression Root parse function
930 * @return {Array|null}
931 */
932 function start( rootExpression ) {
933 var result = nOrMore( 0, rootExpression )();
934 if ( result === null ) {
935 return null;
936 }
937 return [ 'CONCAT' ].concat( result );
938 }
939 // everything above this point is supposed to be stateless/static, but
940 // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
941 // finally let's do some actual work...
942
943 result = start( this.settings.onlyCurlyBraceTransform ? curlyBraceTransformExpression : expression );
944
945 /*
946 * For success, the p must have gotten to the end of the input
947 * and returned a non-null.
948 * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
949 */
950 if ( result === null || pos !== input.length ) {
951 throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
952 }
953 return result;
954 }
955
956 };
957
958 /**
959 * Class that primarily exists to emit HTML from parser ASTs.
960 *
961 * @private
962 * @class
963 * @param {Object} language
964 * @param {Object} magic
965 */
966 mw.jqueryMsg.HtmlEmitter = function ( language, magic ) {
967 var jmsg = this;
968 this.language = language;
969 // eslint-disable-next-line no-jquery/no-each-util
970 $.each( magic, function ( key, val ) {
971 jmsg[ key.toLowerCase() ] = function () {
972 return val;
973 };
974 } );
975
976 /**
977 * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
978 * Walk entire node structure, applying replacements and template functions when appropriate
979 *
980 * @param {Mixed} node Abstract syntax tree (top node or subnode)
981 * @param {Array} replacements for $1, $2, ... $n
982 * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
983 */
984 this.emit = function ( node, replacements ) {
985 var ret, subnodes, operation,
986 jmsg = this;
987 switch ( typeof node ) {
988 case 'string':
989 case 'number':
990 ret = node;
991 break;
992 // typeof returns object for arrays
993 case 'object':
994 // node is an array of nodes
995 // eslint-disable-next-line no-jquery/no-map-util
996 subnodes = $.map( node.slice( 1 ), function ( n ) {
997 return jmsg.emit( n, replacements );
998 } );
999 operation = node[ 0 ].toLowerCase();
1000 if ( typeof jmsg[ operation ] === 'function' ) {
1001 ret = jmsg[ operation ]( subnodes, replacements );
1002 } else {
1003 throw new Error( 'Unknown operation "' + operation + '"' );
1004 }
1005 break;
1006 case 'undefined':
1007 // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
1008 // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
1009 // The logical thing is probably to return the empty string here when we encounter undefined.
1010 ret = '';
1011 break;
1012 default:
1013 throw new Error( 'Unexpected type in AST: ' + typeof node );
1014 }
1015 return ret;
1016 };
1017 };
1018
1019 // For everything in input that follows double-open-curly braces, there should be an equivalent parser
1020 // function. For instance {{PLURAL ... }} will be processed by 'plural'.
1021 // If you have 'magic words' then configure the parser to have them upon creation.
1022 //
1023 // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
1024 // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
1025 mw.jqueryMsg.HtmlEmitter.prototype = {
1026 /**
1027 * Parsing has been applied depth-first we can assume that all nodes here are single nodes
1028 * Must return a single node to parents -- a jQuery with synthetic span
1029 * However, unwrap any other synthetic spans in our children and pass them upwards
1030 *
1031 * @param {Mixed[]} nodes Some single nodes, some arrays of nodes
1032 * @return {jQuery}
1033 */
1034 concat: function ( nodes ) {
1035 var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
1036 // eslint-disable-next-line no-jquery/no-each-util
1037 $.each( nodes, function ( i, node ) {
1038 // Let jQuery append nodes, arrays of nodes and jQuery objects
1039 // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
1040 appendWithoutParsing( $span, node );
1041 } );
1042 return $span;
1043 },
1044
1045 /**
1046 * Return escaped replacement of correct index, or string if unavailable.
1047 * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
1048 * if the specified parameter is not found return the same string
1049 * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
1050 *
1051 * TODO: Throw error if nodes.length > 1 ?
1052 *
1053 * @param {Array} nodes List of one element, integer, n >= 0
1054 * @param {Array} replacements List of at least n strings
1055 * @return {string|jQuery} replacement
1056 */
1057 replace: function ( nodes, replacements ) {
1058 var index = parseInt( nodes[ 0 ], 10 );
1059
1060 if ( index < replacements.length ) {
1061 return replacements[ index ];
1062 } else {
1063 // index not found, fallback to displaying variable
1064 return '$' + ( index + 1 );
1065 }
1066 },
1067
1068 /**
1069 * Transform wiki-link
1070 *
1071 * TODO:
1072 * It only handles basic cases, either no pipe, or a pipe with an explicit
1073 * anchor.
1074 *
1075 * It does not attempt to handle features like the pipe trick.
1076 * However, the pipe trick should usually not be present in wikitext retrieved
1077 * from the server, since the replacement is done at save time.
1078 * It may, though, if the wikitext appears in extension-controlled content.
1079 *
1080 * @param {string[]} nodes
1081 * @return {jQuery}
1082 */
1083 wikilink: function ( nodes ) {
1084 var page, anchor, url, $el;
1085
1086 page = textify( nodes[ 0 ] );
1087 // Strip leading ':', which is used to suppress special behavior in wikitext links,
1088 // e.g. [[:Category:Foo]] or [[:File:Foo.jpg]]
1089 if ( page.charAt( 0 ) === ':' ) {
1090 page = page.slice( 1 );
1091 }
1092 url = mw.util.getUrl( page );
1093
1094 if ( nodes.length === 1 ) {
1095 // [[Some Page]] or [[Namespace:Some Page]]
1096 anchor = page;
1097 } else {
1098 // [[Some Page|anchor text]] or [[Namespace:Some Page|anchor]]
1099 anchor = nodes[ 1 ];
1100 }
1101
1102 $el = $( '<a>' ).attr( {
1103 title: page,
1104 href: url
1105 } );
1106 return appendWithoutParsing( $el, anchor );
1107 },
1108
1109 /**
1110 * Converts array of HTML element key value pairs to object
1111 *
1112 * @param {Array} nodes Array of consecutive key value pairs, with index 2 * n being a
1113 * name and 2 * n + 1 the associated value
1114 * @return {Object} Object mapping attribute name to attribute value
1115 */
1116 htmlattributes: function ( nodes ) {
1117 var i, len, mapping = {};
1118 for ( i = 0, len = nodes.length; i < len; i += 2 ) {
1119 mapping[ nodes[ i ] ] = decodePrimaryHtmlEntities( nodes[ i + 1 ] );
1120 }
1121 return mapping;
1122 },
1123
1124 /**
1125 * Handles an (already-validated) HTML element.
1126 *
1127 * @param {Array} nodes Nodes to process when creating element
1128 * @return {jQuery}
1129 */
1130 htmlelement: function ( nodes ) {
1131 var tagName, attributes, contents, $element;
1132
1133 tagName = nodes.shift();
1134 attributes = nodes.shift();
1135 contents = nodes;
1136 $element = $( document.createElement( tagName ) ).attr( attributes );
1137 return appendWithoutParsing( $element, contents );
1138 },
1139
1140 /**
1141 * Transform parsed structure into external link.
1142 *
1143 * The "href" can be:
1144 * - a jQuery object, treat it as "enclosing" the link text.
1145 * - a function, treat it as the click handler.
1146 * - a string, or our HtmlEmitter jQuery object, treat it as a URI after stringifying.
1147 *
1148 * TODO: throw an error if nodes.length > 2 ?
1149 *
1150 * @param {Array} nodes List of two elements, {jQuery|Function|String} and {string}
1151 * @return {jQuery}
1152 */
1153 extlink: function ( nodes ) {
1154 var $el,
1155 arg = nodes[ 0 ],
1156 contents = nodes[ 1 ];
1157 if ( arg instanceof $ && !arg.hasClass( 'mediaWiki_htmlEmitter' ) ) {
1158 $el = arg;
1159 } else {
1160 $el = $( '<a>' );
1161 if ( typeof arg === 'function' ) {
1162 $el.attr( {
1163 role: 'button',
1164 tabindex: 0
1165 } ).on( 'click keypress', function ( e ) {
1166 if (
1167 e.type === 'click' ||
1168 e.type === 'keypress' && e.which === 13
1169 ) {
1170 arg.call( this, e );
1171 }
1172 } );
1173 } else {
1174 $el.attr( 'href', textify( arg ) );
1175 }
1176 }
1177 return appendWithoutParsing( $el.empty(), contents );
1178 },
1179
1180 /**
1181 * Transform parsed structure into pluralization
1182 * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
1183 * So convert it back with the current language's convertNumber.
1184 *
1185 * @param {Array} nodes List of nodes, [ {string|number}, {string}, {string} ... ]
1186 * @return {string|jQuery} selected pluralized form according to current language
1187 */
1188 plural: function ( nodes ) {
1189 var forms, firstChild, firstChildText, explicitPluralFormNumber, formIndex, form, count,
1190 explicitPluralForms = {};
1191
1192 count = parseFloat( this.language.convertNumber( textify( nodes[ 0 ] ), true ) );
1193 forms = nodes.slice( 1 );
1194 for ( formIndex = 0; formIndex < forms.length; formIndex++ ) {
1195 form = forms[ formIndex ];
1196
1197 if ( form instanceof $ && form.hasClass( 'mediaWiki_htmlEmitter' ) ) {
1198 // This is a nested node, may be an explicit plural form like 5=[$2 linktext]
1199 firstChild = form.contents().get( 0 );
1200 if ( firstChild && firstChild.nodeType === Node.TEXT_NODE ) {
1201 firstChildText = firstChild.textContent;
1202 if ( /^\d+=/.test( firstChildText ) ) {
1203 explicitPluralFormNumber = parseInt( firstChildText.split( /=/ )[ 0 ], 10 );
1204 // Use the digit part as key and rest of first text node and
1205 // rest of child nodes as value.
1206 firstChild.textContent = firstChildText.slice( firstChildText.indexOf( '=' ) + 1 );
1207 explicitPluralForms[ explicitPluralFormNumber ] = form;
1208 forms[ formIndex ] = undefined;
1209 }
1210 }
1211 } else if ( /^\d+=/.test( form ) ) {
1212 // Simple explicit plural forms like 12=a dozen
1213 explicitPluralFormNumber = parseInt( form.split( /=/ )[ 0 ], 10 );
1214 explicitPluralForms[ explicitPluralFormNumber ] = form.slice( form.indexOf( '=' ) + 1 );
1215 forms[ formIndex ] = undefined;
1216 }
1217 }
1218
1219 // Remove explicit plural forms from the forms. They were set undefined in the above loop.
1220 // eslint-disable-next-line no-jquery/no-map-util
1221 forms = $.map( forms, function ( form ) {
1222 return form;
1223 } );
1224
1225 return this.language.convertPlural( count, forms, explicitPluralForms );
1226 },
1227
1228 /**
1229 * Transform parsed structure according to gender.
1230 *
1231 * Usage: {{gender:[ mw.user object | '' | 'male' | 'female' | 'unknown' ] | masculine form | feminine form | neutral form}}.
1232 *
1233 * The first node must be one of:
1234 * - the mw.user object (or a compatible one)
1235 * - an empty string - indicating the current user, same effect as passing the mw.user object
1236 * - a gender string ('male', 'female' or 'unknown')
1237 *
1238 * @param {Array} nodes List of nodes, [ {string|mw.user}, {string}, {string}, {string} ]
1239 * @return {string|jQuery} Selected gender form according to current language
1240 */
1241 gender: function ( nodes ) {
1242 var gender,
1243 maybeUser = nodes[ 0 ],
1244 forms = nodes.slice( 1 );
1245
1246 if ( maybeUser === '' ) {
1247 maybeUser = mw.user;
1248 }
1249
1250 // If we are passed a mw.user-like object, check their gender.
1251 // Otherwise, assume the gender string itself was passed .
1252 if ( maybeUser && maybeUser.options instanceof mw.Map ) {
1253 gender = maybeUser.options.get( 'gender' );
1254 } else {
1255 gender = textify( maybeUser );
1256 }
1257
1258 return this.language.gender( gender, forms );
1259 },
1260
1261 /**
1262 * Transform parsed structure into grammar conversion.
1263 * Invoked by putting `{{grammar:form|word}}` in a message
1264 *
1265 * @param {Array} nodes List of nodes [{Grammar case eg: genitive}, {string word}]
1266 * @return {string|jQuery} selected grammatical form according to current language
1267 */
1268 grammar: function ( nodes ) {
1269 var form = nodes[ 0 ],
1270 word = nodes[ 1 ];
1271 // These could be jQuery objects (passed as message parameters),
1272 // in which case we can't transform them (like rawParams() in PHP).
1273 if ( typeof form === 'string' && typeof word === 'string' ) {
1274 return this.language.convertGrammar( word, form );
1275 }
1276 return word;
1277 },
1278
1279 /**
1280 * Tranform parsed structure into a int: (interface language) message include
1281 * Invoked by putting `{{int:othermessage}}` into a message
1282 *
1283 * TODO Syntax in the included message is not parsed, this seems like a bug?
1284 *
1285 * @param {Array} nodes List of nodes
1286 * @return {string} Other message
1287 */
1288 int: function ( nodes ) {
1289 var msg = textify( nodes[ 0 ] );
1290 return mw.jqueryMsg.getMessageFunction()( msg.charAt( 0 ).toLowerCase() + msg.slice( 1 ) );
1291 },
1292
1293 /**
1294 * Get localized namespace name from canonical name or namespace number.
1295 * Invoked by putting `{{ns:foo}}` into a message
1296 *
1297 * @param {Array} nodes List of nodes
1298 * @return {string} Localized namespace name
1299 */
1300 ns: function ( nodes ) {
1301 var ns = textify( nodes[ 0 ] ).trim();
1302 if ( !/^\d+$/.test( ns ) ) {
1303 ns = mw.config.get( 'wgNamespaceIds' )[ ns.replace( / /g, '_' ).toLowerCase() ];
1304 }
1305 ns = mw.config.get( 'wgFormattedNamespaces' )[ ns ];
1306 return ns || '';
1307 },
1308
1309 /**
1310 * Takes an unformatted number (arab, no group separators and . as decimal separator)
1311 * and outputs it in the localized digit script and formatted with decimal
1312 * separator, according to the current language.
1313 *
1314 * @param {Array} nodes List of nodes
1315 * @return {number|string|jQuery} Formatted number
1316 */
1317 formatnum: function ( nodes ) {
1318 var isInteger = !!nodes[ 1 ] && nodes[ 1 ] === 'R',
1319 number = nodes[ 0 ];
1320
1321 // These could be jQuery objects (passed as message parameters),
1322 // in which case we can't transform them (like rawParams() in PHP).
1323 if ( typeof number === 'string' || typeof number === 'number' ) {
1324 return this.language.convertNumber( number, isInteger );
1325 }
1326 return number;
1327 },
1328
1329 /**
1330 * Lowercase text
1331 *
1332 * @param {Array} nodes List of nodes
1333 * @return {string} The given text, all in lowercase
1334 */
1335 lc: function ( nodes ) {
1336 return textify( nodes[ 0 ] ).toLowerCase();
1337 },
1338
1339 /**
1340 * Uppercase text
1341 *
1342 * @param {Array} nodes List of nodes
1343 * @return {string} The given text, all in uppercase
1344 */
1345 uc: function ( nodes ) {
1346 return textify( nodes[ 0 ] ).toUpperCase();
1347 },
1348
1349 /**
1350 * Lowercase first letter of input, leaving the rest unchanged
1351 *
1352 * @param {Array} nodes List of nodes
1353 * @return {string} The given text, with the first character in lowercase
1354 */
1355 lcfirst: function ( nodes ) {
1356 var text = textify( nodes[ 0 ] );
1357 return text.charAt( 0 ).toLowerCase() + text.slice( 1 );
1358 },
1359
1360 /**
1361 * Uppercase first letter of input, leaving the rest unchanged
1362 *
1363 * @param {Array} nodes List of nodes
1364 * @return {string} The given text, with the first character in uppercase
1365 */
1366 ucfirst: function ( nodes ) {
1367 var text = textify( nodes[ 0 ] );
1368 return text.charAt( 0 ).toUpperCase() + text.slice( 1 );
1369 }
1370 };
1371
1372 /**
1373 * @method
1374 * @member jQuery
1375 * @see mw.jqueryMsg#getPlugin
1376 */
1377 $.fn.msg = mw.jqueryMsg.getPlugin();
1378
1379 // Replace the default message parser with jqueryMsg
1380 oldParser = mw.Message.prototype.parser;
1381 mw.Message.prototype.parser = function () {
1382 // Fall back to mw.msg's simple parser where possible
1383 if (
1384 // Plain text output always uses the simple parser
1385 this.format === 'plain' ||
1386 (
1387 // jqueryMsg parser is needed for messages containing wikitext
1388 !/\{\{|[<>[&]/.test( this.map.get( this.key ) ) &&
1389 // jqueryMsg parser is needed when jQuery objects or DOM nodes are passed in as parameters
1390 !this.parameters.some( function ( param ) {
1391 return param instanceof $ || ( param && param.nodeType !== undefined );
1392 } )
1393 )
1394 ) {
1395 return oldParser.apply( this );
1396 }
1397
1398 if ( !Object.prototype.hasOwnProperty.call( this.map, this.format ) ) {
1399 this.map[ this.format ] = mw.jqueryMsg.getMessageFunction( {
1400 messages: this.map,
1401 // For format 'escaped', escaping part is handled by mediawiki.js
1402 format: this.format
1403 } );
1404 }
1405 return this.map[ this.format ]( this.key, this.parameters );
1406 };
1407
1408 /**
1409 * Parse the message to DOM nodes, rather than HTML string like #parse.
1410 *
1411 * This method is only available when jqueryMsg is loaded.
1412 *
1413 * @since 1.27
1414 * @method parseDom
1415 * @member mw.Message
1416 * @return {jQuery}
1417 */
1418 mw.Message.prototype.parseDom = ( function () {
1419 var $wrapper = $( '<div>' );
1420 return function () {
1421 return $wrapper.msg( this.key, this.parameters ).contents().detach();
1422 };
1423 }() );