Merge "Set default type attribute for button html elements"
[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, 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 function literal() {
308 var result = nOrMore( 1, escapedOrRegularLiteral )();
309 return result === null ? null : result.join('');
310 }
311 whitespace = makeRegexParser( /^\s+/ );
312 dollar = makeStringParser( '$' );
313 digits = makeRegexParser( /^\d+/ );
314
315 function replacement() {
316 var result = sequence( [
317 dollar,
318 digits
319 ] );
320 if ( result === null ) {
321 return null;
322 }
323 return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
324 }
325 openExtlink = makeStringParser( '[' );
326 closeExtlink = makeStringParser( ']' );
327 // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed
328 function extlink() {
329 var result, parsedResult;
330 result = null;
331 parsedResult = sequence( [
332 openExtlink,
333 nonWhitespaceExpression,
334 whitespace,
335 expression,
336 closeExtlink
337 ] );
338 if ( parsedResult !== null ) {
339 result = [ 'LINK', parsedResult[1], parsedResult[3] ];
340 }
341 return result;
342 }
343 // this is the same as the above extlink, except that the url is being passed on as a parameter
344 function extLinkParam() {
345 var result = sequence( [
346 openExtlink,
347 dollar,
348 digits,
349 whitespace,
350 expression,
351 closeExtlink
352 ] );
353 if ( result === null ) {
354 return null;
355 }
356 return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
357 }
358 openLink = makeStringParser( '[[' );
359 closeLink = makeStringParser( ']]' );
360 function link() {
361 var result, parsedResult;
362 result = null;
363 parsedResult = sequence( [
364 openLink,
365 expression,
366 closeLink
367 ] );
368 if ( parsedResult !== null ) {
369 result = [ 'WLINK', parsedResult[1] ];
370 }
371 return result;
372 }
373 templateName = transform(
374 // see $wgLegalTitleChars
375 // not allowing : due to the need to catch "PLURAL:$1"
376 makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ),
377 function ( result ) { return result.toString(); }
378 );
379 function templateParam() {
380 var expr, result;
381 result = sequence( [
382 pipe,
383 nOrMore( 0, paramExpression )
384 ] );
385 if ( result === null ) {
386 return null;
387 }
388 expr = result[1];
389 // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
390 return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0];
391 }
392 pipe = makeStringParser( '|' );
393 function templateWithReplacement() {
394 var result = sequence( [
395 templateName,
396 colon,
397 replacement
398 ] );
399 return result === null ? null : [ result[0], result[2] ];
400 }
401 function templateWithOutReplacement() {
402 var result = sequence( [
403 templateName,
404 colon,
405 paramExpression
406 ] );
407 return result === null ? null : [ result[0], result[2] ];
408 }
409 colon = makeStringParser(':');
410 templateContents = choice( [
411 function () {
412 var res = sequence( [
413 // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
414 // or no placeholders eg: {{GRAMMAR:genitive|{{SITENAME}}}
415 choice( [ templateWithReplacement, templateWithOutReplacement ] ),
416 nOrMore( 0, templateParam )
417 ] );
418 return res === null ? null : res[0].concat( res[1] );
419 },
420 function () {
421 var res = sequence( [
422 templateName,
423 nOrMore( 0, templateParam )
424 ] );
425 if ( res === null ) {
426 return null;
427 }
428 return [ res[0] ].concat( res[1] );
429 }
430 ] );
431 openTemplate = makeStringParser('{{');
432 closeTemplate = makeStringParser('}}');
433 function template() {
434 var result = sequence( [
435 openTemplate,
436 templateContents,
437 closeTemplate
438 ] );
439 return result === null ? null : result[1];
440 }
441 nonWhitespaceExpression = choice( [
442 template,
443 link,
444 extLinkParam,
445 extlink,
446 replacement,
447 literalWithoutSpace
448 ] );
449 paramExpression = choice( [
450 template,
451 link,
452 extLinkParam,
453 extlink,
454 replacement,
455 literalWithoutBar
456 ] );
457 expression = choice( [
458 template,
459 link,
460 extLinkParam,
461 extlink,
462 replacement,
463 literal
464 ] );
465 function start() {
466 var result = nOrMore( 0, expression )();
467 if ( result === null ) {
468 return null;
469 }
470 return [ 'CONCAT' ].concat( result );
471 }
472 // everything above this point is supposed to be stateless/static, but
473 // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
474 // finally let's do some actual work...
475 result = start();
476
477 /*
478 * For success, the p must have gotten to the end of the input
479 * and returned a non-null.
480 * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
481 */
482 if ( result === null || pos !== input.length ) {
483 throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
484 }
485 return result;
486 }
487
488 };
489 /**
490 * htmlEmitter - object which primarily exists to emit HTML from parser ASTs
491 */
492 mw.jqueryMsg.htmlEmitter = function ( language, magic ) {
493 this.language = language;
494 var jmsg = this;
495 $.each( magic, function ( key, val ) {
496 jmsg[ key.toLowerCase() ] = function () {
497 return val;
498 };
499 } );
500 /**
501 * (We put this method definition here, and not in prototype, to make sure it's not overwritten by any magic.)
502 * Walk entire node structure, applying replacements and template functions when appropriate
503 * @param {Mixed} abstract syntax tree (top node or subnode)
504 * @param {Array} replacements for $1, $2, ... $n
505 * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
506 */
507 this.emit = function ( node, replacements ) {
508 var ret, subnodes, operation,
509 jmsg = this;
510 switch ( typeof node ) {
511 case 'string':
512 case 'number':
513 ret = node;
514 break;
515 // typeof returns object for arrays
516 case 'object':
517 // node is an array of nodes
518 subnodes = $.map( node.slice( 1 ), function ( n ) {
519 return jmsg.emit( n, replacements );
520 } );
521 operation = node[0].toLowerCase();
522 if ( typeof jmsg[operation] === 'function' ) {
523 ret = jmsg[ operation ]( subnodes, replacements );
524 } else {
525 throw new Error( 'Unknown operation "' + operation + '"' );
526 }
527 break;
528 case 'undefined':
529 // Parsing the empty string (as an entire expression, or as a paramExpression in a template) results in undefined
530 // Perhaps a more clever parser can detect this, and return the empty string? Or is that useful information?
531 // The logical thing is probably to return the empty string here when we encounter undefined.
532 ret = '';
533 break;
534 default:
535 throw new Error( 'Unexpected type in AST: ' + typeof node );
536 }
537 return ret;
538 };
539 };
540 // For everything in input that follows double-open-curly braces, there should be an equivalent parser
541 // function. For instance {{PLURAL ... }} will be processed by 'plural'.
542 // If you have 'magic words' then configure the parser to have them upon creation.
543 //
544 // An emitter method takes the parent node, the array of subnodes and the array of replacements (the values that $1, $2... should translate to).
545 // Note: all such functions must be pure, with the exception of referring to other pure functions via this.language (convertPlural and so on)
546 mw.jqueryMsg.htmlEmitter.prototype = {
547 /**
548 * Parsing has been applied depth-first we can assume that all nodes here are single nodes
549 * Must return a single node to parents -- a jQuery with synthetic span
550 * However, unwrap any other synthetic spans in our children and pass them upwards
551 * @param {Array} nodes - mixed, some single nodes, some arrays of nodes
552 * @return {jQuery}
553 */
554 concat: function ( nodes ) {
555 var $span = $( '<span>' ).addClass( 'mediaWiki_htmlEmitter' );
556 $.each( nodes, function ( i, node ) {
557 if ( node instanceof jQuery && node.hasClass( 'mediaWiki_htmlEmitter' ) ) {
558 $.each( node.contents(), function ( j, childNode ) {
559 $span.append( childNode );
560 } );
561 } else {
562 // strings, integers, anything else
563 $span.append( node );
564 }
565 } );
566 return $span;
567 },
568
569 /**
570 * Return escaped replacement of correct index, or string if unavailable.
571 * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
572 * if the specified parameter is not found return the same string
573 * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
574 * TODO throw error if nodes.length > 1 ?
575 * @param {Array} of one element, integer, n >= 0
576 * @return {String} replacement
577 */
578 replace: function ( nodes, replacements ) {
579 var index = parseInt( nodes[0], 10 );
580
581 if ( index < replacements.length ) {
582 if ( typeof arg === 'string' ) {
583 // replacement is a string, escape it
584 return mw.html.escape( replacements[index] );
585 } else {
586 // replacement is no string, don't touch!
587 return replacements[index];
588 }
589 } else {
590 // index not found, fallback to displaying variable
591 return '$' + ( index + 1 );
592 }
593 },
594
595 /**
596 * Transform wiki-link
597 * TODO unimplemented
598 * @param nodes
599 */
600 wlink: function () {
601 return 'unimplemented';
602 },
603
604 /**
605 * Transform parsed structure into external link
606 * If the href is a jQuery object, treat it as "enclosing" the link text.
607 * ... function, treat it as the click handler
608 * ... string, treat it as a URI
609 * TODO: throw an error if nodes.length > 2 ?
610 * @param {Array} of two elements, {jQuery|Function|String} and {String}
611 * @return {jQuery}
612 */
613 link: function ( nodes ) {
614 var $el,
615 arg = nodes[0],
616 contents = nodes[1];
617 if ( arg instanceof jQuery ) {
618 $el = arg;
619 } else {
620 $el = $( '<a>' );
621 if ( typeof arg === 'function' ) {
622 $el.click( arg ).attr( 'href', '#' );
623 } else {
624 $el.attr( 'href', arg.toString() );
625 }
626 }
627 $el.append( contents );
628 return $el;
629 },
630
631 /**
632 * This is basically use a combination of replace + link (link with parameter
633 * as url), but we don't want to run the regular replace here-on: inserting a
634 * url as href-attribute of a link will automatically escape it already, so
635 * we don't want replace to (manually) escape it as well.
636 * TODO throw error if nodes.length > 1 ?
637 * @param {Array} of one element, integer, n >= 0
638 * @return {String} replacement
639 */
640 linkparam: function ( nodes, replacements ) {
641 var replacement,
642 index = parseInt( nodes[0], 10 );
643 if ( index < replacements.length) {
644 replacement = replacements[index];
645 } else {
646 replacement = '$' + ( index + 1 );
647 }
648 return this.link( [ replacement, nodes[1] ] );
649 },
650
651 /**
652 * Transform parsed structure into pluralization
653 * n.b. The first node may be a non-integer (for instance, a string representing an Arabic number).
654 * So convert it back with the current language's convertNumber.
655 * @param {Array} of nodes, [ {String|Number}, {String}, {String} ... ]
656 * @return {String} selected pluralized form according to current language
657 */
658 plural: function ( nodes ) {
659 var forms, count;
660 count = parseFloat( this.language.convertNumber( nodes[0], true ) );
661 forms = nodes.slice(1);
662 return forms.length ? this.language.convertPlural( count, forms ) : '';
663 },
664
665 /**
666 * Transform parsed structure according to gender.
667 * Usage {{gender:[ gender | mw.user object ] | masculine form|feminine form|neutral form}}.
668 * The first node is either a string, which can be "male" or "female",
669 * or a User object (not a username).
670 *
671 * @param {Array} of nodes, [ {String|mw.User}, {String}, {String}, {String} ]
672 * @return {String} selected gender form according to current language
673 */
674 gender: function ( nodes ) {
675 var gender, forms;
676
677 if ( nodes[0] && nodes[0].options instanceof mw.Map ) {
678 gender = nodes[0].options.get( 'gender' );
679 } else {
680 gender = nodes[0];
681 }
682
683 forms = nodes.slice( 1 );
684
685 return this.language.gender( gender, forms );
686 },
687
688 /**
689 * Transform parsed structure into grammar conversion.
690 * Invoked by putting {{grammar:form|word}} in a message
691 * @param {Array} of nodes [{Grammar case eg: genitive}, {String word}]
692 * @return {String} selected grammatical form according to current language
693 */
694 grammar: function ( nodes ) {
695 var form = nodes[0],
696 word = nodes[1];
697 return word && form && this.language.convertGrammar( word, form );
698 }
699 };
700 // Deprecated! don't rely on gM existing.
701 // The window.gM ought not to be required - or if required, not required here.
702 // But moving it to extensions breaks it (?!)
703 // Need to fix plugin so it could do attributes as well, then will be okay to remove this.
704 window.gM = mw.jqueryMsg.getMessageFunction();
705 $.fn.msg = mw.jqueryMsg.getPlugin();
706
707 // Replace the default message parser with jqueryMsg
708 oldParser = mw.Message.prototype.parser;
709 mw.Message.prototype.parser = function () {
710 // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
711 // Caching is somewhat problematic, because we do need different message functions for different maps, so
712 // we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
713 // Do not use mw.jqueryMsg unless required
714 if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) {
715 // Fall back to mw.msg's simple parser
716 return oldParser.apply( this );
717 }
718 var messageFunction = mw.jqueryMsg.getMessageFunction( { 'messages': this.map } );
719 return messageFunction( this.key, this.parameters );
720 };
721
722 }( mediaWiki, jQuery ) );