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