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