Localisation updates from http://translatewiki.net.
[mediawiki.git] / resources / mediawiki / mediawiki.jqueryMsg.js
blob043ebcee0fc773b32d83b37ced60353890cb7291
1 /**
2  * Experimental advanced wikitext parser-emitter. 
3  * See: http://www.mediawiki.org/wiki/Extension:UploadWizard/MessageParser for docs
4  * 
5  * @author neilk@wikimedia.org
6  */
8 ( function( mw, $, undefined ) {
10         mw.jqueryMsg = {};
12         /**
13          * Given parser options, return a function that parses a key and replacements, returning jQuery object
14          * @param {Object} parser options
15          * @return {Function} accepting ( String message key, String replacement1, String replacement2 ... ) and returning {jQuery}
16          */
17         function getFailableParserFn( options ) { 
18                 var parser = new mw.jqueryMsg.parser( options ); 
19                 /** 
20                  * Try to parse a key and optional replacements, returning a jQuery object that may be a tree of jQuery nodes.
21                  * If there was an error parsing, return the key and the error message (wrapped in jQuery). This should put the error right into
22                  * the interface, without causing the page to halt script execution, and it hopefully should be clearer how to fix it.
23                  *
24                  * @param {Array} first element is the key, replacements may be in array in 2nd element, or remaining elements.
25                  * @return {jQuery}
26                  */
27                 return function( args ) {
28                         var key = args[0];
29                         var argsArray = $.isArray( args[1] ) ? args[1] : $.makeArray( args ).slice( 1 ); 
30                         var escapedArgsArray = $.map( argsArray, function( arg ) { 
31                                 return typeof arg === 'string' ? mw.html.escape( arg ) : arg;
32                         } );
33                         try {
34                                 return parser.parse( key, escapedArgsArray );
35                         } catch ( e ) {
36                                 return $( '<span></span>' ).append( key + ': ' + e.message );
37                         }
38                 };
39         }
41         /**
42          * Class method. 
43          * Returns a function suitable for use as a global, to construct strings from the message key (and optional replacements).
44          * e.g.  
45          *       window.gM = mediaWiki.parser.getMessageFunction( options );
46          *       $( 'p#headline' ).html( gM( 'hello-user', username ) );
47          *
48          * Like the old gM() function this returns only strings, so it destroys any bindings. If you want to preserve bindings use the
49          * jQuery plugin version instead. This is only included for backwards compatibility with gM().
50          *
51          * @param {Array} parser options
52          * @return {Function} function suitable for assigning to window.gM
53          */
54         mw.jqueryMsg.getMessageFunction = function( options ) { 
55                 var failableParserFn = getFailableParserFn( options );
56                 /** 
57                  * N.B. replacements are variadic arguments or an array in second parameter. In other words:
58                  *    somefunction(a, b, c, d) 
59                  * is equivalent to 
60                  *    somefunction(a, [b, c, d])
61                  *
62                  * @param {String} message key
63                  * @param {Array} optional replacements (can also specify variadically)
64                  * @return {String} rendered HTML as string
65                  */
66                 return function( /* key, replacements */ ) {
67                         return failableParserFn( arguments ).html();
68                 };
69         };
71         /**
72          * Class method. 
73          * Returns a jQuery plugin which parses the message in the message key, doing replacements optionally, and appends the nodes to 
74          * the current selector. Bindings to passed-in jquery elements are preserved. Functions become click handlers for [$1 linktext] links.  
75          * e.g.  
76          *        $.fn.msg = mediaWiki.parser.getJqueryPlugin( options );
77          *        var userlink = $( '<a>' ).click( function() { alert( "hello!!") } );
78          *        $( 'p#headline' ).msg( 'hello-user', userlink );
79          *
80          * @param {Array} parser options
81          * @return {Function} function suitable for assigning to jQuery plugin, such as $.fn.msg
82          */
83         mw.jqueryMsg.getPlugin = function( options ) {
84                 var failableParserFn = getFailableParserFn( options );
85                 /** 
86                  * N.B. replacements are variadic arguments or an array in second parameter. In other words:
87                  *    somefunction(a, b, c, d) 
88                  * is equivalent to 
89                  *    somefunction(a, [b, c, d])
90                  * 
91                  * We append to 'this', which in a jQuery plugin context will be the selected elements.
92                  * @param {String} message key
93                  * @param {Array} optional replacements (can also specify variadically)
94                  * @return {jQuery} this
95                  */
96                 return function( /* key, replacements */ ) {
97                         var $target = this.empty();
98                         $.each( failableParserFn( arguments ).contents(), function( i, node ) {
99                                 $target.append( node );
100                         } );
101                         return $target;
102                 };
103         };
105         var parserDefaults = { 
106                 'magic' : {
107                         'SITENAME' : mw.config.get( 'wgSiteName' )
108                 },
109                 'messages' : mw.messages,
110                 'language' : mw.language
111         };
113         /**
114          * The parser itself.
115          * Describes an object, whose primary duty is to .parse() message keys.
116          * @param {Array} options
117          */
118         mw.jqueryMsg.parser = function( options ) {
119                 this.settings = $.extend( {}, parserDefaults, options );
120                 this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
121         };
123         mw.jqueryMsg.parser.prototype = {
125                 // cache, map of mediaWiki message key to the AST of the message. In most cases, the message is a string so this is identical.
126                 // (This is why we would like to move this functionality server-side).
127                 astCache: {},
129                 /**
130                  * Where the magic happens.
131                  * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
132                  * If an error is thrown, returns original key, and logs the error
133                  * @param {String} message key
134                  * @param {Array} replacements for $1, $2... $n
135                  * @return {jQuery}
136                  */
137                 parse: function( key, replacements ) {
138                         return this.emitter.emit( this.getAst( key ), replacements );
139                 },
141                 /**
142                  * Fetch the message string associated with a key, return parsed structure. Memoized.
143                  * Note that we pass '[' + key + ']' back for a missing message here. 
144                  * @param {String} key
145                  * @return {String|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
146                  */
147                 getAst: function( key ) {
148                         if ( this.astCache[ key ] === undefined ) { 
149                                 var wikiText = this.settings.messages.get( key );
150                                 if ( typeof wikiText !== 'string' ) {
151                                         wikiText = "\\[" + key + "\\]";
152                                 }
153                                 this.astCache[ key ] = this.wikiTextToAst( wikiText );
154                         }
155                         return this.astCache[ key ];    
156                 },
158                 /*
159                  * Parses the input wikiText into an abstract syntax tree, essentially an s-expression.
160                  *
161                  * CAVEAT: This does not parse all wikitext. It could be more efficient, but it's pretty good already.
162                  * n.b. We want to move this functionality to the server. Nothing here is required to be on the client.
163                  * 
164                  * @param {String} message string wikitext
165                  * @throws Error
166                  * @return {Mixed} abstract syntax tree
167                  */
168                 wikiTextToAst: function( input ) {
169                         
170                         // Indicates current position in input as we parse through it.  
171                         // Shared among all parsing functions below. 
172                         var pos = 0;
174                         // =========================================================
175                         // parsing combinators - could be a library on its own
176                         // =========================================================
179                         // Try parsers until one works, if none work return null 
180                         function choice( ps ) {
181                                 return function() {
182                                         for ( var i = 0; i < ps.length; i++ ) {
183                                                 var result = ps[i]();
184                                                 if ( result !== null ) {
185                                                          return result;
186                                                 }
187                                         }
188                                         return null;
189                                 };
190                         }
192                         // try several ps in a row, all must succeed or return null
193                         // this is the only eager one
194                         function sequence( ps ) {
195                                 var originalPos = pos;
196                                 var result = [];
197                                 for ( var i = 0; i < ps.length; i++ ) { 
198                                         var res = ps[i]();
199                                         if ( res === null ) {
200                                                 pos = originalPos;
201                                                 return null;
202                                         } 
203                                         result.push( res );
204                                 }
205                                 return result;
206                         }
208                         // run the same parser over and over until it fails.
209                         // must succeed a minimum of n times or return null
210                         function nOrMore( n, p ) {
211                                 return function() {
212                                         var originalPos = pos;
213                                         var result = [];
214                                         var parsed = p();
215                                         while ( parsed !== null ) {
216                                                 result.push( parsed );
217                                                 parsed = p();
218                                         }
219                                         if ( result.length < n ) {
220                                                 pos = originalPos;
221                                                 return null;
222                                         } 
223                                         return result;
224                                 };
225                         }
227                         // There is a general pattern -- parse a thing, if that worked, apply transform, otherwise return null.
228                         // But using this as a combinator seems to cause problems when combined with nOrMore().
229                         // May be some scoping issue
230                         function transform( p, fn ) {
231                                 return function() { 
232                                         var result = p();
233                                         return result === null ? null : fn( result );
234                                 };
235                         }
237                         // Helpers -- just make ps out of simpler JS builtin types
239                         function makeStringParser( s ) { 
240                                 var len = s.length;
241                                 return function() {
242                                         var result = null;
243                                         if ( input.substr( pos, len ) === s ) {
244                                                  result = s;
245                                                  pos += len;
246                                         }
247                                         return result;
248                                 };
249                         }
251                         function makeRegexParser( regex ) {
252                                 return function() { 
253                                         var matches = input.substr( pos ).match( regex );
254                                         if ( matches === null ) { 
255                                                 return null;
256                                         } 
257                                         pos += matches[0].length;
258                                         return matches[0];
259                                 };
260                         }
261                                                  
263                         /** 
264                          *  =================================================================== 
265                          *  General patterns above this line -- wikitext specific parsers below
266                          *  =================================================================== 
267                          */
269                         // Parsing functions follow. All parsing functions work like this:
270                         // They don't accept any arguments.
271                         // Instead, they just operate non destructively on the string 'input'
272                         // As they can consume parts of the string, they advance the shared variable pos,
273                         // and return tokens (or whatever else they want to return).
275                         // some things are defined as closures and other things as ordinary functions
276                         // converting everything to a closure makes it a lot harder to debug... errors pop up
277                         // but some debuggers can't tell you exactly where they come from. Also the mutually
278                         // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
279                         // This may be because, to save code, memoization was removed
282                         var regularLiteral = makeRegexParser( /^[^{}[\]$\\]/ );
283                         var regularLiteralWithoutBar = makeRegexParser(/^[^{}[\]$\\|]/);
284                         var regularLiteralWithoutSpace = makeRegexParser(/^[^{}[\]$\s]/);
286                         var backslash = makeStringParser( "\\" );
287                         var anyCharacter = makeRegexParser( /^./ );
289                         function escapedLiteral() {
290                                 var result = sequence( [
291                                         backslash, 
292                                         anyCharacter
293                                 ] );
294                                 return result === null ? null : result[1];
295                         }
297                         var escapedOrLiteralWithoutSpace = choice( [
298                                 escapedLiteral,
299                                 regularLiteralWithoutSpace
300                         ] );
302                         var escapedOrLiteralWithoutBar = choice( [
303                                 escapedLiteral,
304                                 regularLiteralWithoutBar
305                         ] );
307                         var escapedOrRegularLiteral = choice( [ 
308                                 escapedLiteral,
309                                 regularLiteral
310                         ] );
312                         // Used to define "literals" without spaces, in space-delimited situations
313                         function literalWithoutSpace() {
314                                  var result = nOrMore( 1, escapedOrLiteralWithoutSpace )();
315                                  return result === null ? null : result.join('');
316                         }
318                         // Used to define "literals" within template parameters. The pipe character is the parameter delimeter, so by default 
319                         // it is not a literal in the parameter
320                         function literalWithoutBar() {
321                                  var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
322                                  return result === null ? null : result.join('');
323                         }
325                         function literal() {
326                                  var result = nOrMore( 1, escapedOrRegularLiteral )();
327                                  return result === null ? null : result.join('');
328                         }
330                         var whitespace = makeRegexParser( /^\s+/ ); 
331                         var dollar = makeStringParser( '$' );
332                         var digits = makeRegexParser( /^\d+/ );   
334                         function replacement() {
335                                 var result = sequence( [
336                                         dollar,
337                                         digits
338                                 ] );
339                                 if ( result === null ) { 
340                                         return null;
341                                 }
342                                 return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
343                         }
346                         var openExtlink = makeStringParser( '[' );
347                         var closeExtlink = makeStringParser( ']' );
349                         // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed
350                         function extlink() {
351                                 var result = null;
352                                 var parsedResult = sequence( [
353                                         openExtlink,
354                                         nonWhitespaceExpression,
355                                         whitespace,
356                                         expression,
357                                         closeExtlink
358                                 ] );
359                                 if ( parsedResult !== null ) {
360                                          result = [ 'LINK', parsedResult[1], parsedResult[3] ];
361                                 }
362                                 return result;
363                         }
365                         var openLink = makeStringParser( '[[' );
366                         var closeLink = makeStringParser( ']]' );
368                         function link() {
369                                 var result = null;
370                                 var parsedResult = sequence( [
371                                         openLink,
372                                         expression,
373                                         closeLink
374                                 ] );
375                                 if ( parsedResult !== null ) {
376                                          result = [ 'WLINK', parsedResult[1] ];
377                                 }
378                                 return result;
379                         }
381                         var templateName = transform( 
382                                 // see $wgLegalTitleChars
383                                 // not allowing : due to the need to catch "PLURAL:$1"
384                                 makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+-]+/ ),
385                                 function( result ) { return result.toString(); }
386                         );
388                         function templateParam() {
389                                 var result = sequence( [ 
390                                         pipe,
391                                         nOrMore( 0, paramExpression )
392                                 ] );
393                                 if ( result === null ) {
394                                         return null;
395                                 }
396                                 var expr = result[1];
397                                 // use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw.
398                                 return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0];
399                         }
401                         var pipe = makeStringParser( '|' );
403                         function templateWithReplacement() {
404                                 var result = sequence( [
405                                         templateName,
406                                         colon,
407                                         replacement
408                                 ] );
409                                 return result === null ? null : [ result[0], result[2] ];
410                         }
412                         function templateWithOutReplacement() {
413                                 var result = sequence( [
414                                         templateName,
415                                         colon,
416                                         paramExpression
417                                 ] );
418                                 return result === null ? null : [ result[0], result[2] ];
419                         }
421                         var colon = makeStringParser(':');
423                         var templateContents = choice( [
424                                 function() {
425                                         var res = sequence( [
426                                                 // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
427                                                 // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
428                                                 choice( [ templateWithReplacement, templateWithOutReplacement ] ),
429                                                 nOrMore( 0, templateParam )
430                                         ] );
431                                         return res === null ? null : res[0].concat( res[1] );
432                                 },
433                                 function() { 
434                                         var res = sequence( [
435                                                 templateName,
436                                                 nOrMore( 0, templateParam ) 
437                                         ] );
438                                         if ( res === null ) {
439                                                 return null;
440                                         }
441                                         return [ res[0] ].concat( res[1] );
442                                 }
443                         ] );
445                         var openTemplate = makeStringParser('{{');
446                         var closeTemplate = makeStringParser('}}');
448                         function template() {
449                                 var result = sequence( [
450                                         openTemplate,
451                                         templateContents,
452                                         closeTemplate
453                                 ] );
454                                 return result === null ? null : result[1];
455                         }
457                         var nonWhitespaceExpression = choice( [
458                                 template,        
459                                 link,
460                                 extlink,
461                                 replacement,
462                                 literalWithoutSpace
463                         ] );
465                         var paramExpression = choice( [
466                                 template,        
467                                 link,
468                                 extlink,
469                                 replacement,
470                                 literalWithoutBar
471                         ] );
473                         var expression = choice( [ 
474                                 template,
475                                 link,
476                                 extlink,
477                                 replacement,
478                                 literal 
479                         ] );
481                         function start() {
482                                 var result = nOrMore( 0, expression )();
483                                 if ( result === null ) {
484                                         return null;
485                                 }
486                                 return [ "CONCAT" ].concat( result );
487                         }
489                         // everything above this point is supposed to be stateless/static, but
490                         // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
492                         // finally let's do some actual work...
494                         var result = start();
495                         
496                         /*
497                          * For success, the p must have gotten to the end of the input 
498                          * and returned a non-null.
499                          * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
500                          */
501                         if (result === null || pos !== input.length) {
502                                 throw new Error( "Parse error at position " + pos.toString() + " in input: " + input );
503                         }
504                         return result;
505                 }
506                         
507         };
509         /**
510          * htmlEmitter - object which primarily exists to emit HTML from parser ASTs
511          */
512         mw.jqueryMsg.htmlEmitter = function( language, magic ) {
513                 this.language = language;
514                 var _this = this;
516                 $.each( magic, function( key, val ) { 
517                         _this[ key.toLowerCase() ] = function() { return val; };
518                 } );
520                 /**
521                  * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
522                  * Walk entire node structure, applying replacements and template functions when appropriate
523                  * @param {Mixed} abstract syntax tree (top node or subnode)
524                  * @param {Array} replacements for $1, $2, ... $n
525                  * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
526                  */
527                 this.emit = function( node, replacements ) {
528                         var ret = null;
529                         var _this = this;
530                         switch( typeof node ) {
531                                 case 'string':
532                                 case 'number':
533                                         ret = node;
534                                         break;
535                                 case 'object': // node is an array of nodes
536                                         var subnodes = $.map( node.slice( 1 ), function( n ) { 
537                                                 return _this.emit( n, replacements );
538                                         } );
539                                         var operation = node[0].toLowerCase();
540                                         if ( typeof _this[operation] === 'function' ) { 
541                                                 ret = _this[ operation ]( subnodes, replacements );
542                                         } else {
543                                                 throw new Error( 'unknown operation "' + operation + '"' );
544                                         }
545                                         break;
546                                 case 'undefined':
547                                         // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
548                                         // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
549                                         // The logical thing is probably to return the empty string here when we encounter undefined.
550                                         ret = '';
551                                         break;
552                                 default:
553                                         throw new Error( 'unexpected type in AST: ' + typeof node );
554                         }
555                         return ret;
556                 };
558         };
560         // For everything in input that follows double-open-curly braces, there should be an equivalent parser
561         // function. For instance {{PLURAL ... }} will be processed by 'plural'. 
562         // If you have 'magic words' then configure the parser to have them upon creation.
563         //
564         // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
565         // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
566         mw.jqueryMsg.htmlEmitter.prototype = {
568                 /**
569                  * Parsing has been applied depth-first we can assume that all nodes here are single nodes
570                  * Must return a single node to parents -- a jQuery with synthetic span
571                  * However, unwrap any other synthetic spans in our children and pass them upwards
572                  * @param {Array} nodes - mixed, some single nodes, some arrays of nodes
573                  * @return {jQuery}
574                  */
575                 concat: function( nodes ) {
576                         var span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
577                         $.each( nodes, function( i, node ) { 
578                                 if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
579                                         $.each( node.contents(), function( j, childNode ) {
580                                                 span.append( childNode );
581                                         } );
582                                 } else {
583                                         // strings, integers, anything else
584                                         span.append( node );
585                                 }
586                         } );
587                         return span;
588                 },
590                 /**
591                  * Return replacement of correct index, or string if unavailable.
592                  * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
593                  * if the specified parameter is not found return the same string
594                  * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
595                  * TODO throw error if nodes.length > 1 ?
596                  * @param {Array} of one element, integer, n >= 0
597                  * @return {String} replacement
598                  */
599                 replace: function( nodes, replacements ) {
600                         var index = parseInt( nodes[0], 10 );
601                         return index < replacements.length ? replacements[index] : '$' + ( index + 1 ); 
602                 },
604                 /** 
605                  * Transform wiki-link
606                  * TODO unimplemented 
607                  */
608                 wlink: function( nodes ) {
609                         return "unimplemented";
610                 },
612                 /**
613                  * Transform parsed structure into external link
614                  * If the href is a jQuery object, treat it as "enclosing" the link text.
615                  *              ... function, treat it as the click handler
616                  *              ... string, treat it as a URI
617                  * TODO: throw an error if nodes.length > 2 ? 
618                  * @param {Array} of two elements, {jQuery|Function|String} and {String}
619                  * @return {jQuery}
620                  */
621                 link: function( nodes ) {
622                         var arg = nodes[0];
623                         var contents = nodes[1];
624                         var $el; 
625                         if ( arg instanceof jQuery ) {
626                                 $el = arg;
627                         } else {
628                                 $el = $( '<a>' );
629                                 if ( typeof arg === 'function' ) {
630                                         $el.click( arg ).attr( 'href', '#' );
631                                 } else {
632                                         $el.attr( 'href', arg.toString() );
633                                 }
634                         }
635                         $el.append( contents ); 
636                         return $el;
637                 },
639                 /**
640                  * Transform parsed structure into pluralization
641                  * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
642                  * So convert it back with the current language's convertNumber.
643                  * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ] 
644                  * @return {String} selected pluralized form according to current language
645                  */
646                 plural: function( nodes ) { 
647                         var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 );
648                         var forms = nodes.slice(1);
649                         return forms.length ? this.language.convertPlural( count, forms ) : '';
650                 },
652                 /**
653                  * Transform parsed structure into gender
654                  * Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}.
655                  * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ] 
656                  * @return {String} selected gender form according to current language
657                  */
658                 gender: function( nodes ) { 
659                         var gender;
660                         if  ( nodes[0] && nodes[0].options instanceof mw.Map ){
661                                 gender = nodes[0].options.get( 'gender' );
662                         } else {
663                                 gender = nodes[0];
664                         }
665                         var forms = nodes.slice(1);
666                         return this.language.gender( gender, forms );
667                 },
669                 /**
670                  * Transform parsed structure into grammar conversion.
671                  * Invoked by putting {{grammar:form|word}} in a message
672                  * @param {Array} of nodes [{Grammar case eg: genitive}, {String word}]
673                  * @return {String} selected grammatical form according to current language
674                  */
675                 grammar: function( nodes ) {
676                         var form = nodes[0];
677                         var word = nodes[1];
678                         return word && form && this.language.convertGrammar( word, form );
679                 }
680         };
682         // deprecated! don't rely on gM existing.
683         // the window.gM ought not to be required - or if required, not required here. But moving it to extensions breaks it (?!)
684         // Need to fix plugin so it could do attributes as well, then will be okay to remove this.
685         window.gM = mw.jqueryMsg.getMessageFunction(); 
687         $.fn.msg = mw.jqueryMsg.getPlugin();
688         
689         // Replace the default message parser with jqueryMsg
690         var oldParser = mw.Message.prototype.parser;
691         mw.Message.prototype.parser = function() {
692                 // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
693                 // Caching is somewhat problematic, because we do need different message functions for different maps, so
694                 // we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
695                 
696                 // Do not use mw.jqueryMsg unless required
697                 if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) {
698                         // Fall back to mw.msg's simple parser
699                         return oldParser.apply( this );
700                 }
701                 
702                 var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } );
703                 return messageFunction( this.key, this.parameters );
704         };
706 } )( mediaWiki, jQuery );