Merge "(bug 41477) Add Language::isSupportedLanguage"
[lhc/web/wiklou.git] / resources / mediawiki / mediawiki.jqueryMsg.js
index def1225..934e906 100644 (file)
@@ -5,7 +5,8 @@
 * @author neilk@wikimedia.org
 */
 ( function ( mw, $ ) {
-       var slice = Array.prototype.slice,
+       var oldParser,
+               slice = Array.prototype.slice,
                parserDefaults = {
                        magic : {
                                'SITENAME' : mw.config.get( 'wgSiteName' )
@@ -30,8 +31,8 @@
                 * @return {jQuery}
                 */
                return function ( args ) {
-                       var key = args[0];
-                       var argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 );
+                       var key = args[0],
+                               argsArray = $.isArray( args[1] ) ? args[1] : slice.call( args, 1 );
                        try {
                                return parser.parse( key, argsArray );
                        } catch ( e ) {
                 * is equivalent to
                 *    somefunction(a, [b, c, d])
                 *
-                * @param {String} message key
-                * @param {Array} optional replacements (can also specify variadically)
-                * @return {String} rendered HTML as string
+                * @param {string} key Message key.
+                * @param {Array|mixed} replacements Optional variable replacements (variadically or an array).
+                * @return {string} Rendered HTML.
                 */
-               return function ( /* key, replacements */ ) {
+               return function () {
                        return failableParserFn( arguments ).html();
                };
        };
                 *    somefunction(a, [b, c, d])
                 *
                 * We append to 'this', which in a jQuery plugin context will be the selected elements.
-                * @param {String} message key
-                * @param {Array} optional replacements (can also specify variadically)
+                * @param {string} key Message key.
+                * @param {Array|mixed} replacements Optional variable replacements (variadically or an array).
                 * @return {jQuery} this
                 */
-               return function ( /* key, replacements */ ) {
+               return function () {
                        var $target = this.empty();
+                       // TODO: Simply $target.append( failableParserFn( arguments ).contents() )
+                       // or Simply $target.append( failableParserFn( arguments ) )
                        $.each( failableParserFn( arguments ).contents(), function ( i, node ) {
                                $target.append( node );
                        } );
                 * Where the magic happens.
                 * Parses a message from the key, and swaps in replacements as necessary, wraps in jQuery
                 * If an error is thrown, returns original key, and logs the error
-                * @param {String} message key
-                * @param {Array} replacements for $1, $2... $n
+                * @param {String} key Message key.
+                * @param {Array} replacements Variable replacements for $1, $2... $n
                 * @return {jQuery}
                 */
                parse: function ( key, replacements ) {
                        if ( this.astCache[ key ] === undefined ) {
                                var wikiText = this.settings.messages.get( key );
                                if ( typeof wikiText !== 'string' ) {
-                                       wikiText = "\\[" + key + "\\]";
+                                       wikiText = '\\[' + key + '\\]';
                                }
                                this.astCache[ key ] = this.wikiTextToAst( wikiText );
                        }
                 * @return {Mixed} abstract syntax tree
                 */
                wikiTextToAst: function ( input ) {
+                       var pos,
+                               regularLiteral, regularLiteralWithoutBar, regularLiteralWithoutSpace, backslash, anyCharacter,
+                               escapedOrLiteralWithoutSpace, escapedOrLiteralWithoutBar, escapedOrRegularLiteral,
+                               whitespace, dollar, digits,
+                               openExtlink, closeExtlink, wikilinkPage, wikilinkContents, openLink, closeLink, templateName, pipe, colon,
+                               templateContents, openTemplate, closeTemplate,
+                               nonWhitespaceExpression, paramExpression, expression, result;
 
                        // Indicates current position in input as we parse through it.
                        // Shared among all parsing functions below.
-                       var pos = 0;
+                       pos = 0;
+
                        // =========================================================
                        // parsing combinators - could be a library on its own
                        // =========================================================
                        // Try parsers until one works, if none work return null
                        function choice( ps ) {
                                return function () {
-                                       for ( var i = 0; i < ps.length; i++ ) {
-                                               var result = ps[i]();
+                                       var i, result;
+                                       for ( i = 0; i < ps.length; i++ ) {
+                                               result = ps[i]();
                                                if ( result !== null ) {
                                                         return result;
                                                }
                        // try several ps in a row, all must succeed or return null
                        // this is the only eager one
                        function sequence( ps ) {
-                               var originalPos = pos;
-                               var result = [];
-                               for ( var i = 0; i < ps.length; i++ ) {
-                                       var res = ps[i]();
+                               var i, res,
+                                       originalPos = pos,
+                                       result = [];
+                               for ( i = 0; i < ps.length; i++ ) {
+                                       res = ps[i]();
                                        if ( res === null ) {
                                                pos = originalPos;
                                                return null;
                        // must succeed a minimum of n times or return null
                        function nOrMore( n, p ) {
                                return function () {
-                                       var originalPos = pos;
-                                       var result = [];
-                                       var parsed = p();
+                                       var originalPos = pos,
+                                               result = [],
+                                               parsed = p();
                                        while ( parsed !== null ) {
                                                result.push( parsed );
                                                parsed = p();
                        // but some debuggers can't tell you exactly where they come from. Also the mutually
                        // recursive functions seem not to work in all browsers then. (Tested IE6-7, Opera, Safari, FF)
                        // This may be because, to save code, memoization was removed
-                       var regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ );
-                       var regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/);
-                       var regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/);
-                       var backslash = makeStringParser( "\\" );
-                       var anyCharacter = makeRegexParser( /^./ );
+                       regularLiteral = makeRegexParser( /^[^{}\[\]$\\]/ );
+                       regularLiteralWithoutBar = makeRegexParser(/^[^{}\[\]$\\|]/);
+                       regularLiteralWithoutSpace = makeRegexParser(/^[^{}\[\]$\s]/);
+                       backslash = makeStringParser( '\\' );
+                       anyCharacter = makeRegexParser( /^./ );
                        function escapedLiteral() {
                                var result = sequence( [
                                        backslash,
                                ] );
                                return result === null ? null : result[1];
                        }
-                       var escapedOrLiteralWithoutSpace = choice( [
+                       escapedOrLiteralWithoutSpace = choice( [
                                escapedLiteral,
                                regularLiteralWithoutSpace
                        ] );
-                       var escapedOrLiteralWithoutBar = choice( [
+                       escapedOrLiteralWithoutBar = choice( [
                                escapedLiteral,
                                regularLiteralWithoutBar
                        ] );
-                       var escapedOrRegularLiteral = choice( [
+                       escapedOrRegularLiteral = choice( [
                                escapedLiteral,
                                regularLiteral
                        ] );
                                 var result = nOrMore( 1, escapedOrLiteralWithoutBar )();
                                 return result === null ? null : result.join('');
                        }
+
+                       // Used for wikilink page names.  Like literalWithoutBar, but
+                       // without allowing escapes.
+                       function unescapedLiteralWithoutBar() {
+                               var result = nOrMore( 1, regularLiteralWithoutBar )();
+                               return result === null ? null : result.join('');
+                       }
+
                        function literal() {
                                 var result = nOrMore( 1, escapedOrRegularLiteral )();
                                 return result === null ? null : result.join('');
                        }
-                       var whitespace = makeRegexParser( /^\s+/ );
-                       var dollar = makeStringParser( '$' );
-                       var digits = makeRegexParser( /^\d+/ );
+                       whitespace = makeRegexParser( /^\s+/ );
+                       dollar = makeStringParser( '$' );
+                       digits = makeRegexParser( /^\d+/ );
 
                        function replacement() {
                                var result = sequence( [
                                }
                                return [ 'REPLACE', parseInt( result[1], 10 ) - 1 ];
                        }
-                       var openExtlink = makeStringParser( '[' );
-                       var closeExtlink = makeStringParser( ']' );
+                       openExtlink = makeStringParser( '[' );
+                       closeExtlink = makeStringParser( ']' );
                        // this extlink MUST have inner text, e.g. [foo] not allowed; [foo bar] is allowed
                        function extlink() {
-                               var result = null;
-                               var parsedResult = sequence( [
+                               var result, parsedResult;
+                               result = null;
+                               parsedResult = sequence( [
                                        openExtlink,
                                        nonWhitespaceExpression,
                                        whitespace,
                                }
                                return [ 'LINKPARAM', parseInt( result[2], 10 ) - 1, result[4] ];
                        }
-                       var openLink = makeStringParser( '[[' );
-                       var closeLink = makeStringParser( ']]' );
+                       openLink = makeStringParser( '[[' );
+                       closeLink = makeStringParser( ']]' );
+                       pipe = makeStringParser( '|' );
+
+                       function template() {
+                               var result = sequence( [
+                                       openTemplate,
+                                       templateContents,
+                                       closeTemplate
+                               ] );
+                               return result === null ? null : result[1];
+                       }
+
+                       wikilinkPage = choice( [
+                               unescapedLiteralWithoutBar,
+                               template
+                       ] );
+
+                       function pipedWikilink() {
+                               var result = sequence( [
+                                       wikilinkPage,
+                                       pipe,
+                                       expression
+                               ] );
+                               return result === null ? null : [ result[0], result[2] ];
+                       }
+
+                       wikilinkContents = choice( [
+                               pipedWikilink,
+                               wikilinkPage // unpiped link
+                       ] );
+
                        function link() {
-                               var result = null;
-                               var parsedResult = sequence( [
+                               var result, parsedResult, parsedLinkContents;
+                               result = null;
+
+                               parsedResult = sequence( [
                                        openLink,
-                                       expression,
+                                       wikilinkContents,
                                        closeLink
                                ] );
                                if ( parsedResult !== null ) {
-                                        result = [ 'WLINK', parsedResult[1] ];
+                                       parsedLinkContents = parsedResult[1];
+                                       result = [ 'WLINK' ].concat( parsedLinkContents );
                                }
                                return result;
                        }
-                       var templateName = transform(
+                       templateName = transform(
                                // see $wgLegalTitleChars
                                // not allowing : due to the need to catch "PLURAL:$1"
                                makeRegexParser( /^[ !"$&'()*,.\/0-9;=?@A-Z\^_`a-z~\x80-\xFF+\-]+/ ),
                                function ( result ) { return result.toString(); }
                        );
                        function templateParam() {
-                               var result = sequence( [
+                               var expr, result;
+                               result = sequence( [
                                        pipe,
                                        nOrMore( 0, paramExpression )
                                ] );
                                if ( result === null ) {
                                        return null;
                                }
-                               var expr = result[1];
-                               // use a "CONCAT" operator if there are multiple nodes, otherwise return the first node, raw.
-                               return expr.length > 1 ? [ "CONCAT" ].concat( expr ) : expr[0];
+                               expr = result[1];
+                               // use a CONCAT operator if there are multiple nodes, otherwise return the first node, raw.
+                               return expr.length > 1 ? [ 'CONCAT' ].concat( expr ) : expr[0];
                        }
-                       var pipe = makeStringParser( '|' );
+
                        function templateWithReplacement() {
                                var result = sequence( [
                                        templateName,
                                ] );
                                return result === null ? null : [ result[0], result[2] ];
                        }
-                       var colon = makeStringParser(':');
-                       var templateContents = choice( [
+                       colon = makeStringParser(':');
+                       templateContents = choice( [
                                function () {
                                        var res = sequence( [
                                                // templates can have placeholders for dynamic replacement eg: {{PLURAL:$1|one car|$1 cars}}
                                        return [ res[0] ].concat( res[1] );
                                }
                        ] );
-                       var openTemplate = makeStringParser('{{');
-                       var closeTemplate = makeStringParser('}}');
-                       function template() {
-                               var result = sequence( [
-                                       openTemplate,
-                                       templateContents,
-                                       closeTemplate
-                               ] );
-                               return result === null ? null : result[1];
-                       }
-                       var nonWhitespaceExpression = choice( [
+                       openTemplate = makeStringParser('{{');
+                       closeTemplate = makeStringParser('}}');
+                       nonWhitespaceExpression = choice( [
                                template,
                                link,
                                extLinkParam,
                                replacement,
                                literalWithoutSpace
                        ] );
-                       var paramExpression = choice( [
+                       paramExpression = choice( [
                                template,
                                link,
                                extLinkParam,
                                replacement,
                                literalWithoutBar
                        ] );
-                       var expression = choice( [
+
+                       expression = choice( [
                                template,
                                link,
                                extLinkParam,
                                replacement,
                                literal
                        ] );
+
                        function start() {
                                var result = nOrMore( 0, expression )();
                                if ( result === null ) {
                                        return null;
                                }
-                               return [ "CONCAT" ].concat( result );
+                               return [ 'CONCAT' ].concat( result );
                        }
                        // everything above this point is supposed to be stateless/static, but
                        // I am deferring the work of turning it into prototypes & objects. It's quite fast enough
                        // finally let's do some actual work...
-                       var result = start();
+                       result = start();
 
                        /*
                         * For success, the p must have gotten to the end of the input
                         * and returned a non-null.
                         * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
                         */
-                       if (result === null || pos !== input.length) {
-                               throw new Error( "Parse error at position " + pos.toString() + " in input: " + input );
+                       if ( result === null || pos !== input.length ) {
+                               throw new Error( 'Parse error at position ' + pos.toString() + ' in input: ' + input );
                        }
                        return result;
                }
                 * @return {Mixed} single-string node or array of nodes suitable for jQuery appending
                 */
                this.emit = function ( node, replacements ) {
-                       var ret = null;
-                       var jmsg = this;
+                       var ret, subnodes, operation,
+                               jmsg = this;
                        switch ( typeof node ) {
                                case 'string':
                                case 'number':
                                        ret = node;
                                        break;
-                               case 'object': // node is an array of nodes
-                                       var subnodes = $.map( node.slice( 1 ), function ( n ) {
+                               // typeof returns object for arrays
+                               case 'object':
+                                       // node is an array of nodes
+                                       subnodes = $.map( node.slice( 1 ), function ( n ) {
                                                return jmsg.emit( n, replacements );
                                        } );
-                                       var operation = node[0].toLowerCase();
+                                       operation = node[0].toLowerCase();
                                        if ( typeof jmsg[operation] === 'function' ) {
                                                ret = jmsg[ operation ]( subnodes, replacements );
                                        } else {
                                                $span.append( childNode );
                                        } );
                                } else {
-                                       // strings, integers, anything else
-                                       $span.append( node );
+                                       // Let jQuery append nodes, arrays of nodes and jQuery objects
+                                       // other things (strings, numbers, ..) are appended as text nodes (not as HTML strings)
+                                       $span.append( $.type( node ) === 'object' ? node : document.createTextNode( node ) );
                                }
                        } );
                        return $span;
                 * Note that we expect the parsed parameter to be zero-based. i.e. $1 should have become [ 0 ].
                 * if the specified parameter is not found return the same string
                 * (e.g. "$99" -> parameter 98 -> not found -> return "$99" )
-                * TODO throw error if nodes.length > 1 ?
+                * TODO: Throw error if nodes.length > 1 ?
                 * @param {Array} of one element, integer, n >= 0
                 * @return {String} replacement
                 */
                        var index = parseInt( nodes[0], 10 );
 
                        if ( index < replacements.length ) {
-                               if ( typeof arg === 'string' ) {
-                                       // replacement is a string, escape it
-                                       return mw.html.escape( replacements[index] );
-                               } else {
-                                       // replacement is no string, don't touch!
-                                       return replacements[index];
-                               }
+                               return replacements[index];
                        } else {
                                // index not found, fallback to displaying variable
                                return '$' + ( index + 1 );
 
                /**
                 * Transform wiki-link
-                * TODO unimplemented
+                *
+                * TODO:
+                * It only handles basic cases, either no pipe, or a pipe with an explicit
+                * anchor.
+                *
+                * It does not attempt to handle features like the pipe trick.
+                * However, the pipe trick should usually not be present in wikitext retrieved
+                * from the server, since the replacement is done at save time.
+                * It may, though, if the wikitext appears in extension-controlled content.
+                *
+                * @param nodes
                 */
                wlink: function ( nodes ) {
-                       return 'unimplemented';
+                       var page, anchor, url;
+
+                       page = nodes[0];
+                       url = mw.util.wikiGetlink( page );
+
+                       // [[Some Page]] or [[Namespace:Some Page]]
+                       if ( nodes.length === 1 ) {
+                               anchor = page;
+                       }
+
+                       /*
+                        * [[Some Page|anchor text]] or
+                        * [[Namespace:Some Page|anchor]
+                        */
+                       else {
+                               anchor = nodes[1];
+                       }
+
+                       return $( '<a />' ).attr( {
+                               title: page,
+                               href: url
+                       } ).text( anchor );
                },
 
                /**
                 * @return {jQuery}
                 */
                link: function ( nodes ) {
-                       var arg = nodes[0];
-                       var contents = nodes[1];
-                       var $el;
+                       var $el,
+                               arg = nodes[0],
+                               contents = nodes[1];
                        if ( arg instanceof jQuery ) {
                                $el = arg;
                        } else {
                 * @return {String} selected pluralized form according to current language
                 */
                plural: function ( nodes ) {
-                       var count = parseInt( this.language.convertNumber( nodes[0], true ), 10 );
-                       var forms = nodes.slice(1);
+                       var forms, count;
+                       count = parseFloat( this.language.convertNumber( nodes[0], true ) );
+                       forms = nodes.slice(1);
                        return forms.length ? this.language.convertPlural( count, forms ) : '';
                },
 
                /**
-                * Transform parsed structure into gender
-                * Usage {{gender:[gender| mw.user object ] | masculine|feminine|neutral}}.
-                * @param {Array} of nodes, [ {String|mw.User}, {String}, {String} , {String} ]
+                * Transform parsed structure according to gender.
+                * Usage {{gender:[ gender | mw.user object ] | masculine form|feminine form|neutral form}}.
+                * The first node is either a string, which can be "male" or "female",
+                * or a User object (not a username).
+                *
+                * @param {Array} of nodes, [ {String|mw.User}, {String}, {String}, {String} ]
                 * @return {String} selected gender form according to current language
                 */
                gender: function ( nodes ) {
-                       var gender;
-                       if  ( nodes[0] && nodes[0].options instanceof mw.Map ){
+                       var gender, forms;
+
+                       if  ( nodes[0] && nodes[0].options instanceof mw.Map ) {
                                gender = nodes[0].options.get( 'gender' );
                        } else {
                                gender = nodes[0];
                        }
-                       var forms = nodes.slice(1);
+
+                       forms = nodes.slice( 1 );
+
                        return this.language.gender( gender, forms );
                },
 
                 * @return {String} selected grammatical form according to current language
                 */
                grammar: function ( nodes ) {
-                       var form = nodes[0];
-                       var word = nodes[1];
+                       var form = nodes[0],
+                               word = nodes[1];
                        return word && form && this.language.convertGrammar( word, form );
+               },
+
+               /**
+                * Tranform parsed structure into a int: (interface language) message include
+                * Invoked by putting {{MediaWiki:othermessage}} into a message
+                * @param {Array} of nodes
+                * @return {string} Other message
+                */
+               int: function ( nodes ) {
+                       return mw.jqueryMsg.getMessageFunction()( nodes[0].toLowerCase() );
+               },
+
+               /**
+                * Takes an unformatted number (arab, no group separators and . as decimal separator)
+                * and outputs it in the localized digit script and formatted with decimal
+                * separator, according to the current language
+                * @param {Array} of nodes
+                * @return {Number|String} formatted number
+                */
+               formatnum: function ( nodes ) {
+                       var isInteger = ( nodes[1] && nodes[1] === 'R' ) ? true : false,
+                               number = nodes[0];
+
+                       return this.language.convertNumber( number, isInteger );
                }
        };
        // Deprecated! don't rely on gM existing.
        $.fn.msg = mw.jqueryMsg.getPlugin();
 
        // Replace the default message parser with jqueryMsg
-       var oldParser = mw.Message.prototype.parser;
+       oldParser = mw.Message.prototype.parser;
        mw.Message.prototype.parser = function () {
                // TODO: should we cache the message function so we don't create a new one every time? Benchmark this maybe?
                // Caching is somewhat problematic, because we do need different message functions for different maps, so
                // we'd have to cache the parser as a member of this.map, which sounds a bit ugly.
                // Do not use mw.jqueryMsg unless required
-               if ( this.map.get( this.key ).indexOf( '{{' ) < 0 ) {
+               if ( !/\{\{|\[/.test(this.map.get( this.key ) ) ) {
                        // Fall back to mw.msg's simple parser
                        return oldParser.apply( this );
                }