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