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