mediawiki.jqueryMsg: Implement `<nowiki>` support
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.jqueryMsg.js
index 2e89f6b..8a89fbc 100644 (file)
@@ -53,7 +53,6 @@
                        // applies to direct calls to jqueryMsg. The default for mediawiki.js itself
                        // is 'text', including when it uses jqueryMsg.
                        format: 'parse'
-
                };
 
        /**
         * @return {jQuery} return.return
         */
        function getFailableParserFn( options ) {
-               var parser = new mw.jqueryMsg.parser( options );
-
                return function ( args ) {
                        var fallback,
+                               parser = new mw.jqueryMsg.parser( options ),
                                key = args[ 0 ],
                                argsArray = $.isArray( args[ 1 ] ) ? args[ 1 ] : slice.call( args, 1 );
                        try {
         * @param {Object} data
         */
        mw.jqueryMsg.setParserDefaults = function ( data ) {
-               if ( data.allowedHtmlElements ) {
-                       parserDefaults.allowedHtmlElements = data.allowedHtmlElements;
-               }
+               $.extend( parserDefaults, data );
+       };
+
+       /**
+        * Get current parser defaults.
+        *
+        * Primarily used for the unit test. Returns a copy.
+        *
+        * @private
+        * @return {Object}
+        */
+       mw.jqueryMsg.getParserDefaults = function () {
+               return $.extend( {}, parserDefaults );
        };
 
        /**
        mw.jqueryMsg.parser = function ( options ) {
                this.settings = $.extend( {}, parserDefaults, options );
                this.settings.onlyCurlyBraceTransform = ( this.settings.format === 'text' || this.settings.format === 'escaped' );
+               this.astCache = {};
 
                this.emitter = new mw.jqueryMsg.htmlEmitter( this.settings.language, this.settings.magic );
        };
                 * @return {jQuery}
                 */
                parse: function ( key, replacements ) {
-                       return this.emitter.emit( this.getAst( key ), replacements );
+                       var ast = this.getAst( key );
+                       return this.emitter.emit( ast, replacements );
                },
 
                /**
                 * @return {string|Array} string of '[key]' if message missing, simple string if possible, array of arrays if needs parsing
                 */
                getAst: function ( key ) {
-                       var wikiText = this.settings.messages.get( key );
-                       if ( typeof wikiText !== 'string' ) {
-                               wikiText = '\\[' + key + '\\]';
+                       var wikiText;
+
+                       if ( !this.astCache.hasOwnProperty( key ) ) {
+                               wikiText = this.settings.messages.get( key );
+                               if ( typeof wikiText !== 'string' ) {
+                                       wikiText = '\\[' + key + '\\]';
+                               }
+                               this.astCache[ key ] = this.wikiTextToAst( wikiText );
                        }
-                       return this.wikiTextToAst( wikiText );
+                       return this.astCache[ key ];
                },
 
                /**
                                return result;
                        }
 
+                       // <nowiki>...</nowiki> tag. The tags are stripped and the contents are returned unparsed.
+                       function nowiki() {
+                               var parsedResult, plainText,
+                                       result = null;
+
+                               parsedResult = sequence( [
+                                       makeStringParser( '<nowiki>' ),
+                                       // We use a greedy non-backtracking parser, so we must ensure here that we don't take too much
+                                       makeRegexParser( /^.*?(?=<\/nowiki>)/ ),
+                                       makeStringParser( '</nowiki>' )
+                               ] );
+                               if ( parsedResult !== null ) {
+                                       plainText = parsedResult[ 1 ];
+                                       result = [ 'CONCAT' ].concat( plainText );
+                               }
+
+                               return result;
+                       }
+
                        templateName = transform(
                                // see $wgLegalTitleChars
                                // not allowing : due to the need to catch "PLURAL:$1"
                                wikilink,
                                extlink,
                                replacement,
+                               nowiki,
                                html,
                                literal
                        ] );
                                        $el.attr( 'href', textify( arg ) );
                                }
                        }
-                       return appendWithoutParsing( $el, contents );
+                       return appendWithoutParsing( $el.empty(), contents );
                },
 
                /**
                                number = nodes[ 0 ];
 
                        return this.language.convertNumber( number, isInteger );
+               },
+
+               /**
+                * Lowercase text
+                *
+                * @param {Array} nodes List of nodes
+                * @return {string} The given text, all in lowercase
+                */
+               lc: function ( nodes ) {
+                       return textify( nodes[ 0 ] ).toLowerCase();
+               },
+
+               /**
+                * Uppercase text
+                *
+                * @param {Array} nodes List of nodes
+                * @return {string} The given text, all in uppercase
+                */
+               uc: function ( nodes ) {
+                       return textify( nodes[ 0 ] ).toUpperCase();
+               },
+
+               /**
+                * Lowercase first letter of input, leaving the rest unchanged
+                *
+                * @param {Array} nodes List of nodes
+                * @return {string} The given text, with the first character in lowercase
+                */
+               lcfirst: function ( nodes ) {
+                       var text = textify( nodes[ 0 ] );
+                       return text.charAt( 0 ).toLowerCase() + text.slice( 1 );
+               },
+
+               /**
+                * Uppercase first letter of input, leaving the rest unchanged
+                *
+                * @param {Array} nodes List of nodes
+                * @return {string} The given text, with the first character in uppercase
+                */
+               ucfirst: function ( nodes ) {
+                       var text = textify( nodes[ 0 ] );
+                       return text.charAt( 0 ).toUpperCase() + text.slice( 1 );
                }
        };
 
        // Replace the default message parser with jqueryMsg
        oldParser = mw.Message.prototype.parser;
        mw.Message.prototype.parser = function () {
-               var messageFunction;
-
-               // 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.format === 'plain' || !/\{\{|[\[<>&]/.test( this.map.get( this.key ) ) ) {
                        // Fall back to mw.msg's simple parser
                        return oldParser.apply( this );
                }
 
-               messageFunction = mw.jqueryMsg.getMessageFunction( {
-                       messages: this.map,
-                       // For format 'escaped', escaping part is handled by mediawiki.js
-                       format: this.format
-               } );
-               return messageFunction( this.key, this.parameters );
+               if ( !this.map.hasOwnProperty( this.format ) ) {
+                       this.map[ this.format ] = mw.jqueryMsg.getMessageFunction( {
+                               messages: this.map,
+                               // For format 'escaped', escaping part is handled by mediawiki.js
+                               format: this.format
+                       } );
+               }
+               return this.map[ this.format ]( this.key, this.parameters );
        };
 
+       /**
+        * Parse the message to DOM nodes, rather than HTML string like #parse.
+        *
+        * This method is only available when jqueryMsg is loaded.
+        *
+        * @method parseDom
+        * @member mw.Message
+        * @return {jQuery}
+        */
+       mw.Message.prototype.parseDom = ( function () {
+               var reusableParent = $( '<div>' );
+               return function () {
+                       return reusableParent.msg( this.key, this.parameters ).contents().detach();
+               };
+       } )();
+
 }( mediaWiki, jQuery ) );