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